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 56ddd0578 [FLINK-40467] Randomise application result store paths
56ddd0578 is described below

commit 56ddd057828193404b424d5495ebc1fcc92f31d6
Author: Dale Lane <[email protected]>
AuthorDate: Mon Sep 14 20:27:20 2026 +0100

    [FLINK-40467] Randomise application result store paths
---
 docs/content.zh/docs/deployment/leader-election.md |  2 +
 docs/content/docs/deployment/leader-election.md    |  2 +
 .../deployment/ApplicationReconciler.java          | 47 +++++++++++++
 .../deployment/ApplicationReconcilerTest.java      | 76 ++++++++++++++++++++++
 4 files changed, 127 insertions(+)

diff --git a/docs/content.zh/docs/deployment/leader-election.md 
b/docs/content.zh/docs/deployment/leader-election.md
index 3e66501a7..2e02824db 100644
--- a/docs/content.zh/docs/deployment/leader-election.md
+++ b/docs/content.zh/docs/deployment/leader-election.md
@@ -97,6 +97,8 @@ drwxr-xr-x 2 9999 9999 60 May 12 09:46 
a6031ec7-ab3e-4b30-ba77-6498e58e6b7f
 drwxr-xr-x 2 9999 9999 60 May 11 15:11 b6fb2a9c-d1cd-4e65-a9a1-e825c4b47543
 ```
 
+From Flink 2.3 the operator applies the same two settings to the 
ApplicationResultStore 
([FLINK-40467](https://issues.apache.org/jira/browse/FLINK-40467)): 
`application-result-store.delete-on-commit` is turned off unless set 
explicitly, and `application-result-store.storage-path` is pointed at a unique 
directory on every cluster launch. This means a terminal application result 
left by an earlier deployment can no longer stop the replacement cluster from 
submitting its new job, and a JobM [...]
+
 ### Limitations
 
 The following are known limitations of the operator's Flink Job HA integration:
diff --git a/docs/content/docs/deployment/leader-election.md 
b/docs/content/docs/deployment/leader-election.md
index 3e66501a7..2e02824db 100644
--- a/docs/content/docs/deployment/leader-election.md
+++ b/docs/content/docs/deployment/leader-election.md
@@ -97,6 +97,8 @@ drwxr-xr-x 2 9999 9999 60 May 12 09:46 
a6031ec7-ab3e-4b30-ba77-6498e58e6b7f
 drwxr-xr-x 2 9999 9999 60 May 11 15:11 b6fb2a9c-d1cd-4e65-a9a1-e825c4b47543
 ```
 
