Skip to content

CAMEL-23902: Fix flaky UndertowWsConsumerRouteTest#24421

Merged
gnodet merged 2 commits into
apache:mainfrom
gnodet:camel-23902-fix-flaky-test-undertowwsconsumerrout
Jul 6, 2026
Merged

CAMEL-23902: Fix flaky UndertowWsConsumerRouteTest#24421
gnodet merged 2 commits into
apache:mainfrom
gnodet:camel-23902-fix-flaky-test-undertowwsconsumerrout

Conversation

@gnodet

@gnodet gnodet commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Claude Code on behalf of Guillaume Nodet

Fix flaky WebSocket tests in UndertowWsConsumerRouteTest that fail intermittently on JDK 17.

Root cause: JDK's WebSocket.sendText() returns a CompletableFuture that must complete before the next send. When two messages are sent in rapid succession on the same connection, the second send fails silently with IllegalStateException because WebsocketTestClient.sendTextMessage() ignores the returned future.

Fix:

  • Replace CountDownLatch-based assertTrue(await(10)) + subsequent assertions with Awaitility polling in echo(), echoMulti(), sendToAll(), and connectionKeyList()
  • Poll using getReceived().size() (index-based, no iterator) to avoid ConcurrentModificationException on the non-thread-safe ArrayList during concurrent polling. Content assertions via getReceived(String.class) are only called after the size check confirms all expected messages arrived.
  • For tests sending multiple messages on the same connection (echo, connectionKeyList), wait for the echo of the first message before sending the next, avoiding the JDK WebSocket pending-send conflict
  • Add awaitility test dependency to camel-undertow

Test plan

  • UndertowWsConsumerRouteTest passes (all 10 tests, 0 flakes)
  • Ran echo() test 3x consecutively — all green
  • Code formatting verified (mvn formatter:format impsort:sort — no changes needed)

🤖 Generated with Claude Code

@gnodet gnodet requested review from davsclaus and oscerd July 4, 2026 17:04
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

🌟 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

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • components/camel-undertow

ℹ️ Dependent modules were not tested because the total number of affected modules exceeded the threshold (50). Use the test-dependents label to force testing all dependents.


🔬 Scalpel shadow comparison — Scalpel: 37 tested, 29 compile-only — current: 37 all tested

Maveniverse Scalpel detected 66 affected modules (current approach: 37).

⚠️ Modules only in Scalpel (29)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-lucene
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-csimple-maven-plugin
  • camel-endpointdsl
  • camel-endpointdsl-support
  • camel-itest
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-kamelet-main
  • camel-launcher
  • camel-report-maven-plugin
  • camel-route-parser
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers
  • camel-yaml-dsl-maven-plugin
  • coverage
  • docs
  • dummy-component

Skip-tests mode would test 37 modules (1 direct + 36 downstream), skip tests for 29 (generated code, meta-modules)

Modules Scalpel would test (37)
  • camel-a2a
  • camel-consul
  • camel-cxf-rest
  • camel-cxf-soap
  • camel-cxf-spring-rest
  • camel-cxf-spring-soap
  • camel-cxf-spring-transport
  • camel-cxf-transport
  • camel-jbang-mcp
  • camel-jbang-plugin-mcp
  • camel-jbang-plugin-route-parser
  • camel-jbang-plugin-tui
  • camel-jbang-plugin-validate
  • camel-jsonpath
  • camel-knative-http
  • camel-launcher-container
  • camel-lra
  • camel-micrometer-observability
  • camel-micrometer-prometheus
  • camel-oauth
  • camel-observability-services
  • camel-openapi-validator
  • camel-opentelemetry
  • camel-opentelemetry2
  • camel-platform-http-main
  • camel-platform-http-vertx
  • camel-rest-openapi
  • camel-slack
  • camel-soap
  • camel-telemetry
  • camel-telemetry-dev
  • camel-undertow
  • camel-undertow-spring-security
  • camel-vertx-http
  • camel-yaml-dsl-validator
  • camel-yaml-dsl-validator-maven-plugin
  • camel-zookeeper
