Skip to content

CAMEL-23937: Add Camel-managed lock renewal for Azure Service Bus async routes#24511

Merged
davsclaus merged 2 commits into
mainfrom
fix/CAMEL-23937
Jul 8, 2026
Merged

CAMEL-23937: Add Camel-managed lock renewal for Azure Service Bus async routes#24511
davsclaus merged 2 commits into
mainfrom
fix/CAMEL-23937

Conversation

@davsclaus

@davsclaus davsclaus commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Claude Code on behalf of davsclaus

  • Add Camel-managed message lock renewal for Azure Service Bus consumers in PEEK_LOCK mode, fixing silent lock expiry for async routes
  • Use ServiceBusReceiverAsyncClient with non-blocking Reactor Mono subscriptions for concurrent lock renewals
  • Integrate with Camel's PeriodTaskScheduler for operational visibility (visible via camel get internal-tasks)

Problem

The Azure SDK's built-in lock renewal (maxAutoLockRenewDuration) is tied to the processMessage callback duration. For async Camel routes, this callback returns immediately — the SDK disposes its lock-renewal subscription, and long-running exchanges silently lose their message locks, causing duplicate delivery and data loss.

Solution

A LockRenewer inner class (modeled after SQS TimeoutExtender) tracks in-flight exchanges in a ConcurrentHashMap and periodically renews their message locks via ServiceBusReceiverAsyncClient.renewMessageLock(). Lock renewal activates when:

  • No custom processorClient is provided
  • Receive mode is PEEK_LOCK
  • maxAutoLockRenewDuration > 0
  • Session mode is disabled (session lock renewal is follow-up CAMEL-23938)

Exchanges are automatically removed from tracking on completion/failure via SynchronizationAdapter.

Lock renewer resources (renewal client and scheduled task) are cleaned up in doStop() so that route stop/start cycles do not leak AMQP connections. Defensive cleanup is also in doShutdown().

Files changed

  • ServiceBusClientFactory.java — Added createServiceBusReceiverAsyncClient() factory method
  • ServiceBusConsumer.java — Added LockRenewer inner class and lifecycle integration
  • ServiceBusConsumerTest.java — Added 9 tests for lock renewal behavior including stop/start lifecycle
  • camel-4x-upgrade-guide-4_22.adoc — Documented the new lock renewal behavior

Test plan

  • Lock renewer created for PEEK_LOCK mode with maxAutoLockRenewDuration > 0
  • Lock renewer NOT created for RECEIVE_AND_DELETE mode
  • Lock renewer NOT created when maxAutoLockRenewDuration is 0
  • Lock renewer NOT created for session-enabled consumers
  • Lock renewer NOT created when custom processorClient provided
  • Lock renewer tracks exchanges and removes them on completion (verified via renewer behavior)
  • doStop cleans up renewal client without closing main client
  • Stop/start cycle properly recreates lock renewer resources
  • Shutdown cleans up renewal client and lock renewer
  • All 32 unit tests pass (mvn verify)

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

…nc routes

The Azure SDK's built-in lock renewal is tied to the processMessage callback
duration, which returns immediately for async Camel routes. This makes
maxAutoLockRenewDuration ineffective, causing silent message-lock expiry,
duplicate delivery, and data loss.

This adds a LockRenewer that uses a ServiceBusReceiverAsyncClient to
periodically renew message locks for in-flight exchanges. It integrates
with Camel's PeriodTaskScheduler for operational visibility and uses
non-blocking Reactor Mono subscriptions for concurrent renewals.

Session-enabled consumers are excluded (follow-up in CAMEL-23938).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Claus Ibsen <claus.ibsen@gmail.com>
@github-actions

github-actions Bot commented Jul 8, 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.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • components/camel-azure/camel-azure-servicebus
  • docs

🔬 Scalpel shadow comparison — Scalpel: 10 tested, 28 compile-only — current: 9 all tested

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

⚠️ Modules only in Scalpel (29)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-lucene
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-csimple-maven-plugin
  • 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

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

Modules Scalpel would test (10)
  • camel-azure-servicebus
  • camel-jbang-mcp
  • camel-jbang-plugin-mcp
  • camel-jbang-plugin-route-parser
  • camel-jbang-plugin-tui
  • camel-jbang-plugin-validate
  • camel-launcher-container
  • camel-yaml-dsl-validator
  • camel-yaml-dsl-validator-maven-plugin
  • docs
Modules with tests skipped (28)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-lucene
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-csimple-maven-plugin
  • 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

All tested modules (38 modules)
  • Camel :: All Components Sync point
  • Camel :: Assembly
  • Camel :: Azure :: ServiceBus
  • Camel :: Catalog :: CSimple Maven Plugin (deprecated)
  • Camel :: Catalog :: Camel Catalog
  • Camel :: Catalog :: Camel Report Maven Plugin
  • Camel :: Catalog :: Camel Route Parser
  • Camel :: Catalog :: Console
  • Camel :: Catalog :: Dummy Component
  • Camel :: Catalog :: Lucene (deprecated)
  • Camel :: Catalog :: Maven
  • Camel :: Catalog :: Suggest
  • Camel :: Component DSL
  • Camel :: Coverage
  • Camel :: Docs
  • Camel :: Endpoint DSL
  • Camel :: Endpoint DSL :: Support
  • Camel :: Integration Tests
  • Camel :: JBang :: Core
  • Camel :: JBang :: Integration tests
  • Camel :: JBang :: MCP
  • Camel :: JBang :: Main
  • Camel :: JBang :: Plugin :: Edit
  • Camel :: JBang :: Plugin :: Generate
  • Camel :: JBang :: Plugin :: Kubernetes
  • Camel :: JBang :: Plugin :: MCP
  • Camel :: JBang :: Plugin :: Route Parser
  • Camel :: JBang :: Plugin :: TUI
  • Camel :: JBang :: Plugin :: Testing
  • Camel :: JBang :: Plugin :: Validate
  • Camel :: Kamelet Main
  • Camel :: Launcher
  • Camel :: Launcher :: Container
  • Camel :: YAML DSL
  • Camel :: YAML DSL :: Deserializers
  • Camel :: YAML DSL :: Maven Plugins
  • Camel :: YAML DSL :: Validator
  • Camel :: YAML DSL :: Validator Maven Plugin

