Skip to content

CAMEL-25837: camel-hazelcast - the queue consumer in poll mode stops without losing items - #28258

Merged
davsclaus merged 2 commits into
apache:mainfrom
allthingssecurity:camel-hazelcast-queue-poll-consumer-stop
Oct 22, 2026
Merged

davsclaus merged 2 commits into
apache:mainfrom
allthingssecurity:camel-hazelcast-queue-poll-consumer-stop

Conversation

@allthingssecurity

@allthingssecurity allthingssecurity commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Description

CAMEL-25837

With queueConsumerMode=Poll the hazelcast-queue consumer takes items with IQueue.poll(pollingTimeout) in its own thread, and a poll removes the item from the queue. doStop stopped that thread with shutdownNow, which interrupts it:

  • An idle route: the interrupt ends the poll on the caller's side only. The poll stays parked on the member until its timeout (pollingTimeout, 10 s by default) and takes the first item offered in that time, which nobody routes. This happens at every stop of the route (stop, restart, failover, shutdown) while producers keep offering.
  • An exchange in flight: the processor was stopped (super.doStop()) before the thread was done and the interrupt ends any blocking call of the route, so the exchange failed silently and its item was lost, also with a graceful shutdown.

Changes (HazelcastQueueConsumer):

  • A single IQueue.poll now waits at most one second (Math.min(pollingTimeout, 1000)); a poll that returns nothing goes round the loop again, so the consumer still waits for items as long as it runs, and sees a stop within a second. An idle consumer makes one poll call per second instead of one per pollingTimeout, and pollingTimeout now only bounds a single poll (and sets the delay after a poll error).
  • doStop no longer interrupts the thread: it waits for the current poll and exchange (graceful executor shutdown, bounded by shutdownAwaitTermination) and stops the processor afterwards.

Without the one-second calls, the graceful wait would last up to pollingTimeout, which is 10 s by default like shutdownAwaitTermination, so the stop would time out and interrupt the poll again.

The listen mode (the default) is unchanged. No option changed, so no upgrade guide entry.

Tests:

  • New HazelcastQueueConsumerPollStopTest (3 tests, an embedded Hazelcast member). Without the change all three fail: the item offered after the stop is taken from the queue (expected: <1> but was: <0>, in itemOfferedAfterTheRouteStoppedStaysInTheQueue and longPollingTimeoutWaitsForAnItemAndStillStopsPromptly), and the exchange in flight never reaches the mock (Expected: <1> but was: <0>). With the change all three pass.
  • longPollingTimeoutWaitsForAnItemAndStillStopsPromptly uses pollingTimeout=60000: an item offered after 3 s of waiting is routed, the route stops in less than 5 s, and an item offered afterwards stays in the queue. With a single poll(pollingTimeout) and the graceful stop, it fails (Stopping the route took 10004 ms).
  • 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).

How it was found: variant analysis of CAMEL-25790 (a consumer with its own loop and executor, and what its stop does to that loop), 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-25837.

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

…without losing items

In poll mode the hazelcast-queue consumer stopped by interrupting its
polling thread (shutdownNow), and the item of a poll is removed from the
queue when the poll returns it:

- an interrupted poll keeps waiting on the member that owns the queue,
  until its pollingTimeout (10 seconds by default). The next item offered
  in that time is taken by that poll and nobody routes it. This happens at
  every stop of an idle route (route stop, restart, master failover,
  application shutdown);
- an exchange that was being routed was interrupted, and the processor
  chain was stopped before the exchange completed, so a graceful shutdown
  did not let it complete: its item was lost.