Modules with tests skipped (29)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-lucene
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-csimple-maven-plugin
  • camel-endpointdsl
  • camel-endpointdsl-support
  • camel-itest
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-kamelet-main
  • camel-launcher
  • camel-report-maven-plugin
  • camel-route-parser
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers
  • camel-yaml-dsl-maven-plugin
  • coverage
  • docs
  • dummy-component

ℹ️ Shadow mode — Scalpel observes but does not affect test execution. Learn more

Build reactor — dependencies compiled but only changed modules were tested (66 modules)
  • Camel :: AI :: A2A
  • Camel :: All Components Sync point
  • Camel :: Assembly
  • Camel :: CXF :: REST
  • Camel :: CXF :: REST :: Spring
  • Camel :: CXF :: SOAP
  • Camel :: CXF :: SOAP :: Spring
  • Camel :: CXF :: Transport
  • Camel :: CXF :: Transport :: Spring
  • Camel :: Catalog :: CSimple Maven Plugin (deprecated)
  • Camel :: Catalog :: Camel Catalog
  • Camel :: Catalog :: Camel Report Maven Plugin
  • Camel :: Catalog :: Camel Route Parser
  • Camel :: Catalog :: Console
  • Camel :: Catalog :: Dummy Component
  • Camel :: Catalog :: Lucene (deprecated)
  • Camel :: Catalog :: Maven
  • Camel :: Catalog :: Suggest
  • Camel :: Common Telemetry
  • Camel :: Component DSL
  • Camel :: Consul
  • Camel :: Coverage
  • Camel :: Docs
  • Camel :: Endpoint DSL
  • Camel :: Endpoint DSL :: Support
  • Camel :: Integration Tests
  • Camel :: JBang :: Core
  • Camel :: JBang :: Integration tests
  • Camel :: JBang :: MCP
  • Camel :: JBang :: Main
  • Camel :: JBang :: Plugin :: Edit
  • Camel :: JBang :: Plugin :: Generate
  • Camel :: JBang :: Plugin :: Kubernetes
  • Camel :: JBang :: Plugin :: MCP
  • Camel :: JBang :: Plugin :: Route Parser
  • Camel :: JBang :: Plugin :: TUI
  • Camel :: JBang :: Plugin :: Testing
  • Camel :: JBang :: Plugin :: Validate
  • Camel :: JSon Path
  • Camel :: Kamelet Main
  • Camel :: Knative HTTP
  • Camel :: Launcher
  • Camel :: Launcher :: Container
  • Camel :: Long-Running-Action
  • Camel :: Micrometer :: Observability 2
  • Camel :: Micrometer :: Prometheus
  • Camel :: OAuth
  • Camel :: Observability Services
  • Camel :: OpenAPI :: Validator
  • Camel :: OpenTelemetry (deprecated)
  • Camel :: Opentelemetry 2
  • Camel :: Platform HTTP :: Main
  • Camel :: Platform HTTP :: Vert.x
  • Camel :: REST OpenApi
  • Camel :: SOAP
  • Camel :: Slack
  • Camel :: Telemetry :: Dev
  • Camel :: Undertow
  • Camel :: Undertow Spring Security
  • Camel :: Vert.x :: HTTP
  • Camel :: YAML DSL
  • Camel :: YAML DSL :: Deserializers
  • Camel :: YAML DSL :: Maven Plugins
  • Camel :: YAML DSL :: Validator
  • Camel :: YAML DSL :: Validator Maven Plugin
  • Camel :: Zookeeper

⚙️ View full build and test results

The WebSocket tests in UndertowWsConsumerRouteTest fail intermittently on
JDK 17 because WebSocket.sendText() returns a CompletableFuture that must
complete before the next send. When two messages are sent in rapid
succession, the second send can fail silently with IllegalStateException.

Replace CountDownLatch-based assertTrue(await(10)) with Awaitility
polling in echo(), echoMulti(), sendToAll() and connectionKeyList().
For tests that send multiple messages on the same connection (echo,
connectionKeyList), wait for the echo of the first message before
sending the next to avoid the JDK WebSocket pending-send conflict.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@davsclaus davsclaus force-pushed the camel-23902-fix-flaky-test-undertowwsconsumerrout branch from 1b2b35d to 1f0f1ed Compare July 5, 2026 06:39

Copilot AI left a comment

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.

Pull request overview

This PR aims to stabilize UndertowWsConsumerRouteTest on JDK 17 by avoiding overlapping WebSocket.sendText() operations and by switching some latch-based waits to Awaitility-based polling.

Changes:

  • Add Awaitility to camel-undertow test dependencies.
  • In echo() and connectionKeyList(), wait for the first message delivery before sending a second message on the same WebSocket connection.
  • Replace several CountDownLatch-based await() usages with await().untilAsserted(...) assertions.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
components/camel-undertow/src/test/java/org/apache/camel/component/undertow/ws/UndertowWsConsumerRouteTest.java Updates WebSocket tests to sequence sends and to use Awaitility for polling assertions.
components/camel-undertow/pom.xml Adds Awaitility as a test-scoped dependency for the updated tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +171 to +175
wsclient1.sendTextMessage("Test1");
wsclient1.sendTextMessage("Test2");

assertTrue(wsclient1.await(10));
// Wait for the first echo before sending the next message to avoid
// IllegalStateException from JDK WebSocket when a send is still pending
await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> assertTrue(wsclient1.getReceived(String.class).contains("Test1")));

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.

Addressed in commit 01d5f72. Changed the Awaitility polling from untilAsserted() with getReceived(String.class).contains() (which iterates via for-each/iterator) to until(() -> getReceived().size() >= 1) which uses index-based size() — no iterator, safe under concurrent modification. The getReceived(String.class) call is now only made after the size gate confirms all expected messages have arrived and the listener thread is done mutating the list.

Comment on lines +195 to +199
await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> {
assertEquals(List.of("Gambas"), wsclient1.getReceived(String.class));
assertEquals(List.of("Calamares"), wsclient2.getReceived(String.class));
});

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.

Addressed in commit 01d5f72. Replaced untilAsserted() with iterator-based getReceived(String.class) by until(() -> getReceived().size() >= 1) for both clients. The getReceived(String.class) assertions now execute only after the size gate confirms delivery is complete — no concurrent iteration.

Comment on lines +216 to +220
.untilAsserted(() -> {
List<String> received1 = wsclient1.getReceived(String.class);
assertEquals(2, received1.size());
assertTrue(received1.contains("Gambas"));
assertTrue(received1.contains("Calamares"));

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.

Addressed in commit 01d5f72. The Awaitility polling now uses until(() -> getReceived().size() >= 2) for both clients (no iterator). The getReceived(String.class) list and contains() checks are only called after the size gate confirms both messages have arrived, so no concurrent modification risk.

Comment on lines +299 to +303
await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> assertConnected(wsclient1));
final String connectionKey1 = assertConnected(wsclient1);
assertNotNull(connectionKey1);
wsclient2.await(10);
await().atMost(10, TimeUnit.SECONDS)

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.

Addressed in commit 01d5f72. The Awaitility polling now uses until(() -> getReceived().size() >= 1) — no iterator. assertConnected() (which calls getReceived(String.class).get(0)) is only invoked after the size gate confirms the CONNECTED message has been received, so the listener thread is done mutating the list at that point.

Comment on lines +317 to +321
await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> {
assertEquals(broadcastMsg, wsclient2.getReceived(String.class).get(0));
assertEquals(broadcastMsg, wsclient3.getReceived(String.class).get(0));
});

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.

Addressed in commit 01d5f72. Both the broadcast and private message waits now use until(() -> getReceived().size() >= 1) for polling. The getReceived(String.class).get(0) content assertion is only made after the size gate confirms delivery — safe from both ConcurrentModificationException and IndexOutOfBoundsException.

…polling

Address Copilot review: getReceived(String.class) iterates over
WebsocketTestClient's non-thread-safe ArrayList via for-each, risking
ConcurrentModificationException while the listener thread adds messages.

Switch Awaitility polling to use getReceived().size() (index-based, no
iterator). Content assertions via getReceived(String.class) are only
called after the size check confirms all expected messages arrived.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gnodet gnodet marked this pull request as ready for review July 6, 2026 07:19
@gnodet gnodet added this to the 4.22.0 milestone Jul 6, 2026
@gnodet gnodet self-assigned this Jul 6, 2026
@gnodet gnodet added the tests label Jul 6, 2026
@gnodet gnodet merged commit ee33542 into apache:main Jul 6, 2026
5 checks passed
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.

5 participants