Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions packaging/src/kubernetes/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand All @@ -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(
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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";

Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -208,17 +208,22 @@ public static PodDisruptionBudget buildPdb(HiveCluster hc, LlapSpec llap) {

/** TezAM Deployment/Service name for a specific LLAP cluster. */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For TezAM we have statefullset, not deployment, no? That actually makes me wonder why we have deployment for LLAP that doesn't allow parallel botstrap and rolling upgrade is sequential

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LlapResourceBuilder.buildTezAmDeployment returns a Deployment, and LLAP is already a StatefulSet

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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -593,18 +594,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);
Expand All @@ -617,6 +607,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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we generalize log method or need both?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both logReplicaChange implementations collapsed into Workloads.logReplicaChange
the reconciler keeps only a few lines wrapper that does the API-server read and passes the resource in

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();
Expand Down Expand Up @@ -668,6 +679,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()
Expand All @@ -687,6 +701,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()
Expand Down Expand Up @@ -910,8 +927,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;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading