Skip to content

[Service Bus] Fix batch send of messages with routing properties#47625

Open
EldertGrootenboer wants to merge 6 commits into
mainfrom
fix/servicebus-uamqp-batch-send-38537522
Open

[Service Bus] Fix batch send of messages with routing properties#47625
EldertGrootenboer wants to merge 6 commits into
mainfrom
fix/servicebus-uamqp-batch-send-38537522

Conversation

@EldertGrootenboer

Copy link
Copy Markdown
Member

Problem

Sending a batch of messages with message_id, session_id, or
partition_key set on the first message raised
TypeError: 'BatchMessage' object is not subscriptable under the uamqp
transport (uamqp_transport=True), and surfaced a secondary
AttributeError: 'list' object has no attribute '_message' on the list send
path. Regression from #42598.

Root cause: ServiceBusMessageBatch._add captured the batch-envelope
message_id/session_id properties and partition_key annotation by calling
the 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]). Under
uamqp the batch envelope is a BatchMessage (not subscriptable), so the
capture raised TypeError.

One root cause, two symptoms:

  • Batch-object path (create_message_batch() + add_message()): the
    TypeError surfaces directly.
  • List path (send_messages([...])): the sender builds a batch via
    _from_list() -> _add(); the same TypeError was then swallowed by a
    broad except TypeError (intended to detect "not a list"), which went on to
    do obj_message._message = ... on the list, producing the reported secondary
    AttributeError.

Under uamqp the outgoing transform auto-assigns a message_id, so even a bare
ServiceBusMessage(b"x") in a batch triggered the crash.

Fix

  • Move batch-envelope property capture behind a new transport method,
    set_batch_envelope_properties(batch_message, message_id, session_id, partition_key), with separate pyamqp and uamqp implementations. The pyamqp
    implementation preserves the existing list-indexed behavior; the uamqp
    implementation sets batch_message.properties = MessageProperties(...) plus
    the partition-key annotation. It is a no-op when all three are falsy and runs
    once per batch.
  • Call it once per batch from _common/message.py (guarded by
    self._count == 1), and drop the now-unused unconditional pyamqp import.
  • Replace the broad except TypeError in the sync and async senders with an
    explicit isinstance(obj_message, list) check, so a genuine TypeError
    raised while building the batch is no longer masked.
    transform_outbound_messages returns a list for iterable input and a single
    ServiceBusMessage otherwise, so the discrimination is behavior-equivalent
    for valid inputs.

Routing-property mapping is consistent with the other Azure SDK languages:
session_id maps to the AMQP group-id, partition_key to the
x-opt-partition-key annotation.

Testing

Added unit coverage in tests/test_message.py:

  • test_set_batch_envelope_properties exercises each branch (no-op,
    message_id only, session_id only, partition_key only) on both transports.
  • strengthened test_servicebus_message_batch to assert the envelope
    properties and annotations on both transports.
  • test_transform_outbound_messages_discriminates_list_vs_single pins the
    list-vs-single contract the sender discrimination relies on.

Result (Python 3.12): targeted run 5 passed. Batch and send behavior across
both transports is additionally covered by the live test_queues /
test_queues_async suites.

What changed

  • sdk/servicebus/azure-servicebus/azure/servicebus/_transport/_base.py
  • sdk/servicebus/azure-servicebus/azure/servicebus/_transport/_pyamqp_transport.py
  • sdk/servicebus/azure-servicebus/azure/servicebus/_transport/_uamqp_transport.py
  • sdk/servicebus/azure-servicebus/azure/servicebus/_common/message.py
  • sdk/servicebus/azure-servicebus/azure/servicebus/_servicebus_sender.py
  • sdk/servicebus/azure-servicebus/azure/servicebus/aio/_servicebus_sender_async.py
  • sdk/servicebus/azure-servicebus/tests/test_message.py
  • sdk/servicebus/azure-servicebus/CHANGELOG.md

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.
EldertGrootenboer and others added 4 commits June 24, 2026 11:33
- 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

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 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 wired ServiceBusMessageBatch._add to call it, removing the unconditional pyamqp import.
  • Replaced try/except TypeError discrimination 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).

@EldertGrootenboer EldertGrootenboer marked this pull request as ready for review June 25, 2026 22:14
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