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

zhangshenghang 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 847b51e390 [Fix][E2E] Split unrelated connector CI fixes from DB2 CDC 
PR (#10925)
847b51e390 is described below

commit 847b51e39092c377572d01fbb8d931a3d75334bc
Author: David Zollo <[email protected]>
AuthorDate: Fri Aug 21 20:28:55 2026 +0800

    [Fix][E2E] Split unrelated connector CI fixes from DB2 CDC PR (#10925)
    
    Co-authored-by: DanielLeens <[email protected]>
    Co-authored-by: Daniel <[email protected]>
    Co-authored-by: DanielLeens <[email protected]>
---
 .../cdc/mysql/MysqlCDCWithFlinkSchemaChangeIT.java |  29 ++-
 .../e2e/connector/file/ftp/FtpFileIT.java          | 281 ++++++++++++++++-----
 ...ftp_file_json_to_assert_with_multipletable.conf |   6 +-
 .../e2e/connector/file/fstp/SftpFileIT.java        | 173 ++++++++-----
 .../seatunnel/e2e/connector/kafka/KafkaIT.java     |  58 ++++-
 .../seatunnel/e2e/connector/kafka/KafkaITTest.java |  38 +++
 .../e2e/connector/kafka/KafkaKerberosIT.java       |  24 ++
 .../kafka_to_kafka_exactly_once_streaming.conf     |  13 +-
 .../e2e/connector/rocketmq/RocketMqIT.java         |  64 ++++-
 .../rocketmq_source_group_offset_to_console.conf   |   5 +-
 .../rocketmq/rocketmq_source_restore.conf          |   6 +-
 .../e2e/connector/tdengine/TDengineIT.java         |  80 +++---
 12 files changed, 575 insertions(+), 202 deletions(-)

diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/mysql/MysqlCDCWithFlinkSchemaChangeIT.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/mysql/MysqlCDCWithFlinkSchemaChangeIT.java
index 025dcc36d1..344fd79135 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/mysql/MysqlCDCWithFlinkSchemaChangeIT.java
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/mysql/MysqlCDCWithFlinkSchemaChangeIT.java
@@ -67,12 +67,17 @@ import static org.awaitility.Awaitility.await;
 public class MysqlCDCWithFlinkSchemaChangeIT extends TestSuiteBase implements 
TestResource {
     /**
      * Flink schema evolution can restart after XA recover/rollback on loaded 
CI runners, so these
-     * assertions need enough time for the job to recover and replay the 
schema-change event.
+     * assertions need enough time for the job to recover and replay the 
schema-change event. The
+     * long bound is a failure deadline, not the expected recovery duration.
      */
-    private static final long SCHEMA_EVOLUTION_ASSERT_TIMEOUT_MILLIS = 
300_000L;
+    private static final long SCHEMA_EVOLUTION_ASSERT_TIMEOUT_MILLIS = 
600_000L;
 
     private static final long STRUCTURE_AND_DATA_ASSERT_TIMEOUT_MILLIS = 
300_000L;
-    private static final int MAX_TIMESTAMP_DRIFT_SECONDS = 60;
+    /**
+     * The timestamp default is evaluated by different MySQL statements during 
CDC replay, so this
+     * tolerance allows loaded CI scheduling jitter without hiding 
minute-level CDC lag.
+     */
+    private static final int MAX_TIMESTAMP_DRIFT_SECONDS = 45;
 
     private static final String MYSQL_DATABASE = "shop";
     private static final String SOURCE_TABLE = "products";
@@ -202,9 +207,9 @@ public class MysqlCDCWithFlinkSchemaChangeIT extends 
TestSuiteBase implements Te
                                     query(String.format(PROJECTION_QUERY, 
database, sourceTable)),
                                     query(String.format(PROJECTION_QUERY, 
database, sinkTable)));
 
-                            // The default value of add_column4 is 
current_timestamp(),so the
-                            // history data of sink table with this column may 
be different from the
-                            // source table because delay of apply schema 
change.
+                            // The default value of add_column4 is 
current_timestamp(), so the
+                            // history data of the sink table can lag briefly 
behind the source
+                            // table while the schema-change event is still 
propagating.
                             String query =
                                     String.format(
                                             "SELECT t1.id AS table1_id, 
t1.add_column4 AS table1_timestamp, "
@@ -272,12 +277,14 @@ public class MysqlCDCWithFlinkSchemaChangeIT extends 
TestSuiteBase implements Te
 
         // case1 add columns with cdc data at same time
         shopDatabase.setTemplateName("add_columns").createAndInitialize();
-        await().atMost(SCHEMA_EVOLUTION_ASSERT_TIMEOUT_MILLIS, 
TimeUnit.MILLISECONDS)
+        await().pollInterval(5, TimeUnit.SECONDS)
+                .atMost(SCHEMA_EVOLUTION_ASSERT_TIMEOUT_MILLIS, 
TimeUnit.MILLISECONDS)
                 .untilAsserted(
                         () ->
                                 
assertSchemaDescriptionEqualsIgnoringColumnOrder(
                                         database, sourceTable, sinkTable));
-        await().atMost(SCHEMA_EVOLUTION_ASSERT_TIMEOUT_MILLIS, 
TimeUnit.MILLISECONDS)
+        await().pollInterval(5, TimeUnit.SECONDS)
+                .atMost(SCHEMA_EVOLUTION_ASSERT_TIMEOUT_MILLIS, 
TimeUnit.MILLISECONDS)
                 .untilAsserted(
                         () -> {
                             assertTableDataEqualsBySourceColumnOrder(
@@ -287,9 +294,9 @@ public class MysqlCDCWithFlinkSchemaChangeIT extends 
TestSuiteBase implements Te
                                     query(String.format(PROJECTION_QUERY, 
database, sourceTable)),
                                     query(String.format(PROJECTION_QUERY, 
database, sinkTable)));
 
-                            // The default value of add_column4 is 
current_timestamp(),so the
-                            // history data of sink table with this column may 
be different from the
-                            // source table because delay of apply schema 
change.
+                            // The default value of add_column4 is 
current_timestamp(), so the
+                            // history data of the sink table can lag briefly 
behind the source
+                            // table while the schema-change event is still 
propagating.
                             String query =
                                     String.format(
                                             "SELECT t1.id AS table1_id, 
t1.add_column4 AS table1_timestamp, "
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/ftp/FtpFileIT.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/ftp/FtpFileIT.java
index 7075de2c0a..d23fe15e50 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/ftp/FtpFileIT.java
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/ftp/FtpFileIT.java
@@ -249,10 +249,11 @@ public class FtpFileIT extends TestSuiteBase implements 
TestResource {
             type = {EngineType.FLINK, EngineType.SPARK},
             disabledReason = "Continuous discovery is a long-running job; only 
run in zeta engine.")
     public void testFtpBinaryUpdateModeContinuousDiscoveryDistcp(TestContainer 
container)
-            throws IOException, InterruptedException {
+            throws Throwable {
         resetContinuousTestPath(CONTINUOUS_DISTCP_PATH);
         String jobId = String.valueOf(JobIdGenerator.newJobId());
         CompletableFuture<Container.ExecResult> jobFuture = null;
+        Throwable testFailure = null;
         try {
             putFtpFile(CONTINUOUS_DISTCP_PATH + "/src/test1.bin", "abc");
 
@@ -297,13 +298,12 @@ public class FtpFileIT extends TestSuiteBase implements 
TestResource {
                                             "def",
                                             readFtpFile(
                                                     CONTINUOUS_DISTCP_PATH + 
"/dst/test2.bin")));
-
-            Container.ExecResult cancelResult = container.cancelJob(jobId);
-            Assertions.assertEquals(0, cancelResult.getExitCode(), 
cancelResult.getStderr());
-            waitContinuousJobExit(container, jobId, jobFuture);
+        } catch (Throwable failure) {
+            testFailure = failure;
+            throw failure;
         } finally {
-            cancelContinuousJobQuietly(container, jobId, jobFuture);
-            deleteFileFromContainer(ftpHomeDir + CONTINUOUS_DISTCP_PATH);
+            cleanupContinuousJob(
+                    container, jobId, jobFuture, ftpHomeDir + 
CONTINUOUS_DISTCP_PATH, testFailure);
         }
     }
 
@@ -313,10 +313,11 @@ public class FtpFileIT extends TestSuiteBase implements 
TestResource {
             type = {EngineType.FLINK, EngineType.SPARK},
             disabledReason = "Continuous discovery is a long-running job; only 
run in zeta engine.")
     public void 
testFtpBinaryUpdateModeContinuousDiscoveryPostSyncDelete(TestContainer 
container)
-            throws IOException, InterruptedException {
+            throws Throwable {
         resetContinuousTestPath(CONTINUOUS_DELETE_PATH);
         String jobId = String.valueOf(JobIdGenerator.newJobId());
         CompletableFuture<Container.ExecResult> jobFuture = null;
+        Throwable testFailure = null;
         try {
             putFtpFile(CONTINUOUS_DELETE_PATH + "/src/delete-test.bin", "abc");
 
@@ -353,13 +354,12 @@ public class FtpFileIT extends TestSuiteBase implements 
TestResource {
                                                     CONTINUOUS_DELETE_PATH
                                                             + 
"/src/delete-test.bin"),
                                             "source file should be deleted 
after checkpoint-gated post-sync commit"));
-
-            Container.ExecResult cancelResult = container.cancelJob(jobId);
-            Assertions.assertEquals(0, cancelResult.getExitCode(), 
cancelResult.getStderr());
-            waitContinuousJobExit(container, jobId, jobFuture);
+        } catch (Throwable failure) {
+            testFailure = failure;
+            throw failure;
         } finally {
-            cancelContinuousJobQuietly(container, jobId, jobFuture);
-            deleteFileFromContainer(ftpHomeDir + CONTINUOUS_DELETE_PATH);
+            cleanupContinuousJob(
+                    container, jobId, jobFuture, ftpHomeDir + 
CONTINUOUS_DELETE_PATH, testFailure);
         }
     }
 
@@ -369,10 +369,11 @@ public class FtpFileIT extends TestSuiteBase implements 
TestResource {
             type = {EngineType.FLINK, EngineType.SPARK},
             disabledReason = "Continuous discovery is a long-running job; only 
run in zeta engine.")
     public void 
testFtpBinaryUpdateModeContinuousDiscoveryPostSyncBackup(TestContainer 
container)
-            throws IOException, InterruptedException {
+            throws Throwable {
         resetContinuousTestPath(CONTINUOUS_BACKUP_PATH);
         String jobId = String.valueOf(JobIdGenerator.newJobId());
         CompletableFuture<Container.ExecResult> jobFuture = null;
+        Throwable testFailure = null;
         try {
             putFtpFile(CONTINUOUS_BACKUP_PATH + "/src/backup-test.bin", "abc");
 
@@ -422,13 +423,12 @@ public class FtpFileIT extends TestSuiteBase implements 
TestResource {
                                                             
"backup-test.bin.v*")
                                                     > 0,
                                             "backup target should contain 
version-suffixed file"));
-
-            Container.ExecResult cancelResult = container.cancelJob(jobId);
-            Assertions.assertEquals(0, cancelResult.getExitCode(), 
cancelResult.getStderr());
-            waitContinuousJobExit(container, jobId, jobFuture);
+        } catch (Throwable failure) {
+            testFailure = failure;
+            throw failure;
         } finally {
-            cancelContinuousJobQuietly(container, jobId, jobFuture);
-            deleteFileFromContainer(ftpHomeDir + CONTINUOUS_BACKUP_PATH);
+            cleanupContinuousJob(
+                    container, jobId, jobFuture, ftpHomeDir + 
CONTINUOUS_BACKUP_PATH, testFailure);
         }
     }
 
@@ -437,11 +437,11 @@ public class FtpFileIT extends TestSuiteBase implements 
TestResource {
             value = {},
             type = {EngineType.FLINK, EngineType.SPARK},
             disabledReason = "Continuous discovery is a long-running job; only 
run in zeta engine.")
-    public void testFtpContinuousBackupRetentionCleanup(TestContainer 
container)
-            throws IOException, InterruptedException {
+    public void testFtpContinuousBackupRetentionCleanup(TestContainer 
container) throws Throwable {
         resetContinuousTestPath(CONTINUOUS_RETENTION_PATH);
         String jobId = String.valueOf(JobIdGenerator.newJobId());
         CompletableFuture<Container.ExecResult> jobFuture = null;
+        Throwable testFailure = null;
         try {
             putFtpFile(CONTINUOUS_RETENTION_PATH + "/src/retention-input.bin", 
"input");
             putFtpFile(CONTINUOUS_RETENTION_PATH + 
"/backup/retention-old.bin.v3_123456", "abc");
@@ -472,27 +472,35 @@ public class FtpFileIT extends TestSuiteBase implements 
TestResource {
                                                     CONTINUOUS_RETENTION_PATH 
+ "/backup",
                                                     "retention-old.bin.v*"),
                                             "retention should remove expired 
SeaTunnel backup files"));
-
-            Container.ExecResult cancelResult = container.cancelJob(jobId);
-            Assertions.assertEquals(0, cancelResult.getExitCode(), 
cancelResult.getStderr());
-            waitContinuousJobExit(container, jobId, jobFuture);
+        } catch (Throwable failure) {
+            testFailure = failure;
+            throw failure;
         } finally {
-            cancelContinuousJobQuietly(container, jobId, jobFuture);
-            deleteFileFromContainer(ftpHomeDir + CONTINUOUS_RETENTION_PATH);
+            cleanupContinuousJob(
+                    container,
+                    jobId,
+                    jobFuture,
+                    ftpHomeDir + CONTINUOUS_RETENTION_PATH,
+                    testFailure);
         }
     }
 
+    /**
+     * Verifies continuous non-recursive FTP sync copies root files while 
ignoring nested files.
+     *
+     * <p>The nested-file assertion protects the connector's non-recursive 
discovery contract.
+     */
     @TestTemplate
     @DisabledOnContainer(
             value = {},
             type = {EngineType.FLINK, EngineType.SPARK},
             disabledReason = "Continuous discovery is a long-running job; only 
run in zeta engine.")
     public void testFtpBinaryUpdateModeContinuousDiscoveryWithNonRecursiveScan(
-            TestContainer container) throws IOException, InterruptedException {
+            TestContainer container) throws Throwable {
         resetContinuousTestPath(CONTINUOUS_NON_RECURSIVE_PATH);
-
         String jobId = String.valueOf(JobIdGenerator.newJobId());
         CompletableFuture<Container.ExecResult> jobFuture = null;
+        Throwable testFailure = null;
         try {
             putFtpFile(CONTINUOUS_NON_RECURSIVE_PATH + "/src/root.bin", 
"root");
             putFtpFile(CONTINUOUS_NON_RECURSIVE_PATH + 
"/src/subdir/nested.bin", "nested");
@@ -523,53 +531,149 @@ public class FtpFileIT extends TestSuiteBase implements 
TestResource {
             Thread.sleep(3000);
             Assertions.assertFalse(
                     isFtpFileExists(CONTINUOUS_NON_RECURSIVE_PATH + 
"/dst/subdir/nested.bin"));
-
-            Container.ExecResult cancelResult = container.cancelJob(jobId);
-            Assertions.assertEquals(0, cancelResult.getExitCode(), 
cancelResult.getStderr());
-            waitContinuousJobExit(container, jobId, jobFuture);
+        } catch (Throwable failure) {
+            testFailure = failure;
+            throw failure;
         } finally {
-            cancelContinuousJobQuietly(container, jobId, jobFuture);
-            deleteFileFromContainer(ftpHomeDir + 
CONTINUOUS_NON_RECURSIVE_PATH);
+            cleanupContinuousJob(
+                    container,
+                    jobId,
+                    jobFuture,
+                    ftpHomeDir + CONTINUOUS_NON_RECURSIVE_PATH,
+                    testFailure);
         }
     }
 
+    /**
+     * Verifies non-recursive FTP distcp updates root files without 
overwriting nested files.
+     *
+     * <p>The stale nested destination file must remain unchanged after the 
job finishes.
+     */
     @TestTemplate
     public void 
testFtpBinaryUpdateModeDistcpWithNonRecursiveScan(TestContainer container)
             throws IOException, InterruptedException {
         resetUpdateTestPath();
-        putFtpFile("/tmp/seatunnel/update/src/root.bin", "root-updated-v2");
-        putFtpFile("/tmp/seatunnel/update/src/subdir/nested.bin", 
"nest-updated-v2");
-        putFtpFile("/tmp/seatunnel/update/dst/root.bin", "root-stale-v1");
-        putFtpFile("/tmp/seatunnel/update/dst/subdir/nested.bin", 
"nest-stale-v1");
-
-        Container.ExecResult execResult =
-                
container.executeJob("/text/ftp_binary_update_non_recursive_distcp.conf");
-        Assertions.assertEquals(0, execResult.getExitCode(), 
execResult.getStderr());
-        Assertions.assertEquals(
-                "root-updated-v2", 
readFtpFile("/tmp/seatunnel/update/dst/root.bin"));
-        Assertions.assertEquals(
-                "nest-stale-v1", 
readFtpFile("/tmp/seatunnel/update/dst/subdir/nested.bin"));
+        try {
+            putFtpFile("/tmp/seatunnel/update/src/root.bin", 
"root-updated-v2");
+            putFtpFile("/tmp/seatunnel/update/src/subdir/nested.bin", 
"nest-updated-v2");
+            putFtpFile("/tmp/seatunnel/update/dst/root.bin", "root-stale-v1");
+            putFtpFile("/tmp/seatunnel/update/dst/subdir/nested.bin", 
"nest-stale-v1");
 
-        deleteFileFromContainer(ftpHomeDir + "/tmp/seatunnel/update");
+            Container.ExecResult execResult =
+                    
container.executeJob("/text/ftp_binary_update_non_recursive_distcp.conf");
+            Assertions.assertEquals(0, execResult.getExitCode(), 
execResult.getStderr());
+            Assertions.assertEquals(
+                    "root-updated-v2", 
readFtpFile("/tmp/seatunnel/update/dst/root.bin"));
+            Assertions.assertEquals(
+                    "nest-stale-v1", 
readFtpFile("/tmp/seatunnel/update/dst/subdir/nested.bin"));
+        } finally {
+            deleteFileFromContainer(ftpHomeDir + "/tmp/seatunnel/update");
+        }
     }
 
+    /**
+     * Verifies strict checksum mode keeps nested FTP files untouched during 
non-recursive scans.
+     */
     @TestTemplate
     public void 
testFtpBinaryUpdateModeStrictChecksumSkipsNestedChangesWithNonRecursiveScan(
             TestContainer container) throws IOException, InterruptedException {
         resetUpdateTestPath();
-        putFtpFile("/tmp/seatunnel/update/src/root.bin", "root-same-v1");
-        putFtpFile("/tmp/seatunnel/update/src/subdir/nested.bin", 
"nest-new-v1");
-        putFtpFile("/tmp/seatunnel/update/dst/root.bin", "root-same-v1");
-        putFtpFile("/tmp/seatunnel/update/dst/subdir/nested.bin", 
"nest-old-v1");
+        try {
+            putFtpFile("/tmp/seatunnel/update/src/root.bin", "root-same-v1");
+            putFtpFile("/tmp/seatunnel/update/src/subdir/nested.bin", 
"nest-new-v1");
+            putFtpFile("/tmp/seatunnel/update/dst/root.bin", "root-same-v1");
+            putFtpFile("/tmp/seatunnel/update/dst/subdir/nested.bin", 
"nest-old-v1");
+
+            Container.ExecResult execResult =
+                    container.executeJob(
+                            
"/text/ftp_binary_update_non_recursive_strict_checksum.conf");
+            Assertions.assertEquals(0, execResult.getExitCode(), 
execResult.getStderr());
+            Assertions.assertEquals(
+                    "root-same-v1", 
readFtpFile("/tmp/seatunnel/update/dst/root.bin"));
+            Assertions.assertEquals(
+                    "nest-old-v1", 
readFtpFile("/tmp/seatunnel/update/dst/subdir/nested.bin"));
+        } finally {
+            deleteFileFromContainer(ftpHomeDir + "/tmp/seatunnel/update");
+        }
+    }
 
-        Container.ExecResult execResult =
-                
container.executeJob("/text/ftp_binary_update_non_recursive_strict_checksum.conf");
-        Assertions.assertEquals(0, execResult.getExitCode(), 
execResult.getStderr());
-        Assertions.assertEquals("root-same-v1", 
readFtpFile("/tmp/seatunnel/update/dst/root.bin"));
-        Assertions.assertEquals(
-                "nest-old-v1", 
readFtpFile("/tmp/seatunnel/update/dst/subdir/nested.bin"));
+    /**
+     * Continuous discovery is only considered stopped after the engine 
reaches CANCELED and the
+     * submit command exits, so cleanup regressions still fail this E2E test.
+     */
+    private void assertContinuousJobStopsAfterCancel(
+            TestContainer container,
+            String jobId,
+            CompletableFuture<Container.ExecResult> jobFuture)
+            throws IOException, InterruptedException {
+        if (jobFuture == null) {
+            return;
+        }
+        Awaitility.await()
+                .atMost(30, TimeUnit.SECONDS)
+                .untilAsserted(
+                        () -> {
+                            Container.ExecResult cancelResult = 
container.cancelJob(jobId);
+                            Assertions.assertEquals(
+                                    0, cancelResult.getExitCode(), 
cancelResult.getStderr());
+                        });
 
-        deleteFileFromContainer(ftpHomeDir + "/tmp/seatunnel/update");
+        Awaitility.await()
+                .atMost(30, TimeUnit.SECONDS)
+                .untilAsserted(
+                        () ->
+                                Assertions.assertEquals(
+                                        "CANCELED",
+                                        container.getJobStatus(jobId),
+                                        "Continuous job should be canceled 
before the test exits."));
+
+        Awaitility.await()
+                .atMost(180, TimeUnit.SECONDS)
+                .pollInterval(2, TimeUnit.SECONDS)
+                .until(jobFuture::isDone);
+
+        try {
+            Container.ExecResult execResult = jobFuture.get(30, 
TimeUnit.SECONDS);
+            Assertions.assertEquals(0, execResult.getExitCode(), 
execResult.getStderr());
+        } catch (Exception e) {
+            throw new RuntimeException("Wait continuous job exit failed.", e);
+        }
+    }
+
+    /**
+     * Stops a continuous job before deleting its shared test path, without 
hiding the original test
+     * failure when cleanup also fails.
+     */
+    private void cleanupContinuousJob(
+            TestContainer container,
+            String jobId,
+            CompletableFuture<Container.ExecResult> jobFuture,
+            String cleanupPath,
+            Throwable testFailure)
+            throws Throwable {
+        Throwable cleanupFailure = null;
+        try {
+            assertContinuousJobStopsAfterCancel(container, jobId, jobFuture);
+        } catch (Throwable failure) {
+            cleanupFailure = failure;
+        }
+
+        if (cleanupFailure == null) {
+            try {
+                deleteFileFromContainer(cleanupPath);
+            } catch (Throwable failure) {
+                cleanupFailure = failure;
+            }
+        }
+
+        if (cleanupFailure == null) {
+            return;
+        }
+        if (testFailure != null) {
+            testFailure.addSuppressed(cleanupFailure);
+        } else {
+            throw cleanupFailure;
+        }
     }
 
     @TestTemplate
@@ -664,6 +768,19 @@ public class FtpFileIT extends TestSuiteBase implements 
TestResource {
         // test read recursive file path
         helper.execute("/text/ftp_file_text_recursive_to_assert.conf");
         helper.execute("/text/ftp_file_text_non_recursive_to_assert.conf");
+
+        String homePath = ftpHomeDir;
+        String sink01 = "/tmp/seatunnel/json/sink/multiplesource/fake01";
+        String sink02 = "/tmp/seatunnel/json/sink/multiplesource/fake02";
+        deleteFileFromContainer(homePath + sink01);
+        deleteFileFromContainer(homePath + sink02);
+        // Keep a dedicated source file for each logical table. Sharing one 
FTP path between the
+        // two multiple-table entries can leave the second reader with no 
physical file to open on
+        // slower CI runs.
+        ensureMultipleTableJsonInputFiles();
+        
helper.execute("/json/ftp_file_json_to_assert_with_multipletable.conf");
+        Assertions.assertEquals(getFileListFromContainer(homePath + 
sink01).size(), 1);
+        Assertions.assertEquals(getFileListFromContainer(homePath + 
sink02).size(), 1);
     }
 
     @TestTemplate
@@ -819,6 +936,11 @@ public class FtpFileIT extends TestSuiteBase implements 
TestResource {
         return catResult.getStdout() == null ? "" : 
catResult.getStdout().trim();
     }
 
+    /**
+     * Checks whether a file exists in the FTP container without creating 
parent directories.
+     *
+     * <p>This helper is used by negative assertions where creating the path 
would hide regressions.
+     */
     private boolean isFtpFileExists(String ftpPath) throws IOException, 
InterruptedException {
         String containerPath = ftpHomeDir + ftpPath;
         Container.ExecResult result =
@@ -984,6 +1106,10 @@ public class FtpFileIT extends TestSuiteBase implements 
TestResource {
     }
 
     private void ensureReadJsonInputFile() throws IOException, 
InterruptedException {
+        // Reset the shared JSON source directory for each template run. The 
FTP container is
+        // reused across engine variants, so stale fake01/fake02 inputs from 
the multiple-table
+        // case can otherwise leak into the single-table assert and inflate 
the row count.
+        deleteFileFromContainer(ftpHomeDir + "/tmp/seatunnel/read/json");
         Container.ExecResult mkdirResult =
                 ftpContainer.execInContainer(
                         "sh",
@@ -1002,6 +1128,39 @@ public class FtpFileIT extends TestSuiteBase implements 
TestResource {
         Assertions.assertEquals(0, chmodResult.getExitCode(), 
chmodResult.getStderr());
     }
 
+    /**
+     * The multiple-table FTP JSON test reads two logical tables from two 
table configs. Give each
+     * table its own physical source file so one reader never depends on a 
file already consumed by
+     * the other table path. This helper owns {@code tmp/seatunnel/read/json} 
for the suite and
+     * rebuilds it before the multiple-table run.
+     */
+    private void ensureMultipleTableJsonInputFiles() throws IOException, 
InterruptedException {
+        // Rebuild the multiple-table source tree from scratch so previous 
template runs never
+        // leave extra JSON files behind for the next engine invocation.
+        deleteFileFromContainer(ftpHomeDir + "/tmp/seatunnel/read/json");
+        copyJsonInputFileTo(
+                ftpHomeDir + 
"/tmp/seatunnel/read/json/fake01/name=tyrantlucifer/hobby=coding");
+        copyJsonInputFileTo(
+                ftpHomeDir + 
"/tmp/seatunnel/read/json/fake02/name=tyrantlucifer/hobby=coding");
+        Container.ExecResult chmodResult =
+                ftpContainer.execInContainer(
+                        "sh", "-c", "chmod -R 777 " + ftpHomeDir + 
"/tmp/seatunnel/read/json");
+        Assertions.assertEquals(0, chmodResult.getExitCode(), 
chmodResult.getStderr());
+    }
+
+    /**
+     * Copies the canonical JSON fixture into an isolated multiple-table input 
directory.
+     *
+     * @param directory destination directory inside the FTP container
+     */
+    private void copyJsonInputFileTo(String directory) throws IOException, 
InterruptedException {
+        Container.ExecResult mkdirResult =
+                ftpContainer.execInContainer("sh", "-c", "mkdir -p " + 
directory);
+        Assertions.assertEquals(0, mkdirResult.getExitCode(), 
mkdirResult.getStderr());
+        ContainerUtil.copyFileIntoContainers(
+                "/json/e2e.json", directory + "/e2e.json", ftpContainer);
+    }
+
     @SneakyThrows
     private List<String> getFileListFromContainer(String path) {
         String command = "ls -1 " + path;
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/resources/json/ftp_file_json_to_assert_with_multipletable.conf
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/resources/json/ftp_file_json_to_assert_with_multipletable.conf
index eb5cf0b7e6..9bbcf39a66 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/resources/json/ftp_file_json_to_assert_with_multipletable.conf
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-ftp-e2e/src/test/resources/json/ftp_file_json_to_assert_with_multipletable.conf
@@ -35,7 +35,7 @@ source {
           port = 21
           user = seatunnel
           password = pass
-          path = "/tmp/seatunnel/read/json"
+          path = "/tmp/seatunnel/read/json/fake01"
           file_format_type = "json"
           schema = {
             table = "fake01"
@@ -78,7 +78,7 @@ source {
           port = 21
           user = seatunnel
           password = pass
-          path = "/tmp/seatunnel/read/json"
+          path = "/tmp/seatunnel/read/json/fake02"
           file_format_type = "json"
           schema = {
             table = "fake02"
@@ -137,4 +137,4 @@ sink {
     filename_time_format = "yyyy.MM.dd"
     is_enable_transaction = true
   }
-}
\ No newline at end of file
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-sftp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/fstp/SftpFileIT.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-sftp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/fstp/SftpFileIT.java
index f3a76c3dc4..8c3a49e560 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-sftp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/fstp/SftpFileIT.java
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-sftp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/fstp/SftpFileIT.java
@@ -285,10 +285,11 @@ public class SftpFileIT extends TestSuiteBase implements 
TestResource {
             type = {EngineType.FLINK, EngineType.SPARK},
             disabledReason = "Continuous discovery is a long-running job; only 
run in zeta engine.")
     public void 
testSftpBinaryUpdateModeContinuousDiscoveryDistcp(TestContainer container)
-            throws IOException, InterruptedException {
+            throws Throwable {
         resetContinuousTestPath();
         String jobId = String.valueOf(JobIdGenerator.newJobId());
         CompletableFuture<Container.ExecResult> jobFuture = null;
+        Throwable testFailure = null;
         try {
             putSftpFile(SFTP_CONTAINER_HOME + 
"/tmp/seatunnel/continuous/src/test1.bin", "abc");
 
@@ -342,12 +343,16 @@ public class SftpFileIT extends TestSuiteBase implements 
TestResource {
                                                     SFTP_CONTAINER_HOME
                                                             + 
"/tmp/seatunnel/continuous/dst/test2.bin")));
 
-            Container.ExecResult cancelResult = container.cancelJob(jobId);
-            Assertions.assertEquals(0, cancelResult.getExitCode(), 
cancelResult.getStderr());
-            waitContinuousJobExit(container, jobId, jobFuture);
+        } catch (Throwable failure) {
+            testFailure = failure;
+            throw failure;
         } finally {
-            cancelContinuousJobQuietly(container, jobId, jobFuture);
-            deleteFileFromContainer(SFTP_CONTAINER_HOME + 
"/tmp/seatunnel/continuous");
+            cleanupContinuousJob(
+                    container,
+                    jobId,
+                    jobFuture,
+                    SFTP_CONTAINER_HOME + "/tmp/seatunnel/continuous",
+                    testFailure);
         }
     }
 
@@ -357,10 +362,11 @@ public class SftpFileIT extends TestSuiteBase implements 
TestResource {
             type = {EngineType.FLINK, EngineType.SPARK},
             disabledReason = "Continuous discovery is a long-running job; only 
run in zeta engine.")
     public void 
testSftpBinaryUpdateModeContinuousDiscoveryWithNonRecursiveScan(
-            TestContainer container) throws IOException, InterruptedException {
+            TestContainer container) throws Throwable {
         resetContinuousTestPath();
         String jobId = String.valueOf(JobIdGenerator.newJobId());
         CompletableFuture<Container.ExecResult> jobFuture = null;
+        Throwable testFailure = null;
         try {
             jobFuture =
                     CompletableFuture.supplyAsync(
@@ -401,12 +407,16 @@ public class SftpFileIT extends TestSuiteBase implements 
TestResource {
                                                     SFTP_CONTAINER_HOME
                                                             + 
"/tmp/seatunnel/continuous/dst/subdir/nested.bin")));
 
-            Container.ExecResult cancelResult = container.cancelJob(jobId);
-            Assertions.assertEquals(0, cancelResult.getExitCode(), 
cancelResult.getStderr());
-            waitContinuousJobExit(container, jobId, jobFuture);
+        } catch (Throwable failure) {
+            testFailure = failure;
+            throw failure;
         } finally {
-            cancelContinuousJobQuietly(container, jobId, jobFuture);
-            deleteFileFromContainer(SFTP_CONTAINER_HOME + 
"/tmp/seatunnel/continuous");
+            cleanupContinuousJob(
+                    container,
+                    jobId,
+                    jobFuture,
+                    SFTP_CONTAINER_HOME + "/tmp/seatunnel/continuous",
+                    testFailure);
         }
     }
 
@@ -416,10 +426,11 @@ public class SftpFileIT extends TestSuiteBase implements 
TestResource {
             type = {EngineType.FLINK, EngineType.SPARK},
             disabledReason = "Continuous discovery is a long-running job; only 
run in zeta engine.")
     public void 
testSftpBinaryUpdateModeContinuousDiscoveryPostSyncDelete(TestContainer 
container)
-            throws IOException, InterruptedException {
+            throws Throwable {
         resetContinuousTestPath();
         String jobId = String.valueOf(JobIdGenerator.newJobId());
         CompletableFuture<Container.ExecResult> jobFuture = null;
+        Throwable testFailure = null;
         try {
             putSftpFile(
                     SFTP_CONTAINER_HOME + 
"/tmp/seatunnel/continuous/src/delete-test.bin", "abc");
@@ -458,12 +469,16 @@ public class SftpFileIT extends TestSuiteBase implements 
TestResource {
                                                             + 
"/tmp/seatunnel/continuous/src/delete-test.bin"),
                                             "source file should be deleted 
after checkpoint-gated post-sync commit"));
 
-            Container.ExecResult cancelResult = container.cancelJob(jobId);
-            Assertions.assertEquals(0, cancelResult.getExitCode(), 
cancelResult.getStderr());
-            waitContinuousJobExit(container, jobId, jobFuture);
+        } catch (Throwable failure) {
+            testFailure = failure;
+            throw failure;
         } finally {
-            cancelContinuousJobQuietly(container, jobId, jobFuture);
-            deleteFileFromContainer(SFTP_CONTAINER_HOME + 
"/tmp/seatunnel/continuous");
+            cleanupContinuousJob(
+                    container,
+                    jobId,
+                    jobFuture,
+                    SFTP_CONTAINER_HOME + "/tmp/seatunnel/continuous",
+                    testFailure);
         }
     }
 
@@ -473,10 +488,11 @@ public class SftpFileIT extends TestSuiteBase implements 
TestResource {
             type = {EngineType.FLINK, EngineType.SPARK},
             disabledReason = "Continuous discovery is a long-running job; only 
run in zeta engine.")
     public void 
testSftpBinaryUpdateModeContinuousDiscoveryPostSyncBackup(TestContainer 
container)
-            throws IOException, InterruptedException {
+            throws Throwable {
         resetContinuousTestPath();
         String jobId = String.valueOf(JobIdGenerator.newJobId());
         CompletableFuture<Container.ExecResult> jobFuture = null;
+        Throwable testFailure = null;
         try {
             putSftpFile(
                     SFTP_CONTAINER_HOME + 
"/tmp/seatunnel/continuous/src/backup-test.bin", "abc");
@@ -528,12 +544,16 @@ public class SftpFileIT extends TestSuiteBase implements 
TestResource {
                                                     > 0,
                                             "backup target should contain 
version-suffixed file"));
 
-            Container.ExecResult cancelResult = container.cancelJob(jobId);
-            Assertions.assertEquals(0, cancelResult.getExitCode(), 
cancelResult.getStderr());
-            waitContinuousJobExit(container, jobId, jobFuture);
+        } catch (Throwable failure) {
+            testFailure = failure;
+            throw failure;
         } finally {
-            cancelContinuousJobQuietly(container, jobId, jobFuture);
-            deleteFileFromContainer(SFTP_CONTAINER_HOME + 
"/tmp/seatunnel/continuous");
+            cleanupContinuousJob(
+                    container,
+                    jobId,
+                    jobFuture,
+                    SFTP_CONTAINER_HOME + "/tmp/seatunnel/continuous",
+                    testFailure);
         }
     }
 
@@ -542,11 +562,11 @@ public class SftpFileIT extends TestSuiteBase implements 
TestResource {
             value = {},
             type = {EngineType.FLINK, EngineType.SPARK},
             disabledReason = "Continuous discovery is a long-running job; only 
run in zeta engine.")
-    public void testSftpContinuousBackupRetentionCleanup(TestContainer 
container)
-            throws IOException, InterruptedException {
+    public void testSftpContinuousBackupRetentionCleanup(TestContainer 
container) throws Throwable {
         resetContinuousTestPath();
         String jobId = String.valueOf(JobIdGenerator.newJobId());
         CompletableFuture<Container.ExecResult> jobFuture = null;
+        Throwable testFailure = null;
         try {
             String retentionFile =
                     SFTP_CONTAINER_HOME
@@ -579,12 +599,16 @@ public class SftpFileIT extends TestSuiteBase implements 
TestResource {
                                                     "retention-old.bin.v*"),
                                             "retention should remove expired 
SeaTunnel backup files"));
 
-            Container.ExecResult cancelResult = container.cancelJob(jobId);
-            Assertions.assertEquals(0, cancelResult.getExitCode(), 
cancelResult.getStderr());
-            waitContinuousJobExit(container, jobId, jobFuture);
+        } catch (Throwable failure) {
+            testFailure = failure;
+            throw failure;
         } finally {
-            cancelContinuousJobQuietly(container, jobId, jobFuture);
-            deleteFileFromContainer(SFTP_CONTAINER_HOME + 
"/tmp/seatunnel/continuous");
+            cleanupContinuousJob(
+                    container,
+                    jobId,
+                    jobFuture,
+                    SFTP_CONTAINER_HOME + "/tmp/seatunnel/continuous",
+                    testFailure);
         }
     }
 
@@ -765,45 +789,34 @@ public class SftpFileIT extends TestSuiteBase implements 
TestResource {
     }
 
     /**
-     * Best-effort cleanup for assertion failures so one continuous test 
cannot leak into the next
-     * test's shared SFTP directories.
+     * Continuous discovery is only considered stopped after the engine 
reaches CANCELED and the
+     * submit command exits, so cleanup regressions still fail this E2E test.
      */
-    private void cancelContinuousJobQuietly(
+    private void assertContinuousJobStopsAfterCancel(
             TestContainer container,
             String jobId,
-            CompletableFuture<Container.ExecResult> jobFuture) {
-        if (jobFuture == null || jobFuture.isDone()) {
+            CompletableFuture<Container.ExecResult> jobFuture)
+            throws IOException, InterruptedException {
+        if (jobFuture == null) {
             return;
         }
-        try {
-            String status = container.getJobStatus(jobId);
-            if (!"CANCELED".equals(status)
-                    && !"FINISHED".equals(status)
-                    && !"FAILED".equals(status)) {
-                container.cancelJob(jobId);
-            }
-            Awaitility.await()
-                    .atMost(180, TimeUnit.SECONDS)
-                    .pollInterval(2, TimeUnit.SECONDS)
-                    .until(jobFuture::isDone);
-        } catch (Exception e) {
-            log.warn("Failed to clean up continuous SFTP job {}.", jobId, e);
-        }
-    }
-
-    /**
-     * Wait for the continuous discovery job to enter the terminal canceled 
state and for the
-     * asynchronous execute call to finish its post-job thread cleanup.
-     */
-    private void waitContinuousJobExit(
-            TestContainer container,
-            String jobId,
-            CompletableFuture<Container.ExecResult> jobFuture) {
         Awaitility.await()
-                .atMost(60, TimeUnit.SECONDS)
+                .atMost(30, TimeUnit.SECONDS)
+                .untilAsserted(
+                        () -> {
+                            Container.ExecResult cancelResult = 
container.cancelJob(jobId);
+                            Assertions.assertEquals(
+                                    0, cancelResult.getExitCode(), 
cancelResult.getStderr());
+                        });
+        Awaitility.await()
+                .atMost(30, TimeUnit.SECONDS)
                 .pollInterval(2, TimeUnit.SECONDS)
                 .untilAsserted(
-                        () -> Assertions.assertEquals("CANCELED", 
container.getJobStatus(jobId)));
+                        () ->
+                                Assertions.assertEquals(
+                                        "CANCELED",
+                                        container.getJobStatus(jobId),
+                                        "Continuous job should be canceled 
before the test exits."));
         Awaitility.await()
                 .atMost(180, TimeUnit.SECONDS)
                 .pollInterval(2, TimeUnit.SECONDS)
@@ -816,6 +829,42 @@ public class SftpFileIT extends TestSuiteBase implements 
TestResource {
         }
     }
 
+    /**
+     * Stops a continuous job before deleting its shared test path, without 
hiding the original test
+     * failure when cleanup also fails.
+     */
+    private void cleanupContinuousJob(
+            TestContainer container,
+            String jobId,
+            CompletableFuture<Container.ExecResult> jobFuture,
+            String cleanupPath,
+            Throwable testFailure)
+            throws Throwable {
+        Throwable cleanupFailure = null;
+        try {
+            assertContinuousJobStopsAfterCancel(container, jobId, jobFuture);
+        } catch (Throwable failure) {
+            cleanupFailure = failure;
+        }
+
+        if (cleanupFailure == null) {
+            try {
+                deleteFileFromContainer(cleanupPath);
+            } catch (Throwable failure) {
+                cleanupFailure = failure;
+            }
+        }
+
+        if (cleanupFailure == null) {
+            return;
+        }
+        if (testFailure != null) {
+            testFailure.addSuppressed(cleanupFailure);
+        } else {
+            throw cleanupFailure;
+        }
+    }
+
     private void waitUntilContainerTimeAfter(long epochSeconds) {
         Awaitility.await()
                 .atMost(10, TimeUnit.SECONDS)
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 626330bc8c..6c6730e0ef 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
@@ -145,7 +145,11 @@ public class KafkaIT extends TestSuiteBase implements 
TestResource {
 
     private List<ConsumerRecord<String, String>> nativeData;
 
-    /** Topics created dynamically during tests; cleaned up in {@link 
#tearDown()}. */
+    /**
+     * Topics created dynamically during tests.
+     *
+     * <p>They are cleaned up in {@link #tearDown()} to keep later Kafka E2E 
cases isolated.
+     */
     private final List<String> dynamicTopics = new CopyOnWriteArrayList<>();
 
     private final AtomicInteger startModeTestSequence = new AtomicInteger();
@@ -1363,7 +1367,11 @@ public class KafkaIT extends TestSuiteBase implements 
TestResource {
                 .count();
     }
 
-    /** Get the current end offset (LEO) on partition-0. */
+    /**
+     * Gets the current end offset on partition-0.
+     *
+     * <p>The exactly-once assertions use this as the visible sink baseline.
+     */
     private long endOffsetOnP0(String topic) {
         try (KafkaConsumer<String, String> c = new 
KafkaConsumer<>(kafkaConsumerConfig())) {
             TopicPartition tp0 = new TopicPartition(topic, 0);
@@ -2517,7 +2525,11 @@ public class KafkaIT extends TestSuiteBase implements 
TestResource {
         createKafkaTopic(topicName);
     }
 
-    /** Build the dynamic `-i key=value` variables for the exactly-once 
streaming template. */
+    /**
+     * Builds the dynamic {@code -i key=value} variables for the exactly-once 
streaming template.
+     *
+     * <p>Each test run uses isolated topics and a dedicated consumer group.
+     */
     private List<String> buildExactlyOnceStreamingVariables(
             String sourceTopic, String sinkTopic, String consumerGroup) {
         return Arrays.asList(
@@ -2785,29 +2797,49 @@ public class KafkaIT extends TestSuiteBase implements 
TestResource {
             long visibleEndOffsetExclusive =
                     
consumer.endOffsets(Collections.singletonList(topicPartition))
                             .get(topicPartition);
-            long lastObservedOffset = startOffset - 1;
+            long nextOffset = startOffset;
             int consecutiveEmptyPolls = 0;
             do {
                 ConsumerRecords<String, String> records = 
consumer.poll(Duration.ofMillis(100));
                 if (records.isEmpty()) {
                     consecutiveEmptyPolls++;
-                    continue;
-                }
-                consecutiveEmptyPolls = 0;
-                for (ConsumerRecord<String, String> record : 
records.records(topicPartition)) {
-                    if (record.offset() >= startOffset && 
seenOffsets.add(record.offset())) {
-                        data.add(record.value());
+                } else {
+                    consecutiveEmptyPolls = 0;
+                    for (ConsumerRecord<String, String> record : 
records.records(topicPartition)) {
+                        if (record.offset() >= startOffset && 
seenOffsets.add(record.offset())) {
+                            data.add(record.value());
+                        }
                     }
-                    lastObservedOffset = Math.max(lastObservedOffset, 
record.offset());
                 }
-            } while (lastObservedOffset < visibleEndOffsetExclusive - 1
-                    && consecutiveEmptyPolls < 20);
+                long currentPosition = consumer.position(topicPartition);
+                // Exactly-once topics can contain transaction control records 
or aborted records
+                // that advance offsets without surfacing visible 
READ_COMMITTED records. Track the
+                // consumer position so we do not stop early while the broker 
is still advancing the
+                // transactional scan range.
+                if (records.isEmpty()
+                        && shouldStopAfterEmptyReadCommittedPoll(
+                                currentPosition, nextOffset, 
consecutiveEmptyPolls)) {
+                    break;
+                }
+                nextOffset = currentPosition;
+            } while (nextOffset < visibleEndOffsetExclusive && 
consecutiveEmptyPolls < 20);
             return data;
         } finally {
             closeKafkaConsumer(consumer);
         }
     }
 
+    /**
+     * Decides whether repeated empty READ_COMMITTED polls are stable enough 
to stop scanning.
+     *
+     * <p>If the broker advances the consumer position across aborted or 
control records, the scan
+     * must continue even when no visible records are returned.
+     */
+    static boolean shouldStopAfterEmptyReadCommittedPoll(
+            long currentPosition, long previousPosition, int 
consecutiveEmptyPolls) {
+        return currentPosition == previousPosition && consecutiveEmptyPolls >= 
20;
+    }
+
     private Properties kafkaManualConsumerConfig() {
         Properties props = kafkaConsumerConfig();
         props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaITTest.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaITTest.java
new file mode 100644
index 0000000000..c30d0ec533
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaITTest.java
@@ -0,0 +1,38 @@
+/*
+ * 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.seatunnel.e2e.connector.kafka;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/** Verifies Kafka E2E helper behavior that is independent from the 
Testcontainers runtime. */
+public class KafkaITTest {
+
+    /**
+     * READ_COMMITTED scans must continue across aborted or control records 
that advance position.
+     */
+    @Test
+    public void shouldContinueEmptyReadCommittedPollWhenPositionAdvances() {
+        // Aborted transactions and control records can move the position 
without returning visible
+        // READ_COMMITTED records, so the helper must continue scanning that 
offset range.
+        
Assertions.assertFalse(KafkaIT.shouldStopAfterEmptyReadCommittedPoll(11L, 10L, 
20));
+        
Assertions.assertFalse(KafkaIT.shouldStopAfterEmptyReadCommittedPoll(15L, 10L, 
30));
+        
Assertions.assertFalse(KafkaIT.shouldStopAfterEmptyReadCommittedPoll(10L, 10L, 
19));
+        
Assertions.assertTrue(KafkaIT.shouldStopAfterEmptyReadCommittedPoll(10L, 10L, 
20));
+    }
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaKerberosIT.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaKerberosIT.java
index 9d24670b78..2c2f5c4609 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaKerberosIT.java
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaKerberosIT.java
@@ -103,6 +103,12 @@ public class KafkaKerberosIT extends TestSuiteBase 
implements TestResource {
     private GenericContainer<?> kafkaContainer;
     private GenericContainer<?> kerberosContainer;
 
+    /** Original JVM Krb5 config property that must be restored after this 
suite. */
+    private String previousKrb5Conf;
+
+    /** Original JVM JAAS config property that must be restored after this 
suite. */
+    private String previousJaasConfig;
+
     private final SeaTunnelRowType SEATUNNEL_ROW_TYPE =
             new SeaTunnelRowType(
                     new String[] {
@@ -143,6 +149,8 @@ public class KafkaKerberosIT extends TestSuiteBase 
implements TestResource {
     @BeforeAll
     @Override
     public void startUp() throws Exception {
+        previousKrb5Conf = System.getProperty("java.security.krb5.conf");
+        previousJaasConfig = 
System.getProperty("java.security.auth.login.config");
         System.setProperty(
                 "java.security.krb5.conf",
                 
ContainerUtil.getResourcesFile("/kerberos/krb5_local.conf").getPath());
@@ -479,5 +487,21 @@ public class KafkaKerberosIT extends TestSuiteBase 
implements TestResource {
         if (kerberosContainer != null) {
             kerberosContainer.close();
         }
+        // KafkaKerberosIT sets global JVM JAAS/Krb5 properties during 
startup. Clear them after
+        // the suite so the plain Kafka E2E tests in the same Maven JVM do not 
inherit Kerberos
+        // authentication settings by mistake.
+        restoreSystemProperty("java.security.krb5.conf", previousKrb5Conf);
+        restoreSystemProperty("java.security.auth.login.config", 
previousJaasConfig);
+    }
+
+    /**
+     * Restores a JVM security property to the value that existed before this 
test suite changed it.
+     */
+    private void restoreSystemProperty(String key, String previousValue) {
+        if (previousValue == null) {
+            System.clearProperty(key);
+        } else {
+            System.setProperty(key, previousValue);
+        }
     }
 }
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 ac82598d49..244bf7dcc0 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,11 +24,16 @@ env {
 source {
   Kafka {
     bootstrap.servers = "kafkaCluster:9092"
-    topic = "${sourceTopic}"
-    consumer.group = "${consumerGroup}"
+    topic = "${sourceTopic:kafka_topic_exactly_once_1}"
+    consumer.group = "${consumerGroup:test_exactly_once}"
     # The default format is json, which is optional
     format = text
-    start_mode = group_offsets
+    # The exactly-once test writes a readiness probe before the job starts, 
while the
+    # consumer group is still new and has no stored offsets. Read from 
earliest so
+    # KafkaSourceSplitEnumerator can assign the initial split on the first run.
+    # Restore tests resume from checkpointed splits, so start_mode is not used 
on
+    # the restored path.
+    start_mode = earliest
     kafka.config = {
           auto.offset.reset = "earliest"
           enable.auto.commit = "false"
@@ -42,7 +47,7 @@ transform {}
 sink{
   kafka {
         format = text
-        topic = "${sinkTopic}"
+        topic = "${sinkTopic:kafka_topic_exactly_once_2}"
         bootstrap.servers = "kafkaCluster:9092"
         semantics = EXACTLY_ONCE
         kafka.config = {
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-rocketmq-e2e/src/test/java/org/apache/seatunnel/e2e/connector/rocketmq/RocketMqIT.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-rocketmq-e2e/src/test/java/org/apache/seatunnel/e2e/connector/rocketmq/RocketMqIT.java
index 8cabce57d1..ef62a04d68 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-rocketmq-e2e/src/test/java/org/apache/seatunnel/e2e/connector/rocketmq/RocketMqIT.java
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-rocketmq-e2e/src/test/java/org/apache/seatunnel/e2e/connector/rocketmq/RocketMqIT.java
@@ -382,15 +382,18 @@ public class RocketMqIT extends TestSuiteBase implements 
TestResource {
     @TestTemplate
     public void testSourceRocketMqStartConfig(TestContainer container)
             throws IOException, InterruptedException {
+        final String topicName = "test_topic_group_" + uniqueTestSuffix();
+        final String consumerGroup = "SeaTunnel-Consumer-Group-" + 
uniqueTestSuffix();
+
         DefaultSeaTunnelRowSerializer serializer =
                 new DefaultSeaTunnelRowSerializer(
-                        "test_topic_group",
+                        topicName,
                         null,
                         SEATUNNEL_ROW_TYPE,
                         DEFAULT_FORMAT,
                         DEFAULT_FIELD_DELIMITER);
-        generateTestData(row -> serializer.serializeRow(row), 
"test_topic_group", 100, 150);
-        testRocketMqGroupOffsetsToConsole(container);
+        generateTestData(row -> serializer.serializeRow(row), topicName, 100, 
150);
+        executeRocketMqGroupOffsetsToConsole(container, topicName, 
consumerGroup);
     }
 
     @TestTemplate
@@ -412,10 +415,17 @@ public class RocketMqIT extends TestSuiteBase implements 
TestResource {
         Assertions.assertEquals(tag, data.get(key).getTag());
     }
 
-    public void testRocketMqGroupOffsetsToConsole(TestContainer container)
+    /**
+     * Uses isolated topic and consumer-group names so template invocations 
cannot share offsets.
+     */
+    private void executeRocketMqGroupOffsetsToConsole(
+            TestContainer container, String topicName, String consumerGroup)
             throws IOException, InterruptedException {
         Container.ExecResult execResult =
-                
container.executeJob("/rocketmq/rocketmq_source_group_offset_to_console.conf");
+                container.executeJob(
+                        
"/rocketmq/rocketmq_source_group_offset_to_console.conf",
+                        Arrays.asList(
+                                "sourceTopic=" + topicName, "consumerGroup=" + 
consumerGroup));
         Assertions.assertEquals(0, execResult.getExitCode(), 
execResult.getStderr());
     }
 
@@ -606,10 +616,18 @@ public class RocketMqIT extends TestSuiteBase implements 
TestResource {
             throws IOException, InterruptedException, MQBrokerException, 
RemotingException,
                     MQClientException, ExecutionException {
 
-        final String sourceTopic = "test_topic_restore";
-        final String sinkTopic = "test_topic_restore_output";
+        final String uniqueSuffix = uniqueTestSuffix();
+        final String sourceTopic = "test_topic_restore_" + uniqueSuffix;
+        final String sinkTopic = "test_topic_restore_output_" + uniqueSuffix;
+        final String consumerGroup = "restore_consumer_group_" + uniqueSuffix;
         final String payload = "Seatunnel RocketMQ Restore Test Data";
         final String jobId = "20260416";
+        final String[] restoreVariables =
+                new String[] {
+                    "sourceTopic=" + sourceTopic,
+                    "sinkTopic=" + sinkTopic,
+                    "consumerGroup=" + consumerGroup
+                };
 
         for (int i = 0; i < 20; i++) {
             Message msg = new Message(sourceTopic, (payload + "_initial_" + 
i).getBytes());
@@ -624,7 +642,9 @@ public class RocketMqIT extends TestSuiteBase implements 
TestResource {
                         () -> {
                             try {
                                 return container.executeJob(
-                                        
"/rocketmq/rocketmq_source_restore.conf", jobId);
+                                        
"/rocketmq/rocketmq_source_restore.conf",
+                                        jobId,
+                                        restoreVariables);
                             } catch (Exception e) {
                                 log.error("First job execution exception", e);
                                 throw new RuntimeException(e);
@@ -675,6 +695,10 @@ public class RocketMqIT extends TestSuiteBase implements 
TestResource {
             producer.send(msg, new MessageQueue(sourceTopic, 
RocketMqContainer.BROKER_NAME, 0));
         }
 
+        Awaitility.await()
+                .pollInterval(2, TimeUnit.SECONDS)
+                .atMost(1, TimeUnit.MINUTES)
+                .until(() -> getTopicMaxOffset(sourceTopic) >= 
srcEndBeforeStart + 25);
         long srcEndAfterAll = getTopicMaxOffset(sourceTopic);
         Assertions.assertTrue(
                 srcEndAfterAll >= srcEndBeforeStart + 25,
@@ -684,7 +708,8 @@ public class RocketMqIT extends TestSuiteBase implements 
TestResource {
         CompletableFuture.runAsync(
                 () -> {
                     try {
-                        
container.restoreJob("/rocketmq/rocketmq_source_restore.conf", jobId);
+                        container.restoreJob(
+                                "/rocketmq/rocketmq_source_restore.conf", 
jobId, restoreVariables);
                     } catch (Exception e) {
                         log.error("Restore job execution exception", e);
                         throw new RuntimeException(e);
@@ -702,13 +727,19 @@ public class RocketMqIT extends TestSuiteBase implements 
TestResource {
         Assertions.assertEquals(
                 expectedTotal,
                 finalSinkOffset,
-                "Sink offset mismatch after restore — possible duplicate 
consumption. "
+                "Sink offset mismatch after restore - possible duplicate 
consumption. "
                         + "Expected: "
                         + expectedTotal
                         + ", actual: "
                         + finalSinkOffset);
-
         List<String> allSinkMessages = pollMessagesFromOffset(sinkTopic, 0);
+        Assertions.assertEquals(
+                expectedTotal,
+                allSinkMessages.size(),
+                "Unexpected sink message count after restore. Expected: "
+                        + expectedTotal
+                        + ", actual: "
+                        + allSinkMessages.size());
         long initialCount =
                 allSinkMessages.stream().filter(body -> 
body.contains("_initial_")).count();
         long additionalCount =
@@ -815,6 +846,17 @@ public class RocketMqIT extends TestSuiteBase implements 
TestResource {
         }
     }
 
+    /**
+     * Returns a collision-resistant suffix for broker resources shared by 
template invocations.
+     * RocketMQ accepts independent topic creation requests, so no 
synchronization is needed for
+     * UUID-backed resource names.
+     *
+     * @return UUID text without separators, suitable for topic and 
consumer-group names
+     */
+    private String uniqueTestSuffix() {
+        return UUID.randomUUID().toString().replace("-", "");
+    }
+
     private void deleteTopicIfExist(String topicName) {
         DefaultMQAdminExt admin = new DefaultMQAdminExt();
         admin.setInstanceName(UUID.randomUUID().toString());
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-rocketmq-e2e/src/test/resources/rocketmq/rocketmq_source_group_offset_to_console.conf
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-rocketmq-e2e/src/test/resources/rocketmq/rocketmq_source_group_offset_to_console.conf
index f727cbd504..9cf1ec1422 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-rocketmq-e2e/src/test/resources/rocketmq/rocketmq_source_group_offset_to_console.conf
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-rocketmq-e2e/src/test/resources/rocketmq/rocketmq_source_group_offset_to_console.conf
@@ -25,10 +25,11 @@ env {
 source {
   Rocketmq {
     name.srv.addr = "rocketmq-e2e:9876"
-    topics = "test_topic_group"
+    topics = "${sourceTopic:test_topic_group}"
     plugin_output = "rocketmq_table"
     format = json
     start.mode = "CONSUME_FROM_GROUP_OFFSETS"
+    consumer.group = "${consumerGroup:SeaTunnel-Consumer-Group}"
     schema = {
       fields {
         id = bigint
@@ -63,4 +64,4 @@ sink {
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-rocketmq-e2e/src/test/resources/rocketmq/rocketmq_source_restore.conf
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-rocketmq-e2e/src/test/resources/rocketmq/rocketmq_source_restore.conf
index 26fd95ac4a..5bb4f05907 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-rocketmq-e2e/src/test/resources/rocketmq/rocketmq_source_restore.conf
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-rocketmq-e2e/src/test/resources/rocketmq/rocketmq_source_restore.conf
@@ -25,11 +25,11 @@ env {
 source {
   Rocketmq {
     name.srv.addr = "rocketmq-e2e:9876"
-    topics = "test_topic_restore"
+    topics = "${sourceTopic:test_topic_restore}"
     plugin_output = "rocketmq_restore_table"
     format = text
     start.mode = "CONSUME_FROM_FIRST_OFFSET"
-    consumer.group = "restore_consumer_group"
+    consumer.group = "${consumerGroup:restore_consumer_group}"
     schema = {
       fields {
         content = string
@@ -44,7 +44,7 @@ transform {
 sink {
   Rocketmq {
     name.srv.addr = "rocketmq-e2e:9876"
-    topic = "test_topic_restore_output"
+    topic = "${sinkTopic:test_topic_restore_output}"
     format = text
     producer.send.sync = true
   }
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-tdengine-e2e/src/test/java/org/apache/seatunnel/e2e/connector/tdengine/TDengineIT.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-tdengine-e2e/src/test/java/org/apache/seatunnel/e2e/connector/tdengine/TDengineIT.java
index 6c60ac6bf1..2b9590f7a5 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-tdengine-e2e/src/test/java/org/apache/seatunnel/e2e/connector/tdengine/TDengineIT.java
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-tdengine-e2e/src/test/java/org/apache/seatunnel/e2e/connector/tdengine/TDengineIT.java
@@ -38,6 +38,7 @@ import lombok.extern.slf4j.Slf4j;
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.ResultSet;
+import java.sql.SQLException;
 import java.sql.Statement;
 import java.time.Duration;
 import java.util.Arrays;
@@ -53,6 +54,14 @@ public class TDengineIT extends TestSuiteBase implements 
TestResource {
     private static final String NETWORK_ALIASES1 = "flink_e2e_tdengine_src";
     private static final String NETWORK_ALIASES2 = "flink_e2e_tdengine_sink";
     private static final int PORT = 6041;
+    /**
+     * Keeps the test database within the vnode budget that small CI machines 
can provide.
+     *
+     * <p>This is purely a CI resource workaround. The STABLE-backed source 
and sink queries in this
+     * suite do not assert per-vgroup parallelism, so a single vgroup is 
expected to behave the same
+     * as the default multi-vgroup layout for the exercised SQL paths.
+     */
+    private static final String SINGLE_VGROUP_DATABASE_OPTIONS = " KEEP 3650 
VGROUPS 1";
 
     private GenericContainer<?> tdengineServer1;
     private GenericContainer<?> tdengineServer2;
@@ -90,17 +99,7 @@ public class TDengineIT extends TestSuiteBase implements 
TestResource {
         log.info("TDengine container started");
         connection1 = createConnect(tdengineServer1);
         connection2 = createConnect(tdengineServer2);
-        // wait for TDengine fully start
-        given().ignoreExceptions()
-                .await()
-                .atLeast(100, TimeUnit.MILLISECONDS)
-                .pollInterval(1, TimeUnit.SECONDS)
-                .atMost(120, TimeUnit.SECONDS)
-                .untilAsserted(
-                        () ->
-                                Assertions.assertEquals(
-                                        Boolean.TRUE,
-                                        connection1.isValid(100) & 
connection2.isValid(100)));
+        waitForTDengineReady();
         testDataCount = generateTestDataSet();
         log.info("tdengine testDataCount=" + testDataCount); // rowCount=8
     }
@@ -108,16 +107,16 @@ public class TDengineIT extends TestSuiteBase implements 
TestResource {
     @SneakyThrows
     private int generateTestDataSet() {
         int rowCount;
-        waitForDatabaseReady(connection1, "CREATE DATABASE power KEEP 3650");
         try (Statement stmt = connection1.createStatement()) {
+            stmt.execute("CREATE DATABASE power" + 
SINGLE_VGROUP_DATABASE_OPTIONS);
             stmt.execute(
                     "CREATE STABLE power.meters (ts TIMESTAMP, current FLOAT, 
voltage INT, phase FLOAT, off BOOL, nc NCHAR(10)) "
                             + "TAGS (location BINARY(64), groupId INT)");
             String sql = getSQL();
             rowCount = stmt.executeUpdate(sql);
         }
-        waitForDatabaseReady(connection2, "CREATE DATABASE power2 KEEP 3650");
         try (Statement stmt = connection2.createStatement()) {
+            stmt.execute("CREATE DATABASE power2" + 
SINGLE_VGROUP_DATABASE_OPTIONS);
             stmt.execute(
                     "CREATE STABLE power2.meters2 (ts TIMESTAMP, current 
FLOAT, voltage INT, phase FLOAT, off BOOL, nc NCHAR(10)) "
                             + "TAGS (location BINARY(64), groupId INT)");
@@ -134,8 +133,8 @@ public class TDengineIT extends TestSuiteBase implements 
TestResource {
                     "CREATE STABLE power2.meters4 (ts TIMESTAMP, current 
FLOAT, voltage INT, phase FLOAT, off BOOL, nc NCHAR(10)) "
                             + "TAGS (location BINARY(64), groupId INT)");
         }
-        waitForDatabaseReady(connection2, "CREATE DATABASE power3 KEEP 3650");
         try (Statement stmt = connection2.createStatement()) {
+            stmt.execute("CREATE DATABASE power3" + 
SINGLE_VGROUP_DATABASE_OPTIONS);
             stmt.execute(
                     "CREATE STABLE power3.meters5 (ts TIMESTAMP, current 
FLOAT, voltage INT, phase FLOAT, off BOOL, nc NCHAR(10)) "
                             + "TAGS (location BINARY(64), groupId INT)");
@@ -143,24 +142,6 @@ public class TDengineIT extends TestSuiteBase implements 
TestResource {
         return rowCount;
     }
 
-    /**
-     * Waits for TDengine dnode to be fully online before creating the 
database. The REST adapter
-     * (port 6041) may accept connections before the dnode registers with the 
management node,
-     * causing "Out of dnodes" errors on CREATE DATABASE.
-     */
-    private void waitForDatabaseReady(Connection connection, String 
createDbSql) {
-        given().ignoreExceptions()
-                .await()
-                .pollInterval(2, TimeUnit.SECONDS)
-                .atMost(60, TimeUnit.SECONDS)
-                .untilAsserted(
-                        () -> {
-                            try (Statement stmt = 
connection.createStatement()) {
-                                stmt.execute(createDbSql);
-                            }
-                        });
-    }
-
     @TestTemplate
     public void testTDengine(TestContainer container) throws Exception {
         Container.ExecResult execResult =
@@ -224,6 +205,41 @@ public class TDengineIT extends TestSuiteBase implements 
TestResource {
         return conn;
     }
 
+    /**
+     * Waits until TDengine accepts JDBC traffic and can allocate vgroups for 
new databases.
+     *
+     * <p>Port readiness alone does not prove that TDengine can create the 
test databases.
+     */
+    private void waitForTDengineReady() {
+        given().ignoreExceptions()
+                .await()
+                .pollInterval(1, TimeUnit.SECONDS)
+                .atMost(120, TimeUnit.SECONDS)
+                .untilAsserted(
+                        () -> {
+                            Assertions.assertTrue(connection1.isValid(100));
+                            Assertions.assertTrue(connection2.isValid(100));
+                            verifyDatabaseCanBeCreated(connection1, 
"seatunnel_ready_src");
+                            verifyDatabaseCanBeCreated(connection2, 
"seatunnel_ready_sink");
+                        });
+    }
+
+    /**
+     * TDengine can report a healthy socket before the storage node is ready 
to create databases.
+     *
+     * <p>The probe database is intentionally kept until the container stops. 
Reusing the same probe
+     * avoids repeated create/drop churn while the service is still becoming 
ready.
+     */
+    private void verifyDatabaseCanBeCreated(Connection connection, String 
databaseName)
+            throws SQLException {
+        try (Statement statement = connection.createStatement()) {
+            statement.execute(
+                    "CREATE DATABASE IF NOT EXISTS "
+                            + databaseName
+                            + SINGLE_VGROUP_DATABASE_OPTIONS);
+        }
+    }
+
     @AfterAll
     @Override
     public void tearDown() throws Exception {

Reply via email to