Skip to content

CAMEL-25221: camel-couchbase - remove a document only after its exchange completed - #27347

Merged
davsclaus merged 2 commits into
apache:mainfrom
allthingssecurity:camel-couchbase-delete-after-processing
Oct 5, 2026
Merged

davsclaus merged 2 commits into
apache:mainfrom
allthingssecurity:camel-couchbase-delete-after-processing

Conversation

@allthingssecurity

@allthingssecurity allthingssecurity commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Description

CAMEL-25221

With consumerProcessedStrategy=delete, CouchbaseConsumer removed every document of a poll in pollWithSqlQuery / pollWithView, while building the exchanges and before processBatch handed any of them to the route. So a document was lost:

  • when its exchange failed (the route never sees it again);
  • when it was never handed to the route at all: processBatch stops at maxMessagesPerPoll and when isBatchAllowed() turns false because the consumer is stopping, and the rows after that point had already been removed.

@davsclaus raised this in the review of #27123 (CAMEL-25171), and @oscerd filed it as CAMEL-25221 with the follow-up options. This PR takes the first one, delete after the exchange has been processed successfully, which the ticket calls the real fix (the comparable camel-mongodb change was CAMEL-25024).

This change:

  • the removal runs in an on-completion (SynchronizationAdapter.onComplete) added to the exchange when it is built, the delete-after-read pattern of other consumers (for example aws2-s3). It runs only for an exchange that went through the route's unit of work and completed: a failed exchange keeps its document for the next poll, and an exchange that was never handed to the route (released by the drain loop from CAMEL-25169, CAMEL-25170, CAMEL-25171, CAMEL-25172: camel-couchbase - close the connection, apply connectTimeout, report route failures, accept persistTo=2 #27123) never runs it. A handled exception (onException().handled(true), dead letter channel) completes the exchange, so the document is removed, as for the file consumer;
  • the on-completion keeps the default allowHandover() == true, so when the route hands the exchange over to another thread (seda with waitForTaskToComplete=Never, an asynchronous producer) the document is removed when that exchange completes, not before the asynchronous part has processed it (with false it would be removed when the consumer's own unit of work ends, which is the data loss this PR fixes). Because that can be after the next poll, the consumer now keeps the IDs of the documents whose exchange is in flight and skips them in later polls, as the in-progress repository of the file and aws2-s3 consumers does (second commit, from @davsclaus's review). An ID is released when its exchange completes or fails, when the exchange is never handed to the route (batch cut short by maxMessagesPerPoll or by the consumer stopping), and when the poll fails part way (for example the fullDocument get of a later row throws). A skipped row creates no exchange, so it does not count towards maxMessagesPerPoll. It is an internal concurrent set rather than a pluggable inProgressRepository endpoint option like aws2-s3's: the guard only has to cover this consumer's own handed-over exchanges, an unbounded set cannot evict an ID that is still in flight (aws2-s3's default is a 10000-entry FIFO cache), and it adds no new option to a bug fix. It is per consumer, so it does not stop a consumer on another node from reading the same document. It applies to delete only: with none / filter the document stays in the bucket and later polls read it again, as before;
  • a failure to remove the document is reported through the consumer's exception handler (the removal used to throw out of poll() and abort the rest of the batch);
  • the comments that pointed at CAMEL-25221 in processBatch / processExchange (added by CAMEL-25169, CAMEL-25170, CAMEL-25171, CAMEL-25172: camel-couchbase - close the connection, apply connectTimeout, report route failures, accept persistTo=2 #27123) are updated;
  • filter and none are unchanged (neither removes anything);
  • upgrade guide: the last sentence of the existing 4.23 === camel-couchbase entry (from CAMEL-25169, CAMEL-25170, CAMEL-25171, CAMEL-25172: camel-couchbase - close the connection, apply connectTimeout, report route failures, accept persistTo=2 #27123) said the document "is removed during the poll, before the route runs"; it is replaced by a ==== consumerProcessedStrategy=delete subsection that describes the new timing, including that a document whose route always fails is now consumed again on every poll, and the in-flight guard with its per-consumer limit.

Tests: new CouchbaseConsumerProcessedStrategyTest runs the consumer in a real route (so the exchange gets a unit of work) with the scheduler not started, calls poll() against a mocked cluster, and verifies Collection.remove:

  • the document still exists while the route runs, and is removed after it (SQL++);
  • a failed exchange keeps its document, the next row of the same poll is removed (SQL++ and view);
  • maxMessagesPerPoll=1 with three rows: only the first is removed;
  • the consumer told to complete only its current task (deferShutdown(CompleteCurrentTaskOnly), as the shutdown strategy does) during the first exchange: only the first is removed;
  • a failing removal is reported to the exception handler;
  • consumerProcessedStrategy=none removes nothing (control).

Without the change 6 of the 7 fail (twice, NeverWantedButInvoked for the failed/undelivered rows, the document already removed while routing, and the removal exception thrown out of poll()); the none control passes.

In-flight guard tests (same class; the couchbase route hands every exchange to seda:async?waitForTaskToComplete=Never, and the route consuming that queue is started by the test when the handed-over exchanges should complete):

  • an exchange handed over and still in flight: the next poll returns 0 and does not deliver the document again (SQL++ and view);
  • after the handed-over exchange completes: it was processed once and the document removed once;
  • after it fails: the document is kept, and the next poll delivers it again;
  • maxMessagesPerPoll=1 with three rows, and the consumer stopping mid batch: the undelivered rows are not left in flight, later polls deliver doc-2 and doc-3 while doc-1 is still in flight;
  • a poll that fails part way does not leave the rows built so far in flight;
  • consumerProcessedStrategy=none still delivers a document whose exchange is in flight (unchanged behaviour, control).

Without the guard 6 of these 8 fail (the document delivered again while in flight); with the guard but without releasing the IDs, 4 fail (failure, maxMessagesPerPoll, stopping, poll failing part way). With both commits the camel-couchbase module passes: 57 unit tests, 0 failures (the 8 Docker IT tests are skipped locally).

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

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 the affected module, including the formatter and import-sort plugins. 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

…nge completed

With consumerProcessedStrategy=delete the consumer removed every document
of a poll while building the exchanges, before processBatch handed any of
them to the route. A document was therefore lost when its exchange failed,
and also when it was never delivered because the batch was cut short by
maxMessagesPerPoll or by the consumer stopping.

The removal now runs in an on-completion of the exchange, as the
delete-after-read consumers of other components do, so it happens only
for an exchange that went through the route and completed. A failed or
undelivered document is kept for a later poll. A failure to remove the
document is reported through the consumer's exception handler.

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

github-actions Bot commented Oct 5, 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.

@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, this fixes the data loss raised on #27123: documents of failed exchanges, of rows beyond maxMessagesPerPoll, and of rows dropped at shutdown were deleted. Deleting on completion follows the established pattern (as in aws2-s3), and the upgrade-guide text and tests cover the new behaviour. One question inline.

Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying. It does not replace specialized review tools or static analysis.

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • components/camel-couchbase
  • docs

🔬 Scalpel shadow comparison — Scalpel: 9 of 698 tested, 26 compile-only — current: 9 all tested

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

Skip-tests mode would test 9 modules (2 direct + 8 downstream), skip tests for 26 (generated code, meta-modules)

Modules Scalpel would test (9)
  • camel-couchbase ← components/camel-couchbase/src/main/java/org/apache/camel/component/couchbase/CouchbaseConsumer.java, components/camel-couchbase/src/test/java/org/apache/camel/component/couchbase/CouchbaseConsumerProcessedStrategyTest.java, components/camel-couchbase/src/test/java/org/apache/camel/component/couchbase/CouchbaseConsumerTest.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-route-parser ← downstream of org.apache.camel:camel-route-parser
  • 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 (26)
  • 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
  • dummy-component

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

⚠️ Some tests are disabled on GitHub Actions (@DisabledIfSystemProperty(named = "ci.env.name")) and require manual verification:

  • components/camel-couchbase: 6 test(s) disabled on GitHub Actions
All tested modules (36 modules, 5m 28s total)

Total reactor time: 5m 28s

Module Duration Status
Camel :: Launcher 46.3s SUCCESS
Camel :: JBang :: Plugin :: TUI 40.3s SUCCESS
Camel :: JBang :: MCP 36.4s SUCCESS
Camel :: Component DSL 24.2s SUCCESS
Camel :: Catalog :: Camel Catalog 21.3s SUCCESS
Camel :: Couchbase 20.4s SUCCESS
Camel :: YAML DSL 17.3s SUCCESS
Camel :: JBang :: Plugin :: Kubernetes 17.0s SUCCESS
Camel :: YAML DSL :: Validator 16.9s SUCCESS
Camel :: Docs 14.4s SUCCESS
Camel :: Kamelet Main 10.3s SUCCESS
Camel :: YAML DSL :: Deserializers 7.9s SUCCESS
Camel :: Catalog :: Camel Route Parser 7.4s SUCCESS
Camel :: JBang :: Plugin :: Testing 7.1s SUCCESS
Camel :: Catalog :: Camel Report Maven Plugin 5.9s SUCCESS
Camel :: JBang :: Plugin :: Validate 5.4s SUCCESS
Camel :: All Components Sync point 5.1s SUCCESS
Camel :: YAML DSL :: Validator Maven Plugin 4.8s SUCCESS
Camel :: YAML DSL :: Maven Plugins 3.0s SUCCESS
Camel :: Catalog :: Maven 2.5s SUCCESS
Camel :: Catalog :: Suggest (deprecated) 2.3s SUCCESS
Camel :: Assembly 1.5s SUCCESS
Camel :: JBang :: Plugin :: Edit 1.4s SUCCESS
Camel :: Coverage 1.3s SUCCESS
Camel :: Catalog :: Dummy Component 1.1s SUCCESS
Camel :: JBang :: Plugin :: Generate 1.1s SUCCESS
Camel :: JBang :: Plugin :: MCP 0.9s SUCCESS
Camel :: Catalog :: Console 0.8s SUCCESS
Camel :: JBang :: Integration tests 0.8s SUCCESS
Camel :: JBang :: Main 0.7s SUCCESS
Camel :: Endpoint DSL :: Support 0.7s SUCCESS
Camel :: Launcher :: Container 0.7s SUCCESS
Camel :: JBang :: Plugin :: Route Parser 0.5s SUCCESS
Camel :: Endpoint DSL n/a
Camel :: Integration Tests n/a
Camel :: JBang :: Core n/a

Top 20 slowest modules:

  • Camel :: Launcher (46.3s)
  • Camel :: JBang :: Plugin :: TUI (40.3s)
  • Camel :: JBang :: MCP (36.4s)
  • Camel :: Component DSL (24.2s)
  • Camel :: Catalog :: Camel Catalog (21.3s)
  • Camel :: Couchbase (20.4s)
  • Camel :: YAML DSL (17.3s)
  • Camel :: JBang :: Plugin :: Kubernetes (17.0s)
  • Camel :: YAML DSL :: Validator (16.9s)
  • Camel :: Docs (14.4s)
  • Camel :: Kamelet Main (10.3s)
  • Camel :: YAML DSL :: Deserializers (7.9s)
  • Camel :: Catalog :: Camel Route Parser (7.4s)
  • Camel :: JBang :: Plugin :: Testing (7.1s)
  • Camel :: Catalog :: Camel Report Maven Plugin (5.9s)
  • Camel :: JBang :: Plugin :: Validate (5.4s)
  • Camel :: All Components Sync point (5.1s)
  • Camel :: YAML DSL :: Validator Maven Plugin (4.8s)
  • Camel :: YAML DSL :: Maven Plugins (3.0s)
  • Camel :: Catalog :: Maven (2.5s)

⚙️ View full build and test results

…its exchange is in flight

With consumerProcessedStrategy=delete the document is removed in an
on-completion that is handed over with the exchange, so it is not removed
before an asynchronous part of the route has processed it. When the route
hands the exchange over to another thread (seda with
waitForTaskToComplete=Never, an asynchronous producer), the next poll could
read the same document again before that exchange completed and deliver it
twice.

The consumer now keeps the IDs of the documents whose exchange is in flight,
as the in-progress repository of the file and aws2-s3 consumers does, and
skips them in later polls. An ID is released when its exchange completes or
fails, when its exchange is never handed to the route (batch cut short by
maxMessagesPerPoll or by the consumer stopping), and when the poll fails
part way. A skipped row does not count towards maxMessagesPerPoll. The other
strategies leave the document in place and are not guarded.

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.

Solid fix for the data loss in CAMEL-25221. The delete-on-completion pattern correctly follows the established Camel conventions (aws2-s3, file consumer), and the in-flight guard handles the async handover case cleanly.

Verified:

  • Thread safety: ConcurrentHashMap.newKeySet() is the right choice — poll-side writes are under the existing lock, callback-side remove() is thread-safe and idempotent. The check-then-act in isInFlight/inFlight.add doesn't race because both run under the poll lock.
  • Resource cleanup: releaseUndelivered correctly drains in-flight IDs before releasing pooled exchanges, so even if PooledExchange.done() fires on-completion callbacks, the double-remove is harmless.
  • Failure partway through a poll: If getDocument() throws for doc N, docs 0..N-1 are already in the exchanges queue with their IDs in inFlight. The catch in poll() → releaseUndelivered cleans up both correctly.
  • processBatch return value: Returns the number of exchanges actually built (excluding skipped in-flight docs), which is the correct semantics for the framework.
  • Test coverage: 15 tests exercise both query paths (SQL++, View), success/failure, maxMessagesPerPoll, shutdown mid-batch, async handover with completion/failure, partial poll failure, and control cases for other strategies. Thorough.
  • Upgrade guide: Accurate, covers the behavior change including the edge case of per-consumer scope and the interaction with error handling (onException, dead letter channel).

ast-grep flagged 3 broad-exception-catch patterns — all are standard Camel patterns (catch-rethrow in poll(), catch-handle in onComplete, pre-existing processExchange). Not issues.

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.

Re-approving after the in-flight tracking commit; the set is only checked/added under the poll lock and is cleared on completion, failure and early stop.

Follow-up thought: if a handed-over exchange never completes (seda queue full/purged, async producer that never calls back), its id stays in the set for the life of the consumer and the document is skipped forever, even across stopRoute/startRoute since the consumer is reused. Clearing the set in doStop/doStart would bound that. Also worth a line in the upgrade note: a view emitting several rows for one document now delivers only the first row under delete.

@allthingssecurity

Copy link
Copy Markdown
Contributor Author

Thanks. Good points, I'll take both in a follow-up once this is merged: clear the in-flight set in doStart (so a stop/start of the route bounds an exchange that never completes, at the cost of a possible re-delivery right after the restart), and add the line to the upgrade note that with delete a view emitting several rows for one document now delivers only the first row.

Claude Code on behalf of allthingssecurity

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

Traced the whole in-flight lifecycle; correct, and it's the real fix CAMEL-25221 asked for (the delete-during-poll was losing documents on failure and on the rows past maxMessagesPerPoll/shutdown).

  • Delete after successful processing. The removal is an onComplete on-completion added when the exchange is built, so it runs only for an exchange that went through the route's unit of work and completed; onFailure keeps the document for the next poll, and a handled exception (handled(true)/DLC) completes the exchange so the document is removed — matching the file/aws2-s3 delete-after-read pattern. Keeping the default allowHandover()==true means a handed-over exchange (seda waitForTaskToComplete=Never, async producer) deletes the document when that exchange completes, not when the consumer's unit of work ends, which is the data loss being fixed.
  • The in-flight guard is released on every path, so no document is leaked into a permanent skip: onComplete (remove + finally release), onFailure (release, keep doc), processBatch's trailing releaseUndelivered for rows cut off by maxMessagesPerPoll or a stopping consumer, and the poll's inner catch → releaseUndelivered when building the batch throws part way. A still-in-flight id is skipped on later polls (isInFlight) without counting toward maxMessagesPerPoll, and the set is per-consumer so it doesn't mask another node. none/filter are untouched.
  • A removal failure now goes through getExceptionHandler() instead of throwing out of poll() and aborting the rest of the batch.

The upgrade-guide subsection correctly documents the new timing, the "a route that always fails re-reads the document each poll" consequence, and the per-consumer in-flight limit. CouchbaseConsumerProcessedStrategyTest covers doc-present-during-route/removed-after, failed-keeps-doc (SQL++ and view), maxMessagesPerPoll=1, deferShutdown(CompleteCurrentTaskOnly), the removal-failure-to-exception-handler path, the none control, and the handover guard via seda:async. CI is green.

Approving.

This review was generated with AI assistance and reviewed/issued by the human operator. Claude Code on behalf of oscerd

@davsclaus davsclaus added this to the 4.23.0 milestone Oct 5, 2026
@davsclaus davsclaus added the bug Something isn't working label Oct 5, 2026
@davsclaus
davsclaus merged commit 6b79e83 into apache:main Oct 5, 2026
7 checks passed
@allthingssecurity
allthingssecurity deleted the camel-couchbase-delete-after-processing branch October 5, 2026 17:11
allthingssecurity added a commit to allthingssecurity/camel that referenced this pull request Oct 9, 2026
…ument the fullDocument default

With useView=true and fullDocument=false the consumer set the message body to
row.valueAs(Object.class), which the Couchbase SDK 3 returns as an Optional, so
the body was Optional[value] (Optional.empty when the view emitted null) instead
of the value. The body is now the value, or null when the view emitted none.
The SQL++ path is not affected: it uses the query row as a JSON string.

The fullDocument option was documented with defaultValue false since it was
added (CAMEL-15792), but the field has always defaulted to true, which keeps
the behaviour from before the option existed (the consumer always fetched the
document). The annotation now says true, and the component JSON, the catalog
and the endpoint DSL are regenerated. No change at runtime.

Upgrade guide: the body change, and, as asked in the review of apache#27347
(CAMEL-25221), that a view or SQL++ query returning several rows for one
document delivers only the first of them with consumerProcessedStrategy=delete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 docs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants