Skip to content

[Service Bus] Bound sync acceptNextSession hang and add session-acquire retry backoff (#49093)#49806

Open
ksalazar-91 wants to merge 3 commits into
Azure:mainfrom
ksalazar-91:ksalazar-91/fix-acceptnextsession-timeout-49093
Open

[Service Bus] Bound sync acceptNextSession hang and add session-acquire retry backoff (#49093)#49806
ksalazar-91 wants to merge 3 commits into
Azure:mainfrom
ksalazar-91:ksalazar-91/fix-acceptnextsession-timeout-49093

Conversation

@ksalazar-91

@ksalazar-91 ksalazar-91 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes intermittent ServiceBusSessionReceiverClient.acceptNextSession() blocking for the full operation timeout (~245s with default retry options) and throwing IllegalStateException: Timeout on blocking read for 245600000000 NANOSECONDS (client-timeout), plus a CPU-burning tight retry loop on the session Processor / async session receiver.

Resolves #49093.

Root cause

  • The sync ServiceBusSessionReceiverClient.accept*Session() wraps the async accept with .timeout(operationTimeout) where operationTimeout = MessageUtils.getTotalTimeout(retryOptions). With default AmqpRetryOptions (tryTimeout=60s, maxRetries=3, delay=800ms, exponential) that is 60 + 60.8 + 61.6 + 63.2 = 245.6s, which matches the reported 245600000000 ns exactly.
  • On the synchronous (non-retry) session-acquire path (timeoutRetryDisabled = true), ServiceBusSessionAcquirer had no client-side per-attempt timeout. When the broker accepts the session-acquire link but never sends its detach (a hung acquire — half-open connection, network partition, or QPid-thread starvation across many idle pumps), the only safety net was the outer 245.6s sync timeout, so the caller blocked for the full duration before failing. The acquirer's own javadoc claimed the client-side timeout was "slightly longer than the broker's timeout", but it was effectively ~4× the try-timeout.
  • On the retry-enabled path (session ServiceBusProcessorClient and ServiceBusSessionReceiverAsyncClient), retries were scheduled with Mono.delay(Duration.ZERO) — no backoff. When acquire attempts fail fast (e.g. the broker repeatedly detaches the accept link quickly), this becomes a tight, CPU-burning retry loop.

Fix (servicebus-only)

ServiceBusSessionAcquirer:

  • Non-retry (sync) path: bound a single acquire attempt with a client-side guard of 2 × tryTimeout. The 2× margin keeps the broker's own timeout detach the usual trigger (so the broker-timeout error still propagates in the healthy "no session available" case), while a hung acquire now fails in bounded time (~120s with defaults) instead of ~245s. On the guard firing, the half-open receive link is disposed (only on cancellation) so the broker-side session lock is released rather than orphaned.
  • Retry-enabled path: replace Mono.delay(Duration.ZERO) with a bounded backoff (honoring the customer's configured AmqpRetryOptions delay, and defaulting to 100ms only when no usable delay (null/zero/negative) is configured), still hopping to Schedulers.parallel() to free the QPid thread. This prevents the tight CPU loop.

ServiceBusSessionReceiverClient: clarified the acceptNextSession() javadoc — it does not wait indefinitely; it throws a retriable timeout after the operation timeout, so callers typically invoke it in a loop.

Reason for the change

Customers running idle session pumps saw acceptNextSession() hang ~245s and CPU spike right around the failures, causing Kubernetes pods to be killed and restarted.

How verified

  • New virtual-time regression tests in ServiceBusSessionAcquirerIsolatedTest:
    • shouldClientSideTimeoutAndDisposeLinkWhenAcquireHangsIfRetryDisabled — a hung acquire fails with TimeoutException at the 2 × tryTimeout guard and the half-open link is disposed.
    • shouldBackOffBetweenRetriesIfRetryEnabled — the second acquire attempt is not made until the backoff elapses (no busy loop).
  • Full ServiceBusSessionAcquirerIsolatedTest (6/6) and the related session unit test classes pass; checkstyle + spotbugs clean (JDK 21).
  • Reproduced the original behavior with a standalone Maven app against an idle session-enabled entity; on the local build the healthy "no session" path still surfaces the broker timeout at ~60–72s with negligible CPU (no regression).

Are there any breaking changes?

No. Behavior on the healthy path is unchanged (broker timeout still propagates); only the hung-acquire bound is tightened and retry spacing is added.

…re retry backoff (Azure#49093)

Bounds a single synchronous session-acquire attempt with a client-side guard of
2x tryTimeout (disposing the half-open link when it fires) so a hung
acceptNextSession()/acceptSession() fails in bounded time instead of blocking for
the full ~245s operation timeout. Adds a bounded backoff between session-acquire
retries on the session processor/async receiver path to avoid a CPU-burning tight
loop when acquire attempts fail fast.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 406b56c0-6215-4747-b9bf-c7d7b8ab1e89
Copilot AI review requested due to automatic review settings July 14, 2026 17:02
@github-actions github-actions Bot added Community Contribution Community members are working on the issue customer-reported Issues that are reported by GitHub users external to the Azure organization. Service Bus labels Jul 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thank you for your contribution @ksalazar-91! We will review the pull request and get back to you soon.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
34 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI 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.

Pull request overview

This PR addresses intermittent hangs and CPU spikes during Service Bus session acquisition by bounding the synchronous session-acquire attempt duration and adding a non-zero backoff between retry attempts on the async/processor path.

Changes:

  • Added a client-side timeout guard for the non-retry (sync) session-acquire path and ensured half-open receive links are disposed on cancellation.
  • Replaced the zero-delay retry scheduling with a bounded backoff to avoid tight retry loops.
  • Added virtual-time regression tests and documented the behavior in CHANGELOG and acceptNextSession() Javadoc.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
sdk/servicebus/azure-messaging-servicebus/src/main/java/com/azure/messaging/servicebus/ServiceBusSessionAcquirer.java Adds client-side guard for hung acquires, cancellation disposal, and retry backoff for retry-enabled session acquisition.
sdk/servicebus/azure-messaging-servicebus/src/main/java/com/azure/messaging/servicebus/ServiceBusSessionReceiverClient.java Clarifies acceptNextSession() behavior in Javadoc regarding operation-timeout behavior.
sdk/servicebus/azure-messaging-servicebus/src/test/java/com/azure/messaging/servicebus/ServiceBusSessionAcquirerIsolatedTest.java Adds virtual-time tests covering hung-acquire timeout/dispose and retry backoff behavior.
sdk/servicebus/azure-messaging-servicebus/CHANGELOG.md Documents the bug fix and behavioral details for session acquisition.

Comment on lines 159 to 163
* @throws UnsupportedOperationException if the queue or topic subscription is not session-enabled.
* @throws IllegalStateException if no session becomes available within the operation timeout (derived from
* the {@link ServiceBusClientBuilder#retryOptions(AmqpRetryOptions)}); this is retriable.
* @throws AmqpException if the operation times out. The timeout duration is the tryTimeout
* of when you build this client with the {@link ServiceBusClientBuilder#retryOptions(AmqpRetryOptions)}.
Comment on lines +106 to +109
final Duration configuredDelay = connectionCacheWrapper.getRetryOptions().getDelay();
this.retryBackoff = (configuredDelay == null || configuredDelay.isZero() || configuredDelay.isNegative())
? Duration.ofMillis(100)
: configuredDelay;
ksalazar-91 and others added 2 commits July 15, 2026 21:46
…re#49093)

Clarify acceptNextSession()/acceptSession() @throws Javadoc: the operation
timeout surfaces as IllegalStateException, while AmqpException covers AMQP-level
failures (not the operation timeout). Add regression tests asserting both
exception paths. Clarify the retry-backoff comment to state it honors the
customer's configured AmqpRetryOptions delay (defaulting to 100ms only when no
usable delay is configured); no behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 406b56c0-6215-4747-b9bf-c7d7b8ab1e89

@EldertGrootenboer EldertGrootenboer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix looks right and the mechanism holds up.

The 2x tryTimeout guard surfaces as IllegalStateException and bounds the wait at ~120s instead of ~245s, matching the updated Javadoc.
The retry-path backoff covers the CPU spike on the pump.

The backoff takes the configured delay verbatim when it's above zero, so a sub-100ms delay can still spin.
A floor like max(configuredDelay, 100ms) would fix it. Non-blocking, but I'd take the one-liner.

Approving.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Community Contribution Community members are working on the issue customer-reported Issues that are reported by GitHub users external to the Azure organization. Service Bus

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] ServiceBusSessionReceiverClient.acceptNextSession times out with IllegalStateException after ~245s

4 participants