Skip to content

[Service Bus] Send server-timeout on management operations - #48563

Open
Pranjal Patel (pranz1996) wants to merge 4 commits into
Azure:mainfrom
pranz1996:pbi/38822503-python-receive-timeout-default
Open

[Service Bus] Send server-timeout on management operations#48563
Pranjal Patel (pranz1996) wants to merge 4 commits into
Azure:mainfrom
pranz1996:pbi/38822503-python-receive-timeout-default

Conversation

@pranz1996

@pranz1996 Pranjal Patel (pranz1996) commented Aug 13, 2026

Copy link
Copy Markdown
Member

Description

Management operations previously sent no timeout bound to the service at all, so a stalled service could hold a management call until the AMQP link itself failed.

They now send the com.microsoft:server-timeout application property, asking the service to bound the operation on its side. This matches the .NET, Java and Go SDKs.

Ported from the merged Go change Azure/azure-sdk-for-go#26499.

Behavior

Caller passed Sent to the service
nothing 60 s (DEFAULT_SERVER_TIMEOUT_MS)
a timeout remaining time − 1 s buffer, clamped at 0

The 1 s buffer means the service answers before the client gives up, so the caller gets a service-side answer rather than an ambiguous local timeout. Java subtracts the same second in MessageUtils.adjustServerTimeout. Values match Go's test table exactly: no deadline → 60000, 10 s → 9000, 60 s → 59000, ≤ 1 s → 0.

The value is clamped at both ends because it is encoded as an AMQP unsigned int: a negative would underflow to a huge positive and invert the intent, and the public APIs bound timeout only at zero, so a sufficiently large value would exceed 2**32 - 1 and make the encoder raise while packing the message instead of performing the operation.

Affected operations

Management path only, sync and async:

Receiver Session Sender
peek_messages() session.get_state() schedule_messages()
receive_deferred_messages() session.set_state() cancel_scheduled_messages()
renew_message_lock() session.renew_lock()
message settlement over the management link list_sessions()

Settlement (complete / abandon / defer / dead_letter) reaches the service over the management link when it cannot be settled on the receive link — for example after the lock has expired, or for a deferred message. That path calls the common handler directly and passes no timeout, so it takes the 60 second default.

Behavior note for settlement. A settle over the management link that would previously have blocked now bounds at 60 seconds and is retried. Because the lock can expire during that window, the caller can surface MessageLockLostError (ERROR_CODE_MESSAGE_LOCK_LOST is mapped in _ERROR_CODE_TO_ERROR_MAPPING) where the call used to block instead. That is the intended trade: a bounded, diagnosable failure rather than an open-ended hang.

Not affected: receive_messages(), receiver iteration, and send_messages() — the data path is unchanged. CBS token auth is also untouched: it runs through _pyamqp/cbs.py, a separate path, which is the same exclusion the Go change makes explicitly.

What a caller sees on a stalled service

The service returns com.microsoft:timeout, which maps to OperationTimeoutError via the existing _ERROR_CODE_TO_ERROR_MAPPING. That error is already retryable=True, so the retry loop retries it — with default settings a persistently stalled service surfaces after roughly four minutes (4 attempts × 60 s + backoff) rather than hanging indefinitely.

No new exception type was needed. Go added an ErrTryTimeoutExhausted sentinel to force a retryable classification; Python's OperationTimeoutError already has it.

Implementation notes

  • Set in _base_handler._mgmt_request_response (and the async twin), after the retry loop computes the remaining time, so the value reflects the time actually left on the attempt rather than when the operation was first requested.
  • One choke point covers all 21 management call sites across 10 files.
  • Both transports pass application_properties through unchanged and both expose AMQP_UINT_VALUE, so no transport change is needed.
  • REQUEST_RESPONSE_TIMEOUT already existed as b"com.microsoft:server-timeout" — the exact key .NET and Java send — but was unused. This change finally uses it.

Testing

New tests/unittests/test_server_timeout.py — 15 unit tests, no credentials or live namespace required:

  • the ms arithmetic, including the clamp and the case where a 60 s caller timeout must still take the buffer path rather than being mistaken for the default
  • the wire contract: key equals com.microsoft:server-timeout, encoded as an AMQP uint of milliseconds
  • that the property actually reaches the outgoing message, on both the associated-link and keep_alive_associated_link=False paths
  • sync/async parity, since the two handlers are maintained separately

