Skip to content

Commit e2e1d9d

Browse files
author
Pranjal Patel
committed
fix(servicebus): cap server-timeout at the AMQP uint maximum
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
1 parent f834941 commit e2e1d9d

4 files changed

Lines changed: 22 additions & 5 deletions

File tree

‎sdk/servicebus/azure-servicebus/CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
- 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))
88
- 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))
9-
- Management operations (peek, deferred receive, 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.
9+
- 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.
1010

1111
### Bugs Fixed
1212

‎sdk/servicebus/azure-servicebus/azure/servicebus/_common/constants.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@
6666
# Subtracted from the caller's remaining time so the service answers before the client
6767
# gives up.
6868
SERVER_TIMEOUT_BUFFER_MS = 1000
69+
# The property is encoded as an AMQP uint, so cap at its maximum (about 49.7 days).
70+
MAX_SERVER_TIMEOUT_MS = 2**32 - 1
6971

7072
MANAGEMENT_PATH_SUFFIX = "/$management"
7173

‎sdk/servicebus/azure-servicebus/azure/servicebus/_common/utils.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
USER_AGENT_PREFIX,
4040
DEFAULT_SERVER_TIMEOUT_MS,
4141
SERVER_TIMEOUT_BUFFER_MS,
42+
MAX_SERVER_TIMEOUT_MS,
4243
)
4344
from ..amqp import AmqpAnnotatedMessage
4445

@@ -95,16 +96,18 @@ def get_server_timeout_ms(timeout: Optional[float]) -> int:
9596
"""Return the server-timeout for a management operation, in milliseconds.
9697
9798
This is a service-side bound, not a client-side one. It clamps at zero, since under
98-
a second of remaining time there is no room for the service to answer first.
99+
a second of remaining time there is no room for the service to answer first, and at
100+
the AMQP uint maximum, since the value is encoded as one.
99101
100102
:param float or None timeout: The caller's remaining timeout in seconds, or None.
101103
:rtype: int
102104
:returns: The remaining time less the buffer, or the default if no timeout was given.
103105
"""
104106
if timeout is None:
105107
return DEFAULT_SERVER_TIMEOUT_MS
106-
remaining_ms = int(timeout * 1000) - SERVER_TIMEOUT_BUFFER_MS
107-
return max(remaining_ms, 0)
108+
capped = min(timeout, MAX_SERVER_TIMEOUT_MS / 1000)
109+
remaining_ms = int(capped * 1000) - SERVER_TIMEOUT_BUFFER_MS
110+
return min(max(remaining_ms, 0), MAX_SERVER_TIMEOUT_MS)
108111

109112

110113
def build_uri(address, entity):

‎sdk/servicebus/azure-servicebus/tests/unittests/test_server_timeout.py‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313

1414
from unittest.mock import MagicMock
1515

16+
import struct
17+
1618
import pytest
1719

18-
from azure.servicebus._common.constants import REQUEST_RESPONSE_TIMEOUT
20+
from azure.servicebus._common.constants import MAX_SERVER_TIMEOUT_MS, REQUEST_RESPONSE_TIMEOUT
1921
from azure.servicebus._common.utils import get_server_timeout_ms
2022
from azure.servicebus._transport._pyamqp_transport import PyamqpTransport
2123

@@ -46,6 +48,16 @@ def test_wire_contract(self):
4648
encoded = PyamqpTransport.AMQP_UINT_VALUE(get_server_timeout_ms(None))
4749
assert encoded == {"TYPE": "UINT", "VALUE": 60000}
4850

51+
@pytest.mark.parametrize("remaining_seconds", [4294968, 5_000_000, 1e12, float("inf")])
52+
def test_capped_at_the_amqp_uint_maximum(self, remaining_seconds):
53+
# `timeout` is only bounded at zero, so a large value would overflow the uint encoder.
54+
result = get_server_timeout_ms(remaining_seconds)
55+
assert result <= MAX_SERVER_TIMEOUT_MS
56+
struct.pack(">I", result) # raises if it does not fit
57+
58+
def test_just_below_the_cap_is_not_clamped(self):
59+
assert get_server_timeout_ms(4294967) == 4294966000
60+
4961

5062
class TestManagementRequestSetsServerTimeout:
5163
"""The property must actually reach the outgoing management message, on both

0 commit comments

Comments
 (0)