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

MartijnVisser pushed a commit to branch release-2.4
in repository https://gitbox.apache.org/repos/asf/flink.git

commit 7818f82ad8371cb47c3742c9a11b9d1b90015206
Author: Dale Lane <[email protected]>
AuthorDate: Tue Oct 20 16:02:12 2026 +0100

    [FLINK-41048][container] Fix MiniClusterJobEntryPointITCase race condition
    
    To be able to submit queries to the MiniCluster, I'd written the
    test to pick an available port number, then give that to the
    minicluster to bind to. Then I could use that port number when
    submitting queries.
    
    The problem is that it introduced a race condition. In the window
    between identifying an available port, and starting a MiniCluster
    to bind to it, something else could grab the port.
    
    I can't recreate this locally. Presumably something about the CI
    environment being more constrained widened the window, or something
    about the Centos 7 (3.10) version the CI workers use changes how
    port numbers are dished out. Either way, it is not safe to assume
    that a port number that's available when the test starts will
    remain so for the duration of the test.
    
    I've changed the minicluster config to use bind port 0 so it
    chooses a free port by itself. This does add some complexity to
    the test, because it means I have to go and discover what port it
    used after it's started to be able to submit queries.
    
    Signed-off-by: Dale Lane <[email protected]>
    (cherry picked from commit e56648e7ea288bb0d7b5f9e4e1ee18fc9bb4f068)
---
 .../entrypoint/MiniClusterJobEntryPointITCase.java | 65 +++++++++++++++-------
 1 file changed, 46 insertions(+), 19 deletions(-)

diff --git 
a/flink-container/src/test/java/org/apache/flink/container/entrypoint/MiniClusterJobEntryPointITCase.java
 
b/flink-container/src/test/java/org/apache/flink/container/entrypoint/MiniClusterJobEntryPointITCase.java
index 0714bbe2b32..7c2f98afa2f 100644
--- 
a/flink-container/src/test/java/org/apache/flink/container/entrypoint/MiniClusterJobEntryPointITCase.java
+++ 
b/flink-container/src/test/java/org/apache/flink/container/entrypoint/MiniClusterJobEntryPointITCase.java
@@ -59,6 +59,7 @@ import 
org.apache.flink.runtime.highavailability.HighAvailabilityServices;
 import 
org.apache.flink.runtime.highavailability.HighAvailabilityServicesFactory;
 import org.apache.flink.runtime.highavailability.JobResultStoreOptions;
 import 
org.apache.flink.runtime.highavailability.nonha.embedded.EmbeddedHaServices;
+import org.apache.flink.runtime.leaderelection.LeaderInformation;
 import org.apache.flink.runtime.metrics.groups.TaskManagerJobMetricGroup;
 import org.apache.flink.runtime.minicluster.MiniClusterConfiguration;
 import org.apache.flink.runtime.state.LocalRecoveryConfig;
@@ -68,14 +69,17 @@ import 
org.apache.flink.runtime.state.changelog.inmemory.InMemoryStateChangelogS
 import org.apache.flink.runtime.taskexecutor.TaskManagerRunner;
 import org.apache.flink.runtime.util.Hardware;
 import org.apache.flink.runtime.util.TestingFatalErrorHandler;
+import org.apache.flink.runtime.util.ZooKeeperUtils;
 import org.apache.flink.runtime.zookeeper.ZooKeeperExtension;
 import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
 import org.apache.flink.streaming.api.functions.sink.v2.DiscardingSink;
 import org.apache.flink.util.NetUtils;
 import org.apache.flink.util.jackson.JacksonMapperFactory;
 
+import 
org.apache.flink.shaded.curator5.org.apache.curator.framework.CuratorFramework;
 import 
org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.JsonNode;
 import 
org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.flink.shaded.zookeeper3.org.apache.zookeeper.KeeperException;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -152,7 +156,6 @@ class MiniClusterJobEntryPointITCase {
     private Path markerFile;
     private JobID jobId;
     private Path log4jConfig;
-    private int restPort;
     private String haClusterId;
 
     @BeforeEach
@@ -160,9 +163,6 @@ class MiniClusterJobEntryPointITCase {
         confDir = Files.createDirectories(tempDir.resolve("conf"));
         markerFile = tempDir.resolve("main-invocations");
         jobId = JobID.generate();
-        try (NetUtils.Port port = NetUtils.getAvailablePort()) {
-            restPort = port.getPort();
-        }
 
         log4jConfig = tempDir.resolve("log4j2.properties");
         Files.write(
@@ -192,8 +192,7 @@ class MiniClusterJobEntryPointITCase {
                         entry(JobResultStoreOptions.DELETE_ON_COMMIT, false),
                         entry(ApplicationResultStoreOptions.DELETE_ON_COMMIT, 
false),
                         entry(RestOptions.ADDRESS, "localhost"),
-                        entry(RestOptions.PORT, restPort),
-                        entry(RestOptions.BIND_PORT, restPort)));
+                        entry(RestOptions.BIND_PORT, 0)));
     }
 
     @Test
