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

gnodet pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 394b09aca966 CAMEL-23216: Fix flaky SFTP tests by verifying SSH 
protocol readiness (#24601)
394b09aca966 is described below

commit 394b09aca966b26fe3649ee629e1ece5b1e91525
Author: Guillaume Nodet <[email protected]>
AuthorDate: Sat Jul 11 17:06:47 2026 +0200

    CAMEL-23216: Fix flaky SFTP tests by verifying SSH protocol readiness 
(#24601)
    
    CAMEL-23216: Fix flaky SFTP tests by verifying SSH protocol readiness
    
    Replace TCP-only readiness check with SSH banner verification (RFC 4253)
    in SftpEmbeddedInfraService.waitForServerReady(). The embedded SSHD
    server accepts TCP connections before the SSH protocol layer finishes
    initializing, causing clients to time out during the SSH handshake —
    the root cause of 591 flaky outcomes across 20+ test containers.
    
    Changes:
    - Verify SSH version banner (SSH-2.0-...) instead of just TCP connectivity
    - Replace Thread.onSpinWait() busy-wait with Thread.sleep(100) backoff
    - Use StandardCharsets.US_ASCII per RFC 4253 §4.2
    - Make waitForServerReady() protected for subclass reuse
    - Track last banner value for better timeout diagnostics
    - Add waitForServerReady() call in SftpCertHostVerificationIT
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../integration/SftpCertHostVerificationIT.java    |  2 +
 .../embedded/SftpEmbeddedInfraService.java         | 49 +++++++++++++++++++---
 2 files changed, 46 insertions(+), 5 deletions(-)

diff --git 
a/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/sftp/integration/SftpCertHostVerificationIT.java
 
b/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/sftp/integration/SftpCertHostVerificationIT.java
index 839696ccef53..e3e246e67011 100644
--- 
a/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/sftp/integration/SftpCertHostVerificationIT.java
+++ 
b/components/camel-ftp/src/test/java/org/apache/camel/component/file/remote/sftp/integration/SftpCertHostVerificationIT.java
@@ -115,6 +115,8 @@ public class SftpCertHostVerificationIT extends 
BaseServerTestSupport {
 
             sshd.start();
             port = sshd.getPort();
+
+            waitForServerReady();
         }
     };
 
diff --git 
a/test-infra/camel-test-infra-ftp/src/main/java/org/apache/camel/test/infra/ftp/services/embedded/SftpEmbeddedInfraService.java
 
b/test-infra/camel-test-infra-ftp/src/main/java/org/apache/camel/test/infra/ftp/services/embedded/SftpEmbeddedInfraService.java
index 35b754ddd1cc..6ce220931b3c 100644
--- 
a/test-infra/camel-test-infra-ftp/src/main/java/org/apache/camel/test/infra/ftp/services/embedded/SftpEmbeddedInfraService.java
+++ 
b/test-infra/camel-test-infra-ftp/src/main/java/org/apache/camel/test/infra/ftp/services/embedded/SftpEmbeddedInfraService.java
@@ -21,6 +21,7 @@ import java.io.File;
 import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.net.Socket;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -142,19 +143,57 @@ public class SftpEmbeddedInfraService extends 
AbstractService implements FtpInfr
         waitForServerReady();
     }
 
-    private void waitForServerReady() throws IOException {
+    /**
+     * Waits for the embedded SFTP server to be fully ready to accept SSH 
connections.
+     * <p/>
+     * This method goes beyond a simple TCP port check — it verifies that the 
SSH protocol layer is initialized by
+     * reading the server's SSH version banner (e.g., "SSH-2.0-..."). A 
TCP-only check can pass while the SSH layer is
+     * still initializing, causing clients to time out during the SSH 
handshake. This was the root cause of widespread
+     * flaky tests in camel-mina-sftp (CAMEL-23216).
+     * <p/>
+     * This method is {@code protected} so that subclasses that override 
{@link #setUpServer()} can call it after
+     * starting their own {@link SshServer} instance.
+     *
+     * @throws IOException if the server does not become ready within 30 
seconds
+     */
+    protected void waitForServerReady() throws IOException {
         long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(30);
         IOException lastException = null;
+        String lastBanner = null;
         while (System.nanoTime() < deadline) {
             try (Socket socket = new Socket()) {
-                socket.connect(new InetSocketAddress("localhost", port), 1000);
-                return;
+                socket.connect(new InetSocketAddress("localhost", port), 2000);
+                socket.setSoTimeout(5000);
+                // Read the SSH version banner to verify the SSH protocol 
layer is ready.
+                // The server sends "SSH-2.0-..." immediately upon accepting a 
connection.
+                // A successful TCP connect alone is not sufficient — the SSH 
handshake can
+                // still time out if the protocol layer hasn't finished 
initializing.
+                // SSH version strings are US-ASCII per RFC 4253 §4.2.
+                byte[] buf = new byte[256];
+                int len = socket.getInputStream().read(buf);
+                if (len > 0) {
+                    lastBanner = new String(buf, 0, len, 
StandardCharsets.US_ASCII).trim();
+                    if (lastBanner.startsWith("SSH-")) {
+                        LOG.debug("SFTP server ready on port {} (banner: {})", 
port, lastBanner);
+                        return;
+                    }
+                }
+                // TCP connected but no SSH banner yet — server still 
initializing
             } catch (IOException e) {
                 lastException = e;
-                Thread.onSpinWait();
+            }
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new IOException("Interrupted while waiting for SFTP 
server on port " + port, e);
             }
         }
-        throw new IOException("SFTP server not ready after 30 seconds on port 
" + port, lastException);
+        String detail = lastBanner != null
+                ? "last banner received: \"" + lastBanner + "\""
+                : "no banner received";
+        throw new IOException(
+                "SFTP server not ready after 30 seconds on port " + port + " 
(" + detail + ")", lastException);
     }
 
     protected PublickeyAuthenticator getPublickeyAuthenticator() {

Reply via email to