This is an automated email from the ASF dual-hosted git repository.

abstractdog pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git


The following commit(s) were added to refs/heads/master by this push:
     new 8395bea350a HIVE-30033: Hive K8s operator: log replica changes (#6773)
8395bea350a is described below

commit 8395bea350ac3dbfa65e6b80d7bb534729bccfb3
Author: Bodor Laszlo <[email protected]>
AuthorDate: Thu Oct 15 21:49:04 2026 +0200

    HIVE-30033: Hive K8s operator: log replica changes (#6773)
---
 packaging/src/kubernetes/pom.xml                   |  31 ++++
 .../operator/dependent/HiveDependentResource.java  |  40 ++---
 .../dependent/HiveServer2DeploymentDependent.java  |   2 +-
 .../operator/dependent/LlapResourceBuilder.java    |  15 +-
 .../dependent/MetastoreDeploymentDependent.java    |   2 +-
 .../operator/reconciler/HiveClusterReconciler.java |  45 ++++--
 .../hive/kubernetes/operator/util/Workloads.java   |  98 ++++++++++++
 .../kubernetes/operator/util/TestWorkloads.java    | 169 +++++++++++++++++++++
 8 files changed, 364 insertions(+), 38 deletions(-)

diff --git a/packaging/src/kubernetes/pom.xml b/packaging/src/kubernetes/pom.xml
index f1d8bbb84e5..999d5ebcf0d 100644
--- a/packaging/src/kubernetes/pom.xml
+++ b/packaging/src/kubernetes/pom.xml
@@ -93,6 +93,37 @@
       <artifactId>log4j-core</artifactId>
       <version>${log4j2.version}</version>
     </dependency>
+    <!-- Test dependencies -->
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-api</artifactId>
+      <version>${junit.jupiter.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-engine</artifactId>
+      <version>${junit.jupiter.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-params</artifactId>
+      <version>${junit.jupiter.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <version>${mockito-core.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-junit-jupiter</artifactId>
+      <version>${mockito-core.version}</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
   <build>
     <sourceDirectory>src/java</sourceDirectory>
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 a64440acab8..7e5944f0564 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
@@ -62,6 +62,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;
 
@@ -161,10 +162,11 @@ protected R handleCreate(R desired, P primary, Context<P> 
context) {
   }
 
   /**
-   * Resolves the replica count to set in the desired workload spec.
+   * Resolves the replica count to set in the desired workload spec, and logs 
it when it differs
+   * from what the workload has now.
    * <p>
-   * Always returns an explicit value — never null. Returning null would cause
-   * JOSDK/SSA to omit spec.replicas, and Kubernetes would default it to 1.
+   * Returns a primitive so the value can never be null: a null spec.replicas 
would make JOSDK/SSA
+   * omit the field, and Kubernetes would default it to 1.
    * <p>
    * When autoscaling is enabled:
    * - On CREATE: returns initialReplicas (minReplicas for the component)
@@ -173,7 +175,20 @@ protected R handleCreate(R desired, P primary, Context<P> 
context) {
    * <p>
    * When autoscaling is disabled: returns staticReplicas (the spec value).
    */
-  protected Integer resolveReplicaCount(P primary, Context<P> context,
+  protected int resolveReplicaCount(P primary, Context<P> context,
+      AutoscalingSpec autoscaling, int staticReplicas, int initialReplicas) {
+    Optional<R> existing = getSecondaryResource(primary, context);
+    int resolved = computeReplicaCount(primary, existing, autoscaling,
+        staticReplicas, initialReplicas);
+    // Without this, every scale of an HS2/Metastore Deployment reached the 
cluster silently:
+    // only the imperative LLAP path and the autoscaler logged, and a bare 
"Reconciled" line
+    // said nothing about the size.
+    Workloads.logReplicaChange(LOG, getComponentName(), 
primary.getMetadata().getNamespace(),
+        getSecondaryResourceName(primary, context), existing.orElse(null), 
resolved);
+    return resolved;
+  }
+
+  private int computeReplicaCount(P primary, Optional<R> 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.
@@ -186,7 +201,6 @@ protected Integer resolveReplicaCount(P primary, Context<P> 
context,
     if (autoscaling == null || !autoscaling.isEnabled()) {
       return staticReplicas;
     }
-    Optional<R> existing = getSecondaryResource(primary, context);
     if (existing.isPresent()) {
       // Check if the autoscaler has made a decision during this operator's 
lifecycle
       Integer managed = HiveClusterAutoscaler.getManagedReplicas(
@@ -196,23 +210,15 @@ protected Integer resolveReplicaCount(P primary, 
Context<P> context,
       if (managed != null) {
         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;
+      // Fallback: operator restarted and MANAGED_REPLICAS is empty — read 
current value. The
+      // workload exists, so spec.replicas is set unless something wrote it 
away; initialReplicas
+      // is the floor either way.
+      return Workloads.replicas(existing.get()).orElse(initialReplicas);
     }
     // First creation: start at minReplicas.
     return initialReplicas;
   }
 
-
   /**
    * Returns the component name for this dependent (used for autoscaler 
replica lookup).
    * Subclasses should override if they manage a workload with autoscaling.
diff --git 
a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/HiveServer2DeploymentDependent.java
 
b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/HiveServer2DeploymentDependent.java
index 6bc6291fd1f..a8e8d953dd7 100644
--- 
a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/HiveServer2DeploymentDependent.java
+++ 
b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/HiveServer2DeploymentDependent.java
@@ -232,7 +232,7 @@ protected Deployment desired(HiveCluster hiveCluster,
     AutoscalingSpec hs2Autoscaling = hs2.autoscaling();
     int initialReplicas = hs2Autoscaling != null && hs2Autoscaling.isEnabled()
         ? Math.max(1, hs2Autoscaling.minReplicas()) : hs2.replicas();
-    Integer replicas = resolveReplicaCount(
+    int replicas = resolveReplicaCount(
         hiveCluster, context, hs2Autoscaling, hs2.replicas(), initialReplicas);
 
     Deployment deployment = new DeploymentBuilder()
diff --git 
a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/LlapResourceBuilder.java
 
b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/LlapResourceBuilder.java
index a2c10d3688c..71acf0db481 100644
--- 
a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/LlapResourceBuilder.java
+++ 
b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/LlapResourceBuilder.java
@@ -60,6 +60,7 @@
 import org.apache.hive.kubernetes.operator.util.HadoopXmlBuilder;
 import org.apache.hive.kubernetes.operator.util.HiveConfigBuilder;
 import org.apache.hive.kubernetes.operator.util.Labels;
+import org.apache.hive.kubernetes.operator.util.Workloads;
 
 import static 
org.apache.hive.kubernetes.operator.autoscaling.MetricsScraper.isPodReady;
 
@@ -74,7 +75,6 @@ public class LlapResourceBuilder
     extends HiveDependentResource<StatefulSet, HiveCluster> {
 
   private static final LlapResourceBuilder INSTANCE = new 
LlapResourceBuilder();
-  private static final String TEZAM_INFIX = "-tezam-";
   private static final String HIVE_CONFIG_VOLUME = "hive-config";
   private static final String LLAP_CONFIG_VOLUME = "llap-config";
 
@@ -108,7 +108,7 @@ private static OwnerReference ownerRef(HiveCluster hc) {
 
   /** Resource name for a specific LLAP cluster: {clusterName}-{llapName}. */
   public static String resourceName(HiveCluster hc, LlapSpec llap) {
-    return hc.getMetadata().getName() + "-" + llap.name();
+    return Workloads.nameFor(hc, ConfigUtils.llapComponentKey(llap.name()));
   }
 
   /** ConfigMap name for a specific LLAP cluster. */
@@ -208,17 +208,22 @@ public static PodDisruptionBudget buildPdb(HiveCluster 
hc, LlapSpec llap) {
 
   /** TezAM Deployment/Service name for a specific LLAP cluster. */
   public static String tezAmResourceName(HiveCluster hc, LlapSpec llap) {
-    return hc.getMetadata().getName() + TEZAM_INFIX + llap.name();
+    return tezAmResourceName(hc, llap.name());
+  }
+
+  /** TezAM Deployment/Service name from an LLAP cluster name (used where only 
the name is in hand). */
+  public static String tezAmResourceName(HiveCluster hc, String llapName) {
+    return Workloads.nameFor(hc, ConfigUtils.tezAmComponentKey(llapName));
   }
 
   /** TezAM ConfigMap name for a specific LLAP cluster. */
   public static String tezAmConfigMapName(HiveCluster hc, LlapSpec llap) {
-    return hc.getMetadata().getName() + TEZAM_INFIX + llap.name() + "-config";
+    return tezAmResourceName(hc, llap) + "-config";
   }
 
   /** TezAM PDB name for a specific LLAP cluster. */
   public static String tezAmPdbName(HiveCluster hc, LlapSpec llap) {
-    return hc.getMetadata().getName() + TEZAM_INFIX + llap.name() + "-pdb";
+    return tezAmResourceName(hc, llap) + "-pdb";
   }
 
   /** Builds the PodDisruptionBudget for a per-LLAP-cluster TezAM. */
diff --git 
a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/MetastoreDeploymentDependent.java
 
b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/MetastoreDeploymentDependent.java
index 73afedd9dba..e72240899cf 100644
--- 
a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/MetastoreDeploymentDependent.java
+++ 
b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/MetastoreDeploymentDependent.java
@@ -142,7 +142,7 @@ protected Deployment desired(HiveCluster hiveCluster,
     AutoscalingSpec msAutoscaling = spec.metastore().autoscaling();
     int initialReplicas = msAutoscaling != null && msAutoscaling.isEnabled()
         ? Math.max(1, msAutoscaling.minReplicas()) : 
spec.metastore().replicas();
-    Integer replicas = resolveReplicaCount(
+    int replicas = resolveReplicaCount(
         hiveCluster, context, msAutoscaling, spec.metastore().replicas(), 
initialReplicas);
 
     Deployment deployment = new DeploymentBuilder()
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 ccb1390c90b..8ade234c3ab 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
@@ -60,6 +60,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;
 
@@ -637,18 +638,7 @@ private static int getMinScrapeInterval(HiveClusterSpec 
spec) {
   private void patchReplicas(KubernetesClient client, HiveCluster resource,
       String component, int replicas) {
     String namespace = resource.getMetadata().getNamespace();
-    // Component keys use prefixes: "llap-{name}" → workload 
"{cluster}-{name}",
-    // "tezam-{name}" → workload "{cluster}-tezam-{name}".
-    String workloadName;
-    if (component.startsWith(ConfigUtils.COMPONENT_LLAP + "-")) {
-      String llapName = 
component.substring(ConfigUtils.COMPONENT_LLAP.length() + 1);
-      workloadName = resource.getMetadata().getName() + "-" + llapName;
-    } else if (component.startsWith(ConfigUtils.COMPONENT_TEZAM + "-")) {
-      String llapName = 
component.substring(ConfigUtils.COMPONENT_TEZAM.length() + 1);
-      workloadName = resource.getMetadata().getName() + "-tezam-" + llapName;
-    } else {
-      workloadName = resource.getMetadata().getName() + "-" + component;
-    }
+    String workloadName = Workloads.nameFor(resource, component);
     try {
       if (component.startsWith(ConfigUtils.COMPONENT_LLAP + "-")) {
         
client.apps().statefulSets().inNamespace(namespace).withName(workloadName).scale(replicas);
@@ -661,6 +651,27 @@ private void patchReplicas(KubernetesClient client, 
HiveCluster resource,
     }
   }
 
+  /**
+   * Reads the workload's current replica count from the API server and hands 
it to
+   * {@link Workloads#logReplicaChange}, so an SSA-driven scale (a user editing
+   * spec.llapClusters[i].replicas, a helm upgrade rewriting it) logs the same 
line the dependents'
+   * SSA does instead of reaching the StatefulSet/Deployment silently. Only 
the read is local to
+   * this class; the message and the "log nothing when unchanged" rule are 
shared. 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 component, int desired, boolean isStatefulSet) {
+    try {
+      HasMetadata current = isStatefulSet
+          ? 
client.apps().statefulSets().inNamespace(ns).withName(workloadName).get()
+          : 
client.apps().deployments().inNamespace(ns).withName(workloadName).get();
+      Workloads.logReplicaChange(LOG, component, 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();
@@ -712,6 +723,9 @@ 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 = LlapResourceBuilder.resourceName(resource, 
llapSpec);
+      logReplicaChange(client, ns, llapWorkload, ConfigUtils.COMPONENT_LLAP, 
replicas,
+          /*isStatefulSet=*/true);
       client.apps().statefulSets().inNamespace(ns)
           .resource(LlapResourceBuilder.buildStatefulSet(resource, llapSpec, 
replicas))
           .forceConflicts()
@@ -731,6 +745,9 @@ private void reconcileLlapClusters(HiveCluster resource, 
KubernetesClient client
         client.services().inNamespace(ns)
             .resource(LlapResourceBuilder.buildTezAmService(resource, 
llapSpec))
             .serverSideApply();
+        String tezAmWorkload = LlapResourceBuilder.tezAmResourceName(resource, 
llapSpec);
+        logReplicaChange(client, ns, tezAmWorkload, 
ConfigUtils.COMPONENT_TEZAM, tezAmReplicas,
+            /*isStatefulSet=*/false);
         client.apps().deployments().inNamespace(ns)
             .resource(LlapResourceBuilder.buildTezAmDeployment(resource, 
llapSpec, tezAmReplicas))
             .forceConflicts()
@@ -950,8 +967,8 @@ private boolean isClusterIdle(HiveCluster resource, 
KubernetesClient client) {
     if (spec.tezAm().isEnabled()) {
       for (var llap : spec.llapClusters()) {
         if (llap.isEnabled()
-            && !isAtMinReplicas(client, ns, name + "-tezam-" + llap.name(), 
false,
-                llap.tezAm().autoscaling().minReplicas())) {
+            && !isAtMinReplicas(client, ns, 
LlapResourceBuilder.tezAmResourceName(resource, llap),
+                false, llap.tezAm().autoscaling().minReplicas())) {
           return false;
         }
       }
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 00000000000..d098cf8d6c0
--- /dev/null
+++ 
b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/util/Workloads.java
@@ -0,0 +1,98 @@
+/*
+ * 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 java.util.OptionalInt;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.apps.Deployment;
+import io.fabric8.kubernetes.api.model.apps.StatefulSet;
+import org.apache.hive.kubernetes.operator.model.HiveCluster;
+import org.slf4j.Logger;
+
+/**
+ * Helpers for the operator's workloads (Deployment/StatefulSet): reading 
fields without the
+ * null-guard boilerplate every caller would otherwise repeat, logging replica 
changes in one
+ * shape, and resolving the workload's K8s name from the autoscaler's 
component key.
+ */
+public final class Workloads {
+
+  private Workloads() {}
+
+  /**
+   * Returns spec.replicas from a Deployment or StatefulSet. Empty when the 
resource is absent,
+   * has no spec, or the field is unset — in practice that means "the workload 
isn't there yet",
+   * since the API server defaults spec.replicas on write. A non-workload 
resource is empty too.
+   */
+  public static OptionalInt replicas(HasMetadata resource) {
+    Integer replicas = null;
+    if (resource instanceof Deployment d && d.getSpec() != null) {
+      replicas = d.getSpec().getReplicas();
+    } else if (resource instanceof StatefulSet s && s.getSpec() != null) {
+      replicas = s.getSpec().getReplicas();
+    }
+    return replicas == null ? OptionalInt.empty() : OptionalInt.of(replicas);
+  }
+
+  /**
+   * Logs the replica count the operator is about to apply to {@code 
namespace/name}. One line
+   * covers both cases: {@code current} is the workload as it exists in the 
cluster, or null when
+   * it doesn't exist yet, which logs as {@code none -> N}. Nothing is logged 
when the count
+   * already matches, so silence means no scale is happening.
+   * <p>
+   * Shared by every scale path — the dependents' SSA and the imperative 
LLAP/TezAM SSAs — so the
+   * operator log reads the same whichever one ran. The caller passes its own 
logger to keep the
+   * log category pointing at the code that is actually scaling.
+   */
+  public static void logReplicaChange(Logger log, String component, String 
namespace, String name,
+      HasMetadata current, int desired) {
+    OptionalInt actual = replicas(current);
+    if (actual.isPresent() && actual.getAsInt() == desired) {
+      return;
+    }
+    log.info("Setting replica count for {} {}/{}: {} -> {}", component, 
namespace, name,
+        actual.isPresent() ? String.valueOf(actual.getAsInt()) : "none", 
desired);
+  }
+
+  /**
+   * Maps an autoscaler component key to the K8s workload name it drives. 
Per-LLAP components
+   * carry the LLAP cluster name in their key ("llap-{name}", "tezam-{name}"); 
everything else
+   * (HS2, Metastore) is a plain "{cluster}-{component}". Kept here so every 
scale path — the
+   * autoscaler's `patchReplicas`, the imperative LLAP/TezAM SSAs, and the 
idle-check reads —
+   * resolves the name the same way.
+   * <ul>
+   *   <li>{@code llap-{name}}  → {@code {cluster}-{name}}</li>
+   *   <li>{@code tezam-{name}} → {@code {cluster}-tezam-{name}}</li>
+   *   <li>otherwise            → {@code {cluster}-{component}}</li>
+   * </ul>
+   */
+  public static String nameFor(HiveCluster hc, String component) {
+    String cluster = hc.getMetadata().getName();
+    if (component.startsWith(ConfigUtils.COMPONENT_LLAP + "-")) {
+      String llapName = 
component.substring(ConfigUtils.COMPONENT_LLAP.length() + 1);
+      return cluster + "-" + llapName;
+    }
+    if (component.startsWith(ConfigUtils.COMPONENT_TEZAM + "-")) {
+      String llapName = 
component.substring(ConfigUtils.COMPONENT_TEZAM.length() + 1);
+      return cluster + "-" + ConfigUtils.COMPONENT_TEZAM + "-" + llapName;
+    }
+    return cluster + "-" + component;
+  }
+}
diff --git 
a/packaging/src/kubernetes/src/test/java/org/apache/hive/kubernetes/operator/util/TestWorkloads.java
 
b/packaging/src/kubernetes/src/test/java/org/apache/hive/kubernetes/operator/util/TestWorkloads.java
new file mode 100644
index 00000000000..3d2e6d53779
--- /dev/null
+++ 
b/packaging/src/kubernetes/src/test/java/org/apache/hive/kubernetes/operator/util/TestWorkloads.java
@@ -0,0 +1,169 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import java.util.OptionalInt;
+
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.api.model.apps.Deployment;
+import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
+import io.fabric8.kubernetes.api.model.apps.StatefulSet;
+import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
+import org.apache.hive.kubernetes.operator.model.HiveCluster;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.slf4j.Logger;
+
+class TestWorkloads {
+
+  @Test
+  void replicasReturnsValueFromDeployment() {
+    Deployment d = new DeploymentBuilder()
+        .withNewSpec().withReplicas(3).endSpec()
+        .build();
+    assertEquals(OptionalInt.of(3), Workloads.replicas(d));
+  }
+
+  @Test
+  void replicasReturnsValueFromStatefulSet() {
+    StatefulSet s = new StatefulSetBuilder()
+        .withNewSpec().withReplicas(5).endSpec()
+        .build();
+    assertEquals(OptionalInt.of(5), Workloads.replicas(s));
+  }
+
+  @Test
+  void replicasIsEmptyWhenDeploymentSpecMissing() {
+    // fabric8 Deployment with no spec set at all
+    Deployment d = new DeploymentBuilder().build();
+    assertFalse(Workloads.replicas(d).isPresent());
+  }
+
+  @Test
+  void replicasIsEmptyWhenStatefulSetSpecMissing() {
+    StatefulSet s = new StatefulSetBuilder().build();
+    assertFalse(Workloads.replicas(s).isPresent());
+  }
+
+  @Test
+  void replicasIsEmptyWhenDeploymentReplicasUnset() {
+    // spec present, but replicas field not set
+    Deployment d = new DeploymentBuilder().withNewSpec().endSpec().build();
+    assertFalse(Workloads.replicas(d).isPresent());
+  }
+
+  @Test
+  void replicasIsEmptyWhenStatefulSetReplicasUnset() {
+    StatefulSet s = new StatefulSetBuilder().withNewSpec().endSpec().build();
+    assertFalse(Workloads.replicas(s).isPresent());
+  }
+
+  @Test
+  void replicasIsEmptyForNonWorkloadResource() {
+    // Anything that's neither a Deployment nor a StatefulSet is empty,
+    // even if it happens to have a "spec" (e.g., a ConfigMap here has none).
+    ConfigMap cm = new ConfigMapBuilder()
+        .withNewMetadata().withName("cm").endMetadata()
+        .build();
+    assertFalse(Workloads.replicas(cm).isPresent());
+  }
+
+  @Test
+  void replicasIsEmptyForNullResource() {
+    assertFalse(Workloads.replicas(null).isPresent());
+  }
+
+  @Test
+  void logReplicaChangeLogsTheDelta() {
+    Logger log = mock(Logger.class);
+    StatefulSet s = new 
StatefulSetBuilder().withNewSpec().withReplicas(12).endSpec().build();
+
+    Workloads.logReplicaChange(log, "llap", "ns", "hive-llap0", s, 15);
+
+    verify(log).info(anyString(), eq("llap"), eq("ns"), eq("hive-llap0"), 
eq("12"), eq(15));
+  }
+
+  /** No workload yet: the same line reports the count the first create will 
set. */
+  @Test
+  void logReplicaChangeLogsNoneAsTheFromValueOnFirstCreate() {
+    Logger log = mock(Logger.class);
+
+    Workloads.logReplicaChange(log, "hiveserver2", "ns", "hive-hiveserver2", 
null, 2);
+
+    verify(log).info(anyString(), eq("hiveserver2"), eq("ns"), 
eq("hive-hiveserver2"),
+        eq("none"), eq(2));
+  }
+
+  /** Silence is the signal that nothing is being scaled, so an unchanged 
count logs nothing. */
+  @Test
+  void logReplicaChangeIsSilentWhenTheCountAlreadyMatches() {
+    Logger log = mock(Logger.class);
+    Deployment d = new 
DeploymentBuilder().withNewSpec().withReplicas(2).endSpec().build();
+
+    Workloads.logReplicaChange(log, "metastore", "ns", "hive-metastore", d, 2);
+
+    verifyNoInteractions(log);
+  }
+
+  /**
+   * Covers the three branches of {@link Workloads#nameFor}:
+   * <ul>
+   *   <li>{@code llap-{name}}  → {@code {cluster}-{name}}</li>
+   *   <li>{@code tezam-{name}} → {@code {cluster}-tezam-{name}}</li>
+   *   <li>otherwise            → {@code {cluster}-{component}}</li>
+   * </ul>
+   * The last two rows exercise the "no dash after the prefix" fall-through: a 
bare
+   * {@code "llap"} or {@code "tezam"} isn't a per-cluster component key and 
must land in the
+   * plain-{cluster}-{component} branch, not be treated as an empty LLAP name.
+   */
+  @ParameterizedTest
+  @CsvSource({
+      // component,                expected workload name
+      "llap-llap0,                 hive-llap0",
+      "llap-my-llap-cluster,       hive-my-llap-cluster",
+      "tezam-llap0,                hive-tezam-llap0",
+      "tezam-my-llap-cluster,      hive-tezam-my-llap-cluster",
+      "hiveserver2,                hive-hiveserver2",
+      "metastore,                  hive-metastore",
+      "llap,                       hive-llap",
+      "tezam,                      hive-tezam",
+  })
+  void nameForMapsComponentKeyToWorkloadName(String component, String 
expected) {
+    HiveCluster hc = hiveCluster("hive");
+    assertEquals(expected, Workloads.nameFor(hc, component));
+  }
+
+  private static HiveCluster hiveCluster(String name) {
+    HiveCluster hc = new HiveCluster();
+    hc.setMetadata(new ObjectMetaBuilder().withName(name).build());
+    return hc;
+  }
+}

Reply via email to