You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@camel.apache.org by "Claus Ibsen (Jira)" <ji...@apache.org> on 2026/07/08 05:54:00 UTC

[jira] [Assigned] (CAMEL-23937) camel-azure-servicebus consumer defeats SDK auto lock-renewal for async routes, causing silent message-lock expiry mid-processing

     [ https://issues.apache.org/jira/browse/CAMEL-23937?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel ]

Claus Ibsen reassigned CAMEL-23937:
-----------------------------------

    Assignee: Claus Ibsen

> camel-azure-servicebus consumer defeats SDK auto lock-renewal for async routes, causing silent message-lock expiry mid-processing
> ---------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: CAMEL-23937
>                 URL: https://issues.apache.org/jira/browse/CAMEL-23937
>             Project: Camel
>          Issue Type: Bug
>          Components: camel-azure-servicebus
>    Affects Versions: 4.14.5
>            Reporter: Abhishek Mittal
>            Assignee: Claus Ibsen
>            Priority: Major
>             Fix For: 4.21.0
>
>
> The Service Bus consumer processes exchanges asynchronously but the underlying SDK's MessagePump disposes the lock-renewal subscription synchronously as soon as Camel's
>   callback returns — not when the exchange is actually settled. Because Camel's consumer returns immediately for async routes, lock auto-renewal is cancelled almost immediately
>   after message receipt, regardless of the configured maxAutoLockRenewDuration.
>   Camel side — org.apache.camel.component.azure.servicebus.ServiceBusConsumer#processMessage (ServiceBusConsumer.java:77-86):
>   private void processMessage(ServiceBusReceivedMessageContext messageContext)
> {       final ServiceBusReceivedMessage message = messageContext.getMessage();       final Exchange exchange = createServiceBusExchange(message);       final ConsumerOnCompletion onCompletion = new ConsumerOnCompletion(messageContext);       // add exchange callback       exchange.getExchangeExtension().addOnCompletion(onCompletion);       // use default consumer callback       AsyncCallback cb = defaultConsumerCallback(exchange, true);       getAsyncProcessor().process(exchange, cb);   }
>   getAsyncProcessor().process(exchange, cb) schedules the route and returns immediately; actual settlement (complete()/abandon()/deadLetter()) happens later, from the
>   ConsumerOnCompletion Synchronization (ServiceBusConsumer.java:165-206) once the exchange finishes — which may be long after this method returns.
>   org.apache.camel.component.azure.servicebus.client.ServiceBusClientFactory#createBaseServiceBusProcessorClient (ServiceBusClientFactory.java:65-80, mirrored in
>   createBaseServiceBusSessionProcessorClient, lines 82-98) explicitly disables the SDK's own auto-complete because Camel manages settlement itself:
>   // We handle auto-complete in the consumer, since we have no way to propagate errors back to the reactive
>   // pipeline messages are published on so the message would be completed even if an error occurs during Exchange
>   // processing.
>   processorClientBuilder.disableAutoComplete();
>   maxAutoLockRenewDuration is still passed through to the SDK builder (ServiceBusClientFactory.java:114 / 134), giving the impression that lock renewal is configured for the full expected processing window.
>   SDK side — com.azure.messaging.servicebus.MessagePump#handleMessage (MessagePump.java:140-159):
>   private void handleMessage(ServiceBusReceivedMessage message) {
>       instrumentation.instrumentProcess(message, ReceiverKind.PROCESSOR, msg -> {
>           final Disposable lockRenewDisposable;
>           if (enableAutoLockRenew)
> {               lockRenewDisposable = client.beginLockRenewal(message);           }
> else
> {               lockRenewDisposable = Disposables.disposed();           }
>           final Throwable error = notifyMessage(message);
>           if (enableAutoDisposition) {
>               if (error == null)
> {                   complete(message);               }
> else
> {                   abandon(message);               }
>           }
>           lockRenewDisposable.dispose();
>           return error;
>       });
>   }
>   notifyMessage(message) (MessagePump.java:161-170) synchronously invokes the Camel processMessage callback above and returns as soon as that call returns — which, for an async Camel route, is almost immediately, well before the exchange is settled. The very next line unconditionally disposes lockRenewDisposable, cancelling auto-renewal at that point,
>   not when processing actually completes. Since enableAutoDisposition is false here (Camel called disableAutoComplete()), the complete()/abandon() branch is skipped too —
>   settlement is left entirely to Camel's later, out-of-band ConsumerOnCompletion callback, by which time the lock has already lost its renewal and may have expired.
>   Net effect: renewal is torn down roughly at t+(sync callback duration), typically a few milliseconds, irrespective of maxAutoLockRenewDuration. Any route whose actual
>   processing time exceeds the entity's LockDuration will have its lock expire while still "in flight" from Camel's perspective.
>   *Steps to reproduce*
>   1. Configure a camel-azure-servicebus consumer endpoint on a topic with PEEK_LOCK receive mode and a short LockDuration (e.g. 30s) on the Service Bus entity.
>   2. Set maxAutoLockRenewDuration to a large value (e.g. 5 minutes) expecting it to keep the lock alive.
>   3. Route processing (async processor, aggregator, throttler, idempotent consumer, or simply business logic) takes longer than 30s.
>   4. Observe: the lock is not renewed past the initial LockDuration window; the broker redelivers the message to another receiver while the first exchange is still processing,
>   and/or the eventual complete()/abandon() call from ConsumerOnCompletion fails or becomes a no-op against the now-invalid lock token, with no error surfaced to the application.
>   A standalone repro project (minimal Camel route + Azure Service Bus emulator/live namespace) is available on request.
>   *Expected behavior*
>   With maxAutoLockRenewDuration configured, the SDK should keep renewing the message lock for the full duration the exchange is actually being processed by Camel (i.e., until
>   ConsumerOnCompletion.onComplete/onFailure fires), up to the configured maximum — not just for the duration of the synchronous callback invocation.
>   *Actual behavior*
>   Lock renewal is disposed essentially immediately after the synchronous processMessage callback returns, because MessagePump.handleMessage ties lockRenewDisposable.dispose() to
>   the callback's return rather than to actual exchange completion. For async Camel routes this happens long before processing finishes, so the lock silently expires
>   mid-processing.
>   *Impact / severity*
>   Data loss / duplicate delivery, silent. Once the lock expires, the broker may redeliver the message to a different receiver while the original exchange is still being
>   processed. The original exchange's later complete() (via ConsumerOnCompletion.onComplete) becomes a no-op against the expired lock, so:
>   - the message may be lost if the redelivered copy is also not properly settled, or
>   - the message may be processed twice (original + redelivered copy), or
>   - in dead-lettering/abandon paths, settlement calls fail silently.
>   No exception is surfaced to the Camel route or the application in the common case — this can pass light/functional testing (where processing is fast) and only surfaces in
>   production under load or with any processing step that pushes total handling time past LockDuration.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)