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

davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new 30f4c98517 [Fix][E2E] Stabilize engine failover test and rebalance 
connector shards (#10949)
30f4c98517 is described below

commit 30f4c98517a48ac1b39c190a51e9c36715696d37
Author: Daniel <[email protected]>
AuthorDate: Sat Jun 13 21:10:50 2026 +0800

    [Fix][E2E] Stabilize engine failover test and rebalance connector shards 
(#10949)
    
    Co-authored-by: Copilot <[email protected]>
    Co-authored-by: davidzollo <[email protected]>
---
 .github/workflows/backend.yml                      |  36 ++++++
 .../jdbc/AbstractSchemaChangeBaseIT.java           |   2 +-
 .../seatunnel/e2e/connector/kafka/KafkaIT.java     |  80 +++++++++++--
 .../kafka_to_kafka_exactly_once_streaming.conf     |   6 +-
 .../engine/e2e/ClusterFailureNoRestoreIT.java      |  38 +++---
 .../engine/e2e/classloader/ClassLoaderITBase.java  | 128 ++++++++++++---------
 .../client/SeaTunnelEngineClusterRoleTest.java     |   1 +
 .../engine/server/event/JobStateEventTest.java     |   8 +-
 8 files changed, 216 insertions(+), 83 deletions(-)

diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml
index e2938d3959..4f34cc94cb 100644
--- a/.github/workflows/backend.yml
+++ b/.github/workflows/backend.yml
@@ -849,6 +849,13 @@ jobs:
           ./mvnw help:evaluate -Dexpression=project.modules -q -DforceStdout 
-pl :seatunnel-connector-v2-e2e >> /tmp/sub_module.txt
           sub_modules=`python 
tools/update_modules_check/update_modules_check.py sub /tmp/sub_module.txt`
           run_it_modules=`python 
tools/update_modules_check/update_modules_check.py sub_it_module "$sub_modules" 
7 1`
+          # Keep the longest Iceberg and HBase suites in a dedicated shard so 
this hosted runner
+          # does not lose heartbeat before the rest of the part-2 modules 
finish.
+          # Handle both first-position (no leading comma) and 
mid/last-position (leading comma).
+          run_it_modules=${run_it_modules//:connector-iceberg-e2e,/}
+          run_it_modules=${run_it_modules//,:connector-iceberg-e2e/}
+          run_it_modules=${run_it_modules//:connector-hbase-e2e,/}
+          run_it_modules=${run_it_modules//,:connector-hbase-e2e/}
           ./mvnw -B -T 1 verify -DskipUT=true -DskipIT=false 
-D"license.skipAddThirdParty"=true -D"skip.ui"=true --no-snapshot-updates -pl 
$run_it_modules -am -Pci
         env:
           MAVEN_OPTS: -Xmx4096m
@@ -1008,6 +1015,35 @@ jobs:
         env:
           MAVEN_OPTS: -Xmx4096m
 
+  all-connectors-it-8:
+    needs: [ changes, sanity-check ]
+    if: needs.changes.outputs.api == 'true' || needs.changes.outputs.engine == 
'true'
+    runs-on: ${{ matrix.os }}
+    env:
+      RUN_ALL_CONTAINER: ${{ needs.changes.outputs.api }}
+      RUN_ZETA_CONTAINER: ${{ needs.changes.outputs.engine }}
+    strategy:
+      matrix:
+        java: [ '8', '11' ]
+        os: [ 'ubuntu-latest' ]
+    timeout-minutes: 210
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up JDK ${{ matrix.java }}
+        uses: actions/setup-java@v3
+        with:
+          java-version: ${{ matrix.java }}
+          distribution: 'temurin'
+          cache: 'maven'
+      - name: free disk space
+        run: tools/github/free_disk_space.sh
+      - name: run connector-v2 integration test (part-8)
+        run: |
+          # These two suites dominated part-2 and triggered hosted-runner 
heartbeat loss.
+          ./mvnw -B -T 1 verify -DskipUT=true -DskipIT=false 
-D"license.skipAddThirdParty"=true -D"skip.ui"=true --no-snapshot-updates -pl 
:connector-iceberg-e2e,:connector-hbase-e2e -am -Pci
+        env:
+          MAVEN_OPTS: -Xmx4096m
+
   jdbc-connectors-it-part-1:
     needs: [ changes, sanity-check ]
     if: needs.changes.outputs.api == 'true' || needs.changes.outputs.engine == 
'true'
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-ddl/src/test/java/org/apache/seatunnel/connectors/jdbc/AbstractSchemaChangeBaseIT.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-ddl/src/test/java/org/apache/seatunnel/connectors/jdbc/AbstractSchemaChangeBaseIT.java
index 32a3d0076f..f95554f79d 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-ddl/src/test/java/org/apache/seatunnel/connectors/jdbc/AbstractSchemaChangeBaseIT.java
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-ddl/src/test/java/org/apache/seatunnel/connectors/jdbc/AbstractSchemaChangeBaseIT.java
@@ -385,7 +385,7 @@ public abstract class AbstractSchemaChangeBaseIT extends 
TestSuiteBase implement
         sourceDatabase.setTemplateName("add_columns").createAndInitialize();
         given().pollDelay(Duration.ofSeconds(5))
                 .await()
-                .atMost(120, TimeUnit.SECONDS)
+                .atMost(SCHEMA_ASSERT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
                 .untilAsserted(
                         () ->
                                 Assertions.assertIterableEquals(
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java
index 6bcd103956..f91c1827a1 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java
@@ -109,6 +109,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Properties;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
@@ -121,6 +122,10 @@ import static 
org.testcontainers.shaded.org.awaitility.Awaitility.given;
 
 @Slf4j
 public class KafkaIT extends TestSuiteBase implements TestResource {
+    private static final String EXACTLY_ONCE_SOURCE_TOPIC_VARIABLE = 
"sourceTopic";
+    private static final String EXACTLY_ONCE_SINK_TOPIC_VARIABLE = "sinkTopic";
+    private static final String EXACTLY_ONCE_CONSUMER_GROUP_VARIABLE = 
"consumerGroup";
+
     private static final String KAFKA_IMAGE_NAME = 
"confluentinc/cp-kafka:7.0.9";
 
     private static final String KAFKA_HOST = "kafkaCluster";
@@ -135,6 +140,9 @@ public class KafkaIT extends TestSuiteBase implements 
TestResource {
 
     private List<ConsumerRecord<String, String>> nativeData;
 
+    /** Topics created dynamically during tests; cleaned up in {@link 
#tearDown()}. */
+    private final List<String> dynamicTopics = new CopyOnWriteArrayList<>();
+
     @BeforeAll
     @Override
     public void startUp() throws Exception {
@@ -266,6 +274,14 @@ public class KafkaIT extends TestSuiteBase implements 
TestResource {
     @AfterAll
     @Override
     public void tearDown() throws Exception {
+        if (!dynamicTopics.isEmpty()) {
+            try (AdminClient adminClient = createKafkaAdmin()) {
+                adminClient.deleteTopics(dynamicTopics).all().get();
+                log.info("Deleted {} dynamic test topics", 
dynamicTopics.size());
+            } catch (Exception e) {
+                log.warn("Failed to delete dynamic test topics: {}", 
e.getMessage());
+            }
+        }
         if (producer != null) {
             producer.close();
         }
@@ -1579,11 +1595,16 @@ public class KafkaIT extends TestSuiteBase implements 
TestResource {
             value = {})
     public void testRestoreKafkaToKafkaExactlyOnceOnStreaming(TestContainer 
container)
             throws InterruptedException, IOException {
-
-        String producerTopic = "kafka_topic_exactly_once_1";
-        String consumerTopic = "kafka_topic_exactly_once_2";
+        String resourceSuffix = Long.toUnsignedString(System.nanoTime());
+        String producerTopic = "kafka_topic_exactly_once_source_" + 
resourceSuffix;
+        String consumerTopic = "kafka_topic_exactly_once_sink_" + 
resourceSuffix;
+        String consumerGroup = "test_exactly_once_" + resourceSuffix;
+        List<String> exactlyOnceVariables =
+                buildExactlyOnceStreamingVariables(producerTopic, 
consumerTopic, consumerGroup);
+        createKafkaTopic(producerTopic);
+        createKafkaTopic(consumerTopic);
         String sourceData = "Seatunnel Exactly Once Example";
-        final String jobId = "18696753645413";
+        final String jobId = Long.toUnsignedString(System.nanoTime());
         long sinkStartOffset = endOffsetOnP0(consumerTopic);
         for (int i = 0; i < 10; i++) {
             ProducerRecord<byte[], byte[]> record =
@@ -1596,7 +1617,9 @@ public class KafkaIT extends TestSuiteBase implements 
TestResource {
                 () -> {
                     try {
                         container.executeJob(
-                                
"/kafka/kafka_to_kafka_exactly_once_streaming.conf", jobId);
+                                
"/kafka/kafka_to_kafka_exactly_once_streaming.conf",
+                                jobId,
+                                exactlyOnceVariables.toArray(new String[0]));
                     } catch (Exception e) {
                         log.error("Commit task exception :" + e.getMessage());
                         throw new RuntimeException(e);
@@ -1630,7 +1653,9 @@ public class KafkaIT extends TestSuiteBase implements 
TestResource {
                 () -> {
                     try {
                         container.restoreJob(
-                                
"/kafka/kafka_to_kafka_exactly_once_streaming.conf", jobId);
+                                
"/kafka/kafka_to_kafka_exactly_once_streaming.conf",
+                                jobId,
+                                exactlyOnceVariables.toArray(new String[0]));
                     } catch (Exception e) {
                         throw new RuntimeException(e);
                     }
@@ -1655,9 +1680,14 @@ public class KafkaIT extends TestSuiteBase implements 
TestResource {
             type = EngineType.SPARK,
             value = {})
     public void testKafkaToKafkaExactlyOnceOnStreaming(TestContainer 
container) {
-
-        String producerTopic = "kafka_topic_exactly_once_1";
-        String consumerTopic = "kafka_topic_exactly_once_2";
+        String resourceSuffix = Long.toUnsignedString(System.nanoTime());
+        String producerTopic = "kafka_topic_exactly_once_source_" + 
resourceSuffix;
+        String consumerTopic = "kafka_topic_exactly_once_sink_" + 
resourceSuffix;
+        String consumerGroup = "test_exactly_once_" + resourceSuffix;
+        List<String> exactlyOnceVariables =
+                buildExactlyOnceStreamingVariables(producerTopic, 
consumerTopic, consumerGroup);
+        createKafkaTopic(producerTopic);
+        createKafkaTopic(consumerTopic);
         String sourceData = "Seatunnel Exactly Once Example";
         long sinkStartOffset = endOffsetOnP0(consumerTopic);
         for (int i = 0; i < 10; i++) {
@@ -1671,7 +1701,9 @@ public class KafkaIT extends TestSuiteBase implements 
TestResource {
         CompletableFuture.supplyAsync(
                 () -> {
                     try {
-                        
container.executeJob("/kafka/kafka_to_kafka_exactly_once_streaming.conf");
+                        container.executeJob(
+                                
"/kafka/kafka_to_kafka_exactly_once_streaming.conf",
+                                exactlyOnceVariables);
                     } catch (Exception e) {
                         log.error("Commit task exception :" + e.getMessage());
                         throw new RuntimeException(e);
@@ -2013,6 +2045,34 @@ public class KafkaIT extends TestSuiteBase implements 
TestResource {
         return AdminClient.create(props);
     }
 
+    /**
+     * Create a dedicated Kafka topic for the exactly-once tests so each 
method reads its own data
+     * and never reuses offsets from earlier runs in the same class.
+     */
+    private void createKafkaTopic(String topicName) {
+        NewTopic topic = new NewTopic(topicName, 1, (short) 1);
+        topic.configs(Collections.singletonMap("retention.ms", "-1"));
+        try (AdminClient adminClient = createKafkaAdmin()) {
+            
adminClient.createTopics(Collections.singletonList(topic)).all().get();
+            dynamicTopics.add(topicName);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IllegalStateException(
+                    "Interrupted while creating Kafka topic " + topicName, e);
+        } catch (ExecutionException e) {
+            throw new IllegalStateException("Failed to create Kafka topic " + 
topicName, e);
+        }
+    }
+
+    /** Build the dynamic `-i key=value` variables for the exactly-once 
streaming template. */
+    private List<String> buildExactlyOnceStreamingVariables(
+            String sourceTopic, String sinkTopic, String consumerGroup) {
+        return Arrays.asList(
+                EXACTLY_ONCE_SOURCE_TOPIC_VARIABLE + "=" + sourceTopic,
+                EXACTLY_ONCE_SINK_TOPIC_VARIABLE + "=" + sinkTopic,
+                EXACTLY_ONCE_CONSUMER_GROUP_VARIABLE + "=" + consumerGroup);
+    }
+
     private void initKafkaProducer() {
         Properties props = new Properties();
         String bootstrapServers = kafkaContainer.getBootstrapServers();
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafka_to_kafka_exactly_once_streaming.conf
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafka_to_kafka_exactly_once_streaming.conf
index ddbc6034b0..ac82598d49 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafka_to_kafka_exactly_once_streaming.conf
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafka_to_kafka_exactly_once_streaming.conf
@@ -24,8 +24,8 @@ env {
 source {
   Kafka {
     bootstrap.servers = "kafkaCluster:9092"
-    topic = "kafka_topic_exactly_once_1"
-    consumer.group = "test_exactly_once"
+    topic = "${sourceTopic}"
+    consumer.group = "${consumerGroup}"
     # The default format is json, which is optional
     format = text
     start_mode = group_offsets
@@ -42,7 +42,7 @@ transform {}
 sink{
   kafka {
         format = text
-        topic = "kafka_topic_exactly_once_2"
+        topic = "${sinkTopic}"
         bootstrap.servers = "kafkaCluster:9092"
         semantics = EXACTLY_ONCE
         kafka.config = {
diff --git 
a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/ClusterFailureNoRestoreIT.java
 
b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/ClusterFailureNoRestoreIT.java
index 3e46dbf675..b0ddf6a0e4 100644
--- 
a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/ClusterFailureNoRestoreIT.java
+++ 
b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/ClusterFailureNoRestoreIT.java
@@ -56,6 +56,22 @@ public class ClusterFailureNoRestoreIT {
     private static final String TEST_TEMPLATE_FILE_NAME =
             "cluster_batch_fake_to_localfile_no_restore_template.conf";
 
+    /**
+     * Keep the bounded fake source busy long enough to observe worker 
shutdown before the batch job
+     * converges on its own.
+     */
+    private static final long NO_RESTORE_BATCH_ROW_NUM_PER_PARALLELISM = 
20_000L;
+
+    /** Wait for the batch job to enter the steady RUNNING state before 
shutting a worker down. */
+    private static final long PRE_SHUTDOWN_RUNNING_TIMEOUT_SECONDS = 30L;
+
+    /**
+     * Give the running batch topology a short warm-up window before shutting 
down a worker. The
+     * LocalFile sink used by this test commits files transactionally, so 
intermediate file lines
+     * are not a reliable progress signal while the job is still running.
+     */
+    private static final long PRE_SHUTDOWN_RUNNING_GRACE_SECONDS = 5L;
+
     private static final String DYNAMIC_TEST_CASE_NAME = 
"dynamic_test_case_name";
 
     private static final String DYNAMIC_TEST_ROW_NUM_PER_PARALLELISM =
@@ -68,7 +84,7 @@ public class ClusterFailureNoRestoreIT {
             throws Exception {
         String testCaseName = 
"testBatchJobWithoutCheckpointAndRetryConvergesAfterWorkerShutdown";
         String testClusterName = "ClusterFailureNoRestoreIT_batch_no_restore";
-        long testRowNumber = 10000;
+        long testRowNumber = NO_RESTORE_BATCH_ROW_NUM_PER_PARALLELISM;
         int testParallelism = 6;
 
         HazelcastInstanceImpl node1 = null;
@@ -108,21 +124,13 @@ public class ClusterFailureNoRestoreIT {
             ClientJobProxy clientJobProxy = jobExecutionEnv.execute();
 
             Awaitility.await()
-                    .atMost(60, TimeUnit.SECONDS)
-                    .pollInterval(500, TimeUnit.MILLISECONDS)
+                    .atMost(PRE_SHUTDOWN_RUNNING_TIMEOUT_SECONDS, 
TimeUnit.SECONDS)
                     .untilAsserted(
-                            () -> {
-                                Long lineNumberFromDir =
-                                        
FileUtils.getFileLineNumberFromDir(testResources.getLeft());
-                                JobStatus status = 
clientJobProxy.getJobStatus();
-                                log.warn(
-                                        
"\n====================={}=====================\n",
-                                        lineNumberFromDir);
-                                Assertions.assertTrue(lineNumberFromDir > 1);
-                                Assertions.assertFalse(
-                                        status.isEndState(),
-                                        "job finished before worker shutdown: 
" + status);
-                            });
+                            () ->
+                                    Assertions.assertEquals(
+                                            JobStatus.RUNNING, 
clientJobProxy.getJobStatus()));
+
+            TimeUnit.SECONDS.sleep(PRE_SHUTDOWN_RUNNING_GRACE_SECONDS);
 
             CompletableFuture<JobResult> waitForCompleteFuture =
                     
CompletableFuture.supplyAsync(clientJobProxy::waitForJobCompleteV2);
diff --git 
a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/classloader/ClassLoaderITBase.java
 
b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/classloader/ClassLoaderITBase.java
index bef36e35df..26cea8651c 100644
--- 
a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/classloader/ClassLoaderITBase.java
+++ 
b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/classloader/ClassLoaderITBase.java
@@ -39,7 +39,7 @@ import java.util.concurrent.TimeUnit;
 
 import static io.restassured.RestAssured.given;
 import static 
org.apache.seatunnel.e2e.common.util.ContainerUtil.PROJECT_ROOT_PATH;
-import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasItem;
 
 public abstract class ClassLoaderITBase extends SeaTunnelEngineContainer {
 
@@ -49,6 +49,12 @@ public abstract class ClassLoaderITBase extends 
SeaTunnelEngineContainer {
 
     private static final String colon = ":";
 
+    /**
+     * Disable-cache mode creates fresh classloaders for each submitted job, 
but the growth should
+     * stay linear relative to the cluster's initial classloader baseline.
+     */
+    private static final int DISABLE_CACHE_MODE_CLASSLOADERS_PER_JOB = 2;
+
     abstract boolean cacheMode();
 
     private static final Path config = Paths.get(SEATUNNEL_HOME, "config");
@@ -60,16 +66,12 @@ public abstract class ClassLoaderITBase extends 
SeaTunnelEngineContainer {
     @Test
     public void testFakeSourceToInMemorySink() throws IOException, 
InterruptedException {
         LOG.info("test classloader with cache mode: {}", cacheMode());
+        int initialClassLoaderCount = getClassLoaderCount();
         for (int i = 0; i < 10; i++) {
             // load in memory sink which already leak thread with classloader
             Container.ExecResult execResult = executeJob(server, CONF_FILE);
             Assertions.assertEquals(0, execResult.getExitCode());
-            Assertions.assertTrue(containsDaemonThread());
-            if (cacheMode()) {
-                Assertions.assertTrue(3 >= getClassLoaderCount());
-            } else {
-                Assertions.assertTrue(3 + 2 * i >= getClassLoaderCount());
-            }
+            assertClassLoaderStateEventuallyStable(initialClassLoaderCount, i);
         }
     }
 
@@ -99,46 +101,51 @@ public abstract class ClassLoaderITBase extends 
SeaTunnelEngineContainer {
                             Assertions.assertEquals(
                                     1, 
response.jsonPath().getList("members").size());
                         });
+        int initialClassLoaderCount = getClassLoaderCount();
         for (int i = 0; i < 10; i++) {
             // load in memory sink which already leak thread with classloader
-            given().body(
-                            "{\n"
-                                    + "\t\"env\": {\n"
-                                    + "\t\t\"parallelism\": 10,\n"
-                                    + "\t\t\"job.mode\": \"BATCH\"\n"
-                                    + "\t},\n"
-                                    + "\t\"source\": [\n"
-                                    + "\t\t{\n"
-                                    + "\t\t\t\"plugin_name\": 
\"FakeSource\",\n"
-                                    + "\t\t\t\"plugin_output\": \"fake\",\n"
-                                    + "\t\t\t\"parallelism\": 10,\n"
-                                    + "\t\t\t\"schema\": {\n"
-                                    + "\t\t\t\t\"fields\": {\n"
-                                    + "\t\t\t\t\t\"name\": \"string\",\n"
-                                    + "\t\t\t\t\t\"age\": \"int\",\n"
-                                    + "\t\t\t\t\t\"score\": \"double\"\n"
-                                    + "\t\t\t\t}\n"
-                                    + "\t\t\t}\n"
-                                    + "\t\t}\n"
-                                    + "\t],\n"
-                                    + "\t\"transform\": [],\n"
-                                    + "\t\"sink\": [\n"
-                                    + "\t\t{\n"
-                                    + "\t\t\t\"plugin_name\": \"InMemory\",\n"
-                                    + "\t\t\t\"plugin_input\": \"fake\"\n"
-                                    + "\t\t}\n"
-                                    + "\t]\n"
-                                    + "}")
-                    .header("Content-Type", "application/json; charset=utf-8")
-                    .post(
-                            http
-                                    + server.getHost()
-                                    + colon
-                                    + server.getFirstMappedPort()
-                                    + RestConstant.CONTEXT_PATH
-                                    + RestConstant.REST_URL_SUBMIT_JOB)
-                    .then()
-                    .statusCode(200);
+            String jobId =
+                    given().body(
+                                    "{\n"
+                                            + "\t\"env\": {\n"
+                                            + "\t\t\"parallelism\": 10,\n"
+                                            + "\t\t\"job.mode\": \"BATCH\"\n"
+                                            + "\t},\n"
+                                            + "\t\"source\": [\n"
+                                            + "\t\t{\n"
+                                            + "\t\t\t\"plugin_name\": 
\"FakeSource\",\n"
+                                            + "\t\t\t\"plugin_output\": 
\"fake\",\n"
+                                            + "\t\t\t\"parallelism\": 10,\n"
+                                            + "\t\t\t\"schema\": {\n"
+                                            + "\t\t\t\t\"fields\": {\n"
+                                            + "\t\t\t\t\t\"name\": 
\"string\",\n"
+                                            + "\t\t\t\t\t\"age\": \"int\",\n"
+                                            + "\t\t\t\t\t\"score\": 
\"double\"\n"
+                                            + "\t\t\t\t}\n"
+                                            + "\t\t\t}\n"
+                                            + "\t\t}\n"
+                                            + "\t],\n"
+                                            + "\t\"transform\": [],\n"
+                                            + "\t\"sink\": [\n"
+                                            + "\t\t{\n"
+                                            + "\t\t\t\"plugin_name\": 
\"InMemory\",\n"
+                                            + "\t\t\t\"plugin_input\": 
\"fake\"\n"
+                                            + "\t\t}\n"
+                                            + "\t]\n"
+                                            + "}")
+                            .header("Content-Type", "application/json; 
charset=utf-8")
+                            .post(
+                                    http
+                                            + server.getHost()
+                                            + colon
+                                            + server.getFirstMappedPort()
+                                            + RestConstant.CONTEXT_PATH
+                                            + RestConstant.REST_URL_SUBMIT_JOB)
+                            .then()
+                            .statusCode(200)
+                            .extract()
+                            .jsonPath()
+                            .getString("jobId");
 
             Awaitility.await()
                     .atMost(2, TimeUnit.MINUTES)
@@ -154,15 +161,30 @@ public abstract class ClassLoaderITBase extends 
SeaTunnelEngineContainer {
                                                             + "/FINISHED")
                                             .then()
                                             .statusCode(200)
-                                            .body("[0].jobStatus", 
equalTo("FINISHED")));
-            Thread.sleep(5000);
-            Assertions.assertTrue(containsDaemonThread());
-            if (cacheMode()) {
-                Assertions.assertTrue(3 >= getClassLoaderCount());
-            } else {
-                Assertions.assertTrue(3 + 2 * i >= getClassLoaderCount());
-            }
+                                            .body("jobId", hasItem(jobId)));
+            assertClassLoaderStateEventuallyStable(initialClassLoaderCount, i);
+        }
+    }
+
+    private void assertClassLoaderStateEventuallyStable(
+            int initialClassLoaderCount, int iteration) {
+        Awaitility.await()
+                .atMost(30, TimeUnit.SECONDS)
+                .untilAsserted(
+                        () -> {
+                            Assertions.assertTrue(containsDaemonThread());
+                            Assertions.assertTrue(
+                                    getClassLoaderCount()
+                                            <= getClassLoaderUpperBound(
+                                                    initialClassLoaderCount, 
iteration));
+                        });
+    }
+
+    private int getClassLoaderUpperBound(int initialClassLoaderCount, int 
iteration) {
+        if (cacheMode()) {
+            return initialClassLoaderCount;
         }
+        return initialClassLoaderCount + 
DISABLE_CACHE_MODE_CLASSLOADERS_PER_JOB * (iteration + 1);
     }
 
     private int getClassLoaderCount() throws IOException, InterruptedException 
{
diff --git 
a/seatunnel-engine/seatunnel-engine-client/src/test/java/org/apache/seatunnel/engine/client/SeaTunnelEngineClusterRoleTest.java
 
b/seatunnel-engine/seatunnel-engine-client/src/test/java/org/apache/seatunnel/engine/client/SeaTunnelEngineClusterRoleTest.java
index 4da8347206..7f37656c64 100644
--- 
a/seatunnel-engine/seatunnel-engine-client/src/test/java/org/apache/seatunnel/engine/client/SeaTunnelEngineClusterRoleTest.java
+++ 
b/seatunnel-engine/seatunnel-engine-client/src/test/java/org/apache/seatunnel/engine/client/SeaTunnelEngineClusterRoleTest.java
@@ -462,6 +462,7 @@ public class SeaTunnelEngineClusterRoleTest {
                                                             
.listJobStatus(true)
                                                             
.contains("RUNNING")));
             jobClient.cancelJob(jobId);
+            // Master handoff can delay terminal status propagation on the 
slower JDK 8 CI lane.
             await().atMost(120000, TimeUnit.MILLISECONDS)
                     .untilAsserted(
                             () -> {
diff --git 
a/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/event/JobStateEventTest.java
 
b/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/event/JobStateEventTest.java
index d028b8e578..9bf877b6ce 100644
--- 
a/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/event/JobStateEventTest.java
+++ 
b/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/event/JobStateEventTest.java
@@ -38,6 +38,12 @@ import static org.awaitility.Awaitility.await;
 
 class JobStateEventTest extends AbstractSeaTunnelServerTest {
 
+    /**
+     * The commit-error batch job may restore several times before reaching 
the final FAILED state,
+     * so this event test needs the same longer timeout as the checkpoint 
restore regression test.
+     */
+    private static final long FAILED_JOB_EVENT_TIMEOUT_SECONDS = 240L;
+
     @Test
     void testJobStateEvent() {
 
@@ -90,7 +96,7 @@ class JobStateEventTest extends AbstractSeaTunnelServerTest {
 
         long jobIdFailed = System.currentTimeMillis();
         startJob(jobIdFailed, STREAM_CONF_WITH_ERROR_PATH, false);
-        await().atMost(60, TimeUnit.SECONDS)
+        await().atMost(FAILED_JOB_EVENT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
                 .untilAsserted(
                         () ->
                                 Assertions.assertEquals(

Reply via email to