Skip to content

perf: zero-copy receive via Body.ToMemory(); fix exception-swallowing message loss#93

Merged
niemyjski merged 3 commits into
mainfrom
perf/zero-copy-receive-fix-exception-swallow
Jun 25, 2026
Merged

perf: zero-copy receive via Body.ToMemory(); fix exception-swallowing message loss#93
niemyjski merged 3 commits into
mainfrom
perf/zero-copy-receive-fix-exception-swallow

Conversation

@niemyjski

Copy link
Copy Markdown
Member

Summary

Two independent fixes to AzureServiceBusMessageBus.OnMessageAsync:

  1. Zero-copy receive — use Body.ToMemory() instead of Body.ToArray()
  2. Critical bug fix — exception handlers were swallowing exceptions, causing AutoCompleteMessages = true to permanently ack messages that failed processing

Companion to FoundatioFx/Foundatio#530 which changes IMessage.Data from byte[] to ReadOnlyMemory<byte>.


Change 1: Zero-copy receive (Body.ToMemory())

// Before
var message = new Message(brokeredMessage.Body.ToArray(), DeserializeMessageBody)

// After
var message = new Message(brokeredMessage.Body.ToMemory(), DeserializeMessageBody)

BinaryData.ToMemory() returns a ReadOnlyMemory<byte> backed by the same managed array as the BinaryData — no copy. Azure Service Bus does not pool or reclaim the body buffer after the handler returns (unlike RabbitMQ), so passing the memory directly is safe for the entire message lifetime.

MemoryMarshal.TryGetArray() in the serializer extension always succeeds for array-backed ReadOnlyMemory<byte>, so deserialization also remains zero-copy on this path.


Change 2: Fix critical message-loss bug

Before — exceptions were caught and swallowed:

catch (MessageBusException)
{
    // logged, but exception is swallowed
}
catch (Exception ex)
{
    _logger.LogError(ex, ...);
    // exception is swallowed
}

After — exceptions are rethrown:

catch (MessageBusException)
{
    // logged. Rethrow so SDK abandons → increments delivery count → DLQ after MaxDeliveryCount
    throw;
}
catch (Exception ex)
{
    _logger.LogError(ex, "...");
    throw;
}

Why this matters

With AutoCompleteMessages = true (the SDK default):

Handler returns normally → SDK completes (acks) the message — message is gone forever
Handler throws → SDK abandons the message → delivery count incremented → DLQ after MaxDeliveryCount

The previous code swallowed all exceptions, so the handler always "returned normally" from the SDK's perspective. Any message that triggered a MessageBusException (bad payload, type not found, etc.) or an unexpected error was silently and permanently lost — never retried, never dead-lettered.

With the fix, failures trigger abandon → retry → DLQ, which is the expected behavior for any reliable message bus.

Comment thread tests/Foundatio.AzureServiceBus.Tests/Foundatio.AzureServiceBus.Tests.csproj Outdated
Per review feedback, revert the message handler's exception-handling blocks back
to main so this PR is scoped to the zero-copy receive change only. Also remove
the unrelated Microsoft.NET.Test.Sdk package override added to the test csproj.

The only remaining change is brokeredMessage.Body.ToArray() -> ToMemory() for a
zero-copy receive, which is safe for Azure Service Bus because BinaryData owns a
stable buffer (not a pooled/reclaimed transport buffer).
@niemyjski niemyjski self-assigned this Jun 25, 2026
@niemyjski
niemyjski merged commit 4fd493f into main Jun 25, 2026
4 checks passed
@niemyjski
niemyjski deleted the perf/zero-copy-receive-fix-exception-swallow branch June 25, 2026 17:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant