Skip to content

CAMEL-25840: camel-hazelcast - the SEDA consumer waits for its threads before it stops the processor - #28261

Merged
Croway merged 2 commits into
apache:mainfrom
allthingssecurity:camel-hazelcast-seda-consumer-stop-waits-for-loops
Oct 22, 2026
Merged

Croway merged 2 commits into
apache:mainfrom
allthingssecurity:camel-hazelcast-seda-consumer-stop-waits-for-loops

Conversation

@allthingssecurity

@allthingssecurity allthingssecurity commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Description

CAMEL-25840

HazelcastSedaConsumer.doStop shut its executor down without waiting and then stopped the processor, while its threads usually still waited in queue.poll(pollTimeout) (1 s by default). A poll takes the item from the queue: an item offered in that window was taken by the stopping consumer, its exchange failed with RouteStoppingException, and without transacted (the default) the item was lost, with one WARN. On a busy queue this happens at nearly every stop of the route (stop, restart, failover, shutdown), up to one item per consumer thread.

Changes (HazelcastSedaConsumer.doStop):

  • Wait for the consumer threads to end their current poll and route what it returned (graceful executor shutdown, bounded by shutdownAwaitTermination), then stop the processor.
  • A single poll waits at most one second (Math.min(pollTimeout, 1000), as CAMEL-25837: camel-hazelcast - the queue consumer in poll mode stops without losing items #28258 does for the queue consumer); a poll that returns nothing goes round the loop again (and commits the empty transaction) as before. Without it the graceful wait would only help while pollTimeout is below shutdownAwaitTermination (10 s): with a larger pollTimeout every stop would wait 10 s, log Forcing shutdown of ExecutorService and interrupt the poll, which in Hazelcast 5.4 does not end the poll on the member (review follow-up).
  • Shut down the error-delay scheduler after the threads, since they use it: a thread in the delay after a failed poll waits (BackgroundTask.run, a latch) for a task of that scheduler, and shutting the scheduler down cancels that task. On main the scheduler was shut down first, so such a thread was left waiting for ever; with the graceful wait it would also hold the stop until the 10 s timeout.

A stop takes at most about one second longer. No option changed, so no upgrade guide entry. Besides doStop, only the timeout of the poll call changes; the branch merges cleanly with the change of CAMEL-25816 to the loop of the same class (checked with git merge-tree against main).

Tests:

  • New HazelcastSedaConsumerStopTest (3 tests; an embedded Hazelcast member, and a mocked instance for the failing queue): itemOfferedAfterTheRouteStoppedStaysInTheQueue stops the idle route while it polls, then offer an item. Without the change the item is taken from the queue (expected: <1> but was: <0>); with the change it stays and the next start routes it.
  • longPollTimeoutStillStopsPromptlyAndLeavesTheItemInTheQueue (same class, pollTimeout=60000): an item is routed, the route stops in less than 5 s, and an item offered afterwards stays in the queue. Without the 1 s cap it fails: Stopping the route took 10002 ms (the graceful timeout).
  • threadInTheDelayAfterAFailedPollEndsWhenTheRouteStops (same class): a route on a queue whose every poll fails (onErrorDelay=3000) is stopped while its thread waits in the error delay; the stop takes less than 8 s and the thread ends. Without the change the thread is left (A consumer thread is left: [... hazelcast-seda://failing ...]).
  • camel-hazelcast unit tests with the change: 245 run, 0 failures, 1 error (HazelcastUserConfigSerializationFilterWarningTest.warnsForUserClientConfigWithoutFilter: a client that cannot reach a cluster here; it errors the same way without the change), 1 flake (HazelcastAggregationRepositoryRoutesTest.checkAggregationFromTwoRoutes failed once and passed on the rerun; it does not use the SEDA consumer).

How it was found: variant analysis of CAMEL-25790 (a consumer with its own loops, and what its stop does to them), next to CAMEL-25837, then the JUnit test.

What was not run: a Hazelcast client (only an embedded member); the full root build.

Target

  • I checked that the commit is targeting the correct branch (Camel 4 uses the main branch)

Tracking

  • If this is a large change, bug fix, or code improvement, I checked there is a JIRA issue filed for the change (usually before you start working on it): CAMEL-25840.

Apache Camel coding standards and style

  • I checked that each commit in the pull request has a meaningful subject line and body.
  • I have run mvn clean install -DskipTests locally from root folder and I have committed all auto-generated changes.
    (I built and tested camel-hazelcast. No option and no component page changed. I did not run the full root build.)

AI-assisted contributions

  • If this PR includes AI-generated code, commits have proper co-authorship attribution (e.g., Co-authored-by trailers) and the PR description identifies the AI tool used.
    This PR was prepared with Claude Code (Claude Opus 5.5). The commit carries a Co-Authored-By trailer.

Claude Code on behalf of allthingssecurity

🤖 Generated with Claude Code

…s before it stops the processor

HazelcastSedaConsumer.doStop shut its executor down without waiting and
then stopped the processor, while its threads could still be waiting in
poll(pollTimeout) (1 second by default). A poll takes the item from the
queue: an item offered in that time was taken, its exchange failed with
RouteStoppingException and, without transacted, the item was lost.

doStop now waits for the consumer threads to end their current poll and
exchange (a graceful executor shutdown) before it stops the processor,
and shuts the error-delay scheduler down after them, since the threads
use it: a thread in the delay after a failed poll waits for a task of
that scheduler, which a shutdown cancels, so it was left waiting for
ever when the scheduler was shut down first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

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.

@davsclaus davsclaus 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.

Thanks Shashank,

The new order is right. BackgroundTask.run uses scheduleWithFixedDelay and then waits on latch.await() with no limit (withUnlimitedDuration()). ScheduledThreadPoolExecutor.shutdown() cancels periodic tasks, so on main a thread in the error delay was left waiting for ever. With the scheduler shut down after the consumer threads, the delay ends and the thread sees the stop. The change only touches doStop, so it merges cleanly with CAMEL-25816 (d4688eb). It does not overlap with #28258 or #28264. LGTM once the build jobs are green.

  1. Not blocking: the graceful wait only helps while pollTimeout is below shutdownAwaitTermination (10 s by default). With a larger pollTimeout, every stop now waits 10 s, logs Forcing shutdown of ExecutorService at WARN and then interrupts the poll. In Hazelcast 5.4 the interrupt does not fix it. On a client, ClientInvocationFuture.onInterruptDetected ends the wait but the poll stays parked on the member and takes the next item. On an embedded member, AbstractInvocationFuture.get() ignores the interrupt and parks until the poll completes, so the item is polled and then routed into a stopped processor. Either way the item is lost again, after up to 2 x 10 s. #28258 avoids this for the queue consumer by polling at most 1 s at a time. Here it is even simpler: a null poll just goes back round the loop (and commits the empty transaction), so queue.poll(Math.min(pollTimeout, 1000), TimeUnit.MILLISECONDS) keeps the behaviour and bounds the stop to about 1 s. During a CamelContext shutdown, DefaultShutdownStrategy stops the consumers one route after the other, so that bound adds up over many routes. It would also make the two consumers behave the same on stop.
  2. Not blocking: a thread in the error delay still waits the full onErrorDelay before it sees the stop (1 s by default, so fine). The queue consumer ends that delay at once with its stopLatch. Worth doing the same here if you touch this again, but not needed for this PR.

Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

… at one second

Per review: the graceful wait in doStop only helped while pollTimeout was below shutdownAwaitTermination; with a
larger pollTimeout every stop waited 10 s and then interrupted the poll, which in Hazelcast 5.4 does not end the
poll on the member. A single poll now waits Math.min(pollTimeout, 1000) ms (as the queue consumer does since
CAMEL-25837); a poll that returns nothing goes round the loop and commits the empty transaction as before.
New test longPollTimeoutStillStopsPromptlyAndLeavesTheItemInTheQueue (pollTimeout=60000).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@allthingssecurity

Copy link
Copy Markdown
Contributor Author

Thanks Claus. Pushed ee5eb3f:

  1. A single poll is capped at 1 s (Math.min(pollTimeout, 1000), as in CAMEL-25837: camel-hazelcast - the queue consumer in poll mode stops without losing items #28258), so a stop is bounded to about a second for any pollTimeout, and the two consumers behave the same. New test longPollTimeoutStillStopsPromptlyAndLeavesTheItemInTheQueue (pollTimeout=60000).
  2. The error delay with a stop latch: not in this PR, as you say. It is 1 s by default. I can do it the next time this class is touched.

camel-hazelcast: 245 tests, 0 failures, 1 error (HazelcastUserConfigSerializationFilterWarningTest, a client that cannot reach a cluster here; the same on main). Without the cap, the new test fails with Stopping the route took 10002 ms.

Claude Code on behalf of allthingssecurity

@gnodet-bot gnodet-bot 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.

Fix is correct and well-tested. The ordering in doStop() is right: shutdownGraceful(executor) waits for consumer threads to finish their current poll (capped at MAX_POLL_WAIT = 1 s, so threads return quickly), then taskExecutor is shut down (safe, since threads no longer need it), then super.doStop() stops the processor. Without the fix, the processor was stopped before the threads — a thread returning from a poll with an item in hand would hit a stopped processor and lose the item.

The shutdownGraceful semantics are sound: Camel's BaseExecutorServiceManager calls executor.shutdown() (orderly — no interruption), then awaitTermination for up to 10 s. Since the poll is now capped at 1 s, all threads return within 1 s, see isRunAllowed() == false (the service status is already STOPPING when doStop() runs), and exit the loop. The 10 s graceful timeout is more than enough.

Pre-fix test validation (static):

  • itemOfferedAfterTheRouteStoppedStaysInTheQueue: waits for the consumer to be in poll(), stops the route, offers an item, asserts it stays in the queue. Without the fix, the consumer thread returns from poll, gets the item, and calls the stopped processor → RouteStoppingException → item silently discarded. ✅ Correctly exercises the fixed path.
  • longPollTimeoutStillStopsPromptlyAndLeavesTheItemInTheQueue: verifies the Math.min(pollTimeout, MAX_POLL_WAIT) cap — without it, a 60 s pollTimeout would cause the stop to wait for the graceful timeout (10 s) before forcing shutdown. ✅ Correct.
  • threadInTheDelayAfterAFailedPollEndsWhenTheRouteStops: verifies the taskExecutor is shut down AFTER the consumer threads (not before) — without the fix, taskExecutor.shutdown() cancelled the latch task the consumer thread was waiting on, leaving it blocked forever. ✅ Correct.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • components/camel-hazelcast

🔁 1 test passed only after a retry on JDK 25 (1 retried attempt)

Recovered flaky tests on JDK 25 (1)
Module Test Failed attempts First failure
dsl/camel-jbang/camel-jbang-plugin-tui LaunchManagerOutcomeTest.startedWhenTheStatusFileSaysSo 1 expected: null but was: LaunchOutcome[ok=false, log=null]

ℹ️ These tests did not fail the build. Surefire retried them and they passed.
Retries are enabled project-wide by surefire.rerunFailingTestsCount in parent/pom.xml.


🔬 Scalpel shadow comparison — Scalpel: 8 of 707 tested, 27 compile-only — current: 8 all tested

Maveniverse Scalpel detected 8 affected modules (current approach: 8).

Skip-tests mode would test 8 modules (1 direct + 7 downstream), skip tests for 27 (generated code, meta-modules)

Modules Scalpel would test (8)
  • camel-hazelcast ← components/camel-hazelcast/src/main/java/org/apache/camel/component/hazelcast/seda/HazelcastSedaConsumer.java, components/camel-hazelcast/src/test/java/org/apache/camel/component/hazelcast/HazelcastSedaConsumerStopTest.java
  • camel-jbang-mcp ← downstream of org.apache.camel:camel-catalog
  • camel-jbang-plugin-mcp ← downstream of org.apache.camel:camel-jbang-core
  • camel-jbang-plugin-tui ← downstream of org.apache.camel:camel-catalog
  • camel-jbang-plugin-validate ← downstream of org.apache.camel:camel-yaml-dsl-validator
  • camel-launcher-container ← downstream of org.apache.camel:camel-launcher
  • camel-yaml-dsl-validator ← downstream of org.apache.camel:camel-catalog
  • camel-yaml-dsl-validator-maven-plugin ← downstream of org.apache.camel:camel-yaml-dsl-validator
Modules with tests skipped (27)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • 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

All tested modules (35 modules, 4m 53s total)

Total reactor time: 4m 53s

Module Duration Status
Camel :: JBang :: Plugin :: TUI 58.3s SUCCESS
Camel :: Launcher 32.7s SUCCESS
Camel :: JBang :: MCP 27.1s SUCCESS
Camel :: JBang :: Plugin :: Validate 24.6s SUCCESS
Camel :: YAML DSL 21.0s SUCCESS
Camel :: YAML DSL :: Validator 19.4s SUCCESS
Camel :: Catalog :: Camel Catalog 17.2s SUCCESS
Camel :: Component DSL 11.8s SUCCESS
Camel :: Docs 10.9s SUCCESS
Camel :: Kamelet Main 9.3s SUCCESS
Camel :: JBang :: Plugin :: Kubernetes 9.2s SUCCESS
Camel :: JBang :: Plugin :: Testing 7.2s SUCCESS
Camel :: Catalog :: Camel Report Maven Plugin 6.7s SUCCESS
Camel :: Catalog :: Camel Route Parser 6.1s SUCCESS
Camel :: YAML DSL :: Deserializers 5.8s SUCCESS
Camel :: YAML DSL :: Validator Maven Plugin 4.7s SUCCESS
Camel :: All Components Sync point 4.5s SUCCESS
Camel :: YAML DSL :: Maven Plugins 2.5s SUCCESS
Camel :: Catalog :: Maven 2.4s SUCCESS
Camel :: Catalog :: Suggest (deprecated) 1.9s SUCCESS
Camel :: JBang :: Plugin :: Edit 1.4s SUCCESS
Camel :: JBang :: Integration tests 1.2s SUCCESS
Camel :: Catalog :: Dummy Component 1.1s SUCCESS
Camel :: Assembly 1.1s SUCCESS
Camel :: Coverage 1.0s SUCCESS
Camel :: JBang :: Plugin :: Generate 1.0s SUCCESS
Camel :: Catalog :: Console 0.7s SUCCESS
Camel :: Endpoint DSL :: Support 0.6s SUCCESS
Camel :: JBang :: Main 0.6s SUCCESS
Camel :: Launcher :: Container 0.6s SUCCESS
Camel :: JBang :: Plugin :: MCP 0.5s SUCCESS
Camel :: Endpoint DSL n/a
Camel :: HazelCast n/a
Camel :: Integration Tests n/a
Camel :: JBang :: Core n/a

Top 20 slowest modules:

  • Camel :: JBang :: Plugin :: TUI (58.3s)
  • Camel :: Launcher (32.7s)
  • Camel :: JBang :: MCP (27.1s)
  • Camel :: JBang :: Plugin :: Validate (24.6s)
  • Camel :: YAML DSL (21.0s)
  • Camel :: YAML DSL :: Validator (19.4s)
  • Camel :: Catalog :: Camel Catalog (17.2s)
  • Camel :: Component DSL (11.8s)
  • Camel :: Docs (10.9s)
  • Camel :: Kamelet Main (9.3s)
  • Camel :: JBang :: Plugin :: Kubernetes (9.2s)
  • Camel :: JBang :: Plugin :: Testing (7.2s)
  • Camel :: Catalog :: Camel Report Maven Plugin (6.7s)
  • Camel :: Catalog :: Camel Route Parser (6.1s)
  • Camel :: YAML DSL :: Deserializers (5.8s)
  • Camel :: YAML DSL :: Validator Maven Plugin (4.7s)
  • Camel :: All Components Sync point (4.5s)
  • Camel :: YAML DSL :: Maven Plugins (2.5s)
  • Camel :: Catalog :: Maven (2.4s)
  • Camel :: Catalog :: Suggest (deprecated) (1.9s)

⚙️ View full build and test results

@davsclaus davsclaus 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.

Thanks Shashank,

The new commit looks good. A single poll now waits at most MAX_POLL_WAIT (1 s), so shutdownGraceful no longer runs into its 10 s timeout and the forced interrupt when pollTimeout is large. The default pollTimeout is already 1000, so the default behaviour does not change. A null poll goes back round the loop as before. The new test longPollTimeoutStillStopsPromptlyAndLeavesTheItemInTheQueue covers the capped case, and the queue and SEDA consumers now stop the same way. LGTM once the build jobs are green.

Claude Code on behalf of @davsclaus. This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

@Croway
Croway merged commit bd49733 into apache:main Oct 22, 2026
6 checks passed
@allthingssecurity
allthingssecurity deleted the camel-hazelcast-seda-consumer-stop-waits-for-loops branch October 22, 2026 12:57
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