Skip to content

CAMEL-22525: Fix flaky AS2 tests by eliminating port allocation race condition#22981

Open
gnodet wants to merge 2 commits intoapache:mainfrom
gnodet:CAMEL-22525-fix-flaky-as2-port-binding
Open

CAMEL-22525: Fix flaky AS2 tests by eliminating port allocation race condition#22981
gnodet wants to merge 2 commits intoapache:mainfrom
gnodet:CAMEL-22525-fix-flaky-as2-port-binding

Conversation

@gnodet
Copy link
Copy Markdown
Contributor

@gnodet gnodet commented May 6, 2026

CAMEL-22525

Summary

  • Add getLocalPort() to AS2ServerConnection and AS2AsyncMDNServerConnection to expose the actual bound port
  • Eliminate the TOCTOU race condition in AS2 tests by binding to port 0 (OS assigns a free port atomically) instead of pre-allocating a port with AvailablePortFinder which probes then closes a socket, leaving a window for another process to steal the port
  • Rename non-final static fields to camelCase (TARGET_PORTtargetPort, RECIPIENT_DELIVERY_ADDRESSrecipientDeliveryAddress)

Root Cause

AvailablePortFinder.find() opens a ServerSocket on port 0, records the assigned port, then closes the socket. When AS2ServerConnection later 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:

  1. Create AS2ServerConnection with port 0 — the OS assigns a free port atomically during bind()
  2. Read the actual port back via the new getLocalPort() method
  3. Use that port to configure the client

This completely eliminates the race condition since port assignment and binding happen in a single atomic operation.

Test plan

  • All 94 camel-as2-api tests pass
  • All 12 camel-as2-component tests pass (1 skipped: manual Mendelson test)
  • Code formatted with mvn formatter:format impsort:sort

…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>
@gnodet gnodet requested review from davsclaus and oscerd May 6, 2026 05:27
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

🧪 CI tested the following changed modules:

  • components/camel-as2/camel-as2-api
  • components/camel-as2/camel-as2-component
All tested modules (9 modules)
  • Camel :: AS2 :: API
  • Camel :: AS2 :: Component
  • Camel :: JBang :: MCP
  • Camel :: JBang :: Plugin :: Route Parser
  • Camel :: JBang :: Plugin :: TUI
  • Camel :: JBang :: Plugin :: Validate
  • Camel :: Launcher :: Container
  • Camel :: YAML DSL :: Validator
  • Camel :: YAML DSL :: Validator Maven Plugin

⚙️ View full build and test results

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]*$'."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]*$'."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we return 0 to let the system pick an ephemeral port?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants