Skip to content

CAMEL-26286: camel-disruptor - removing one of the routes that consume from a disruptor endpoint no longer shuts down its disruptor - #28639

Merged
davsclaus merged 2 commits into
apache:mainfrom
allthingssecurity:camel-disruptor-remove-route-keeps-shared-endpoint-v2
Oct 29, 2026
Merged

davsclaus merged 2 commits into
apache:mainfrom
allthingssecurity:camel-disruptor-remove-route-keeps-shared-endpoint-v2

Conversation

@allthingssecurity

@allthingssecurity allthingssecurity commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Description

CAMEL-26286

Every route that consumes from the same disruptor: uri shares one DisruptorEndpoint. When a route is removed, the core stops and shuts down the endpoint of its consumer (RouteService.doShutdown), also when other routes still consume from it. DisruptorEndpoint.doStop then calls DisruptorReference.removeEndpoint, which shuts down the disruptor and its executor, as this endpoint is the only one of its reference (another endpoint shares the reference only for a uri with the same queue name and other options). So with three routes from("disruptor:orders?multipleConsumers=true"), removing one of them (a route template removed, a reload, JMX or controlbus) leaves the two others without messages, and every send fails with IllegalStateException: Disruptor was not started (caused by DisruptorNotStartedException). A CamelContext.stop() afterwards takes about 10 seconds. camel-seda has the same sharing and guards against it in SedaEndpoint.stop()/shutdown(); the same scenario on seda: keeps delivering to the other two routes.

Changes:

  • DisruptorEndpoint overrides stop() and shutdown(): they do nothing (DEBUG log) while other consumers still use the endpoint, so the disruptor stays up for them. Without consumers, the endpoint stops and shuts down as before. The check runs under the endpoint's service lock, which a consumer takes to register, so a consumer that starts at the same time is either seen by the check or registers after the stop (review follow-up).
  • The consumers of suspended routes count too. A suspended DisruptorConsumer leaves the disruptor (as before, so the disruptor stops handing it events), but the endpoint now keeps it in a set of suspended consumers until it resumes or stops. Otherwise suspending two routes and removing a third left the endpoint without consumers, so it was stopped and shut down, and the suspended routes resumed on a disruptor without them: they never received anything again.
  • Unlike SedaEndpoint, producers are not counted. A producer of the uri of a removed route, such as the cached producer of a ProducerTemplate, would otherwise keep the old endpoint in the disruptor, and the endpoint that replaces it (the context removes the endpoint from its registry when no other route uses it) could not join it: DisruptorReference keeps its endpoints in a set based on equals, that is on the uri. The first version of this change counted producers too, and DisruptorRemoveRouteThenAddAgainTest failed with it.
  • disruptor-vm endpoints inherit the change. No option, generated file or upgrade guide change (nothing users relied on changes).
  • Backport candidates, for the maintainers to label if wanted (contributors cannot set port/* labels): camel-4.22.x, 4.18.x and 4.14.x have the same DisruptorEndpoint.doStop (read, not run).

Tests:

  • New DisruptorRemoveRouteOfSharedEndpointTest, MockEndpoint expectations as in the module's other tests:
    • removingOneOfSeveralConsumersKeepsTheOthersConsuming fails on main (3 runs): IllegalStateException: Disruptor was not started.
    • removingAConsumerRouteKeepsTheSuspendedOnes: routes a and c suspended, route b removed, a and c resumed, one message: without the suspended consumers it fails after the mock's wait with mock://c Received message count. Expected: <1> but was: <0>.
    • consumerRouteRemovedAndAddedAgainWhileAProducerRouteUsesTheQueue (the consumer route reloaded while a producer route stays) passes on main and with the change; it is the control for the path the change leaves as it is.
    • consumerRegisteringWhileTheEndpointIsAskedToStopKeepsTheEndpointStarted (review follow-up): the test holds the lock as a registering consumer does, another thread asks the endpoint to stop and waits for the lock, then a consumer registers. With the check outside the lock the endpoint stops (expected: <true> but was: <false>). No sleep: it waits until the stopping thread is queued on the lock.
  • camel-disruptor: 122 tests, 0 failures. The formatter and import-sort plugins ran. DisruptorRemoveRouteOfSharedEndpointTest passed 20 runs in one JVM.

How it was found: a random lifecycle-sequence harness (stop, start, suspend, resume, remove and add of several consumer routes on shared endpoints, with a probe message after every step) over seda, disruptor, timer, scheduler and other consumers shrank the failure to a single remove.

Not changed: producers are not counted (above), so removing the last consumer route of a uri still stops its endpoint while a producer of another route sends to it, as on main.

Also not changed: the core stops the endpoint of a removed route's consumer even when other routes still use it. Other components whose endpoint doStop releases something the other routes of the same uri still use may have the same problem; I can file a follow-up JIRA for the ones I can reproduce.

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

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 camel-disruptor and ran its tests, including the formatter and import-sort plugins. No generated file changes. 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

…e from a disruptor endpoint no longer shuts down its disruptor

Every route that consumes from the same disruptor uri shares one endpoint,
and removing a route stops and shuts down the endpoint of its consumer. That
shut down the disruptor of the other routes: they received nothing any more
and producers failed with "Disruptor was not started". The endpoint now
ignores stop and shutdown while other consumers use it, as SedaEndpoint does.
The consumer of a suspended route counts too: a suspended consumer leaves
the disruptor (as before) but is kept as a suspended consumer of the
endpoint until it resumes or stops, so that removing another route does not
shut down the disruptor that the suspended routes resume on.

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.

Mirrors the SedaEndpoint stop/shutdown guard. Counting suspended consumers is right (doStop from SUSPENDED removes it, and the lock is reentrant), not counting producers is justified and covered by a control test, and context shutdown still stops the endpoint because the consumers stop first. Will approve once CI is green.

Nit: the emptiness check at DisruptorEndpoint.java:289/299 runs outside the service lock, so a consumer starting concurrently could race it (low risk).

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

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • components/camel-disruptor

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

Recovered flaky tests on JDK 21 (1)
Module Test Failed attempts First failure
dsl/camel-jbang/camel-jbang-plugin-tui MonitorStartupTest.browserOnlyServesUntilTheServerStops 1 Connection refused (connect failed)

ℹ️ 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 709 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-disruptor ← components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/DisruptorConsumer.java, components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/DisruptorEndpoint.java, components/camel-disruptor/src/test/java/org/apache/camel/component/disruptor/DisruptorRemoveRouteOfSharedEndpointTest.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 20s total)

Total reactor time: 5m 20s

Module Duration Status
Camel :: JBang :: MCP 52.0s SUCCESS
Camel :: JBang :: Plugin :: Validate 48.1s SUCCESS
Camel :: YAML DSL :: Validator 27.9s SUCCESS
Camel :: YAML DSL 25.1s SUCCESS
Camel :: Catalog :: Camel Catalog 24.5s SUCCESS
Camel :: Component DSL 21.4s SUCCESS
Camel :: Kamelet Main 18.7s SUCCESS
Camel :: JBang :: Plugin :: Kubernetes 15.1s SUCCESS
Camel :: Docs 15.0s SUCCESS
Camel :: YAML DSL :: Deserializers 9.1s SUCCESS
Camel :: Catalog :: Camel Report Maven Plugin 8.2s SUCCESS
Camel :: Catalog :: Camel Route Parser 8.0s SUCCESS
Camel :: JBang :: Plugin :: Testing 7.8s SUCCESS
Camel :: Assembly 6.5s SUCCESS
Camel :: All Components Sync point 5.7s SUCCESS
Camel :: YAML DSL :: Validator Maven Plugin 5.3s SUCCESS
Camel :: YAML DSL :: Maven Plugins 3.8s SUCCESS
Camel :: Catalog :: Maven 3.3s SUCCESS
Camel :: Catalog :: Suggest (deprecated) 2.6s SUCCESS
Camel :: Coverage 2.0s SUCCESS
Camel :: JBang :: Plugin :: Generate 1.7s SUCCESS
Camel :: Catalog :: Console 1.4s SUCCESS
Camel :: Catalog :: Dummy Component 1.3s SUCCESS
Camel :: JBang :: Plugin :: Edit 1.3s SUCCESS
Camel :: JBang :: Main 1.2s SUCCESS
Camel :: JBang :: Integration tests 1.0s SUCCESS
Camel :: Launcher :: Container 0.8s SUCCESS
Camel :: Endpoint DSL :: Support 0.8s SUCCESS
Camel :: JBang :: Plugin :: MCP 0.6s SUCCESS
Camel :: Disruptor n/a
Camel :: Endpoint DSL n/a
Camel :: Integration Tests n/a
Camel :: JBang :: Core n/a
Camel :: JBang :: Plugin :: TUI n/a
Camel :: Launcher n/a

Top 20 slowest modules:

  • Camel :: JBang :: MCP (52.0s)
  • Camel :: JBang :: Plugin :: Validate (48.1s)
  • Camel :: YAML DSL :: Validator (27.9s)
  • Camel :: YAML DSL (25.1s)
  • Camel :: Catalog :: Camel Catalog (24.5s)
  • Camel :: Component DSL (21.4s)
  • Camel :: Kamelet Main (18.7s)
  • Camel :: JBang :: Plugin :: Kubernetes (15.1s)
  • Camel :: Docs (15.0s)
  • Camel :: YAML DSL :: Deserializers (9.1s)
  • Camel :: Catalog :: Camel Report Maven Plugin (8.2s)
  • Camel :: Catalog :: Camel Route Parser (8.0s)
  • Camel :: JBang :: Plugin :: Testing (7.8s)
  • Camel :: Assembly (6.5s)
  • Camel :: All Components Sync point (5.7s)
  • Camel :: YAML DSL :: Validator Maven Plugin (5.3s)
  • Camel :: YAML DSL :: Maven Plugins (3.8s)
  • Camel :: Catalog :: Maven (3.3s)
  • Camel :: Catalog :: Suggest (deprecated) (2.6s)
  • Camel :: Coverage (2.0s)

⚙️ View full build and test results

…r its lock before it stops or shuts down

DisruptorEndpoint.stop() and shutdown() checked that no consumer (and no suspended consumer) uses the endpoint
before they took the service lock, so a consumer that registered between the check and the stop (onStarted takes
the same lock) ran on an endpoint that then stopped its disruptor. The check now runs under the lock; the lock is
reentrant, so super.stop() and super.shutdown() take it again as before.

DisruptorRemoveRouteOfSharedEndpointTest holds the lock as a registering consumer does, asks the endpoint to stop
from another thread, registers a consumer and then releases the lock: the endpoint must stay started.

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

Copy link
Copy Markdown
Contributor Author

@davsclaus thanks for the review. The nit is fixed in dca9d85 (one new commit on top): stop() and shutdown() now check for consumers and suspended consumers under the endpoint's service lock, the one onStarted/onSuspended/onStopped take, so a consumer that starts concurrently is either seen by the check or registers after the stop. The lock is reentrant, so super.stop()/super.shutdown() take it again as before, and no new lock order comes in (they already held it around doStop/doShutdown).

Test: consumerRegisteringWhileTheEndpointIsAskedToStopKeepsTheEndpointStarted holds the lock as a registering consumer does, lets another thread call stop() until it is queued on the lock, then registers a consumer. With the old check outside the lock it fails (the endpoint stops); no sleep. camel-disruptor: 122 tests, 0 failures; the test class passed 20 runs in one JVM.

No open follow-ups.

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.

LGTM. The fix is correct and mirrors the established SedaEndpoint guard pattern exactly.

Review summary:

  • Lock reentrancy (onSuspended → onStopped both acquire lock): safe — BaseService.lock is a ReentrantLock.
  • Consumer lifecycle tracking: leak-free — onStopped, onStarted (resume), and onSuspended all maintain suspendedConsumers consistently.
  • stop()/shutdown() override: correct — endpoint stays STARTED while active or suspended consumers exist; proceeds normally when the last consumer leaves.
  • Lock ordering (endpoint lock → DisruptorReference lock): no inversion, no deadlock risk.
  • Test coverage: four targeted scenarios including a lock-hold race test (consumerRegisteringWhileTheEndpointIsAskedToStopKeepsTheEndpointStarted) that exercises the TOCTOU window directly. Author confirmed three of four tests fail on main without the fix.

Backport note: The PR description targets camel-4.22.x, 4.18.x, and 4.14.x, but no port/* labels are set. Please add port/camel-4.22.x, port/camel-4.18.x, and port/camel-4.14.x to trigger the backport bot.

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.

The changes since my review address my points; CI is green. Thanks.

Reviewed with 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 29, 2026
@davsclaus davsclaus added the bug Something isn't working label Oct 29, 2026
@davsclaus
davsclaus merged commit b81559f into apache:main Oct 29, 2026
6 checks passed
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