diff --git a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/HiveDependentResource.java b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/HiveDependentResource.java
index bb031b50ac29..3d16f81072d9 100644
--- a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/HiveDependentResource.java
+++ b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/HiveDependentResource.java
@@ -54,6 +54,7 @@
import org.apache.hive.kubernetes.operator.model.spec.SecretKeyRef;
import org.apache.hive.kubernetes.operator.model.spec.ProbeSpec;
import org.apache.hive.kubernetes.operator.util.ConfigUtils;
+import org.apache.hive.kubernetes.operator.util.Workloads;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -164,6 +165,15 @@ protected R handleCreate(R desired, P primary, Context
context) {
*/
protected Integer resolveReplicaCount(P primary, Context
context,
AutoscalingSpec autoscaling, int staticReplicas, int initialReplicas) {
+ Optional existing = getSecondaryResource(primary, context);
+ Integer resolved = computeReplicaCount(primary, existing, autoscaling,
+ staticReplicas, initialReplicas);
+ logReplicaChange(primary, existing, resolved);
+ return resolved;
+ }
+
+ private Integer computeReplicaCount(P primary, Optional existing,
+ AutoscalingSpec autoscaling, int staticReplicas, int initialReplicas) {
// Suspended cluster → 0 replicas (dependent resources natively respect suspend).
// Exception: HMS stays running if includeMetastore=false in autoSuspend config.
if (primary instanceof HiveCluster hc && hc.getSpec().suspend()) {
@@ -175,7 +185,6 @@ protected Integer resolveReplicaCount(P primary, Context context,
if (autoscaling == null || !autoscaling.isEnabled()) {
return staticReplicas;
}
- Optional existing = getSecondaryResource(primary, context);
if (existing.isPresent()) {
// Check if the autoscaler has made a decision during this operator's lifecycle
Integer managed = HiveClusterAutoscaler.getManagedReplicas(
@@ -186,21 +195,37 @@ protected Integer resolveReplicaCount(P primary, Context context,
return managed;
}
// Fallback: operator restarted and MANAGED_REPLICAS is empty — read current value
- R resource = existing.get();
- if (resource instanceof io.fabric8.kubernetes.api.model.apps.Deployment d) {
- return d.getSpec() != null && d.getSpec().getReplicas() != null
- ? d.getSpec().getReplicas() : initialReplicas;
- }
- if (resource instanceof io.fabric8.kubernetes.api.model.apps.StatefulSet s) {
- return s.getSpec() != null && s.getSpec().getReplicas() != null
- ? s.getSpec().getReplicas() : initialReplicas;
- }
- return initialReplicas;
+ Integer current = Workloads.replicas(existing.get());
+ return current != null ? current : initialReplicas;
}
// First creation: start at minReplicas.
return initialReplicas;
}
+ /**
+ * Emits an INFO line when the SSA about to run will actually change the workload's replica
+ * count. Silence means the count already matches, so no scale is happening. Without this,
+ * every scale of an HS2/Metastore Deployment reached the cluster silently (only the imperative
+ * LLAP path and the autoscaler logged); a bare "Reconciled" line said nothing about the size.
+ * Uses the same "Scaling ... A -> B" shape the imperative LLAP path emits.
+ */
+ private void logReplicaChange(P primary, Optional existing, Integer desired) {
+ String component = getComponentName();
+ if (component == null || desired == null) {
+ return;
+ }
+ String ns = primary.getMetadata().getNamespace();
+ String name = existing.map(r -> r.getMetadata().getName()).orElse(component);
+ if (existing.isEmpty()) {
+ LOG.info("Creating {} {}/{} with {} replicas", component, ns, name, desired);
+ return;
+ }
+ Integer current = Workloads.replicas(existing.get());
+ if (current != null && !current.equals(desired)) {
+ LOG.info("Scaling {} {}/{}: {} -> {} replicas", component, ns, name, current, desired);
+ }
+ }
+
/**
* Returns the component name for this dependent (used for autoscaler replica lookup).
diff --git a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/reconciler/HiveClusterReconciler.java b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/reconciler/HiveClusterReconciler.java
index ec37d6820ad1..816efc9eee43 100644
--- a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/reconciler/HiveClusterReconciler.java
+++ b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/reconciler/HiveClusterReconciler.java
@@ -59,6 +59,7 @@
import org.apache.hive.kubernetes.operator.model.status.ComponentStatus;
import org.apache.hive.kubernetes.operator.util.ConfigUtils;
import org.apache.hive.kubernetes.operator.util.Labels;
+import org.apache.hive.kubernetes.operator.util.Workloads;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -577,6 +578,30 @@ private void patchReplicas(KubernetesClient client, HiveCluster resource,
}
}
+ /**
+ * Emits an INFO log when the reconciler's server-side apply is about to change the workload's
+ * replica count. Without this, an SSA-driven scale (a user editing spec.llapClusters[i].replicas,
+ * a helm upgrade rewriting it) reaches the StatefulSet/Deployment silently -- only the
+ * autoscaler path {@link #patchReplicas} logged its scales, so a plain scale looked like the
+ * operator was doing nothing. Read failures are swallowed at DEBUG: the SSA below runs either
+ * way, and a missing pre-scale line is not worth failing the reconcile over.
+ */
+ private void logReplicaChange(KubernetesClient client, String ns, String workloadName,
+ String kind, int desired, boolean isStatefulSet) {
+ try {
+ Integer current = Workloads.replicas(isStatefulSet
+ ? client.apps().statefulSets().inNamespace(ns).withName(workloadName).get()
+ : client.apps().deployments().inNamespace(ns).withName(workloadName).get());
+ if (current == null) {
+ LOG.info("Creating {} {}/{} with {} replicas", kind, ns, workloadName, desired);
+ } else if (current != desired) {
+ LOG.info("Scaling {} {}/{}: {} -> {} replicas", kind, ns, workloadName, current, desired);
+ }
+ } catch (Exception e) {
+ LOG.debug("Could not read current replicas for {}/{}: {}", ns, workloadName, e.getMessage());
+ }
+ }
+
private void patchSuspendSpec(KubernetesClient client, HiveCluster resource, boolean suspend) {
String ns = resource.getMetadata().getNamespace();
String name = resource.getMetadata().getName();
@@ -627,6 +652,8 @@ private void reconcileLlapClusters(HiveCluster resource, KubernetesClient client
// brief scale-up-then-down on first create (K8s defaults to 1 if omitted).
// resolveLlapReplicaCount already reads the autoscaler's managed value,
// so this is always the correct replica count.
+ String llapWorkload = clusterName + "-" + llapSpec.name();
+ logReplicaChange(client, ns, llapWorkload, "llap", replicas, /*isStatefulSet=*/true);
client.apps().statefulSets().inNamespace(ns)
.resource(LlapResourceBuilder.buildStatefulSet(resource, llapSpec, replicas))
.forceConflicts()
@@ -646,6 +673,8 @@ private void reconcileLlapClusters(HiveCluster resource, KubernetesClient client
client.services().inNamespace(ns)
.resource(LlapResourceBuilder.buildTezAmService(resource, llapSpec))
.serverSideApply();
+ String tezAmWorkload = clusterName + "-tezam-" + llapSpec.name();
+ logReplicaChange(client, ns, tezAmWorkload, "tezam", tezAmReplicas, /*isStatefulSet=*/false);
client.apps().deployments().inNamespace(ns)
.resource(LlapResourceBuilder.buildTezAmDeployment(resource, llapSpec, tezAmReplicas))
.forceConflicts()
diff --git a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/util/Workloads.java b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/util/Workloads.java
new file mode 100644
index 000000000000..973076894967
--- /dev/null
+++ b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/util/Workloads.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.hive.kubernetes.operator.util;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.apps.Deployment;
+import io.fabric8.kubernetes.api.model.apps.StatefulSet;
+
+/**
+ * Small helpers over Deployment/StatefulSet that read fields without the
+ * null-guard boilerplate every caller would otherwise repeat.
+ */
+public final class Workloads {
+
+ private Workloads() {}
+
+ /**
+ * Returns spec.replicas from a Deployment or StatefulSet, or null when the resource is
+ * absent, has no spec, or the field is unset. A non-workload resource returns null too.
+ */
+ public static Integer replicas(HasMetadata resource) {
+ if (resource instanceof Deployment d) {
+ return d.getSpec() == null ? null : d.getSpec().getReplicas();
+ }
+ if (resource instanceof StatefulSet s) {
+ return s.getSpec() == null ? null : s.getSpec().getReplicas();
+ }
+ return null;
+ }
+}