The consumer now lets its thread end on its own: it still waits up to
pollingTimeout for an item, but in IQueue.poll calls of at most one
second, so it sees a stop within a second; doStop waits for the current
poll and exchange to complete (a graceful executor shutdown) and stops the
processor afterwards.

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 analysis holds for Hazelcast 5.4. On a client, ClientInvocationFuture.onInterruptDetected ends the caller's wait while the PollOperation stays parked on the member. On an embedded member it is even worse: AbstractInvocationFuture.get() ignores the interrupt and keeps parking until the poll completes. So not interrupting, waiting for the thread, and stopping the processor afterwards (moving super.doStop() to the end) is the right fix. The listen mode is unchanged. This does not overlap with #28261 (SEDA) or #28264 (topic). LGTM once the build jobs are green.

  1. Optional: pollItem() can be simpler. When it returns null, the while (isRunAllowed()) loop in run() just calls it again, so the deadline loop does the same as a single queue.poll(Math.min(config.getPollingTimeout(), MAX_POLL_WAIT), TimeUnit.MILLISECONDS). Either way, pollingTimeout now has no visible effect on polling (it only sets the delay after a poll error). That's fine, but it means the deadline code adds nothing.
  2. Not blocking: a stop now waits for the exchange in flight (up to shutdownAwaitTermination) instead of interrupting it. That is the point of the fix and an improvement. #28261 takes the same approach for the SEDA consumer but still polls for the full pollTimeout. I suggested the same 1 s cap there, so both consumers behave the same.
  3. Optional: see the inline comment on the test helper.

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

…meout, and count only this context's consumer threads in the test

Per review:
- pollItem() is a single IQueue.poll(Math.min(pollingTimeout, MAX_POLL_WAIT)): a poll that returns nothing goes
  round the loop in run() again, so the deadline loop added nothing.
- HazelcastQueueConsumerPollStopTest counts only the QueueConsumer threads of this CamelContext, so a thread left
  by another test class in the same fork does not make the wait time out.

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

Copy link
Copy Markdown
Contributor Author

Thanks Claus. Pushed 44fe4de:

  1. pollItem() is now the single queue.poll(Math.min(pollingTimeout, MAX_POLL_WAIT), MILLISECONDS) you suggested. The deadline loop is gone, and the javadoc and the constant's comment say that the loop in run() polls again. The PR description now says that pollingTimeout only bounds a single poll (and sets the delay after a poll error).
  2. Noted. CAMEL-25840: camel-hazelcast - the SEDA consumer waits for its threads before it stops the processor #28261 now caps its poll at 1 s the same way.
  3. The test helper counts only the threads of this context (see inline).

camel-hazelcast: 245 tests, 0 failures, 1 error (HazelcastUserConfigSerializationFilterWarningTest, a client that cannot reach a cluster here; the same on main).

Claude Code on behalf of allthingssecurity

@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/queue/HazelcastQueueConsumer.java, components/camel-hazelcast/src/test/java/org/apache/camel/component/hazelcast/HazelcastQueueConsumerPollStopTest.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, 5m 21s total)

Total reactor time: 5m 21s

Module Duration Status
Camel :: Launcher 53.0s SUCCESS
Camel :: JBang :: MCP 44.0s SUCCESS
Camel :: JBang :: Plugin :: Validate 29.4s SUCCESS
Camel :: YAML DSL :: Validator 23.0s SUCCESS
Camel :: Catalog :: Camel Catalog 22.2s SUCCESS
Camel :: YAML DSL 22.0s SUCCESS
Camel :: Component DSL 19.3s SUCCESS
Camel :: Docs 14.0s SUCCESS
Camel :: JBang :: Plugin :: Testing 13.1s SUCCESS
Camel :: Kamelet Main 12.3s SUCCESS
Camel :: JBang :: Plugin :: Kubernetes 12.2s SUCCESS
Camel :: Catalog :: Camel Route Parser 7.9s SUCCESS
Camel :: Catalog :: Camel Report Maven Plugin 7.5s SUCCESS
Camel :: YAML DSL :: Deserializers 7.3s SUCCESS
Camel :: YAML DSL :: Validator Maven Plugin 6.2s SUCCESS
Camel :: All Components Sync point 5.3s SUCCESS
Camel :: Assembly 4.1s SUCCESS
Camel :: YAML DSL :: Maven Plugins 3.0s SUCCESS
Camel :: Catalog :: Maven 2.7s SUCCESS
Camel :: Catalog :: Suggest (deprecated) 2.3s SUCCESS
Camel :: JBang :: Plugin :: Edit 1.9s SUCCESS
Camel :: Coverage 1.4s SUCCESS
Camel :: Catalog :: Dummy Component 1.3s SUCCESS
Camel :: JBang :: Plugin :: Generate 1.3s SUCCESS
Camel :: Catalog :: Console 0.8s SUCCESS
Camel :: JBang :: Main 0.8s SUCCESS
Camel :: Launcher :: Container 0.7s SUCCESS
Camel :: JBang :: Integration tests 0.7s SUCCESS
Camel :: Endpoint DSL :: Support 0.7s SUCCESS
Camel :: JBang :: Plugin :: MCP 0.6s SUCCESS
Camel :: Endpoint DSL n/a
Camel :: HazelCast n/a
Camel :: Integration Tests n/a
Camel :: JBang :: Core n/a
Camel :: JBang :: Plugin :: TUI n/a

