Skip to content

CAMEL-25861: camel-azure-cosmosdb - the consumer starts the change feed processor again when it cannot start - #28637

Open
allthingssecurity wants to merge 3 commits into
apache:mainfrom
allthingssecurity:camel-azure-cosmosdb-consumer-start-retry-v2
Open

allthingssecurity wants to merge 3 commits into
apache:mainfrom
allthingssecurity:camel-azure-cosmosdb-consumer-start-retry-v2

Conversation

@allthingssecurity

@allthingssecurity allthingssecurity commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Description

CAMEL-25861

CosmosDbConsumer subscribed once to ChangeFeedProcessor.start() in doStart. The start reads the lease and feed containers, so it fails when Cosmos DB cannot be reached when the route starts, when the lease container does not exist yet (createLeaseContainerIfNotExists is false by default), or when the role assignment of a managed identity is not applied yet. The failure went to the exception handler ("Error processing exchange") and nothing started the processor again: the route stayed started and never received a change until it was restarted. Stopping such a route also failed, because the SDK's stop() throws on a processor that has not started.

(With createDatabaseIfNotExists, createContainerIfNotExists or their lease variants, building the processor in doInit already reads or creates the containers, so when Cosmos DB cannot be reached at that moment the route fails to start, as before; the retry covers the start that fails after that.)

This is the same pattern as CAMEL-25857 in camel-kubernetes.

Changes:

  • doStart starts the processor with Mono.defer(() -> changeFeedProcessor.start()).retryWhen(Retry.backoff(...)): 1 s, then doubling up to 30 s, until it starts or the consumer is stopped. The first failed attempt of each start goes to the exception handler of the consumer, as the one failure did before, so with bridgeErrorHandler=true it is routed to the error handler once and not once per attempt; each further failed attempt is logged as one WARN line with its cause, and the stack trace at DEBUG (the attempts go on every 30 s for as long as Cosmos DB cannot be reached). A failure that ends after the consumer stopped is not reported. (Review follow-up: before, no attempt went to the exception handler.)
  • Each attempt after a failure uses a new change feed processor. A processor whose start failed after it built its partition manager cannot start again: in azure-cosmos 4.75.0 PartitionManagerImpl.start() calls PartitionLoadBalancerImpl.start() while it assembles the start, which marks the load balancer as started before the lease bootstrap runs, so after a failure in the bootstrap (a 429 or 503 while the leases are created, no write permission on the lease container) every later start() throws IllegalStateException: Partition load balancer already started. A failure before that (the feed or lease container cannot be read) could be retried on the same processor; a new one works in both cases.
  • The attempt builds the new processor, not the retry hook, and every attempt runs on Reactor's boundedElastic scheduler (subscribeOn before retryWhen, so each resubscription goes through it). Building a processor blocks: CosmosDbContainerOperations.captureEventsWithChangeFeed zips the feed and lease containers and calls block() (with the create*IfNotExists options, it creates them first). A failure to build the processor is retried like a failure to start it. The failure of a start is signalled on a thread of the Cosmos DB client or of a Reactor scheduler, where block() throws IllegalStateException (BlockingSingleSubscriber.blockingGet checks Schedulers.isInNonBlockingThread() first, in reactor-core 3.7.11, which camel-azure-cosmosdb gets from azure-core 1.57.0, as in 3.8.x; the parent's reactor-version 3.8.8 does not manage reactor-core); thrown from the retry hook, that would end the retry, and the consumer would again stay started without receiving anything. When the processor starts after failed attempts, one INFO line says so.
  • The change feed processor is created in a package-private createChangeFeedProcessor(), which the test overrides (instead of setting the private field by reflection). It builds the client wrapper it uses as a local, no longer as a field that each attempt reassigned on a boundedElastic thread (review follow-up).
  • doStop cancels the pending attempts and stops the processor. A start() that is in flight is not cancelled: in azure-cosmos 4.75, isStarted() is false and stop() throws (The ChangeFeedProcessor instance has not fully started) until the processor has built its partition manager, and while it loads the leases it starts a worker for each lease this host owns, outside the Mono that start() returns. The processor, the attempts and doStop hand over under a lock, with a generation that doStart and doStop increment: a processor built after the stop is not started, doStop stops a starting processor if it can already, and the attempt observes the end of every start and stops the processor then if the consumer stopped meanwhile. A start that fails is stopped too if it started (a failure while loading the leases leaves the workers of the leases loaded before running). (Review follow-up.)

No option changed. The 4.24 upgrade guide says that the consumer starts the change feed processor again when its start fails, and how the failures are reported (review follow-up).

Tests:

  • New CosmosDbConsumerFirstStartFailsTest (5 tests). consumerStartsTheChangeFeedAgainWhenTheFirstStartFails: a change feed processor whose first start() fails and whose second start delivers one change. Without the change it fails after 20 s with the change feed processor was started 1 time(s), the consumer is started but receives nothing. With the change the change is received about 1 s after the WARN.
  • consumerStartsANewChangeFeedProcessorWhenTheFirstOneFailedWhileInitializingTheLeases (same class): the first processor fails its start and then throws Partition load balancer already started as the real one does; the consumer must start a second processor. Retrying the same processor fails after 20 s (the failed change feed processor was started 5 time(s), no new one was started).
  • consumerStartsANewChangeFeedProcessorWhenTheStartFailsOnAReactorThread (same class): the first processor's start fails on a Reactor parallel thread (Mono.delay(...).then(Mono.error(...))), and the test's createChangeFeedProcessor() ends in Mono.zip(...).block() as the real one does. With a retry hook that builds the processor (the first version of this change) the hook runs on parallel-2, block() throws and the retry ends: it fails after 20 s (no new change feed processor was started, built on the threads [main, parallel-2]).
  • theFirstFailedStartGoesToTheExceptionHandlerAndTheFollowingOnesAreLogged (same class, review follow-up): two failed starts, then the third starts; the consumer's exception handler gets the first failure only. Without the follow-up it gets none.
  • consumerTriesAgainWhenANewChangeFeedProcessorCannotBeBuilt (same class): after the first start fails, building the next processor throws once (as with createLeaseContainerIfNotExists=true while Cosmos DB cannot be reached), and the third one starts. With the first version of this change, the exception from the retry hook ended the retry: it fails after 20 s.
  • New CosmosDbConsumerStopWhileStartingTest (6 tests), with a processor that behaves as the azure-cosmos 4.75 one (isStarted() once its partition manager is built, stop() throws before that, the workers of the owned leases start while the leases are loaded, outside the returned Mono). The consumer stops while the start is before the partition manager and the start then completes (the processor must be stopped); while the leases are loaded (stopped at once); while the next attempt builds a processor (it must not be started); a start fails after it started workers (stopped before the next processor starts); a restart after the processor started; a restart while the first start is in flight (the new processor receives the change, the old one is stopped when its start completes). Without the follow-up commit, 4 of the 6 fail (the lease-phase stop and the restart after a completed start also pass there; they guard that this still works).
  • camel-azure-cosmosdb unit tests with the change: 58 run, 0 failures (22 ITs skipped, as without the credentials). CosmosDbConsumerFirstStartFailsTest and CosmosDbConsumerStopWhileStartingTest each passed 20 runs in a row in one JVM.

How it was found: a review of consumers whose first connect or subscribe runs once and asynchronously, then the JUnit test.

What was not run: a test against Cosmos DB or its emulator, the ITs, 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-25861.

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-azure-cosmosdb. No option 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). Each commit carries a Co-Authored-By trailer.

Claude Code on behalf of allthingssecurity

🤖 Generated with Claude Code

…ed processor again when it cannot start

The consumer subscribed once to ChangeFeedProcessor.start() in doStart. The
start reads the lease and feed containers, so it fails when Cosmos DB
cannot be reached yet, the lease container does not exist yet or a role
assignment is not applied yet. The failure went to the exception handler
and the processor was never started again: the route stayed started and
never received a change, until it was restarted.

