CAMEL-22525: Fix flaky AS2 tests by eliminating port allocation race condition#22981
CAMEL-22525: Fix flaky AS2 tests by eliminating port allocation race condition#22981gnodet wants to merge 2 commits intoapache:mainfrom
Conversation
…condition Eliminate the TOCTOU race between AvailablePortFinder probing a port (open/close ServerSocket) and AS2ServerConnection binding to it. Instead, bind to port 0 so the OS assigns a free port atomically, then read the actual port via the new getLocalPort() method. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
🌟 Thank you for your contribution to the Apache Camel project! 🌟 🐫 Apache Camel Committers, please review the following items:
|
|
🧪 CI tested the following changed modules:
All tested modules (9 modules)
|
| protected static final Duration HTTP_CONNECTION_POOL_TTL = Duration.ofMinutes(15); | ||
| protected static final Certificate[] VALIDATE_SIGNING_CERTIFICATE_CHAIN = null; | ||
| protected static final String RECIPIENT_DELIVERY_ADDRESS = "http://localhost:" + TARGET_PORT.getPort() + "/handle-receipts"; | ||
| protected static String RECIPIENT_DELIVERY_ADDRESS; |
There was a problem hiding this comment.
given that it is no more a final attribute:
"Rename this field "RECIPIENT_DELIVERY_ADDRESS" to match the regular expression '^[a-z][a-zA-Z0-9]*$'."
There was a problem hiding this comment.
Claude Code on behalf of Guillaume Nodet
Good catch — renamed RECIPIENT_DELIVERY_ADDRESS to recipientDeliveryAddress since it's no longer a constant. Fixed in 7413a4a.
| protected static final String TARGET_HOST = "localhost"; | ||
| @RegisterExtension | ||
| protected static AvailablePortFinder.Port TARGET_PORT = AvailablePortFinder.find(); | ||
| protected static int TARGET_PORT; |
There was a problem hiding this comment.
given that it is no more a final attribute:
"Rename this field "RECIPIENT_DELIVERY_ADDRESS" to match the regular expression '^[a-z][a-zA-Z0-9]*$'."
There was a problem hiding this comment.
Claude Code on behalf of Guillaume Nodet
Good catch — renamed TARGET_PORT to targetPort since it's no longer a constant. Fixed in 7413a4a.
| } | ||
|
|
||
| public int getLocalPort() { | ||
| return serversocket != null ? serversocket.getLocalPort() : -1; |
There was a problem hiding this comment.
shouldn't we return 0 to let the system pick an ephemeral port?
There was a problem hiding this comment.
Claude Code on behalf of Guillaume Nodet
Returning -1 is intentional — it follows the ServerSocket.getLocalPort() convention where -1 means "not bound / closed". Returning 0 would be misleading since 0 means "pick an ephemeral port" in socket APIs, which could cause silent bugs if the value is accidentally passed to another new ServerSocket(0) or bind() call. The -1 sentinel makes the error explicit.
Rename TARGET_PORT and RECIPIENT_DELIVERY_ADDRESS to camelCase since they are no longer final constants. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CAMEL-22525
Summary
getLocalPort()toAS2ServerConnectionandAS2AsyncMDNServerConnectionto expose the actual bound portAvailablePortFinderwhich probes then closes a socket, leaving a window for another process to steal the portTARGET_PORT→targetPort,RECIPIENT_DELIVERY_ADDRESS→recipientDeliveryAddress)Root Cause
AvailablePortFinder.find()opens aServerSocketon port 0, records the assigned port, then closes the socket. WhenAS2ServerConnectionlater tries to bind to that port, another process may have already claimed it — a classic TOCTOU (time-of-check-to-time-of-use) race condition. This was particularly flaky on s390x CI where multiple processes compete for ports.The prior fix (SO_REUSEADDR in #22095) helped with TIME_WAIT state but didn't eliminate the fundamental race.
Fix
Instead of pre-allocating a port number, tests now:
AS2ServerConnectionwith port 0 — the OS assigns a free port atomically duringbind()getLocalPort()methodThis completely eliminates the race condition since port assignment and binding happen in a single atomic operation.
Test plan
mvn formatter:format impsort:sort