[Service Bus] Fix batch send of messages with routing properties#47625
[Service Bus] Fix batch send of messages with routing properties#47625EldertGrootenboer wants to merge 6 commits into
Conversation
Sending a batch of messages with message_id, session_id, or partition_key
set raised TypeError ("'BatchMessage' object is not subscriptable") on the
uamqp transport, and surfaced a secondary AttributeError on the list send
path.
- Route batch-envelope property capture through a new transport abstraction
(set_batch_envelope_properties) with pyamqp and uamqp implementations, so
routing properties land on the AMQP batch envelope on both transports.
- Discriminate the batch path from the single-message path with an explicit
isinstance(list) check instead of a broad except TypeError that masked the
real error.
- Add unit coverage for the envelope capture on both transports and for the
list-vs-single discriminator contract.
Regression from #42598.
- pylint 4.0.4 does not honor a standalone disable comment inside an else suite (only inside an except suite), so converting the try/except to if/else left two protected-access warnings on _message unsuppressed - Move the disable inline onto the _message accesses in both sync and async ServiceBusSender.send_messages, matching the existing file convention
There was a problem hiding this comment.
Pull request overview
This PR fixes a regression (from #42598) where sending a batch of Service Bus messages whose first message carried a message_id, session_id, or partition_key crashed under the uamqp transport with TypeError: 'BatchMessage' object is not subscriptable, plus a masked secondary AttributeError on the list-send path. The root cause was that ServiceBusMessageBatch._add captured batch-envelope routing properties by calling pyamqp-specific helpers (set_message_properties / set_message_annotations) unconditionally, which index the message as a list — invalid for a uamqp BatchMessage.
The fix introduces a transport-abstraction method set_batch_envelope_properties() (abstract in _base.py, implemented separately for pyamqp and uamqp), routes the capture through it, and replaces the broad except TypeError in both senders with an explicit isinstance(obj_message, list) discriminator so genuine TypeErrors are no longer swallowed.
Changes:
- Added a transport-specific
set_batch_envelope_properties()hook and wiredServiceBusMessageBatch._addto call it, removing the unconditional pyamqp import. - Replaced
try/except TypeErrordiscrimination with an explicit list check in the sync and async senders. - Added unit coverage (per-branch envelope-property capture on both transports, batch-envelope assertions, and a list-vs-single discriminator contract test) plus a CHANGELOG entry.
I verified: imports are correct on every touched module (List/Optional/cast in pyamqp, MessageProperties/BatchMessage/_X_OPT_PARTITION_KEY at runtime in uamqp), the removed set_message_properties/set_message_annotations imports are no longer referenced in message.py while _X_OPT_PARTITION_KEY is still used, the pyamqp Properties indices (message_id=0, group_id=10) are correct, the pyamqp batch envelope is a plain list (subscriptable) while uamqp uses BatchMessage, ServiceBusMessageBatch inputs are handled before the new discriminator, and transform_outbound_messages returns a list vs single message consistent with the new check. I found no concrete issues to comment on.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
azure/servicebus/_transport/_base.py |
Adds the abstract set_batch_envelope_properties to the transport interface, following the existing @staticmethod @abstractmethod pattern. |
azure/servicebus/_transport/_pyamqp_transport.py |
Implements the hook preserving the list-indexed pyamqp behavior (Properties index 0/10, partition-key annotation). |
azure/servicebus/_transport/_uamqp_transport.py |
Implements the hook for uamqp via MessageProperties and the partition-key annotation, fixing the crash. |
azure/servicebus/_common/message.py |
Routes batch-envelope capture through the transport hook and drops the now-unused unconditional pyamqp import. |
azure/servicebus/_servicebus_sender.py |
Replaces broad except TypeError with an explicit isinstance(..., list) discriminator. |
azure/servicebus/aio/_servicebus_sender_async.py |
Mirrors the sync sender discrimination change. |
tests/test_message.py |
Adds per-branch and discriminator unit tests; strengthens existing batch test on both transports. |
CHANGELOG.md |
Documents the regression fix under 7.14.4 (Unreleased). |
Problem
Sending a batch of messages with
message_id,session_id, orpartition_keyset on the first message raisedTypeError: 'BatchMessage' object is not subscriptableunder the uamqptransport (
uamqp_transport=True), and surfaced a secondaryAttributeError: 'list' object has no attribute '_message'on the list sendpath. Regression from #42598.
Root cause:
ServiceBusMessageBatch._addcaptured the batch-envelopemessage_id/session_idproperties andpartition_keyannotation by callingthe module-level pyamqp helpers (
set_message_properties/set_message_annotations) unconditionally, regardless of the active transport.Those helpers index the message as a list (
message[3],message[2]). Underuamqp the batch envelope is a
BatchMessage(not subscriptable), so thecapture raised
TypeError.One root cause, two symptoms:
create_message_batch()+add_message()): theTypeErrorsurfaces directly.send_messages([...])): the sender builds a batch via_from_list()->_add(); the sameTypeErrorwas then swallowed by abroad
except TypeError(intended to detect "not a list"), which went on todo
obj_message._message = ...on the list, producing the reported secondaryAttributeError.Under uamqp the outgoing transform auto-assigns a
message_id, so even a bareServiceBusMessage(b"x")in a batch triggered the crash.Fix
set_batch_envelope_properties(batch_message, message_id, session_id, partition_key), with separate pyamqp and uamqp implementations. The pyamqpimplementation preserves the existing list-indexed behavior; the uamqp
implementation sets
batch_message.properties = MessageProperties(...)plusthe partition-key annotation. It is a no-op when all three are falsy and runs
once per batch.
_common/message.py(guarded byself._count == 1), and drop the now-unused unconditional pyamqp import.except TypeErrorin the sync and async senders with anexplicit
isinstance(obj_message, list)check, so a genuineTypeErrorraised while building the batch is no longer masked.
transform_outbound_messagesreturns a list for iterable input and a singleServiceBusMessageotherwise, so the discrimination is behavior-equivalentfor valid inputs.
Routing-property mapping is consistent with the other Azure SDK languages:
session_idmaps to the AMQPgroup-id,partition_keyto thex-opt-partition-keyannotation.Testing
Added unit coverage in
tests/test_message.py:test_set_batch_envelope_propertiesexercises each branch (no-op,message_id only, session_id only, partition_key only) on both transports.
test_servicebus_message_batchto assert the envelopeproperties and annotations on both transports.
test_transform_outbound_messages_discriminates_list_vs_singlepins thelist-vs-single contract the sender discrimination relies on.
Result (Python 3.12): targeted run
5 passed. Batch and send behavior acrossboth transports is additionally covered by the live
test_queues/test_queues_asyncsuites.What changed
sdk/servicebus/azure-servicebus/azure/servicebus/_transport/_base.pysdk/servicebus/azure-servicebus/azure/servicebus/_transport/_pyamqp_transport.pysdk/servicebus/azure-servicebus/azure/servicebus/_transport/_uamqp_transport.pysdk/servicebus/azure-servicebus/azure/servicebus/_common/message.pysdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_sender.pysdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_sender_async.pysdk/servicebus/azure-servicebus/tests/test_message.pysdk/servicebus/azure-servicebus/CHANGELOG.md