⚙️ View full build and test results

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

Nice feature, Claus — plugging the dead SDK lock-renewal for async routes is a real gap, and the test coverage (7 tests, no Thread.sleep) is good. One concern I would like to raise before merge:

Renewal resources have an asymmetric lifecycle across route restarts. renewalClient and lockRenewer are created in doStart() and scheduled via PluginHelper.getPeriodTaskScheduler(...).schedulePeriodTask(...), but they are only torn down in doShutdown()doStop() is left untouched. A route stop/start cycle (suspend/resume, dynamic route control) invokes doStop()doStart() without doShutdown(), so on restart:

  • doStart() overwrites renewalClient with a new async AMQP client without closing the previous one → connection leak; and
  • it calls schedulePeriodTask(...) again. PeriodTaskScheduler (core SPI) exposes only schedulePeriodTask / scheduledTask — there is no unschedule — and lockRenewer.cancel() only flips an internal run=false guard, it does not remove the registered task. So renewals accumulate for the CamelContext lifetime.

The sibling Sqs2Consumer.TimeoutExtender avoids this by owning its own ScheduledExecutorService and shutting it down in doStop(). Moving the renewer/client teardown to doStop() (and re-creating in doStart()) would make the lifecycle symmetric.

Minor:

  • The new behavior (Camel now opens an extra async client and actively renews locks) is user-visible — worth a short note in azure-servicebus-component.adoc / the upgrade guide, even though it reuses the existing maxAutoLockRenewDuration option.
  • lockRenewerTracksAndRemovesExchanges asserts getPendingExchangesSize() (the ConsumerOnCompletion counter) rather than the LockRenewer's own entries map, so it does not actually verify renewer tracking/removal.

If consumer restarts are considered out of scope here, feel free to treat the lifecycle point as a non-blocking note.

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

- Fix lifecycle asymmetry: move lock renewer and renewal client cleanup
  to doStop() so route stop/start cycles don't leak AMQP connections
- Keep defensive cleanup in doShutdown() for safety
- Add test for stop/start cycle verifying resources are recreated
- Improve lockRenewerTracksAndRemovesExchanges test to verify actual
  renewer behavior rather than just the pendingExchanges counter
- Add upgrade guide note for the new lock renewal behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Claus Ibsen <claus.ibsen@gmail.com>
@davsclaus

Copy link
Copy Markdown
Contributor Author

@oscerd Thanks for the thorough review! All three points addressed in de75e63:

Lifecycle asymmetry (blocking): Moved lock renewer cancel + renewal client close into doStop(), so a route stop/start cycle now properly tears down the old resources before doStart() creates fresh ones. Also kept defensive cleanup in doShutdown() for safety. Added a stopStartCycleRecreatesLockRenewer test that verifies resources are properly recreated across a stop/start cycle.

Documentation (suggestion): Added upgrade guide note in camel-4x-upgrade-guide-4_22.adoc describing the new Camel-managed lock renewal behavior and when it activates.

Test assertion (suggestion): Reworked lockRenewerTracksAndRemovesExchanges to capture the scheduled LockRenewer task, complete the exchange, then run the renewer and verify it does NOT attempt renewMessageLock() — proving the exchange was actually removed from the renewer's tracking, not just the pendingExchanges counter.

Claude Code on behalf of davsclaus

@github-actions github-actions Bot added the docs label Jul 8, 2026
@davsclaus davsclaus requested a review from oscerd July 8, 2026 13:35
@davsclaus davsclaus added this to the 4.22.0 milestone Jul 8, 2026
@davsclaus davsclaus self-assigned this Jul 8, 2026
@davsclaus davsclaus added the enhancement New feature or request label Jul 8, 2026
@davsclaus davsclaus merged commit 45034f3 into main Jul 8, 2026
6 checks passed
@davsclaus davsclaus deleted the fix/CAMEL-23937 branch July 8, 2026 20:29
davsclaus added a commit that referenced this pull request Jul 9, 2026
…or Azure Service Bus async routes (#24559)

CAMEL-23937: Add Camel-managed lock renewal for Azure Service Bus async routes

The Azure SDK's built-in lock renewal (maxAutoLockRenewDuration) is tied
to the processMessage callback duration, which returns immediately for
async Camel routes. This adds a LockRenewer that uses a
ServiceBusReceiverAsyncClient to periodically renew message locks for
in-flight exchanges in PEEK_LOCK mode. Integrates with Camel's
PeriodTaskScheduler for operational visibility. Session-enabled consumers
are excluded (follow-up in CAMEL-23938).

Closes #24511

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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.

2 participants