Start it again with a backoff (1 s, up to 30 s) until it starts or the
consumer is stopped, logging one WARN line for each failed attempt (the
stack trace at DEBUG). Each attempt after a failure uses a new change
feed processor: one whose start failed while it initialized its leases
throws "Partition load balancer already started" from every later
start(). The attempt builds it, not the retry hook: building a change
feed processor calls block() (and with the create*IfNotExists options
creates the containers), and the failure of a start is signalled on a
thread of the Cosmos DB client or of a Reactor scheduler, where block()
throws, which ended the retry. Each attempt runs on the boundedElastic
scheduler, where it may block, and a failure to build the processor is
retried as a failure to start it. When the processor starts after failed
attempts, one INFO line says so. On stop, the pending attempts are
cancelled, and stop() is only called on a processor that has started (it
throws on one that has not).

The change feed processor is created in a package-private method, so that
the test can give its own.

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

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

The retry logic is well-structured and the problem analysis in the PR description is thorough. One concurrency gap in doStop() needs attention before this merges.

Missing backport labels: This is a bug fix for a consumer that silently stops receiving changes when Cosmos DB is temporarily unavailable — users with transient network partitions or delayed role assignment rollouts are affected until they restart the route. Given the similar fix in camel-kubernetes (CAMEL-25857, PR #28380) was backported, this should also carry port/camel-4.22.x and port/camel-4.18.x labels.

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

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

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

…ose start ends after the consumer stopped

doStop cancelled the attempts to start and called stop() only on a
processor that isStarted(). In azure-cosmos 4.75, isStarted() is false and
stop() throws until the processor has built its partition manager, so a
processor whose start was before that point was left to the cancel, and
was stopped only if the cancel stopped its start in time. A start that
failed while it loaded the leases was not stopped either: the workers of
the leases it had loaded before, which run outside the Mono that start()
returns, kept running next to the next processor.

The processor, the attempts to start it and doStop now hand over under a
lock, with a generation that doStart and doStop increment. An attempt of
an older generation does not start the processor it built, or stops it
when its start ends if doStop came just as it called start(). doStop does
not cancel a start that is in flight: it stops the processor if it can
already (isStarted()), and the end of the start is always observed and
signalled under the lock: before doStop, which then stops the processor
as before, or after it, and then the attempt stops the processor if it
started. A start that fails is stopped too if it started. A restart
initializes the consumer again, which builds a new processor, as before;
the attempt of the old generation still stops the old one. A build that
fails because doStop interrupted it is not reported as a failure.

The tests of CosmosDbConsumerFirstStartFailsTest deliver their change
while start() runs, so the consumer may stop before the start completed:
their processors now stop once, as the real one, and the tests wait for
the processor to be stopped.

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

Copy link
Copy Markdown
Contributor Author

@gnodet thanks for the review. Both points are addressed in d1af39f (one new commit on top), see the inline replies.

  • The race: the consumer no longer relies on cancelling the start. The processor, the attempts and doStop hand over under a lock, with a generation, and a processor whose start ends after the stop is stopped by its attempt. A failed start that had already started the workers of some leases is now stopped too; before, those workers kept running next to the next processor.
  • Tests: new CosmosDbConsumerStopWhileStartingTest, 6 tests with a processor that behaves as the azure-cosmos 4.75 one. 4 of them fail on the previous head. The class passed 21 runs in a row in one JVM (126 tests, 0 failures). camel-azure-cosmosdb unit tests: 57 run, 0 failures, 22 ITs skipped (no credentials).
  • Backports: I cannot set labels. If you want them, please add port/camel-4.22.x and port/camel-4.18.x. Both commits apply cleanly to camel-4.22.x and camel-4.18.x (checked with git merge-tree). On camel-4.18.x the two test classes need org.apache.camel.test.junit5.CamelTestSupport instead of junit6, as the module still uses junit5 there.

I updated the description to match.

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.

The follow-up commit (d1af39ff2c0b) fully addresses both findings from the previous review.

Processor leak race (thread 1): The lock+generation approach is the right solution given the SDK's constraints. The author's analysis of azure-cosmos 4.75's IncrementalChangeFeedProcessorImpl (verified via javap) explains exactly why neither of the two suggested fixes was sufficient:

  • Unconditional stop() catches the pre-partition-manager case but misses the lease-worker leak
  • A volatile boolean stopping flag would reset on restart, breaking the generation invariant

The generation counter under a final Object lock handles all six stop-while-starting scenarios, confirmed by CosmosDbConsumerStopWhileStartingTest. The acknowledged residual window (lease workers running briefly until the start ends when stop arrives before the partition manager exists) is unavoidable given stop() throwing until the partition manager is built — this is an SDK constraint, not a code defect.

onErrorListener (thread 2): The author's reasoning is correct — the handler is reachable when the Reactor scheduler rejects the delay before the next retry attempt. The comment added (// the retries are not limited...) explains this adequately. I withdraw the suggestion to remove it.

Backport labels: The author confirmed both commits apply cleanly to camel-4.22.x and camel-4.18.x (verified with git merge-tree, test class needs CamelTestSupport import adjusted on 4.18.x). Since the contributor cannot set labels, please add port/camel-4.22.x and port/camel-4.18.x.

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

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

gnodet-bot's two earlier findings (leak race in stop, onErrorListener) are fixed and the hand-off with lock plus generation counter holds up for stop/restart/late-callback interleavings. Will approve once CI is green.

  • Question (CosmosDbConsumer.java:114/128): a failed start no longer reaches the consumer's ExceptionHandler; it is one WARN per attempt, forever, so bridgeErrorHandler users lose the signal. Add a line to the upgrade guide or make it a deliberate no.
  • Suggestion: the consumer reports started while never connected; a health-check hook could follow.
  • Nit: createChangeFeedProcessor() reassigns clientWrapper as a side effect on boundedElastic threads (harmless, one attempt at a time). The body cites reactor-core 3.8.7, the parent pom has 3.8.8.

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

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • components/camel-azure/camel-azure-cosmosdb

🔁 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 708 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-azure-cosmosdb ← components/camel-azure/camel-azure-cosmosdb/src/main/java/org/apache/camel/component/azure/cosmosdb/CosmosDbConsumer.java, components/camel-azure/camel-azure-cosmosdb/src/test/java/org/apache/camel/component/azure/cosmosdb/CosmosDbConsumerFirstStartFailsTest.java, components/camel-azure/camel-azure-cosmosdb/src/test/java/org/apache/camel/component/azure/cosmosdb/CosmosDbConsumerStopWhileStartingTest.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 14s total)

Total reactor time: 5m 14s

Module Duration Status
Camel :: JBang :: MCP 47.1s SUCCESS
Camel :: JBang :: Plugin :: Validate 29.1s SUCCESS
Camel :: YAML DSL :: Validator 27.7s SUCCESS
Camel :: Catalog :: Camel Catalog 23.9s SUCCESS
Camel :: Azure :: CosmosDB 23.3s SUCCESS
Camel :: YAML DSL 22.0s SUCCESS
Camel :: Component DSL 20.8s SUCCESS
Camel :: JBang :: Plugin :: Testing 17.0s SUCCESS
Camel :: Kamelet Main 15.8s SUCCESS
Camel :: JBang :: Plugin :: Kubernetes 15.1s SUCCESS
Camel :: Docs 14.6s SUCCESS
Camel :: YAML DSL :: Deserializers 8.5s SUCCESS
Camel :: Catalog :: Camel Route Parser 8.1s SUCCESS
Camel :: Catalog :: Camel Report Maven Plugin 7.7s SUCCESS
Camel :: All Components Sync point 5.3s SUCCESS
Camel :: YAML DSL :: Validator Maven Plugin 4.4s SUCCESS
Camel :: Assembly 4.2s SUCCESS
Camel :: YAML DSL :: Maven Plugins 3.0s SUCCESS
Camel :: Catalog :: Maven 3.0s SUCCESS
Camel :: Catalog :: Suggest (deprecated) 2.4s SUCCESS
Camel :: Coverage 1.6s SUCCESS
Camel :: Catalog :: Dummy Component 1.3s SUCCESS
Camel :: JBang :: Plugin :: Generate 1.2s SUCCESS
Camel :: JBang :: Plugin :: Edit 1.2s SUCCESS
Camel :: Endpoint DSL :: Support 1.1s SUCCESS
Camel :: Catalog :: Console 1.1s SUCCESS
Camel :: JBang :: Integration tests 1.1s SUCCESS
Camel :: JBang :: Plugin :: MCP 0.8s SUCCESS
Camel :: Launcher :: Container 0.7s SUCCESS
Camel :: JBang :: Main 0.7s SUCCESS
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 (47.1s)
  • Camel :: JBang :: Plugin :: Validate (29.1s)
  • Camel :: YAML DSL :: Validator (27.7s)
  • Camel :: Catalog :: Camel Catalog (23.9s)
  • Camel :: Azure :: CosmosDB (23.3s)
  • Camel :: YAML DSL (22.0s)
  • Camel :: Component DSL (20.8s)
  • Camel :: JBang :: Plugin :: Testing (17.0s)
  • Camel :: Kamelet Main (15.8s)
  • Camel :: JBang :: Plugin :: Kubernetes (15.1s)
  • Camel :: Docs (14.6s)
  • Camel :: YAML DSL :: Deserializers (8.5s)
  • Camel :: Catalog :: Camel Route Parser (8.1s)
  • Camel :: Catalog :: Camel Report Maven Plugin (7.7s)
  • Camel :: All Components Sync point (5.3s)
  • Camel :: YAML DSL :: Validator Maven Plugin (4.4s)
  • Camel :: Assembly (4.2s)
  • Camel :: YAML DSL :: Maven Plugins (3.0s)
  • Camel :: Catalog :: Maven (3.0s)
  • Camel :: Catalog :: Suggest (deprecated) (2.4s)

⚙️ View full build and test results

…e consumer's ExceptionHandler

- The first failed attempt of each start of the consumer goes to its ExceptionHandler, as the one failure to start
  did before the attempts were retried, so with bridgeErrorHandler=true the error handler gets it once rather than
  once per attempt. Each further attempt is one WARN line, as before. A failure that ends after the consumer stopped
  is logged at DEBUG only.
- createChangeFeedProcessor() builds the CosmosAsyncClientWrapper it uses as a local: it was a field that each
  attempt reassigned on a boundedElastic thread.
- The 4.24 upgrade guide says that the consumer starts the change feed processor again when its start fails, and how
  the failures are reported.

CosmosDbConsumerFirstStartFailsTest gets a test with two failed starts and a third that works: the ExceptionHandler
gets the first failure only.

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

Copy link
Copy Markdown
Contributor Author

@davsclaus thanks for the review. Changes in 45f45d8 (one new commit on top):

  • ExceptionHandler (CosmosDbConsumer.java:114/128): the first failed attempt of each start now goes to the consumer's ExceptionHandler, as the one failure to start did before; each further attempt is one WARN line. So with bridgeErrorHandler=true the error handler gets the failure once per start, not every 30 seconds. A failure that ends after the consumer stopped is not reported, and a throwing handler does not end the retry. New test theFirstFailedStartGoesToTheExceptionHandlerAndTheFollowingOnesAreLogged (two failed starts, then a working one: the handler gets only the first); it fails without the change. The 4.24 upgrade guide also has an entry now for the retry and how failures are reported.
  • clientWrapper: createChangeFeedProcessor() builds the wrapper as a local, no longer reassigning a field on the boundedElastic threads.
  • reactor-core in the body: I corrected it, but not to 3.8.8. The parent's reactor-version (3.8.8) does not manage reactor-core; camel-azure-cosmosdb resolves 3.7.11 from azure-core 1.57.0 (dependency:build-classpath). BlockingSingleSubscriber.blockingGet checks Schedulers.isInNonBlockingThread() first in 3.7.11 and 3.8.7 (javap). The body now says so.

camel-azure-cosmosdb: 58 tests, 0 failures (22 ITs skipped); CosmosDbConsumerFirstStartFailsTest and CosmosDbConsumerStopWhileStartingTest each passed 20 runs in one JVM.

Open follow-up: the health check you suggested (the consumer reports started while the processor is not). I would do it as a separate JIRA, a readiness check that is DOWN until the change feed processor has started.

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.

The change looks good. The fix correctly addresses the issue by separating the concern of starting the change feed processor from the endpoint lifecycle management.

Summary of changes:

  • onStartAttemptFailed now routes the first failure to the configured ExceptionHandler (instead of always retrying silently), giving users visibility into connectivity problems
  • clientWrapper is properly scoped as a local variable in createChangeFeedProcessor(), eliminating the unwanted side-effect on the field
  • The Mono.create pattern with isStale() guard is standard Reactor idiom — when doStop() disposes the subscription, returning early from the sink is the correct way to handle cancellation

No issues found. Backport labels (port/camel-4.22.x, port/camel-4.18.x) may be worth considering if this fix is relevant to LTS branches.

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

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

Read the consumer change closely — this is tricky async-lifecycle code (a start that is retried, runs on a blocking scheduler, and can complete after the consumer stops), so it deserved the scrutiny. It's very carefully done.

The fix itself is right: the change feed processor's start() reads the lease/feed containers, so it legitimately fails when Cosmos DB isn't reachable yet, the lease container doesn't exist, or a role assignment hasn't propagated — and before, that left the route up but silent forever. Retrying on Schedulers.boundedElastic() (correct, since building/starting blocks and would be rejected on a non-blocking scheduler) with Retry.backoff(…, 1s).maxBackoff(30s) until it starts or the consumer stops is the sensible behaviour.

What I checked carefully is the stop/in-flight-start race, and the locking discipline holds up:

  • generation is bumped under lock in both doStart and doStop, and every attempt carries its startGeneration; isStale is the single comparison. So once doStop runs, in-flight attempts are stale and won't install or start a processor.
  • The key invariant — the end of a start() is observed under the lock, so either doStop sees starting and deals with the processor, or the completion handler (onStarted/onStartFailed) sees the stale generation and stops it — is sound. A processor that finishes starting after the consumer stopped is stopped by onStarted; a partial start whose lease workers already came up is stopped by onStartFailed; and doStop, for a mid-start processor, both nulls it and calls stopIfStarted itself. No path leaks a running processor.
  • Double-stop is safe: stopIfStarted gates on isStarted() and swallows IllegalStateException, with a comment that doStop and the start's end can both reach it. And rebuilding a fresh processor after a failed start is the right handling of azure-cosmos marking the load balancer started before the leases initialise (Partition load balancer already started).

The error-reporting contract is preserved thoughtfully: the first failure of each start still goes to the ExceptionHandler (so bridgeErrorHandler routes it once, not once per attempt), with subsequent attempts as a single WARN line each and the stack trace at DEBUG — and a stale failure (consumer stopped) is DEBUG only, not reported. The two new tests target exactly the two behaviours that matter (…FirstStartFails = the retry, …StopWhileStarting = the race), and this PR actually has CI (re-running at the moment).

The upgrade-guide entry is accurate and complete — it documents the new retry/backoff, the old "stayed started and never received a change" behaviour, and the reporting contract.

Two non-blocking notes:

  • The retry is effectively unbounded (Long.MAX_VALUE), which matches the intent ("until it starts or the consumer stops"), but a permanently-misconfigured consumer (lease container never created, role never granted) will then emit a WARN roughly every 30s forever. That's defensible as visibility, just flagging that a stuck consumer is now a steady WARN drip rather than a single failure — a camel.* knob to cap attempts could be a future nicety, not needed here.
  • failedStarts as a volatile long is fine here (written/read along the one Reactor chain, reset in doStart which happens-before subscribe); no concern, just noting I looked.

No blockers.

Claude Code on behalf of oscerd

@github-actions github-actions Bot added the docs label Oct 29, 2026
@allthingssecurity

Copy link
Copy Markdown
Contributor Author

@oscerd thanks for the close read. Agreed on both notes. The steady WARN every ~30s for a consumer that can never start is intended, so the problem stays visible. An option to cap the attempts (and then fail the route or report through the health check) fits the health-check follow-up I mentioned to Claus, and I'll file it as one follow-up JIRA rather than add it here.

Claude Code on behalf of allthingssecurity

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