This is an automated email from the ASF dual-hosted git repository.
gyfora pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-kubernetes-operator.git
The following commit(s) were added to refs/heads/main by this push:
new fdfcc695 [FLINK-35746][Kubernetes-Operator] Add getJobConfiguration
and getJobCheckpointConfiguration (#1000)
fdfcc695 is described below
commit fdfcc695daba16e4f4a8c9a6c08b7f470787de00
Author: Nishita Pattanayak <[email protected]>
AuthorDate: Tue Aug 11 19:40:46 2026 +0530
[FLINK-35746][Kubernetes-Operator] Add getJobConfiguration and
getJobCheckpointConfiguration (#1000)
---
.../operator/config/FlinkConfigManager.java | 70 +++++
.../operator/controller/FlinkResourceContext.java | 63 +++-
.../operator/observer/JobStatusObserver.java | 29 ++
.../AbstractFlinkResourceReconciler.java | 7 +-
.../deployment/AbstractJobReconciler.java | 4 +
.../deployment/ApplicationReconciler.java | 13 +-
.../reconciler/deployment/SessionReconciler.java | 7 +
.../sessionjob/SessionJobReconciler.java | 15 +-
.../operator/service/AbstractFlinkService.java | 111 +++++++
.../kubernetes/operator/service/FlinkService.java | 6 +
.../utils/FlinkRuntimeConfigurationUtils.java | 242 ++++++++++++++
.../kubernetes/operator/TestingFlinkService.java | 30 ++
.../operator/config/FlinkConfigManagerTest.java | 37 +++
.../operator/observer/JobStatusObserverTest.java | 348 +++++++++++++++++++++
.../operator/service/AbstractFlinkServiceTest.java | 132 ++++++++
15 files changed, 1108 insertions(+), 6 deletions(-)
diff --git
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/config/FlinkConfigManager.java
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/config/FlinkConfigManager.java
index 9b9126bc..9507973d 100644
---
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/config/FlinkConfigManager.java
+++
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/config/FlinkConfigManager.java
@@ -49,6 +49,7 @@ import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -88,6 +89,17 @@ public class FlinkConfigManager {
private final Consumer<Set<String>> namespaceListener;
private volatile ConcurrentHashMap<FlinkVersion, List<String>>
relevantFlinkVersionPrefixes;
+ private final Cache<RuntimeConfigCacheKey, Map<String, String>>
runtimeConfigCache;
+
+ /** Cache key for runtime configuration overrides, scoped to a specific
job instance. */
+ @Value
+ @Builder
+ private static class RuntimeConfigCacheKey {
+ String namespace;
+ String name;
+ String jobId;
+ }
+
protected static final Pattern FLINK_VERSION_PATTERN =
Pattern.compile(
VERSION_CONF_PREFIX.replaceAll("\\.", "\\\\\\.")
@@ -132,6 +144,14 @@ public class FlinkConfigManager {
}
});
+ this.runtimeConfigCache =
+ CacheBuilder.newBuilder()
+ .maximumSize(
+ defaultConfig.get(
+
KubernetesOperatorConfigOptions.OPERATOR_CONFIG_CACHE_SIZE))
+ .expireAfterAccess(cacheTimeout)
+ .build();
+
updateDefaultConfig(defaultConfig);
ScheduledExecutorService executorService =
Executors.newSingleThreadScheduledExecutor();
executorService.scheduleWithFixedDelay(
@@ -355,6 +375,56 @@ public class FlinkConfigManager {
return conf;
}
+ private RuntimeConfigCacheKey cacheKey(String namespace, String name,
String jobId) {
+ return
RuntimeConfigCacheKey.builder().namespace(namespace).name(name).jobId(jobId).build();
+ }
+
+ /**
+ * Store runtime configuration overrides in the cache. Called by observers
after fetching
+ * configuration from the Flink REST API.
+ *
+ * @param namespace Resource namespace
+ * @param name Resource name
+ * @param jobId Job ID string
+ * @param config Runtime configuration key-value pairs
+ */
+ public void putRuntimeConfig(
+ String namespace, String name, String jobId, Map<String, String>
config) {
+ runtimeConfigCache.put(
+ cacheKey(namespace, name, jobId),
Collections.unmodifiableMap(config));
+ LOG.debug("Cached runtime configuration with {} entries",
config.size());
+ }
+
+ /**
+ * Get cached runtime configuration overrides for a specific job instance.
+ *
+ * @param namespace Resource namespace
+ * @param name Resource name
+ * @param jobId Job ID string
+ * @return Cached runtime config if present
+ */
+ public Optional<Map<String, String>> getRuntimeConfig(
+ String namespace, String name, String jobId) {
+ return Optional.ofNullable(
+ runtimeConfigCache.getIfPresent(cacheKey(namespace, name,
jobId)));
+ }
+
+ /**
+ * Invalidate all cached runtime configurations for a given resource.
Should be called when a
+ * job is cancelled or an upgrade replaces the running job.
+ *
+ * @param namespace Resource namespace
+ * @param name Resource name
+ */
+ public void invalidateRuntimeConfig(String namespace, String name) {
+ runtimeConfigCache
+ .asMap()
+ .keySet()
+ .removeIf(
+ key -> namespace.equals(key.getNamespace()) &&
name.equals(key.getName()));
+ LOG.debug("Invalidated runtime configuration cache");
+ }
+
private void addOperatorConfigsFromSpec(AbstractFlinkSpec spec,
Configuration conf) {
// Observe config should include the latest operator related settings
if (spec.getFlinkConfiguration() != null) {
diff --git
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/controller/FlinkResourceContext.java
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/controller/FlinkResourceContext.java
index 15c0242d..8a0be908 100644
---
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/controller/FlinkResourceContext.java
+++
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/controller/FlinkResourceContext.java
@@ -40,6 +40,8 @@ import lombok.RequiredArgsConstructor;
import javax.annotation.Nullable;
+import java.util.Map;
+import java.util.Optional;
import java.util.function.Function;
/** Context for reconciling a Flink resource. */
@@ -56,6 +58,7 @@ public abstract class FlinkResourceContext<CR extends
AbstractFlinkResource<?, ?
private FlinkOperatorConfiguration operatorConfig;
private Configuration observeConfig;
+ private Optional<Map<String, String>> runtimeConfig;
private FlinkService flinkService;
private KubernetesJobAutoScalerContext autoScalerContext;
@@ -103,7 +106,65 @@ public abstract class FlinkResourceContext<CR extends
AbstractFlinkResource<?, ?
if (observeConfig != null) {
return observeConfig;
}
- return observeConfig = createObserveConfig();
+ observeConfig = createObserveConfig();
+ if (observeConfig != null) {
+ getRuntimeConfig().ifPresent(config ->
config.forEach(observeConfig::setString));
+ }
+ return observeConfig;
+ }
+
+ /**
+ * Get the cached runtime configuration for the current job, memoized per
context to avoid
+ * repeated cache lookups within a single reconciliation cycle. Only a
present result is
+ * memoized so that a later {@link #putRuntimeConfig(Map)} within the same
cycle is visible to
+ * subsequent callers.
+ *
+ * @return Cached runtime config if present.
+ */
+ public Optional<Map<String, String>> getRuntimeConfig() {
+ if (runtimeConfig != null && runtimeConfig.isPresent()) {
+ return runtimeConfig;
+ }
+ var jobStatus = resource.getStatus().getJobStatus();
+ if (jobStatus == null || jobStatus.getJobId() == null) {
+ return Optional.empty();
+ }
+ return runtimeConfig =
+ configManager.getRuntimeConfig(
+ resource.getMetadata().getNamespace(),
+ resource.getMetadata().getName(),
+ jobStatus.getJobId());
+ }
+
+ /**
+ * Store runtime configuration in the global cache and refresh the
context-level memoized view
+ * so subsequent accesses within the same reconciliation cycle avoid an
extra cache lookup.
+ *
+ * @param config Runtime configuration key-value pairs fetched from the
Flink REST API.
+ */
+ public void putRuntimeConfig(Map<String, String> config) {
+ var jobStatus = resource.getStatus().getJobStatus();
+ if (jobStatus == null || jobStatus.getJobId() == null) {
+ return;
+ }
+ configManager.putRuntimeConfig(
+ resource.getMetadata().getNamespace(),
+ resource.getMetadata().getName(),
+ jobStatus.getJobId(),
+ config);
+ runtimeConfig =
+ configManager.getRuntimeConfig(
+ resource.getMetadata().getNamespace(),
+ resource.getMetadata().getName(),
+ jobStatus.getJobId());
+ observeConfig = null;
+ }
+
+ /**
+ * @return The config manager for this context.
+ */
+ public FlinkConfigManager getConfigManager() {
+ return configManager;
}
/**
diff --git
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/JobStatusObserver.java
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/JobStatusObserver.java
index e205e3b8..b1936481 100644
---
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/JobStatusObserver.java
+++
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/observer/JobStatusObserver.java
@@ -91,6 +91,7 @@ public class JobStatusObserver<R extends
AbstractFlinkResource<?, ?>> {
if (newJobStatusOpt.isPresent()) {
var newJobStatus = newJobStatusOpt.get();
updateJobStatus(ctx, newJobStatus);
+ fetchAndCacheRuntimeConfig(ctx, newJobStatus);
ReconciliationUtils.checkAndUpdateStableSpec(resource.getStatus());
// see if the JM server is up, try to get the exceptions
if (!previousJobStatus.isGloballyTerminalState()) {
@@ -111,6 +112,34 @@ public class JobStatusObserver<R extends
AbstractFlinkResource<?, ?>> {
return false;
}
+ private void fetchAndCacheRuntimeConfig(
+ FlinkResourceContext<R> ctx, JobStatusMessage clusterJobStatus) {
+ // Skip only globally-terminal states (FINISHED/CANCELED/FAILED) where
the JM REST API is
+ // gone; non-terminal transitional states (INITIALIZING, RESTARTING,
RECONCILING, etc.)
+ // still expose reachable config endpoints and any REST failure is
caught below.
+ if (clusterJobStatus.getJobState().isGloballyTerminalState()) {
+ return;
+ }
+
+ if (ctx.getRuntimeConfig().isPresent()) {
+ LOG.debug("Runtime configuration already cached");
+ return;
+ }
+
+ LOG.debug("Fetching runtime configuration");
+ var jobStatus = ctx.getResource().getStatus().getJobStatus();
+ try {
+ var runtimeConfig =
+ ctx.getFlinkService()
+ .getRuntimeConfiguration(
+ ctx.getObserveConfig(),
+ JobID.fromHexString(jobStatus.getJobId()));
+ ctx.putRuntimeConfig(runtimeConfig);
+ } catch (Exception e) {
+ LOG.warn("Failed to fetch runtime configuration, will retry next
cycle", e);
+ }
+ }
+
/**
* Observe the exceptions raised in the job manager and take appropriate
action.
*
diff --git
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/AbstractFlinkResourceReconciler.java
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/AbstractFlinkResourceReconciler.java
index 7f8af3ca..3c16b350 100644
---
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/AbstractFlinkResourceReconciler.java
+++
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/AbstractFlinkResourceReconciler.java
@@ -363,7 +363,12 @@ public abstract class AbstractFlinkResourceReconciler<
var scaled = ctx.getFlinkService().scale(ctx, deployConfig);
if (scaled) {
- ReconciliationUtils.updateStatusForDeployedSpec(ctx.getResource(),
deployConfig, clock);
+ var resource = ctx.getResource();
+ ctx.getConfigManager()
+ .invalidateRuntimeConfig(
+ resource.getMetadata().getNamespace(),
+ resource.getMetadata().getName());
+ ReconciliationUtils.updateStatusForDeployedSpec(resource,
deployConfig, clock);
}
return scaled;
diff --git
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/AbstractJobReconciler.java
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/AbstractJobReconciler.java
index e57aa51b..51f3b3be 100644
---
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/AbstractJobReconciler.java
+++
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/AbstractJobReconciler.java
@@ -599,6 +599,10 @@ public abstract class AbstractJobReconciler<
protected void resubmitJob(FlinkResourceContext<CR> ctx, boolean
requireHaMetadata)
throws Exception {
LOG.info("Resubmitting Flink job...");
+ ctx.getConfigManager()
+ .invalidateRuntimeConfig(
+ ctx.getResource().getMetadata().getNamespace(),
+ ctx.getResource().getMetadata().getName());
SPEC specToRecover =
ReconciliationUtils.getDeployedSpec(ctx.getResource());
var upgradeStatePath =
diff --git
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/ApplicationReconciler.java
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/ApplicationReconciler.java
index 6f0f0e85..99ff5f78 100644
---
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/ApplicationReconciler.java
+++
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/ApplicationReconciler.java
@@ -150,6 +150,11 @@ public class ApplicationReconciler
var status = relatedResource.getStatus();
var flinkService = ctx.getFlinkService();
+ ctx.getConfigManager()
+ .invalidateRuntimeConfig(
+ relatedResource.getMetadata().getNamespace(),
+ relatedResource.getMetadata().getName());
+
ClusterHealthEvaluator.removeLastValidClusterHealthInfo(
relatedResource.getStatus().getClusterInfo());
@@ -230,10 +235,12 @@ public class ApplicationReconciler
@Override
protected boolean cancelJob(FlinkResourceContext<FlinkDeployment> ctx,
SuspendMode suspendMode)
throws Exception {
+ var resource = ctx.getResource();
+ ctx.getConfigManager()
+ .invalidateRuntimeConfig(
+ resource.getMetadata().getNamespace(),
resource.getMetadata().getName());
var cancelTs = Instant.now();
- var result =
- ctx.getFlinkService()
- .cancelJob(ctx.getResource(), suspendMode,
ctx.getObserveConfig());
+ var result = ctx.getFlinkService().cancelJob(resource, suspendMode,
ctx.getObserveConfig());
result.getSavepointPath()
.ifPresent(location -> setUpgradeSavepointPath(ctx, location,
cancelTs));
return result.isPending();
diff --git
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/SessionReconciler.java
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/SessionReconciler.java
index c38afce0..0c3c1812 100644
---
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/SessionReconciler.java
+++
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/deployment/SessionReconciler.java
@@ -103,6 +103,13 @@ public class SessionReconciler
boolean requireHaMetadata)
throws Exception {
var cr = ctx.getResource();
+ // Defensive invalidate for consistency with the other deploy()
overrides. In practice this
+ // is a no-op for session-mode FlinkDeployments (jobs live on
FlinkSessionJob resources
+ // with their own cache keys), but it makes the "every deploy()
invalidates" invariant
+ // explicit and safe against future changes to session-cluster caching.
+ ctx.getConfigManager()
+ .invalidateRuntimeConfig(
+ cr.getMetadata().getNamespace(),
cr.getMetadata().getName());
setOwnerReference(cr, deployConfig);
ctx.getFlinkService().submitSessionCluster(deployConfig);
cr.getStatus().setJobManagerDeploymentStatus(JobManagerDeploymentStatus.DEPLOYING);
diff --git
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/sessionjob/SessionJobReconciler.java
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/sessionjob/SessionJobReconciler.java
index 4491ab97..dd088dd7 100644
---
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/sessionjob/SessionJobReconciler.java
+++
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/reconciler/sessionjob/SessionJobReconciler.java
@@ -76,6 +76,15 @@ public class SessionJobReconciler
boolean requireHaMetadata)
throws Exception {
+ // Any deploy path produces a new job submission, so any previously
cached runtime config
+ // for this resource is stale. Defensive invalidate here keeps every
deploy() override
+ // consistent and avoids relying on transitive invalidation by
upstream callers.
+ var relatedResource = ctx.getResource();
+ ctx.getConfigManager()
+ .invalidateRuntimeConfig(
+ relatedResource.getMetadata().getNamespace(),
+ relatedResource.getMetadata().getName());
+
eventRecorder.triggerEvent(
ctx.getResource(),
EventRecorder.Type.Normal,
@@ -113,10 +122,14 @@ public class SessionJobReconciler
@Override
protected boolean cancelJob(FlinkResourceContext<FlinkSessionJob> ctx,
SuspendMode suspendMode)
throws Exception {
+ var resource = ctx.getResource();
+ ctx.getConfigManager()
+ .invalidateRuntimeConfig(
+ resource.getMetadata().getNamespace(),
resource.getMetadata().getName());
var cancelTs = Instant.now();
var result =
ctx.getFlinkService()
- .cancelSessionJob(ctx.getResource(), suspendMode,
ctx.getObserveConfig());
+ .cancelSessionJob(resource, suspendMode,
ctx.getObserveConfig());
result.getSavepointPath()
.ifPresent(location -> setUpgradeSavepointPath(ctx, location,
cancelTs));
return result.isPending();
diff --git
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/service/AbstractFlinkService.java
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/service/AbstractFlinkService.java
index 08467957..bc0e9ccf 100644
---
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/service/AbstractFlinkService.java
+++
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/service/AbstractFlinkService.java
@@ -55,6 +55,7 @@ import
org.apache.flink.kubernetes.operator.reconciler.ReconciliationUtils;
import org.apache.flink.kubernetes.operator.utils.EnvUtils;
import org.apache.flink.kubernetes.operator.utils.EventRecorder;
import org.apache.flink.kubernetes.operator.utils.ExceptionUtils;
+import
org.apache.flink.kubernetes.operator.utils.FlinkRuntimeConfigurationUtils;
import org.apache.flink.kubernetes.operator.utils.FlinkUtils;
import org.apache.flink.runtime.client.JobStatusMessage;
import
org.apache.flink.runtime.highavailability.nonha.standalone.StandaloneClientHAServices;
@@ -67,10 +68,14 @@ import
org.apache.flink.runtime.rest.handler.async.AsynchronousOperationResult;
import org.apache.flink.runtime.rest.messages.DashboardConfiguration;
import org.apache.flink.runtime.rest.messages.EmptyMessageParameters;
import org.apache.flink.runtime.rest.messages.EmptyRequestBody;
+import org.apache.flink.runtime.rest.messages.JobConfigHeaders;
+import org.apache.flink.runtime.rest.messages.JobConfigInfo;
import org.apache.flink.runtime.rest.messages.JobExceptionsHeaders;
import org.apache.flink.runtime.rest.messages.JobExceptionsInfoWithHistory;
+import org.apache.flink.runtime.rest.messages.JobMessageParameters;
import org.apache.flink.runtime.rest.messages.JobsOverviewHeaders;
import org.apache.flink.runtime.rest.messages.TriggerId;
+import
org.apache.flink.runtime.rest.messages.checkpoints.CheckpointConfigHeaders;
import
org.apache.flink.runtime.rest.messages.checkpoints.CheckpointIdPathParameter;
import org.apache.flink.runtime.rest.messages.checkpoints.CheckpointInfo;
import
org.apache.flink.runtime.rest.messages.checkpoints.CheckpointStatisticDetailsHeaders;
@@ -82,6 +87,7 @@ import
org.apache.flink.runtime.rest.messages.checkpoints.CheckpointTriggerReque
import
org.apache.flink.runtime.rest.messages.checkpoints.CheckpointingStatistics;
import
org.apache.flink.runtime.rest.messages.checkpoints.CheckpointingStatisticsHeaders;
import
org.apache.flink.runtime.rest.messages.job.JobExceptionsMessageParameters;
+import
org.apache.flink.runtime.rest.messages.job.JobManagerJobConfigurationHeaders;
import org.apache.flink.runtime.rest.messages.job.metrics.JobMetricsHeaders;
import
org.apache.flink.runtime.rest.messages.job.savepoints.SavepointDisposalRequest;
import
org.apache.flink.runtime.rest.messages.job.savepoints.SavepointDisposalTriggerHeaders;
@@ -909,6 +915,111 @@ public abstract class AbstractFlinkService implements
FlinkService {
}
}
+ @Override
+ public Map<String, String> getRuntimeConfiguration(Configuration conf,
JobID jobId)
+ throws Exception {
+ LOG.debug("Fetching runtime configuration");
+ Map<String, String> runtimeConfig = new HashMap<>();
+ boolean fetchFailed = false;
+
+ try {
+ runtimeConfig.putAll(getJobManagerConfiguration(conf, jobId));
+ } catch (Exception e) {
+ fetchFailed = true;
+ LOG.error("Failed to fetch JobManager configuration", e);
+ }
+
+ try {
+ runtimeConfig.putAll(getJobConfiguration(conf, jobId));
+ } catch (Exception e) {
+ fetchFailed = true;
+ LOG.error("Failed to fetch job configuration", e);
+ }
+
+ try {
+ runtimeConfig.putAll(getJobCheckpointConfiguration(conf, jobId));
+ } catch (Exception e) {
+ fetchFailed = true;
+ LOG.error("Failed to fetch checkpoint configuration", e);
+ }
+
+ if (runtimeConfig.isEmpty() && fetchFailed) {
+ throw new RuntimeException("All runtime config REST fetches failed
for job " + jobId);
+ }
+
+ LOG.debug("Fetched merged runtime configuration with {} entries",
runtimeConfig.size());
+ return runtimeConfig;
+ }
+
+ private Map<String, String> getJobManagerConfiguration(Configuration conf,
JobID jobId)
+ throws Exception {
+ LOG.debug("Fetching JobManager configuration");
+ try (var clusterClient = getClusterClient(conf)) {
+ var parameters = new JobMessageParameters();
+ parameters.jobPathParameter.resolve(jobId);
+
+ var configurationInfo =
+ clusterClient
+ .sendRequest(
+
JobManagerJobConfigurationHeaders.getInstance(),
+ parameters,
+ EmptyRequestBody.getInstance())
+ .get(
+
operatorConfig.getFlinkClientTimeout().toSeconds(),
+ TimeUnit.SECONDS);
+
+ Map<String, String> jmConfig = new HashMap<>();
+ configurationInfo.forEach(entry -> jmConfig.put(entry.getKey(),
entry.getValue()));
+ LOG.debug("Fetched {} JobManager configuration entries",
jmConfig.size());
+ return jmConfig;
+ }
+ }
+
+ private Map<String, String> getJobConfiguration(Configuration conf, JobID
jobId)
+ throws Exception {
+ LOG.debug("Fetching job configuration");
+ try (var clusterClient = getClusterClient(conf)) {
+ var jobConfigHeaders = JobConfigHeaders.getInstance();
+ var parameters = new JobMessageParameters();
+ parameters.jobPathParameter.resolve(jobId);
+
+ JobConfigInfo configurationInfo =
+ clusterClient
+ .sendRequest(
+ jobConfigHeaders, parameters,
EmptyRequestBody.getInstance())
+ .get(
+
operatorConfig.getFlinkClientTimeout().toSeconds(),
+ TimeUnit.SECONDS);
+
+ Map<String, String> jobConfig =
+
FlinkRuntimeConfigurationUtils.mapJobConfiguration(configurationInfo);
+ LOG.debug("Fetched {} job configuration entries",
jobConfig.size());
+ return jobConfig;
+ }
+ }
+
+ private Map<String, String> getJobCheckpointConfiguration(Configuration
conf, JobID jobId)
+ throws Exception {
+ LOG.debug("Fetching checkpoint configuration");
+ try (var clusterClient = getClusterClient(conf)) {
+ var checkpointConfigHeaders =
CheckpointConfigHeaders.getInstance();
+ var parameters = new JobMessageParameters();
+ parameters.jobPathParameter.resolve(jobId);
+
+ var checkpointConfigInfo =
+ clusterClient
+ .sendRequest(
+ checkpointConfigHeaders,
+ parameters,
+ EmptyRequestBody.getInstance())
+ .get(
+
operatorConfig.getFlinkClientTimeout().toSeconds(),
+ TimeUnit.SECONDS);
+
+ return
FlinkRuntimeConfigurationUtils.mapCheckpointConfiguration(checkpointConfigInfo);
+ }
+ }
+
@VisibleForTesting
protected void runJar(
JobSpec job,
diff --git
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/service/FlinkService.java
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/service/FlinkService.java
index b1a078fb..64a04e2a 100644
---
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/service/FlinkService.java
+++
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/service/FlinkService.java
@@ -133,6 +133,12 @@ public interface FlinkService {
AbstractFlinkResource resource, JobID jobId, Configuration
observeConfig)
throws Exception;
+ /**
+ * Fetches the merged runtime configuration for a running job by querying
the JM config, job
+ * execution config, and checkpoint config REST endpoints. Later layers
override earlier ones.
+ */
+ Map<String, String> getRuntimeConfiguration(Configuration conf, JobID
jobId) throws Exception;
+
/** Result of a cancel operation. */
@AllArgsConstructor
class CancelResult {
diff --git
a/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/utils/FlinkRuntimeConfigurationUtils.java
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/utils/FlinkRuntimeConfigurationUtils.java
new file mode 100644
index 00000000..eb464224
--- /dev/null
+++
b/flink-kubernetes-operator/src/main/java/org/apache/flink/kubernetes/operator/utils/FlinkRuntimeConfigurationUtils.java
@@ -0,0 +1,242 @@
+/*
+ * 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.flink.kubernetes.operator.utils;
+
+import org.apache.flink.configuration.CheckpointingOptions;
+import org.apache.flink.configuration.CoreOptions;
+import org.apache.flink.configuration.PipelineOptions;
+import org.apache.flink.configuration.StateBackendOptions;
+import org.apache.flink.configuration.StateChangelogOptions;
+import org.apache.flink.runtime.rest.messages.JobConfigInfo;
+import org.apache.flink.runtime.rest.messages.checkpoints.CheckpointConfigInfo;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Utility class for mapping Flink REST API responses (execution config,
checkpoint config) into
+ * Flink {@link org.apache.flink.configuration.ConfigOption} key-value pairs
suitable for the
+ * runtime configuration cache.
+ */
+public class FlinkRuntimeConfigurationUtils {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(FlinkRuntimeConfigurationUtils.class);
+
+ private static final
org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind
+ .ObjectMapper
+ CHECKPOINT_CONFIG_MAPPER =
+ new
org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind
+ .ObjectMapper();
+
+ /**
+ * Maps REST API JSON field names from {@link CheckpointConfigInfo} to
Flink {@link
+ * org.apache.flink.configuration.ConfigOption} keys. Both sides use
Flink's own constants for
+ * compile-time safety.
+ */
+ public enum CheckpointConfigMapping {
+ PROCESSING_MODE(
+ CheckpointConfigInfo.FIELD_NAME_PROCESSING_MODE,
+ CheckpointingOptions.CHECKPOINTING_CONSISTENCY_MODE.key(),
+ false),
+ INTERVAL(
+ CheckpointConfigInfo.FIELD_NAME_CHECKPOINT_INTERVAL,
+ CheckpointingOptions.CHECKPOINTING_INTERVAL.key(),
+ true),
+ TIMEOUT(
+ CheckpointConfigInfo.FIELD_NAME_CHECKPOINT_TIMEOUT,
+ CheckpointingOptions.CHECKPOINTING_TIMEOUT.key(),
+ true),
+ MIN_PAUSE(
+ CheckpointConfigInfo.FIELD_NAME_CHECKPOINT_MIN_PAUSE,
+ CheckpointingOptions.MIN_PAUSE_BETWEEN_CHECKPOINTS.key(),
+ true),
+ MAX_CONCURRENT(
+ CheckpointConfigInfo.FIELD_NAME_CHECKPOINT_MAX_CONCURRENT,
+ CheckpointingOptions.MAX_CONCURRENT_CHECKPOINTS.key(),
+ false),
+ TOLERABLE_FAILURES(
+ CheckpointConfigInfo.FIELD_NAME_TOLERABLE_FAILED_CHECKPOINTS,
+ CheckpointingOptions.TOLERABLE_FAILURE_NUMBER.key(),
+ false),
+ UNALIGNED(
+ CheckpointConfigInfo.FIELD_NAME_UNALIGNED_CHECKPOINTS,
+ CheckpointingOptions.ENABLE_UNALIGNED.key(),
+ false),
+ ALIGNED_TIMEOUT(
+ CheckpointConfigInfo.FIELD_NAME_ALIGNED_CHECKPOINT_TIMEOUT,
+ CheckpointingOptions.ALIGNED_CHECKPOINT_TIMEOUT.key(),
+ true),
+ CHECKPOINTS_AFTER_TASKS_FINISH(
+ CheckpointConfigInfo.FIELD_NAME_CHECKPOINTS_AFTER_TASKS_FINISH,
+
CheckpointingOptions.ENABLE_CHECKPOINTS_AFTER_TASKS_FINISH.key(),
+ false),
+ STATE_CHANGELOG(
+ CheckpointConfigInfo.FIELD_NAME_STATE_CHANGELOG,
+ StateChangelogOptions.ENABLE_STATE_CHANGE_LOG.key(),
+ false),
+ PERIODIC_MATERIALIZATION_INTERVAL(
+
CheckpointConfigInfo.FIELD_NAME_PERIODIC_MATERIALIZATION_INTERVAL,
+ StateChangelogOptions.PERIODIC_MATERIALIZATION_INTERVAL.key(),
+ true),
+ CHANGELOG_STORAGE(
+ CheckpointConfigInfo.FIELD_NAME_CHANGELOG_STORAGE,
+ StateChangelogOptions.STATE_CHANGE_LOG_STORAGE.key(),
+ false);
+
+ private final String jsonField;
+ private final String configKey;
+ private final boolean isDuration;
+
+ CheckpointConfigMapping(String jsonField, String configKey, boolean
isDuration) {
+ this.jsonField = jsonField;
+ this.configKey = configKey;
+ this.isDuration = isDuration;
+ }
+
+ public String getJsonField() {
+ return jsonField;
+ }
+ }
+
+ private static final Map<String, String> STATE_BACKEND_NAMES =
+ Map.of(
+ "EmbeddedRocksDBStateBackend", "rocksdb",
+ "HashMapStateBackend", "hashmap");
+
+ private static final Map<String, String> CHECKPOINT_STORAGE_NAMES =
+ Map.of(
+ "FileSystemCheckpointStorage", "filesystem",
+ "JobManagerCheckpointStorage", "jobmanager");
+
+ private FlinkRuntimeConfigurationUtils() {}
+
+ /**
+ * Extract execution configuration (parallelism, object-reuse, global job
parameters) from a
+ * {@link JobConfigInfo} REST response.
+ */
+ public static Map<String, String> mapJobConfiguration(JobConfigInfo
configurationInfo) {
+ Map<String, String> jobConfig = new HashMap<>();
+ if (configurationInfo == null ||
configurationInfo.getExecutionConfigInfo() == null) {
+ return jobConfig;
+ }
+ var execInfo = configurationInfo.getExecutionConfigInfo();
+ jobConfig.put(
+ CoreOptions.DEFAULT_PARALLELISM.key(),
String.valueOf(execInfo.getParallelism()));
+ jobConfig.put(PipelineOptions.OBJECT_REUSE.key(),
String.valueOf(execInfo.isObjectReuse()));
+ jobConfig.putAll(execInfo.getGlobalJobParameters());
+ return jobConfig;
+ }
+
+ /**
+ * Convert a {@link CheckpointConfigInfo} REST response into Flink
configuration key-value
+ * pairs.
+ */
+ public static Map<String, String> mapCheckpointConfiguration(
+ CheckpointConfigInfo checkpointConfigInfo) {
+ Map<String, Object> rawResponse =
+ CHECKPOINT_CONFIG_MAPPER.convertValue(
+ checkpointConfigInfo,
+ new
org.apache.flink.shaded.jackson2.com.fasterxml.jackson.core.type
+ .TypeReference<
+ Map<String, Object>>() {});
+
+ LOG.debug("Raw checkpoint configuration response: {}", rawResponse);
+ return mapCheckpointFields(rawResponse);
+ }
+
+ private static Map<String, String> mapCheckpointFields(Map<String, Object>
rawResponse) {
+ Map<String, String> mappedConfig = new HashMap<>();
+
+ for (CheckpointConfigMapping mapping :
CheckpointConfigMapping.values()) {
+ if (!rawResponse.containsKey(mapping.jsonField)) {
+ continue;
+ }
+ String value = String.valueOf(rawResponse.get(mapping.jsonField));
+
+ if (mapping == CheckpointConfigMapping.PROCESSING_MODE) {
+ value = value.toUpperCase();
+ }
+ if (mapping.isDuration) {
+ value += "ms";
+ }
+
+ mappedConfig.put(mapping.configKey, value);
+ }
+
+ mapExternalizedCheckpointInfo(rawResponse, mappedConfig);
+ mapStateBackendAndStorage(rawResponse, mappedConfig);
+
+ LOG.debug(
+ "Mapped {} checkpoint configuration entries: {}",
+ mappedConfig.size(),
+ mappedConfig);
+ return mappedConfig;
+ }
+
+ private static void mapExternalizedCheckpointInfo(
+ Map<String, Object> rawResponse, Map<String, String> mappedConfig)
{
+ Object externalizationObj =
+
rawResponse.get(CheckpointConfigInfo.FIELD_NAME_EXTERNALIZED_CHECKPOINT_CONFIG);
+ if (externalizationObj instanceof Map) {
+ @SuppressWarnings("unchecked")
+ Map<String, Object> externalization = (Map<String, Object>)
externalizationObj;
+ Boolean enabled =
+ (Boolean)
+ externalization.get(
+
CheckpointConfigInfo.ExternalizedCheckpointInfo
+ .FIELD_NAME_ENABLED);
+ Boolean deleteOnCancellation =
+ (Boolean)
+ externalization.get(
+
CheckpointConfigInfo.ExternalizedCheckpointInfo
+
.FIELD_NAME_DELETE_ON_CANCELLATION);
+
+ String retention = "NO_EXTERNALIZED_CHECKPOINTS";
+ if (Boolean.TRUE.equals(enabled)) {
+ retention =
+ Boolean.TRUE.equals(deleteOnCancellation)
+ ? "DELETE_ON_CANCELLATION"
+ : "RETAIN_ON_CANCELLATION";
+ }
+ mappedConfig.put(
+
CheckpointingOptions.EXTERNALIZED_CHECKPOINT_RETENTION.key(), retention);
+ }
+ }
+
+ private static void mapStateBackendAndStorage(
+ Map<String, Object> rawResponse, Map<String, String> mappedConfig)
{
+ if
(rawResponse.containsKey(CheckpointConfigInfo.FIELD_NAME_STATE_BACKEND)) {
+ String raw =
+
String.valueOf(rawResponse.get(CheckpointConfigInfo.FIELD_NAME_STATE_BACKEND));
+ mappedConfig.put(
+ StateBackendOptions.STATE_BACKEND.key(),
+ STATE_BACKEND_NAMES.getOrDefault(raw, raw.toLowerCase()));
+ }
+ if
(rawResponse.containsKey(CheckpointConfigInfo.FIELD_NAME_CHECKPOINT_STORAGE)) {
+ String raw =
+ String.valueOf(
+
rawResponse.get(CheckpointConfigInfo.FIELD_NAME_CHECKPOINT_STORAGE));
+ mappedConfig.put(
+ CheckpointingOptions.CHECKPOINT_STORAGE.key(),
+ CHECKPOINT_STORAGE_NAMES.getOrDefault(raw,
raw.toLowerCase()));
+ }
+ }
+}
diff --git
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/TestingFlinkService.java
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/TestingFlinkService.java
index b17fb624..fac68a61 100644
---
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/TestingFlinkService.java
+++
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/TestingFlinkService.java
@@ -789,6 +789,36 @@ public class TestingFlinkService extends
AbstractFlinkService {
jobExceptionsMap.put(jobId, newExceptionHistory);
}
+ private final Map<JobID, Map<String, String>> runtimeJmConfigs = new
HashMap<>();
+ private final Map<JobID, Map<String, String>> runtimeJobConfigs = new
HashMap<>();
+ private final Map<JobID, Map<String, String>> runtimeCheckpointConfigs =
new HashMap<>();
+ @Setter private Exception runtimeConfigFetchException;
+
+ public void setRuntimeJmConfig(JobID jobId, Map<String, String> config) {
+ runtimeJmConfigs.put(jobId, new HashMap<>(config));
+ }
+
+ public void setRuntimeJobConfig(JobID jobId, Map<String, String> config) {
+ runtimeJobConfigs.put(jobId, new HashMap<>(config));
+ }
+
+ public void setRuntimeCheckpointConfig(JobID jobId, Map<String, String>
config) {
+ runtimeCheckpointConfigs.put(jobId, new HashMap<>(config));
+ }
+
+ @Override
+ public Map<String, String> getRuntimeConfiguration(Configuration conf,
JobID jobId)
+ throws Exception {
+ if (runtimeConfigFetchException != null) {
+ throw runtimeConfigFetchException;
+ }
+ Map<String, String> merged = new HashMap<>();
+ merged.putAll(runtimeJmConfigs.getOrDefault(jobId, new HashMap<>()));
+ merged.putAll(runtimeJobConfigs.getOrDefault(jobId, new HashMap<>()));
+ merged.putAll(runtimeCheckpointConfigs.getOrDefault(jobId, new
HashMap<>()));
+ return merged;
+ }
+
public void setSavepointTriggerException(Exception exception) {
this.savepointTriggerException = exception;
}
diff --git
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/config/FlinkConfigManagerTest.java
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/config/FlinkConfigManagerTest.java
index 715c072b..d528d869 100644
---
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/config/FlinkConfigManagerTest.java
+++
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/config/FlinkConfigManagerTest.java
@@ -467,4 +467,41 @@ public class FlinkConfigManagerTest {
assertTrue(completed2.get());
assertTrue(completed3.get());
}
+
+ @Test
+ public void testRuntimeConfigCachePutAndGet() {
+ var configManager = new FlinkConfigManager(new Configuration());
+
+ assertFalse(configManager.getRuntimeConfig("ns", "app",
"job1").isPresent());
+
+ Map<String, String> runtimeConf = Map.of("parallelism.default", "4",
"key1", "val1");
+ configManager.putRuntimeConfig("ns", "app", "job1", runtimeConf);
+
+ var cached = configManager.getRuntimeConfig("ns", "app", "job1");
+ assertTrue(cached.isPresent());
+ assertEquals("4", cached.get().get("parallelism.default"));
+ assertEquals("val1", cached.get().get("key1"));
+
+ assertFalse(configManager.getRuntimeConfig("ns", "app",
"job2").isPresent());
+ assertFalse(configManager.getRuntimeConfig("ns", "other",
"job1").isPresent());
+ }
+
+ @Test
+ public void testRuntimeConfigInvalidate() {
+ var configManager = new FlinkConfigManager(new Configuration());
+
+ configManager.putRuntimeConfig("ns", "app", "job1", Map.of("k1",
"v1"));
+ configManager.putRuntimeConfig("ns", "app", "job2", Map.of("k2",
"v2"));
+ configManager.putRuntimeConfig("ns", "other", "job3", Map.of("k3",
"v3"));
+
+ assertTrue(configManager.getRuntimeConfig("ns", "app",
"job1").isPresent());
+ assertTrue(configManager.getRuntimeConfig("ns", "app",
"job2").isPresent());
+ assertTrue(configManager.getRuntimeConfig("ns", "other",
"job3").isPresent());
+
+ configManager.invalidateRuntimeConfig("ns", "app");
+
+ assertFalse(configManager.getRuntimeConfig("ns", "app",
"job1").isPresent());
+ assertFalse(configManager.getRuntimeConfig("ns", "app",
"job2").isPresent());
+ assertTrue(configManager.getRuntimeConfig("ns", "other",
"job3").isPresent());
+ }
}
diff --git
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/observer/JobStatusObserverTest.java
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/observer/JobStatusObserverTest.java
index 67a0a929..e0cca019 100644
---
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/observer/JobStatusObserverTest.java
+++
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/observer/JobStatusObserverTest.java
@@ -956,6 +956,354 @@ public class JobStatusObserverTest extends
OperatorTestBase {
return job;
}
+ @Test
+ public void testRuntimeConfigFetchAndCache() throws Exception {
+ var deployment = initDeployment();
+ var status = deployment.getStatus();
+ var jobStatus = status.getJobStatus();
+ jobStatus.setState(JobStatus.RUNNING);
+
+ FlinkResourceContext<AbstractFlinkResource<?, ?>> ctx =
getResourceContext(deployment);
+
+ // Configure the mock service to return runtime config
+ var jobId = JobID.fromHexString(jobStatus.getJobId());
+ flinkService.submitApplicationCluster(
+ deployment.getSpec().getJob(),
ctx.getDeployConfig(deployment.getSpec()), false);
+ flinkService.setRuntimeJobConfig(
+ jobId,
+ Map.of("parallelism.default", "8",
"execution.checkpointing.interval", "60000"));
+ flinkService.setRuntimeCheckpointConfig(
+ jobId, Map.of("execution.checkpointing.mode", "EXACTLY_ONCE"));
+
+ // Verify runtime config is not cached before observation
+ assertFalse(
+ ctx.getConfigManager()
+ .getRuntimeConfig(
+ deployment.getMetadata().getNamespace(),
+ deployment.getMetadata().getName(),
+ jobStatus.getJobId())
+ .isPresent());
+
+ // Observe the job - this should fetch and cache runtime config
+ observer.observe(ctx);
+
+ // Verify runtime config is now cached
+ var cachedConfig =
+ ctx.getConfigManager()
+ .getRuntimeConfig(
+ deployment.getMetadata().getNamespace(),
+ deployment.getMetadata().getName(),
+ jobStatus.getJobId());
+ assertTrue(cachedConfig.isPresent());
+ assertEquals("8", cachedConfig.get().get("parallelism.default"));
+ assertEquals("60000",
cachedConfig.get().get("execution.checkpointing.interval"));
+ assertEquals("EXACTLY_ONCE",
cachedConfig.get().get("execution.checkpointing.mode"));
+ }
+
+ @Test
+ public void testRuntimeConfigNotFetchedWhenJobNotOnCluster() throws
Exception {
+ var deployment = initDeployment();
+ var status = deployment.getStatus();
+ var jobStatus = status.getJobStatus();
+ jobStatus.setState(JobStatus.CREATED);
+
+ FlinkResourceContext<AbstractFlinkResource<?, ?>> ctx =
getResourceContext(deployment);
+
+ // Don't submit the job to the cluster -- observe() won't find a
matching job,
+ // so fetchAndCacheRuntimeConfig should never be called.
+ observer.observe(ctx);
+
+ assertFalse(
+ ctx.getConfigManager()
+ .getRuntimeConfig(
+ deployment.getMetadata().getNamespace(),
+ deployment.getMetadata().getName(),
+ jobStatus.getJobId())
+ .isPresent());
+ }
+
+ @ParameterizedTest
+ @EnumSource(
+ value = JobStatus.class,
+ names = {"FINISHED", "CANCELED", "FAILED"})
+ public void testRuntimeConfigNotFetchedWhenGloballyTerminal(JobStatus
terminalState)
+ throws Exception {
+ var deployment = initDeployment();
+ var status = deployment.getStatus();
+ var jobStatus = status.getJobStatus();
+ jobStatus.setState(terminalState);
+
+ FlinkResourceContext<AbstractFlinkResource<?, ?>> ctx =
getResourceContext(deployment);
+
+ var jobId = JobID.fromHexString(jobStatus.getJobId());
+ flinkService.submitApplicationCluster(
+ deployment.getSpec().getJob(),
ctx.getDeployConfig(deployment.getSpec()), false);
+ var job =
+ flinkService.listJobs().stream()
+ .filter(t -> t.f1.getJobId().equals(jobId))
+ .findAny()
+ .orElseThrow();
+ job.f1 = new JobStatusMessage(jobId, job.f1.getJobName(),
terminalState, 0);
+ flinkService.setRuntimeJobConfig(jobId, Map.of("parallelism.default",
"8"));
+
+ observer.observe(ctx);
+
+ assertFalse(
+ ctx.getConfigManager()
+ .getRuntimeConfig(
+ deployment.getMetadata().getNamespace(),
+ deployment.getMetadata().getName(),
+ jobStatus.getJobId())
+ .isPresent());
+ }
+
+ @Test
+ public void testRuntimeConfigMemoizedWithinReconciliationCycle() throws
Exception {
+ var deployment = initDeployment();
+ var jobStatus = deployment.getStatus().getJobStatus();
+ jobStatus.setState(JobStatus.RUNNING);
+
+ FlinkResourceContext<AbstractFlinkResource<?, ?>> ctx =
getResourceContext(deployment);
+
+ var jobId = JobID.fromHexString(jobStatus.getJobId());
+ flinkService.submitApplicationCluster(
+ deployment.getSpec().getJob(),
ctx.getDeployConfig(deployment.getSpec()), false);
+ flinkService.setRuntimeJobConfig(jobId, Map.of("parallelism.default",
"4"));
+
+ // Populate the memoized context-level view.
+ observer.observe(ctx);
+
+ var firstView = ctx.getRuntimeConfig();
+ assertTrue(firstView.isPresent());
+
+ // Mutate the global cache underneath the context. If the context is
honouring its memo,
+ // subsequent getRuntimeConfig() calls must NOT observe this change
within the same cycle.
+ ctx.getConfigManager()
+ .putRuntimeConfig(
+ deployment.getMetadata().getNamespace(),
+ deployment.getMetadata().getName(),
+ jobStatus.getJobId(),
+ Map.of("parallelism.default", "999"));
+
+ assertEquals("4",
ctx.getRuntimeConfig().get().get("parallelism.default"));
+ }
+
+ @Test
+ public void testRuntimeConfigNotFetchedWhenAlreadyCached() throws
Exception {
+ var deployment = initDeployment();
+ var status = deployment.getStatus();
+ var jobStatus = status.getJobStatus();
+ jobStatus.setState(JobStatus.RUNNING);
+
+ FlinkResourceContext<AbstractFlinkResource<?, ?>> ctx =
getResourceContext(deployment);
+
+ var jobId = JobID.fromHexString(jobStatus.getJobId());
+ flinkService.submitApplicationCluster(
+ deployment.getSpec().getJob(),
ctx.getDeployConfig(deployment.getSpec()), false);
+
+ // Pre-populate the cache
+ ctx.getConfigManager()
+ .putRuntimeConfig(
+ deployment.getMetadata().getNamespace(),
+ deployment.getMetadata().getName(),
+ jobStatus.getJobId(),
+ Map.of("pre-cached", "value"));
+
+ // Configure mock to return different values
+ flinkService.setRuntimeJobConfig(jobId, Map.of("parallelism.default",
"999"));
+
+ // Observe the job
+ observer.observe(ctx);
+
+ // Verify the cached config is unchanged (not refetched)
+ var cachedConfig =
+ ctx.getConfigManager()
+ .getRuntimeConfig(
+ deployment.getMetadata().getNamespace(),
+ deployment.getMetadata().getName(),
+ jobStatus.getJobId());
+ assertTrue(cachedConfig.isPresent());
+ assertEquals("value", cachedConfig.get().get("pre-cached"));
+ assertFalse(cachedConfig.get().containsKey("parallelism.default"));
+ }
+
+ @Test
+ public void testObserveConfigIncludesRuntimeOverrides() throws Exception {
+ var deployment = initDeployment();
+ var status = deployment.getStatus();
+ var jobStatus = status.getJobStatus();
+ jobStatus.setState(JobStatus.RUNNING);
+
+ // Set user configuration
+
deployment.getSpec().getFlinkConfiguration().put("parallelism.default", "2");
+
+ FlinkResourceContext<AbstractFlinkResource<?, ?>> ctx =
getResourceContext(deployment);
+
+ var jobId = JobID.fromHexString(jobStatus.getJobId());
+ flinkService.submitApplicationCluster(
+ deployment.getSpec().getJob(),
ctx.getDeployConfig(deployment.getSpec()), false);
+
+ // Configure runtime config that overrides user config
+ flinkService.setRuntimeJobConfig(jobId, Map.of("parallelism.default",
"10"));
+
+ // Observe the job - this caches runtime config
+ observer.observe(ctx);
+
+ // Get observe config - should include runtime overrides
+ var observeConfig = ctx.getObserveConfig();
+ assertEquals("10", observeConfig.getString("parallelism.default",
null));
+ }
+
+ @Test
+ public void testSessionJobRuntimeConfigFetchAndCache() throws Exception {
+ var sessionJob = initSessionJob();
+ var status = sessionJob.getStatus();
+ var jobStatus = status.getJobStatus();
+ jobStatus.setState(JobStatus.RUNNING);
+
+ FlinkResourceContext<AbstractFlinkResource<?, ?>> ctx =
+ getResourceContext(
+ sessionJob,
+
TestUtils.createContextWithReadyFlinkDeployment(kubernetesClient));
+
+ var jobId = JobID.fromHexString(jobStatus.getJobId());
+ flinkService.submitJobToSessionCluster(
+ sessionJob.getMetadata(),
+ sessionJob.getSpec(),
+ jobId,
+ ctx.getDeployConfig(sessionJob.getSpec()),
+ null);
+
+ // Configure the mock service to return runtime config
+ flinkService.setRuntimeJobConfig(
+ jobId, Map.of("parallelism.default", "4", "restart-strategy",
"fixed-delay"));
+ flinkService.setRuntimeCheckpointConfig(
+ jobId, Map.of("execution.checkpointing.interval", "30000"));
+
+ // Observe the job - this should fetch and cache runtime config
+ observer.observe(ctx);
+
+ // Verify runtime config is cached
+ var cachedConfig =
+ ctx.getConfigManager()
+ .getRuntimeConfig(
+ sessionJob.getMetadata().getNamespace(),
+ sessionJob.getMetadata().getName(),
+ jobStatus.getJobId());
+ assertTrue(cachedConfig.isPresent());
+ assertEquals("4", cachedConfig.get().get("parallelism.default"));
+ assertEquals("fixed-delay",
cachedConfig.get().get("restart-strategy"));
+ assertEquals("30000",
cachedConfig.get().get("execution.checkpointing.interval"));
+ }
+
+ @Test
+ public void testRuntimeConfigFetchFailureDoesNotBreakObservation() throws
Exception {
+ var deployment = initDeployment();
+ var status = deployment.getStatus();
+ var jobStatus = status.getJobStatus();
+ jobStatus.setState(JobStatus.RUNNING);
+
+ FlinkResourceContext<AbstractFlinkResource<?, ?>> ctx =
getResourceContext(deployment);
+
+ var jobId = JobID.fromHexString(jobStatus.getJobId());
+ flinkService.submitApplicationCluster(
+ deployment.getSpec().getJob(),
ctx.getDeployConfig(deployment.getSpec()), false);
+
+ // Configure mock to throw exception when fetching runtime config
+ flinkService.setRuntimeConfigFetchException(new Exception("REST API
unavailable"));
+
+ // Observe should not fail even if runtime config fetch fails
+ boolean result = observer.observe(ctx);
+ assertTrue(result);
+
+ // When both fetches fail, nothing should be cached so we retry next
cycle
+ var cachedConfig =
+ ctx.getConfigManager()
+ .getRuntimeConfig(
+ deployment.getMetadata().getNamespace(),
+ deployment.getMetadata().getName(),
+ jobStatus.getJobId());
+ assertFalse(cachedConfig.isPresent());
+
+ // Clear the exception and observe again -- should now fetch and cache
successfully
+ flinkService.setRuntimeConfigFetchException(null);
+ flinkService.setRuntimeJobConfig(jobId, Map.of("parallelism.default",
"4"));
+ flinkService.setRuntimeCheckpointConfig(
+ jobId, Map.of("execution.checkpointing.interval", "5000"));
+
+ observer.observe(ctx);
+
+ var retriedConfig =
+ ctx.getConfigManager()
+ .getRuntimeConfig(
+ deployment.getMetadata().getNamespace(),
+ deployment.getMetadata().getName(),
+ jobStatus.getJobId());
+ assertTrue(retriedConfig.isPresent());
+ assertEquals("4", retriedConfig.get().get("parallelism.default"));
+ assertEquals("5000",
retriedConfig.get().get("execution.checkpointing.interval"));
+ }
+
+ @Test
+ public void testPartialFetchFailureCachesSuccessfulPortion() throws
Exception {
+ var deployment = initDeployment();
+ var status = deployment.getStatus();
+ var jobStatus = status.getJobStatus();
+ jobStatus.setState(JobStatus.RUNNING);
+
+ FlinkResourceContext<AbstractFlinkResource<?, ?>> ctx =
getResourceContext(deployment);
+
+ var jobId = JobID.fromHexString(jobStatus.getJobId());
+ flinkService.submitApplicationCluster(
+ deployment.getSpec().getJob(),
ctx.getDeployConfig(deployment.getSpec()), false);
+
+ // Only set job config (no checkpoint config) -- checkpoint fetch will
return empty map
+ // but won't throw, so this tests the case where one succeeds with data
+ flinkService.setRuntimeJobConfig(jobId, Map.of("parallelism.default",
"6"));
+
+ observer.observe(ctx);
+
+ // Should be cached since at least one fetch returned data
+ var cachedConfig =
+ ctx.getConfigManager()
+ .getRuntimeConfig(
+ deployment.getMetadata().getNamespace(),
+ deployment.getMetadata().getName(),
+ jobStatus.getJobId());
+ assertTrue(cachedConfig.isPresent());
+ assertEquals("6", cachedConfig.get().get("parallelism.default"));
+ }
+
+ @Test
+ public void testCachedRuntimeConfigIsImmutable() throws Exception {
+ var deployment = initDeployment();
+ var status = deployment.getStatus();
+ var jobStatus = status.getJobStatus();
+ jobStatus.setState(JobStatus.RUNNING);
+
+ FlinkResourceContext<AbstractFlinkResource<?, ?>> ctx =
getResourceContext(deployment);
+
+ var jobId = JobID.fromHexString(jobStatus.getJobId());
+ flinkService.submitApplicationCluster(
+ deployment.getSpec().getJob(),
ctx.getDeployConfig(deployment.getSpec()), false);
+ flinkService.setRuntimeJobConfig(jobId, Map.of("parallelism.default",
"4"));
+
+ observer.observe(ctx);
+
+ var cachedConfig =
+ ctx.getConfigManager()
+ .getRuntimeConfig(
+ deployment.getMetadata().getNamespace(),
+ deployment.getMetadata().getName(),
+ jobStatus.getJobId());
+ assertTrue(cachedConfig.isPresent());
+
+ // Attempting to mutate the cached map should throw
+ var configMap = cachedConfig.get();
+ org.junit.jupiter.api.Assertions.assertThrows(
+ UnsupportedOperationException.class, () ->
configMap.put("new.key", "value"));
+ }
+
private void simulateAsyncSuspendInProgress(AbstractFlinkResource<?, ?>
resource) {
// A stateless or last-state session job suspend goes through an
asynchronous cancellation,
// the reconciler only initiates it, records UPGRADING and waits for
the observer
diff --git
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/service/AbstractFlinkServiceTest.java
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/service/AbstractFlinkServiceTest.java
index 064c0926..a6f3ed69 100644
---
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/service/AbstractFlinkServiceTest.java
+++
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/service/AbstractFlinkServiceTest.java
@@ -52,6 +52,7 @@ import
org.apache.flink.kubernetes.operator.exception.UpgradeFailureException;
import org.apache.flink.kubernetes.operator.observer.CheckpointFetchResult;
import org.apache.flink.kubernetes.operator.observer.SavepointFetchResult;
import org.apache.flink.kubernetes.operator.reconciler.ReconciliationUtils;
+import
org.apache.flink.kubernetes.operator.utils.FlinkRuntimeConfigurationUtils;
import org.apache.flink.runtime.checkpoint.CheckpointStatsStatus;
import org.apache.flink.runtime.clusterframework.types.ResourceID;
import org.apache.flink.runtime.clusterframework.types.ResourceProfile;
@@ -60,19 +61,24 @@ import org.apache.flink.runtime.messages.Acknowledge;
import org.apache.flink.runtime.rest.RestClient;
import org.apache.flink.runtime.rest.handler.async.AsynchronousOperationResult;
import org.apache.flink.runtime.rest.handler.async.TriggerResponse;
+import org.apache.flink.runtime.rest.messages.ConfigurationInfo;
+import org.apache.flink.runtime.rest.messages.ConfigurationInfoEntry;
import org.apache.flink.runtime.rest.messages.DashboardConfiguration;
+import org.apache.flink.runtime.rest.messages.JobConfigInfo;
import org.apache.flink.runtime.rest.messages.JobExceptionsInfoWithHistory;
import org.apache.flink.runtime.rest.messages.MessageHeaders;
import org.apache.flink.runtime.rest.messages.MessageParameters;
import org.apache.flink.runtime.rest.messages.RequestBody;
import org.apache.flink.runtime.rest.messages.ResponseBody;
import org.apache.flink.runtime.rest.messages.TriggerId;
+import org.apache.flink.runtime.rest.messages.checkpoints.CheckpointConfigInfo;
import org.apache.flink.runtime.rest.messages.checkpoints.CheckpointInfo;
import org.apache.flink.runtime.rest.messages.checkpoints.CheckpointStatistics;
import
org.apache.flink.runtime.rest.messages.checkpoints.CheckpointStatusMessageParameters;
import
org.apache.flink.runtime.rest.messages.checkpoints.CheckpointTriggerMessageParameters;
import
org.apache.flink.runtime.rest.messages.checkpoints.CheckpointingStatistics;
import
org.apache.flink.runtime.rest.messages.checkpoints.CheckpointingStatisticsHeaders;
+import
org.apache.flink.runtime.rest.messages.job.JobManagerJobConfigurationHeaders;
import
org.apache.flink.runtime.rest.messages.job.metrics.JobMetricsMessageParameters;
import org.apache.flink.runtime.rest.messages.job.metrics.Metric;
import
org.apache.flink.runtime.rest.messages.job.metrics.MetricCollectionResponseBody;
@@ -135,8 +141,10 @@ import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
@@ -1537,6 +1545,130 @@ public class AbstractFlinkServiceTest {
.readValue(jsonParser, JobExceptionsInfoWithHistory.class);
}
+ @Test
+ void testAllCheckpointConfigInfoFieldNamesCoveredByMapping() throws
Exception {
+ Set<String> declared =
discoverFieldNameConstants(CheckpointConfigInfo.class);
+ assertFalse(declared.isEmpty(), "Should discover FIELD_NAME_*
constants");
+
+ Set<String> handled = new HashSet<>();
+ for (FlinkRuntimeConfigurationUtils.CheckpointConfigMapping m :
+
FlinkRuntimeConfigurationUtils.CheckpointConfigMapping.values()) {
+ handled.add(m.getJsonField());
+ }
+
handled.add(CheckpointConfigInfo.FIELD_NAME_EXTERNALIZED_CHECKPOINT_CONFIG);
+ handled.add(CheckpointConfigInfo.FIELD_NAME_STATE_BACKEND);
+ handled.add(CheckpointConfigInfo.FIELD_NAME_CHECKPOINT_STORAGE);
+
+ declared.removeAll(handled);
+ assertTrue(
+ declared.isEmpty(),
+ "CheckpointConfigInfo FIELD_NAME constants not covered by "
+ + "CheckpointConfigMapping or explicit handlers: "
+ + declared);
+ }
+
+ @Test
+ void testAllExecutionConfigInfoFieldNamesCoveredByMapping() throws
Exception {
+ Set<String> declared =
discoverFieldNameConstants(JobConfigInfo.ExecutionConfigInfo.class);
+ assertFalse(declared.isEmpty(), "Should discover FIELD_NAME_*
constants");
+
+ Set<String> handled =
+ new HashSet<>(
+ Set.of(
+
JobConfigInfo.ExecutionConfigInfo.FIELD_NAME_PARALLELISM,
+
JobConfigInfo.ExecutionConfigInfo.FIELD_NAME_OBJECT_REUSE_MODE,
+
JobConfigInfo.ExecutionConfigInfo.FIELD_NAME_GLOBAL_JOB_PARAMETERS,
+ // Informational only, not runtime config
overrides
+
JobConfigInfo.ExecutionConfigInfo.FIELD_NAME_EXECUTION_MODE,
+
JobConfigInfo.ExecutionConfigInfo.FIELD_NAME_RESTART_STRATEGY));
+
+ declared.removeAll(handled);
+ assertTrue(
+ declared.isEmpty(),
+ "ExecutionConfigInfo FIELD_NAME constants not covered: "
+ + declared
+ + ". Add mapping or exclude explicitly.");
+ }
+
+ @Test
+ void testMapJobConfigurationMapsAllExpectedFields() {
+ JobConfigInfo configInfo =
+ new JobConfigInfo(
+ new JobID(),
+ "test-job",
+ new JobConfigInfo.ExecutionConfigInfo(
+ "PIPELINED",
+ "fixedDelay",
+ 4,
+ true,
+ Map.of("user.param", "value1")));
+
+ Map<String, String> result =
FlinkRuntimeConfigurationUtils.mapJobConfiguration(configInfo);
+
+ assertEquals("4", result.get("parallelism.default"));
+ assertEquals("true", result.get("pipeline.object-reuse"));
+ assertEquals("value1", result.get("user.param"));
+ assertEquals(3, result.size());
+ }
+
+ @Test
+ void testMapJobConfigurationHandlesNullGracefully() {
+
assertTrue(FlinkRuntimeConfigurationUtils.mapJobConfiguration(null).isEmpty());
+ assertTrue(
+ FlinkRuntimeConfigurationUtils.mapJobConfiguration(
+ new JobConfigInfo(new JobID(), "test-job",
null))
+ .isEmpty());
+ }
+
+ @Test
+ void testGetRuntimeConfigurationIncludesJmConfig() throws Exception {
+ var jmConfig = new ConfigurationInfo();
+ jmConfig.add(new ConfigurationInfoEntry("jobmanager.scheduler",
"Adaptive"));
+ jmConfig.add(new ConfigurationInfoEntry("state.savepoints.dir",
"/s3/savepoints"));
+ jmConfig.add(new ConfigurationInfoEntry("high-availability",
"ZOOKEEPER"));
+
+ var service =
+ getTestingService(
+ (headers, params, body) -> {
+ if (headers instanceof
JobManagerJobConfigurationHeaders) {
+ return
CompletableFuture.completedFuture((ResponseBody) jmConfig);
+ }
+ return CompletableFuture.failedFuture(
+ new UnsupportedOperationException());
+ });
+
+ var result = service.getRuntimeConfiguration(configuration,
JobID.generate());
+ assertEquals("Adaptive", result.get("jobmanager.scheduler"));
+ assertEquals("/s3/savepoints", result.get("state.savepoints.dir"));
+ assertEquals("ZOOKEEPER", result.get("high-availability"));
+ }
+
+ @Test
+ void testGetRuntimeConfigurationThrowsWhenAllFetchesFail() throws
Exception {
+ var service =
+ getTestingService(
+ (headers, params, body) ->
+ CompletableFuture.failedFuture(
+ new RuntimeException("connection
refused")));
+
+ assertThrows(
+ RuntimeException.class,
+ () -> service.getRuntimeConfiguration(configuration,
JobID.generate()));
+ }
+
+ /** Discovers all {@code FIELD_NAME_*} string constants on a class via
reflection. */
+ private static Set<String> discoverFieldNameConstants(Class<?> clazz)
throws Exception {
+ Set<String> names = new HashSet<>();
+ for (java.lang.reflect.Field f : clazz.getDeclaredFields()) {
+ if (f.getName().startsWith("FIELD_NAME_")
+ && f.getType() == String.class
+ && java.lang.reflect.Modifier.isStatic(f.getModifiers())) {
+ names.add((String) f.get(null));
+ }
+ }
+ return names;
+ }
+
class TestingService extends AbstractFlinkService {
RestClusterClient<String> clusterClient;