Top 20 slowest modules:

  • Camel :: Launcher (53.0s)
  • Camel :: JBang :: MCP (44.0s)
  • Camel :: JBang :: Plugin :: Validate (29.4s)
  • Camel :: YAML DSL :: Validator (23.0s)
  • Camel :: Catalog :: Camel Catalog (22.2s)
  • Camel :: YAML DSL (22.0s)
  • Camel :: Component DSL (19.3s)
  • Camel :: Docs (14.0s)
  • Camel :: JBang :: Plugin :: Testing (13.1s)
  • Camel :: Kamelet Main (12.3s)
  • Camel :: JBang :: Plugin :: Kubernetes (12.2s)
  • Camel :: Catalog :: Camel Route Parser (7.9s)
  • Camel :: Catalog :: Camel Report Maven Plugin (7.5s)
  • Camel :: YAML DSL :: Deserializers (7.3s)
  • Camel :: YAML DSL :: Validator Maven Plugin (6.2s)
  • Camel :: All Components Sync point (5.3s)
  • Camel :: Assembly (4.1s)
  • Camel :: YAML DSL :: Maven Plugins (3.0s)
  • Camel :: Catalog :: Maven (2.7s)
  • Camel :: Catalog :: Suggest (deprecated) (2.3s)

⚙️ View full build and test results

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

CAMEL-25837: Hazelcast queue consumer shutdown fix ✅

This is a well-designed fix for a real data-loss bug in the Hazelcast queue consumer's poll mode. Verified locally — tests fail before the fix and pass after.

What the fix does

  1. stopLatch.countDown() before executor shutdown — signals the consumer task to exit cleanly before the executor is torn down, preventing the task from being forcibly interrupted mid-exchange.

  2. shutdownNow() → shutdownGraceful(executor) / executor.shutdown() — replaces abrupt termination with graceful shutdown. The default shutdownAwaitTermination of 10 s comfortably covers the worst case: ≤1 s poll (capped by MAX_POLL_WAIT) + in-flight exchange processing time.

  3. MAX_POLL_WAIT = 1000L + Math.min(config.getPollingTimeout(), MAX_POLL_WAIT) — caps single poll waits at 1 s. This ensures the consumer thread sees the stop signal within 1 s even if the configured pollingTimeout is large, without losing any queued items.

  4. super.doStop() moved to the end — correct ordering: signal → drain → delegate to parent.

Static analysis

broad-exception-catch in the catch (Exception e) block is pre-existing, unchanged code — not introduced by this PR.

Companion PR

#28261 applies the same fix to the SEDA consumer — not a duplicate, a necessary companion.

No issues found.

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

@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. 44fe4de addresses both points:

  • pollItem() is now a single poll(Math.min(pollingTimeout, MAX_POLL_WAIT)). It behaves the same as the deadline loop: a null goes round run() again, and a stop is seen within 1 s. pollingTimeout of 0 or less behaves as before the PR.
  • The test counts only threads named (<context>) and QueueConsumer. The closing paren keeps camel-1 from matching camel-10, and the threads of an earlier test in the class are gone because its stop waits for them. The count of 3 is awaited, so the short gaps between 1 s polls do not make it flaky.

LGTM, to merge once CI is 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.

@davsclaus davsclaus added this to the 4.24.0 milestone Oct 22, 2026
@davsclaus davsclaus added the bug Something isn't working label Oct 22, 2026
@davsclaus
davsclaus merged commit 0dc6d20 into apache:main Oct 22, 2026
6 checks passed
@allthingssecurity
allthingssecurity deleted the camel-hazelcast-queue-poll-consumer-stop branch October 23, 2026 01:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants