Abhishek Mittal created CAMEL-23937:
---------------------------------------

             Summary: 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


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)

Reply via email to