Skip to content

Commit 950c503

Browse files
ashleysommerCopilotPranjal Patelpranz1996
authored
fix: Save lock-token from receive_deferred_messages AMQP payload, into ServiceBusReceivedMessage (#42471)
* Save lock-token from `receive_deferred_message` AMQP payload, so it can be used as the message's lock_token property. This allows the deferred message to be correctly renewed or settled (abandoned, completed, deferred again). * Use `kwargs.get()` instead of `kwargs.pop()` to get the optional received lock token in the constructor Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * test: add regression tests and changelog entry for deferred message lock-token (#42454) Adds unit tests in test_message.py verifying parse_received_message populates lock_token for deferred messages and leaves it None for peeked messages, plus a CHANGELOG bug-fix entry. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Pranjal Patel <pranjalpatel@microsoft.com> Co-authored-by: Pranjal Patel <patelpranzp25@gmail.com>
1 parent af54c26 commit 950c503

5 files changed

Lines changed: 67 additions & 2 deletions

File tree

sdk/servicebus/azure-servicebus/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Bugs Fixed
66

7+
- Fixed a bug where messages returned by `receive_deferred_messages` had a `lock_token` of `None`, which prevented settling (completing, abandoning, dead-lettering, deferring) or renewing the lock on a deferred message in `PEEK_LOCK` mode. The lock token is now read from the `lock-token` field of the management-link response for deferred messages. ([#42454](https://github.com/Azure/azure-sdk-for-python/issues/42454))
78
- Read `com.microsoft:max-message-batch-size` vendor property from the AMQP sender link to correctly limit batch size on Premium large-message entities, where `max-message-size` can be up to 100 MB but the batch limit is 1 MB.
89

910
## 7.14.3 (2025-11-11)

sdk/servicebus/azure-servicebus/azure/servicebus/_common/message.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,7 @@ def __init__(
787787
self._settled = receive_mode == ServiceBusReceiveMode.RECEIVE_AND_DELETE
788788
self._delivery_tag = self._amqp_transport.get_message_delivery_tag(message, frame)
789789
self._delivery_id = self._amqp_transport.get_message_delivery_id(message, frame) # only used by pyamqp
790+
self._received_lock_token = kwargs.get("lock_token", None)
790791
self._received_timestamp_utc = utc_now()
791792
self._is_deferred_message = kwargs.get("is_deferred_message", False)
792793
self._is_peeked_message = kwargs.get("is_peeked_message", False)
@@ -1090,6 +1091,10 @@ def lock_token(self) -> Optional[Union[uuid.UUID, str]]:
10901091
if self._settled:
10911092
return None
10921093

1094+
if self._received_lock_token:
1095+
# this will already be a uuid.UUID
1096+
return self._received_lock_token
1097+
10931098
if self._delivery_tag:
10941099
return uuid.UUID(bytes_le=self._delivery_tag)
10951100

sdk/servicebus/azure-servicebus/azure/servicebus/_transport/_pyamqp_transport.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -840,15 +840,21 @@ def parse_received_message( # pylint:disable=docstring-keyword-should-match-keyw
840840
:keyword ~azure.servicebus.ServiceBusReceiver receiver: Required.
841841
:keyword bool is_peeked_message: Optional. For peeked messages.
842842
:keyword bool is_deferred_message: Optional. For deferred messages.
843+
:keyword uuid.UUID lock_token: Optional. Lock token, if it is given by the message receiver.
843844
:keyword ~azure.servicebus.ServiceBusReceiveMode receive_mode: Optional.
844845
:return: List of service bus received messages.
845846
:rtype: list[~azure.servicebus.ServiceBusReceivedMessage]
846847
"""
848+
is_deferred_message = kwargs.get("is_deferred_message", False)
847849
parsed = []
848850
if message.value:
849851
for m in message.value[b"messages"]:
850852
wrapped = decode_payload(memoryview(m[b"message"]))
851-
parsed.append(message_type(wrapped, **kwargs))
853+
if is_deferred_message and b"lock-token" in m:
854+
lock_token = m[b"lock-token"]
855+
else:
856+
lock_token = kwargs.pop("lock_token", None)
857+
parsed.append(message_type(wrapped, lock_token=lock_token, **kwargs))
852858
return parsed
853859

854860
@staticmethod

sdk/servicebus/azure-servicebus/azure/servicebus/_transport/_uamqp_transport.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -883,14 +883,20 @@ def parse_received_message( # pylint:disable=docstring-keyword-should-match-keyw
883883
:keyword ~azure.servicebus.ServiceBusReceiver receiver: Required.
884884
:keyword bool is_peeked_message: Optional. For peeked messages.
885885
:keyword bool is_deferred_message: Optional. For deferred messages.
886+
:keyword uuid.UUID lock_token: Optional. Lock token, if it is given by the message receiver.
886887
:keyword ~azure.servicebus.ServiceBusReceiveMode receive_mode: Optional.
887888
:return: List of ServiceBusReceivedMessage.
888889
:rtype: list[~azure.servicebus.ServiceBusReceivedMessage]
889890
"""
891+
is_deferred_message = kwargs.get("is_deferred_message", False)
890892
parsed = []
891893
for m in message.get_data()[b"messages"]:
892894
wrapped = Message.decode_from_bytes(bytearray(m[b"message"]))
893-
parsed.append(message_type(wrapped, **kwargs))
895+
if is_deferred_message and b"lock-token" in m:
896+
lock_token = m[b"lock-token"]
897+
else:
898+
lock_token = kwargs.pop("lock_token", None)
899+
parsed.append(message_type(wrapped, lock_token=lock_token, **kwargs))
894900
return parsed
895901

896902
@staticmethod

sdk/servicebus/azure-servicebus/tests/test_message.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import os
2+
import uuid
23
from decimal import Decimal
4+
from unittest import mock
35
import pytest
46

57
try:
@@ -28,6 +30,7 @@
2830
from azure.servicebus.amqp import AmqpAnnotatedMessage, AmqpMessageBodyType, AmqpMessageProperties, AmqpMessageHeader
2931
from azure.servicebus._pyamqp.message import Message
3032
from azure.servicebus._pyamqp._message_backcompat import LegacyBatchMessage
33+
from azure.servicebus._pyamqp._encode import encode_payload
3134
from azure.servicebus._transport._pyamqp_transport import PyamqpTransport
3235

3336
from devtools_testutils import AzureMgmtRecordedTestCase
@@ -360,6 +363,50 @@ def test_servicebus_message_time_to_live():
360363
assert message.time_to_live == timedelta(days=1)
361364

362365

366+
def _pyamqp_encoded_inner_message(body=b"data"):
367+
# Encode a minimal AMQP message payload, as the service wraps each
368+
# deferred/peeked message inside the management-link response.
369+
output = bytearray()
370+
encode_payload(output, Message(data=[body]))
371+
return bytes(output)
372+
373+
374+
def test_servicebus_received_message_deferred_has_lock_token():
375+
# Regression test for #42454: receive_deferred_messages returned messages
376+
# with lock_token=None. For deferred messages the service returns the
377+
# lock-token alongside the message in the management-link response (not as a
378+
# delivery-tag), so parse_received_message must read m[b"lock-token"].
379+
lock_token = uuid.uuid4()
380+
mgmt_response = Message(
381+
value={b"messages": [{b"message": _pyamqp_encoded_inner_message(), b"lock-token": lock_token}]}
382+
)
383+
parsed = PyamqpTransport.parse_received_message(
384+
mgmt_response,
385+
ServiceBusReceivedMessage,
386+
receiver=mock.Mock(),
387+
is_deferred_message=True,
388+
receive_mode=ServiceBusReceiveMode.PEEK_LOCK,
389+
)
390+
assert len(parsed) == 1
391+
assert parsed[0].lock_token == lock_token
392+
393+
394+
def test_servicebus_received_message_peeked_has_no_lock_token():
395+
# Peeked messages are not locked and carry no lock-token in the response,
396+
# so lock_token must remain None (the deferred-only extraction must not leak
397+
# into the peek path).
398+
mgmt_response = Message(value={b"messages": [{b"message": _pyamqp_encoded_inner_message()}]})
399+
parsed = PyamqpTransport.parse_received_message(
400+
mgmt_response,
401+
ServiceBusReceivedMessage,
402+
receiver=mock.Mock(),
403+
is_peeked_message=True,
404+
receive_mode=ServiceBusReceiveMode.PEEK_LOCK,
405+
)
406+
assert len(parsed) == 1
407+
assert parsed[0].lock_token is None
408+
409+
363410
class TestServiceBusMessageBackcompat(AzureMgmtRecordedTestCase):
364411

365412
@pytest.mark.liveTest

0 commit comments

Comments
 (0)