@@ -479,14 +478,14 @@ class MiniClusterJobEntryPointITCase {
 
         // a separate HA cluster, as for a job moved to a new deployment, so 
that the job can only
         // be restored from the savepoint and not recovered from HA metadata
+        haClusterId = haClusterId + "-restored";
         final List<String> arguments =
                 new ArrayList<>(
                         List.of(
                                 "--fromSavepoint",
                                 savepointPath,
                                 dynamicProperty(
-                                        HighAvailabilityOptions.HA_CLUSTER_ID,
-                                        haClusterId + "-restored")));
+                                        HighAvailabilityOptions.HA_CLUSTER_ID, 
haClusterId)));
         arguments.addAll(unboundedJobArguments());
 
         final Process second = startProcess("second", 
MiniClusterJobEntryPoint.class, arguments);
@@ -1148,7 +1147,7 @@ class MiniClusterJobEntryPointITCase {
                 jobId, expectedState, name, TIMEOUT, lastState.orElse("none"));
     }
 
-    private Optional<String> queryJobState() throws InterruptedException {
+    private Optional<String> queryJobState() throws Exception {
         return queryRest("/jobs/" + jobId).map(json -> 
json.get("state").asText());
     }
 
@@ -1210,9 +1209,34 @@ class MiniClusterJobEntryPointITCase {
         }
     }
 
-    private Optional<JsonNode> queryRest(String path) throws 
InterruptedException {
+    /**
+     * Returns the REST address published by the cluster's leader. After a 
process is killed, this
+     * may briefly be its stale address until its ZooKeeper session expires; 
requests to it are
+     * refused and retried.
+     */
+    private Optional<String> restUrl() throws Exception {
+        final String path =
+                "/flink/"
+                        + haClusterId
+                        + ZooKeeperUtils.generateConnectionInformationPath(
+                                
ZooKeeperUtils.getLeaderPath(ZooKeeperUtils.getRestServerNode()));
+        try {
+            final LeaderInformation leader =
+                    
ZooKeeperUtils.readLeaderInformation(zooKeeperClient().getData().forPath(path));
+            return leader.isEmpty() ? Optional.empty() : 
Optional.of(leader.getLeaderAddress());
+        } catch (KeeperException e) {
+            // not yet published, or ZooKeeper is briefly unavailable
+            return Optional.empty();
+        }
+    }
+
+    private Optional<JsonNode> queryRest(String path) throws Exception {
+        final Optional<String> url = restUrl();
+        if (url.isEmpty()) {
+            return Optional.empty();
+        }
         final HttpRequest request =
-                HttpRequest.newBuilder(URI.create("http://localhost:"; + 
restPort + path))
+                HttpRequest.newBuilder(URI.create(url.get() + path))
                         .timeout(REQUEST_TIMEOUT)
                         .GET()
                         .build();
@@ -1224,14 +1248,16 @@ class MiniClusterJobEntryPointITCase {
             }
             return Optional.of(OBJECT_MAPPER.readTree(response.body()));
         } catch (IOException e) {
-            // REST endpoint not up yet
+            // endpoint is busy, killed, or shutting down
             return Optional.empty();
         }
     }
 
     private JsonNode postRest(String path, JsonNode body) throws Exception {
+        final String url =
+                restUrl().orElseThrow(() -> new IllegalStateException("No REST 
leader published."));
         final HttpRequest request =
-                HttpRequest.newBuilder(URI.create("http://localhost:"; + 
restPort + path))
+                HttpRequest.newBuilder(URI.create(url + path))
                         .timeout(REQUEST_TIMEOUT)
                         .header("Content-Type", "application/json")
                         
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
@@ -1295,13 +1321,14 @@ class MiniClusterJobEntryPointITCase {
     }
 
     private boolean zooKeeperNodeExists(String path) throws Exception {
+        return zooKeeperClient().checkExists().forPath(path) != null;
+    }
+
+    private static CuratorFramework zooKeeperClient() {
         return ZOOKEEPER
-                        .getCustomExtension()
-                        .getZooKeeperClient(new TestingFatalErrorHandler())
-                        .usingNamespace(null)
-                        .checkExists()
-                        .forPath(path)
-                != null;
+                .getCustomExtension()
+                .getZooKeeperClient(new TestingFatalErrorHandler())
+                .usingNamespace(null);
     }
 
     private String executionPlanZooKeeperPath() {

Reply via email to