api.md and api.metadata.yml are unchanged — verified by running azpysdk apistub .; apiMdSha256 is identical. Everything added lives in private modules, so there is no public API surface change.

Local results: 109 unit tests pass, pylint 10.00/10, black clean.

Related

All SDK Contribution checklist:

  • The pull request does not introduce [breaking changes]
  • CHANGELOG is updated for new features, bug fixes or other significant changes.
  • I have read the contribution guidelines.

General Guidelines and Best Practices

  • Title of the pull request is clear and informative.
  • There are a small number of commits, each of which have an informative message.

Testing Guidelines

  • Pull request includes test coverage for the included changes.

Management operations (peek, deferred receive, lock renewal, session state,
session listing, schedule/cancel scheduled) now send the
`com.microsoft:server-timeout` application property, asking the service to
bound the operation on its side.

The value is the caller's remaining time less a one second buffer, so the
service answers before the client gives up, or 60 seconds when the caller
supplied no timeout. Previously no bound was sent at all, so a stalled
service could hold a management call until the AMQP link itself failed.

The property is set in the base handler, after the retry loop computes the
remaining time, so it reflects the time actually left on the attempt. Both
transports pass application properties through unchanged, so no transport
change is needed. REQUEST_RESPONSE_TIMEOUT already held the correct
vendor-prefixed key and was previously unused.

Matches the .NET, Java and Go SDKs (.NET ServiceBusRetryOptions.TryTimeout,
Java MessageUtils.adjustServerTimeout, Go defaultServerTimeout).

Refs: AB#38822503
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
9 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

Adds service-side timeout bounds to Service Bus management operations.

Changes:

  • Calculates buffered server timeouts with a 60-second default.
  • Adds the timeout property to synchronous and asynchronous requests.
  • Adds unit coverage and release notes.

Reviewed changes

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

Show a summary per file
File Description
_base_handler.py Adds timeout to sync requests.
aio/_base_handler_async.py Adds timeout to async requests.
_common/constants.py Defines timeout constants.
_common/utils.py Calculates timeout milliseconds.
test_server_timeout.py Tests calculation and request propagation.
CHANGELOG.md Documents the behavior.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread sdk/servicebus/azure-servicebus/CHANGELOG.md Outdated
Comment thread sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py Outdated
@azure-pipelines

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

Address review feedback on the management server-timeout change.

The public management APIs bound `timeout` only at zero, so a caller could
pass a value whose millisecond form exceeds a uint32 (about 49.7 days). The
AMQP encoder would then raise while packing the message rather than
performing the operation. Clamp before scaling as well as after, so a very
large or infinite float cannot overflow the int conversion either.

Also document message settlement over the management link as affected. It
reaches the service through the common handler when a message cannot be
settled on the receive link, for example after the lock has expired or for
a deferred message, and passes no timeout, so it takes the 60 second
default.

Refs: AB#38822503
Copilot AI review requested due to automatic review settings August 13, 2026 00:50

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

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py:110

  • The upper clamp is applied before subtracting the buffer, so any timeout above the AMQP range is encoded as MAX_SERVER_TIMEOUT_MS - 1000, not MAX_SERVER_TIMEOUT_MS as the documented “remaining time minus buffer, then clamp” behavior requires. Compute the buffered milliseconds first and then clamp; the cap test should also assert equality to the maximum so this boundary is covered.
    capped = min(timeout, MAX_SERVER_TIMEOUT_MS / 1000)
    remaining_ms = int(capped * 1000) - SERVER_TIMEOUT_BUFFER_MS
    return min(max(remaining_ms, 0), MAX_SERVER_TIMEOUT_MS)

Copilot AI review requested due to automatic review settings August 13, 2026 01:08

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

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (1)

sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py:109

  • The cap is applied before subtracting the buffer, so it starts truncating one second too early. For example, timeout=4_294_968 should produce 4_294_967_000 ms (remaining time minus one second, still below the uint maximum), but this returns 4_294_966_295. Cap the millisecond value at MAX + buffer before subtracting so the documented formula holds across the upper boundary.
    capped = min(timeout, MAX_SERVER_TIMEOUT_MS / 1000)
    remaining_ms = int(capped * 1000) - SERVER_TIMEOUT_BUFFER_MS

@j7nw4r Johnathan W (j7nw4r) 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 design is right. _do_retryable_operation recomputes the remaining time before each attempt, so _mgmt_request_response is the correct place to set this. The key matches Java, and the millisecond table matches Go.

Two asks. Nothing tests the response half. Every test asserts what the client sends, but the changelog promises a retryable OperationTimeoutError, and that rests on the service returning com.microsoft:timeout in errorCondition. Pin it without a live namespace: push a fake non-200 response with that condition through mgmt_handlers.default and assert the type.

Second, the uamqp path is untested and is the one place the value type changes. Under pyamqp it is a plain dict; under uamqp a uamqp.types.AMQPuInt. AMQP_UINT_VALUE has never gone into application_properties before. One test that stubs the uamqp type closes it.

Worth a line in the description too: a settle over the management link that times out at 60 seconds now retries, and the retry can raise MessageLockLostError where the call used to block.

Comment on lines +2 to +3
parserVersion: 0.3.31
pythonVersion: 3.13.15

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.

apiMdSha256 is unchanged, so no API moved. Only the parser and interpreter versions shift, and the new pythonVersion is your local one. Please drop this commit.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I kept parserVersion change -> without that API.md consistency check fails


- Added `ServiceBusReceivedMessage.from_bytes()` classmethod to construct a `ServiceBusReceivedMessage` from raw AMQP payload bytes without requiring the deprecated `uamqp` library. ([#43979](https://github.com/Azure/azure-sdk-for-python/issues/43979))
- Added `ServiceBusClient.list_queue_sessions()` and `ServiceBusClient.list_subscription_sessions()` (sync and async) to list session IDs for entities with active messages, with optional filtering by session-state update timestamp. The methods return an `ItemPaged[str]` (`AsyncItemPaged[str]` on the async client) so callers can iterate every session transparently or page with `by_page()`. Implements the `com.microsoft:get-message-sessions` management operation. ([#46575](https://github.com/Azure/azure-sdk-for-python/pull/46575))
- Management operations (peek, deferred receive, message settlement over the management link, lock renewal, session state, session listing, schedule/cancel) now send `com.microsoft:server-timeout`: the caller's remaining time less one second, or 60 seconds when none was given. Previously no bound was sent, so a stalled service held the call until the AMQP link failed; it now raises a retryable `OperationTimeoutError`, so a persistently stalled service surfaces after roughly four minutes at default retry settings. Matches the .NET, Java and Go SDKs.

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.

This changes existing operations, so it is not a new feature. Move it to ### Other Changes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks! Updated that.

return DEFAULT_SERVER_TIMEOUT_MS
capped = min(timeout, MAX_SERVER_TIMEOUT_MS / 1000)
remaining_ms = int(capped * 1000) - SERVER_TIMEOUT_BUFFER_MS
return min(max(remaining_ms, 0), MAX_SERVER_TIMEOUT_MS)

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 outer min never binds, since capped is already bounded. return max(int(capped * 1000) - SERVER_TIMEOUT_BUFFER_MS, 0) says the same thing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed it properly. Please review that part again.

Address review feedback.

Nothing tested the response half: every assertion covered what the client
sends, while the changelog promises a retryable OperationTimeoutError that
rests on the service returning com.microsoft:timeout in errorCondition.
Push a fake non-200 response with that condition through
mgmt_handlers.default and assert the type, for both transports, with no
live namespace.

uamqp is the one place the value type changes, since pyamqp uses a plain
dict where uamqp uses an AMQPuInt that has not previously gone into
application_properties. One test stubs the transport type to prove the
property is built from the transport's own AMQP_UINT_VALUE, and one
encodes a real message when uamqp is installed.

Also drop the redundant outer min in get_server_timeout_ms, which never
binds because the buffer is subtracted after the cap, and move the
changelog entry to Other Changes since it changes existing operations
rather than adding a feature.

api.metadata.yml carries the parserVersion bump only. That field is gated
by the consistency check and the package still had 0.3.30 while the check
generates 0.3.31. apiMdSha256 and pythonVersion are unchanged.

Refs: AB#38822503
Copilot AI review requested due to automatic review settings August 14, 2026 16:57
@pranz1996
Pranjal Patel (pranz1996) force-pushed the pbi/38822503-python-receive-timeout-default branch from f2bb172 to 12a85bc Compare August 14, 2026 16:57

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

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (1)

sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py:109

  • The upper clamp is applied before subtracting the one-second buffer, so large/infinite timeouts can never produce MAX_SERVER_TIMEOUT_MS; they top out at MAX_SERVER_TIMEOUT_MS - 1000. This does not implement “remaining time minus the buffer, clamped to the uint range” and leaves the top second of the valid uint range unreachable. Subtract first, then clamp the millisecond result.
    capped = min(timeout, MAX_SERVER_TIMEOUT_MS / 1000)
    return max(int(capped * 1000) - SERVER_TIMEOUT_BUFFER_MS, 0)

@github-actions

Copy link
Copy Markdown
Contributor
[Pilot] PR Pipeline Failure Analysis

A CI pipeline failed on this pull request. Here is an automated analysis of what went wrong and how to get the build green.

What failed

The python - pullrequest pipeline (build #6704426) failed across 6 platforms (macOS 3.11, Ubuntu 3.13, Ubuntu 3.14, Ubuntu 3.10, Ubuntu 3.10 coverage, Windows 3.12) with the same single failing test:

  • sdk.servicebus.azure-servicebus.tests.unittests.test_server_timeout.TestServiceTimeoutResponse.test_uamqp_maps_the_same_condition

This is a test failure. The test test_uamqp_maps_the_same_condition in the new test_server_timeout.py file introduced by this PR is failing on every tested platform, suggesting the test assertion does not match the actual behavior of the uAMQP condition mapping.

Recommended next steps

  • Investigate test_uamqp_maps_the_same_condition in sdk/servicebus/azure-servicebus/tests/unittests/test_server_timeout.py — the test is failing on all platforms, which points to a logic or assertion error in the test or the code it exercises.
  • Check the uAMQP error condition mapping used in the test against the actual _ERROR_CODE_TO_ERROR_MAPPING or equivalent. Ensure the expected condition values match what the uAMQP transport actually surfaces.
  • Run the failing test locally: pytest sdk/servicebus/azure-servicebus/tests/unittests/test_server_timeout.py::TestServiceTimeoutResponse::test_uamqp_maps_the_same_condition -v to see the assertion diff.
  • See the CI troubleshooting guide: https://aka.ms/ci-fix
  • Push new commits to address the failures; this comment updates automatically on the next failing run.
Raw pipeline analysis (azsdk ci analyze)
Analyzing pipeline https://github.com/Azure/azure-sdk-for-python/pull/48563...
Getting failed workflow runs for commit 12a85bc1b6895410ad02aab45f97aae4a72dda8f in Azure/azure-sdk-for-python
Build: 6704426 Project: public PipelineUrl: https://dev.azure.com/azure-sdk/public/_build/results?buildId=6704426

Failed Tests:
{
  "macos311": [
    "sdk.servicebus.azure-servicebus.tests.unittests.test_server_timeout.TestServiceTimeoutResponse.test_uamqp_maps_the_same_condition"
  ],
  "Ubuntu2404_313": [
    "sdk.servicebus.azure-servicebus.tests.unittests.test_server_timeout.TestServiceTimeoutResponse.test_uamqp_maps_the_same_condition"
  ],
  "ubuntu2404_310_coverage": [
    "sdk.servicebus.azure-servicebus.tests.unittests.test_server_timeout.TestServiceTimeoutResponse.test_uamqp_maps_the_same_condition"
  ],
  "Ubuntu2404_314": [
    "sdk.servicebus.azure-servicebus.tests.unittests.test_server_timeout.TestServiceTimeoutResponse.test_uamqp_maps_the_same_condition"
  ],
  "ubuntu2404_310": [
    "sdk.servicebus.azure-servicebus.tests.unittests.test_server_timeout.TestServiceTimeoutResponse.test_uamqp_maps_the_same_condition"
  ],
  "windows2022_312": [
    "sdk.servicebus.azure-servicebus.tests.unittests.test_server_timeout.TestServiceTimeoutResponse.test_uamqp_maps_the_same_condition"
  ]
}

Failing checks:
  python - pullrequest [FAILURE] https://dev.azure.com/azure-sdk/29ec6040-b234-4e31-b139-33dc4287b756/_build/results?buildId=6704426
  python - pullrequest (Build Test Ubuntu2404_313) [FAILURE] https://dev.azure.com/azure-sdk/29ec6040-b234-4e31-b139-33dc4287b756/_build/results?buildId=6704426&view=logs&jobId=7adb94b4-bc75-5e80-2fbf-a7b780a808b6
  python - pullrequest (Build Test Ubuntu2404_314) [FAILURE] https://dev.azure.com/azure-sdk/29ec6040-b234-4e31-b139-33dc4287b756/_build/results?buildId=6704426&view=logs&jobId=47f4651c-d36a-53a8-086f-e1b3a7e1e277
  python - pullrequest (Build Test macos311) [FAILURE] https://dev.azure.com/azure-sdk/29ec6040-b234-4e31-b139-33dc4287b756/_build/results?buildId=6704426&view=logs&jobId=8d612083-8e4c-5501-f3a1-a3c24d0ecf0d
  python - pullrequest (Build Test ubuntu2404_310) [FAILURE] https://dev.azure.com/azure-sdk/29ec6040-b234-4e31-b139-33dc4287b756/_build/results?buildId=6704426&view=logs&jobId=3bcd3a81-ec8d-5e2f-af56-3a8253cdbe04
  python - pullrequest (Build Test ubuntu2404_310_coverage) [FAILURE] https://dev.azure.com/azure-sdk/29ec6040-b234-4e31-b139-33dc4287b756/_build/results?buildId=6704426&view=logs&jobId=597daf58-4c14-517e-851b-b0b7aee2cf14
  python - pullrequest (Build Test windows2022_312) [FAILURE] https://dev.azure.com/azure-sdk/29ec6040-b234-4e31-b139-33dc4287b756/_build/results?buildId=6704426&view=logs&jobId=2e621875-ed8e-5a82-d007-84b63928f666

Copilot detected the failing pipeline and generated the analysis above. To have it attempt a fix automatically, reply with @copilot please fix the failing pipeline on this PR.

Generated by Pipeline Analysis - Next Steps · 22.4 AIC · ⌖ 9.11 AIC · ⊞ 6.6K ·

…sport

The test skipped on azure.servicebus._transport._uamqp_transport, but that
module imports cleanly without uamqp: UamqpTransport is defined inside the
try block that imports it, so the module is present while the class is not.
importorskip therefore succeeded and the attribute access raised
AttributeError on every CI leg, which do not install uamqp.

Skip on uamqp itself and import the class afterwards. Verified both ways:
with uamqp installed the test runs, and with uamqp unavailable it skips.

Refs: AB#38822503
Copilot AI review requested due to automatic review settings August 14, 2026 18:05

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

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (2)

sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py:109

  • The upper clamp is applied before subtracting the buffer, so an over-limit caller timeout never produces the documented AMQP uint maximum. For example, 5,000,000 seconds yields 4,294,966,295 rather than clamping the buffered value to 4,294,967,295. Clamp after accounting for the buffer (and strengthen the test to assert equality at the cap).
    capped = min(timeout, MAX_SERVER_TIMEOUT_MS / 1000)
    return max(int(capped * 1000) - SERVER_TIMEOUT_BUFFER_MS, 0)

sdk/servicebus/azure-servicebus/CHANGELOG.md:25

  • This overstates the guarantee: server-timeout is only a broker-side request. When the caller omits timeout, both client transports still have no local expiry, so a broker that goes silent or ignores the property can still block indefinitely. Qualify the four-minute outcome as contingent on the service returning com.microsoft:timeout, and retain guidance to pass timeout when a hard client-side bound is required.
- Management operations (peek, deferred receive, message settlement over the management link, lock renewal, session state, session listing, schedule/cancel) now send `com.microsoft:server-timeout`: the caller's remaining time less one second, or 60 seconds when none was given. Previously no bound was sent, so a stalled service held the call until the AMQP link failed; it now raises a retryable `OperationTimeoutError`, so a persistently stalled service surfaces after roughly four minutes at default retry settings. Matches the .NET, Java and Go SDKs.

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.

3 participants