+From Flink 2.3 the operator applies the same two settings to the 
ApplicationResultStore 
([FLINK-40467](https://issues.apache.org/jira/browse/FLINK-40467)): 
`application-result-store.delete-on-commit` is turned off unless set 
explicitly, and `application-result-store.storage-path` is pointed at a unique 
directory on every cluster launch. This means a terminal application result 
left by an earlier deployment can no longer stop the replacement cluster from 
submitting its new job, and a JobM [...]
+
 ### Limitations
 
 The following are known limitations of the operator's Flink Job HA integration:
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 99ff5f789..38e8df1e6 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
@@ -20,6 +20,8 @@ package 
org.apache.flink.kubernetes.operator.reconciler.deployment;
 import org.apache.flink.api.common.JobID;
 import org.apache.flink.api.common.JobStatus;
 import org.apache.flink.autoscaler.JobAutoScaler;
+import org.apache.flink.configuration.ConfigOption;
+import org.apache.flink.configuration.ConfigOptions;
 import org.apache.flink.configuration.Configuration;
 import org.apache.flink.configuration.HighAvailabilityOptions;
 import org.apache.flink.configuration.PipelineOptionsInternal;
@@ -72,6 +74,24 @@ public class ApplicationReconciler
     static final String MSG_RECOVERY = "Recovering lost deployment";
     static final String MSG_RESTART_UNHEALTHY = "Restarting unhealthy job";
 
+    /**
+     * Mirrors {@code ApplicationResultStoreOptions#STORAGE_PATH}, introduced 
in Flink 2.3. The
+     * option is unknown and ignored in earlier versions.
+     */
+    static final ConfigOption<String> APPLICATION_RESULT_STORE_STORAGE_PATH =
+            ConfigOptions.key("application-result-store.storage-path")
+                    .stringType()
+                    .noDefaultValue();
+
+    /**
+     * Mirrors {@code ApplicationResultStoreOptions#DELETE_ON_COMMIT}, 
introduced in Flink 2.3. The
+     * option is unknown and ignored in earlier versions.
+     */
+    static final ConfigOption<Boolean> 
APPLICATION_RESULT_STORE_DELETE_ON_COMMIT =
+            ConfigOptions.key("application-result-store.delete-on-commit")
+                    .booleanType()
+                    .defaultValue(true);
+
     public ApplicationReconciler(
             EventRecorder eventRecorder,
             StatusRecorder<FlinkDeployment, FlinkDeploymentStatus> 
statusRecorder,
@@ -173,6 +193,7 @@ public class ApplicationReconciler
 
         setOwnerReference(relatedResource, deployConfig);
         setRandomJobResultStorePath(deployConfig);
+        setRandomApplicationResultStorePath(deployConfig);
 
         if (status.getJobManagerDeploymentStatus() != 
JobManagerDeploymentStatus.MISSING) {
             
Preconditions.checkArgument(ReconciliationUtils.isJobInTerminalState(status));
@@ -273,6 +294,32 @@ public class ApplicationReconciler
         }
     }
 
+    private static void setRandomApplicationResultStorePath(Configuration 
effectiveConfig) {
+        if (effectiveConfig.contains(HighAvailabilityOptions.HA_STORAGE_PATH)) 
{
+
+            // With shutdown-on-application-finish disabled, a JobManager that 
restarts
+            // after an application has terminated would find no result and 
run the
+            // application's main() again. Retaining the entry avoids that.
+            if 
(!effectiveConfig.contains(APPLICATION_RESULT_STORE_DELETE_ON_COMMIT)) {
+                effectiveConfig.set(APPLICATION_RESULT_STORE_DELETE_ON_COMMIT, 
false);
+            }
+
+            // The application result store shares the HA storage path, which 
outlives
+            // the HA metadata deleted on upgrade. Dirty entries are recovered 
unkeyed,
+            // so a terminal entry left behind by a previous deployment makes 
the
+            // replacement cluster skip submitting the new job.
+            // Giving each deployment a unique path makes sure that nothing 
stale is
+            // recovered, including entries retained above.
+            effectiveConfig.set(
+                    APPLICATION_RESULT_STORE_STORAGE_PATH,
+                    
effectiveConfig.getString(HighAvailabilityOptions.HA_STORAGE_PATH)
+                            + "/application-result-store/"
+                            + 
effectiveConfig.getString(KubernetesConfigOptions.CLUSTER_ID)
+                            + "/"
+                            + UUID.randomUUID());
+        }
+    }
+
     @Override
     public boolean reconcileOtherChanges(FlinkResourceContext<FlinkDeployment> 
ctx)
             throws Exception {
diff --git 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/reconciler/deployment/ApplicationReconcilerTest.java
 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/reconciler/deployment/ApplicationReconcilerTest.java
index d1188c7ff..5b6b36422 100644
--- 
a/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/reconciler/deployment/ApplicationReconcilerTest.java
+++ 
b/flink-kubernetes-operator/src/test/java/org/apache/flink/kubernetes/operator/reconciler/deployment/ApplicationReconcilerTest.java
@@ -131,6 +131,8 @@ import static 
org.apache.flink.kubernetes.operator.config.KubernetesOperatorConf
 import static 
org.apache.flink.kubernetes.operator.reconciler.SnapshotType.CHECKPOINT;
 import static 
org.apache.flink.kubernetes.operator.reconciler.SnapshotType.SAVEPOINT;
 import static 
org.apache.flink.kubernetes.operator.reconciler.deployment.AbstractFlinkResourceReconciler.MSG_SUBMIT;
+import static 
org.apache.flink.kubernetes.operator.reconciler.deployment.ApplicationReconciler.APPLICATION_RESULT_STORE_DELETE_ON_COMMIT;
+import static 
org.apache.flink.kubernetes.operator.reconciler.deployment.ApplicationReconciler.APPLICATION_RESULT_STORE_STORAGE_PATH;
 import static 
org.apache.flink.kubernetes.operator.reconciler.deployment.ApplicationReconciler.MSG_RECOVERY;
 import static 
org.apache.flink.kubernetes.operator.utils.SnapshotUtils.getLastSnapshotStatus;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -802,6 +804,80 @@ public class ApplicationReconcilerTest extends 
OperatorTestBase {
         assertNotEquals(path1, path2);
     }
 
+    /**
+     * The application result store lives under the HA storage path and 
survives the HA metadata
+     * deletion performed on upgrade, so a terminal entry left by the previous 
deployment would
+     * otherwise be recovered by the replacement cluster, which then skips 
submitting the new job.
+     */
+    @Test
+    public void testRandomApplicationResultStorePath() throws Exception {
+        FlinkDeployment flinkApp = TestUtils.buildApplicationCluster();
+        final String haStoragePath = "file:///flink-data/ha";
+        flinkApp.getSpec()
+                .getFlinkConfiguration()
+                .put(HighAvailabilityOptions.HA_STORAGE_PATH.key(), 
haStoragePath);
+
+        ObjectMeta deployMeta = flinkApp.getMetadata();
+        FlinkDeploymentStatus status = flinkApp.getStatus();
+        FlinkDeploymentSpec spec = flinkApp.getSpec();
+        Configuration deployConfig = configManager.getDeployConfig(deployMeta, 
spec);
+
+        
status.getJobStatus().setState(org.apache.flink.api.common.JobStatus.FINISHED);
+        status.setJobManagerDeploymentStatus(JobManagerDeploymentStatus.READY);
+        reconciler
+                .getReconciler()
+                .deploy(getResourceContext(flinkApp), spec, deployConfig, 
Optional.empty(), false);
+        String path1 = deployConfig.get(APPLICATION_RESULT_STORE_STORAGE_PATH);
+        assertThat(path1).startsWith(haStoragePath);
+
+        
status.getJobStatus().setState(org.apache.flink.api.common.JobStatus.FINISHED);
+        status.setJobManagerDeploymentStatus(JobManagerDeploymentStatus.READY);
+        reconciler
+                .getReconciler()
+                .deploy(getResourceContext(flinkApp), spec, deployConfig, 
Optional.empty(), false);
+        String path2 = deployConfig.get(APPLICATION_RESULT_STORE_STORAGE_PATH);
+        assertThat(path2).startsWith(haStoragePath).isNotEqualTo(path1);
+    }
+
+    /**
+     * A terminal application result must outlive its cleanup, otherwise a 
JobManager restart before
+     * the cluster is shut down finds no record of the application and runs it 
again.
+     */
+    @Test
+    public void testApplicationResultStoreRetainsResults() throws Exception {
+        var deployConfig = deployWithHaStorage(Map.of());
+        
assertThat(deployConfig.get(APPLICATION_RESULT_STORE_DELETE_ON_COMMIT)).isFalse();
+    }
+
+    @Test
+    public void testApplicationResultStoreDeleteOnCommitNotOverridden() throws 
Exception {
+        var deployConfig =
+                deployWithHaStorage(
+                        
Map.of(APPLICATION_RESULT_STORE_DELETE_ON_COMMIT.key(), "true"));
+        
assertThat(deployConfig.get(APPLICATION_RESULT_STORE_DELETE_ON_COMMIT)).isTrue();
+    }
+
+    /** Deploys an application cluster with HA storage configured, plus any 
extra config. */
+    private Configuration deployWithHaStorage(Map<String, String> extraConfig) 
throws Exception {
+        final FlinkDeployment flinkApp = TestUtils.buildApplicationCluster();
+        flinkApp.getSpec()
+                .getFlinkConfiguration()
+                .put(HighAvailabilityOptions.HA_STORAGE_PATH.key(), 
"file:///flink-data/ha");
+        flinkApp.getSpec().getFlinkConfiguration().putAllFrom(extraConfig);
+
+        final Configuration deployConfig =
+                configManager.getDeployConfig(flinkApp.getMetadata(), 
flinkApp.getSpec());
+        reconciler
+                .getReconciler()
+                .deploy(
+                        getResourceContext(flinkApp),
+                        flinkApp.getSpec(),
+                        deployConfig,
+                        Optional.empty(),
+                        false);
+        return deployConfig;
+    }
+
     @Test
     public void testAlwaysSavepointOnFlinkVersionChange() throws Exception {
         var deployment = TestUtils.buildApplicationCluster(FlinkVersion.v1_18);

Reply via email to