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 e0219246c7 [CI][E2E] Stabilize SQL Server XA setup for schema 
evolution (#11137)
e0219246c7 is described below

commit e0219246c74435f962f294aa680231193e36ed83
Author: Daniel <[email protected]>
AuthorDate: Sun Jul 5 16:16:34 2026 +0800

    [CI][E2E] Stabilize SQL Server XA setup for schema evolution (#11137)
    
    Co-authored-by: DanielLeens <[email protected]>
---
 .../connectors/jdbc/SqlServerSchemaChangeIT.java   | 201 ++++++++++++++-------
 1 file changed, 138 insertions(+), 63 deletions(-)

diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-ddl/src/test/java/org/apache/seatunnel/connectors/jdbc/SqlServerSchemaChangeIT.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-ddl/src/test/java/org/apache/seatunnel/connectors/jdbc/SqlServerSchemaChangeIT.java
index 7f52dfcdcd..ef19e80305 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-ddl/src/test/java/org/apache/seatunnel/connectors/jdbc/SqlServerSchemaChangeIT.java
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-ddl/src/test/java/org/apache/seatunnel/connectors/jdbc/SqlServerSchemaChangeIT.java
@@ -30,6 +30,8 @@ import lombok.extern.slf4j.Slf4j;
 import java.io.IOException;
 import java.time.Duration;
 
+import static org.awaitility.Awaitility.await;
+
 @Slf4j
 public class SqlServerSchemaChangeIT extends AbstractSchemaChangeBaseIT {
 
@@ -41,12 +43,41 @@ public class SqlServerSchemaChangeIT extends 
AbstractSchemaChangeBaseIT {
     private static final String SQLSERVER_USER = "sa";
     private static final String ACCEPT_EULA = "ACCEPT_EULA";
     private static final String Y = "Y";
-    private static final String SA_PASSWORD = "SA_PASSWORD";
+    /**
+     * Uses the supported SQL Server container password variable instead of 
deprecated SA_PASSWORD.
+     */
+    private static final String MSSQL_SA_PASSWORD = "MSSQL_SA_PASSWORD";
+
     private static final String SQLSERVER_PASSWORD = "paanssy1234$";
     private static final int SQLSERVER_PORT = 1433;
-    private static final int SQLSERVER_XA_PORT = 5022;
-    private static final Duration SQLSERVER_SQLCMD_READY_TIMEOUT = 
Duration.ofMinutes(2);
-    private static final Duration SQLSERVER_SQLCMD_RETRY_INTERVAL = 
Duration.ofSeconds(2);
+    /** Exposes the RPC endpoint mapper required by SQL Server MSDTC inside 
containers. */
+    private static final int SQLSERVER_RPC_PORT = 135;
+    /** Exposes the MSDTC listener required by XA transactions in 
containerized SQL Server. */
+    private static final int SQLSERVER_DTC_PORT = 51000;
+    /** Executes real SQL Server login probes instead of relying on container 
logs alone. */
+    private static final String SQLSERVER_COMMAND = 
"/opt/mssql-tools18/bin/sqlcmd";
+    /** Gives SQL Server enough time to finish startup tasks before login and 
XA checks begin. */
+    private static final Duration SQLSERVER_READY_TIMEOUT = 
Duration.ofMinutes(2);
+    /** Exercises an authenticated query to prove the server is actually 
accepting connections. */
+    private static final String SQLSERVER_READY_QUERY = "SET NOCOUNT ON; 
SELECT 1";
+    /**
+     * SQL Server 2022 can emit the client-ready log before recovery and 
`msdb` upgrades finish in
+     * CI, so XA installation must wait for the stronger recovery-complete 
signal.
+     */
+    private static final String SQLSERVER_RECOVERY_COMPLETE_LOG = ".*Recovery 
is complete\\..*";
+    /**
+     * Verifies that the XA initialization procedure is visible before the 
exactly-once test runs.
+     */
+    private static final String SQLSERVER_XA_PROCEDURE_QUERY =
+            "SET NOCOUNT ON; SELECT CASE WHEN 
OBJECT_ID('master..xp_sqljdbc_xa_init_ex') IS NOT NULL THEN 1 ELSE 0 END";
+    /**
+     * Newer SQL Server Linux containers do not always expose the 
external-user toggle. Guard the
+     * setup statement so the XA bootstrap path stays compatible across image 
variants.
+     */
+    private static final String SQLSERVER_ENABLE_EXTERNAL_USER_IF_SUPPORTED =
+            "IF EXISTS (SELECT 1 FROM sys.configurations WHERE name = 
'external user enabled') "
+                    + "BEGIN EXEC sp_configure 'external user enabled', 1; 
RECONFIGURE; END";
+
     private final String SQLSERVER_JDBC_URL =
             "jdbc:sqlserver://%s:%s;databaseName=%s;"
                     + 
"useBulkCopyForBatchInsert=true;delayLoadingLobs=true;useFmtOnly=false;"
@@ -89,45 +120,46 @@ public class SqlServerSchemaChangeIT extends 
AbstractSchemaChangeBaseIT {
                         .withNetwork(NETWORK)
                         .withNetworkAliases(SQLSERVER_CONTAINER_HOST)
                         .withEnv(ACCEPT_EULA, Y)
-                        .withEnv(SA_PASSWORD, SQLSERVER_PASSWORD)
-                        .withEnv("MSSQL_ENABLE_HADR", "1")
-                        .withEnv("MSSQL_AGENT_ENABLED", "1")
-                        .withExposedPorts(SQLSERVER_PORT, SQLSERVER_XA_PORT)
-                        .waitingFor(
-                                Wait.forLogMessage(
-                                        ".*SQL Server is now ready for client 
connections.*\\n", 1))
+                        .withEnv(MSSQL_SA_PASSWORD, SQLSERVER_PASSWORD)
+                        .withEnv("MSSQL_AGENT_ENABLED", "true")
+                        // MSDTC ports are required for distributed XA 
transactions in SQL Server
+                        // Linux containers.
+                        .withEnv("MSSQL_RPC_PORT", 
String.valueOf(SQLSERVER_RPC_PORT))
+                        .withEnv("MSSQL_DTC_TCP_PORT", 
String.valueOf(SQLSERVER_DTC_PORT))
+                        .withExposedPorts(SQLSERVER_PORT)
+                        
.waitingFor(Wait.forLogMessage(SQLSERVER_RECOVERY_COMPLETE_LOG, 1))
                         .withStartupTimeout(Duration.ofMinutes(10))
                         .withLogConsumer(
                                 new Slf4jLogConsumer(
                                         
DockerLoggerFactory.getLogger(SQLSERVER_IMAGE)));
 
         container.setPortBindings(
-                Lists.newArrayList(
-                        String.format("%d:%d", SQLSERVER_PORT, SQLSERVER_PORT),
-                        String.format("%d:%d", SQLSERVER_XA_PORT, 
SQLSERVER_XA_PORT)));
+                Lists.newArrayList(String.format("%d:%d", SQLSERVER_PORT, 
SQLSERVER_PORT)));
 
         container.start();
         try {
-            // This set of commands prepares for the subsequent enabling of 
the external user
-            // enabled configuration (for XA transaction support)
-            execSqlcmdWithRetry(
+            waitForSqlServerLogin(container);
+            // Prepare the SQL Server instance before installing the XA 
procedures that the
+            // exactly-once connector path depends on.
+            assertSqlCommandSucceeded(
                     container,
-                    "configure advanced options",
-                    "EXEC sp_configure 'show advanced options', 1; 
RECONFIGURE;");
-
-            // Enable external user access permissions, which is a requirement 
for SQL Server to
-            // support XA distributed transactions.
-            execSqlcmdWithRetry(
+                    "EXEC sp_configure 'show advanced options', 1; 
RECONFIGURE;",
+                    "enable SQL Server advanced options");
+            // Some SQL Server variants still expose this switch, while the 
current Linux image no
+            // longer does. Keep the bootstrap compatible by enabling it only 
when the option
+            // exists.
+            assertSqlCommandSucceeded(
                     container,
-                    "enable external user access",
-                    "EXEC sp_configure 'external user enabled', 1; 
RECONFIGURE;");
+                    SQLSERVER_ENABLE_EXTERNAL_USER_IF_SUPPORTED,
+                    "enable external user access");
 
             log.info("Installing stored procedures sp_sqljdbc_xa_install.");
-            execSqlcmdWithRetry(
+            assertSqlCommandSucceeded(
                     container,
-                    "install SQL Server XA stored procedures",
                     "IF NOT EXISTS (SELECT * FROM sys.objects WHERE name = 
'xp_sqljdbc_xa_init_ex') "
-                            + "EXEC sp_sqljdbc_xa_install");
+                            + "EXEC sp_sqljdbc_xa_install",
+                    "install SQL Server XA stored procedures");
+            waitForXaStoredProcedure(container);
         } catch (IOException | InterruptedException e) {
             log.error("XA procedure installation failed: ", e);
             throw new RuntimeException(e);
@@ -141,43 +173,86 @@ public class SqlServerSchemaChangeIT extends 
AbstractSchemaChangeBaseIT {
     }
 
     /**
-     * SQL Server 2022 can emit the generic ready log before system database 
upgrades finish, so
-     * retry sqlcmd setup steps until login and execution both succeed.
+     * Recovery-complete is the earliest safe container log marker, and a real 
authenticated probe
+     * confirms that `sqlcmd` can connect before XA setup starts.
+     */
+    private void waitForSqlServerLogin(GenericContainer<?> container) {
+        await().pollDelay(Duration.ofSeconds(1))
+                .pollInterval(Duration.ofSeconds(2))
+                .atMost(SQLSERVER_READY_TIMEOUT)
+                .until(() -> isSqlCommandSuccessful(container, 
SQLSERVER_READY_QUERY));
+    }
+
+    /**
+     * XA setup must complete before the exactly-once test starts, otherwise 
the job fails later
+     * with a missing xp_sqljdbc_xa_init_ex procedure.
      */
-    private void execSqlcmdWithRetry(GenericContainer<?> container, String 
description, String sql)
+    private void waitForXaStoredProcedure(GenericContainer<?> container) {
+        await().pollDelay(Duration.ofSeconds(1))
+                .pollInterval(Duration.ofSeconds(2))
+                .atMost(SQLSERVER_READY_TIMEOUT)
+                .until(
+                        () ->
+                                sqlCommandOutputContains(
+                                        container, 
SQLSERVER_XA_PROCEDURE_QUERY, "1"));
+    }
+
+    /**
+     * Fails fast when a setup statement is rejected so the test does not 
continue on false success.
+     */
+    private void assertSqlCommandSucceeded(
+            GenericContainer<?> container, String sql, String description)
             throws IOException, InterruptedException {
-        long deadline = System.nanoTime() + 
SQLSERVER_SQLCMD_READY_TIMEOUT.toNanos();
-        Container.ExecResult lastResult = null;
-        while (System.nanoTime() < deadline) {
-            lastResult =
-                    container.execInContainer(
-                            "/opt/mssql-tools18/bin/sqlcmd",
-                            "-S",
-                            "localhost",
-                            "-U",
-                            SQLSERVER_USER,
-                            "-P",
-                            SQLSERVER_PASSWORD,
-                            "-Q",
-                            sql,
-                            "-C");
-            if (lastResult.getExitCode() == 0) {
-                return;
-            }
-            log.info(
-                    "sqlcmd step [{}] is not ready yet, exitCode={}, 
stdout={}, stderr={}",
-                    description,
-                    lastResult.getExitCode(),
-                    lastResult.getStdout(),
-                    lastResult.getStderr());
-            Thread.sleep(SQLSERVER_SQLCMD_RETRY_INTERVAL.toMillis());
+        Container.ExecResult execResult = executeSqlCommand(container, sql);
+        if (execResult.getExitCode() != 0) {
+            throw new IllegalStateException(
+                    String.format(
+                            "Failed to %s, exitCode=%s, stdout=%s, stderr=%s",
+                            description,
+                            execResult.getExitCode(),
+                            execResult.getStdout(),
+                            execResult.getStderr()));
+        }
+    }
+
+    /** Returns true only when sqlcmd completes successfully with the supplied 
statement. */
+    private boolean isSqlCommandSuccessful(GenericContainer<?> container, 
String sql) {
+        try {
+            return executeSqlCommand(container, sql).getExitCode() == 0;
+        } catch (IOException | InterruptedException e) {
+            return false;
         }
-        throw new IllegalStateException(
-                String.format(
-                        "Timed out waiting for sqlcmd step [%s] to succeed, 
last exitCode=%s, stdout=%s, stderr=%s",
-                        description,
-                        lastResult == null ? null : lastResult.getExitCode(),
-                        lastResult == null ? null : lastResult.getStdout(),
-                        lastResult == null ? null : lastResult.getStderr()));
+    }
+
+    /** Checks the normalized sqlcmd output for the expected probe result. */
+    private boolean sqlCommandOutputContains(
+            GenericContainer<?> container, String sql, String expected) {
+        try {
+            Container.ExecResult execResult = executeSqlCommand(container, 
sql);
+            return execResult.getExitCode() == 0
+                    && execResult.getStdout().replaceAll("\\s+", 
"").equals(expected);
+        } catch (IOException | InterruptedException e) {
+            return false;
+        }
+    }
+
+    /** Executes sqlcmd with consistent flags so login, setup, and probe 
checks share one path. */
+    private Container.ExecResult executeSqlCommand(GenericContainer<?> 
container, String sql)
+            throws IOException, InterruptedException {
+        return container.execInContainer(
+                SQLSERVER_COMMAND,
+                "-S",
+                "localhost",
+                "-U",
+                SQLSERVER_USER,
+                "-P",
+                SQLSERVER_PASSWORD,
+                "-Q",
+                sql,
+                "-b",
+                "-h",
+                "-1",
+                "-W",
+                "-C");
     }
 }

Reply via email to