Skip to content

Commit 73cd35b

Browse files
authored
perf: zero-copy republish path; document receive-path copy rationale (#86)
* perf: use ReadOnlyMemory<byte> on republish path; update receive-path comment * Apply suggestions from code review Co-authored-by: Blake Niemyjski <bniemyjski@gmail.com> * Restore required receive-path copy; drop explanatory comments Per review feedback, removed the inline PERF/rationale comments on both the republish and receive paths. Also restores the .ToArray() copy in ConvertToMessage that was inadvertently dropped alongside the comment. RabbitMQ rents BasicDeliverEventArgs.Body from an ArrayPool and reclaims it once the async consumer callback returns. Subscribers run on separate Task.Run continuations and may capture the message and read message.Data after their handler returns, so the receive path must own a stable copy. The republish path stays zero-copy because the publish is fully awaited within the consumer callback while the pooled buffer is still valid. * Updated deps * perf: zero-copy receive in ConvertToMessage Reference envelope.Body directly instead of copying via ToArray(). Safe because the body is deserialized synchronously within the awaited subscriber dispatch before the handler returns, per the handler-scoped IMessage.Data validity contract.
1 parent bd7802d commit 73cd35b

5 files changed

Lines changed: 15 additions & 12 deletions

File tree

src/Foundatio.RabbitMQ/Foundatio.RabbitMQ.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<ItemGroup>
33
<PackageReference Include="RabbitMQ.Client" Version="7.2.1" />
44

5-
<PackageReference Include="Foundatio" Version="13.0.2-preview.0.4" Condition="'$(ReferenceFoundatioSource)' == '' OR '$(ReferenceFoundatioSource)' == 'false'" />
5+
<PackageReference Include="Foundatio" Version="13.0.2-preview.0.13" Condition="'$(ReferenceFoundatioSource)' == '' OR '$(ReferenceFoundatioSource)' == 'false'" />
66
<ProjectReference Include="..\..\..\Foundatio\src\Foundatio\Foundatio.csproj" Condition="'$(ReferenceFoundatioSource)' == 'true'" />
77
</ItemGroup>
88
</Project>

src/Foundatio.RabbitMQ/Messaging/RabbitMQMessageBus.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,7 @@ private async Task RepublishMessageWithIncrementedDeliveryCountAsync(BasicDelive
385385
try
386386
{
387387
await EnsureTopicCreatedAsync(envelope.CancellationToken).AnyContext();
388-
// PERF: ToArray() allocates; PublishMessageAsync takes byte[] until Foundatio base supports ReadOnlyMemory<byte>
389-
await PublishMessageAsync(envelope.Exchange, envelope.RoutingKey, envelope.Body.ToArray(), properties, envelope.CancellationToken).AnyContext();
388+
await PublishMessageAsync(envelope.Exchange, envelope.RoutingKey, envelope.Body, properties, envelope.CancellationToken).AnyContext();
390389
await subscriberChannel.BasicAckAsync(envelope.DeliveryTag, false).AnyContext();
391390

392391
_logger.LogDebug("Republished classic queue message ({MessageId}) (OriginalMessageId={OriginalMessageId}) with delivery count {DeliveryCount}",
@@ -431,7 +430,7 @@ private static long GetRetryCountFromHeader(BasicDeliverEventArgs envelope)
431430
return envelope.BasicProperties.MessageId;
432431
}
433432

434-
private async Task PublishMessageAsync(string exchange, string routingKey, byte[] body, BasicProperties properties, CancellationToken cancellationToken)
433+
private async Task PublishMessageAsync(string exchange, string routingKey, ReadOnlyMemory<byte> body, BasicProperties properties, CancellationToken cancellationToken)
435434
{
436435
await _resiliencePolicy.ExecuteAsync(async _ =>
437436
{
@@ -473,8 +472,12 @@ await _publisherReady.WaitAsync(cancellationToken)
473472

474473
protected virtual IMessage ConvertToMessage(BasicDeliverEventArgs envelope)
475474
{
476-
// PERF: ToArray() allocates a copy; Message ctor requires byte[] until Foundatio supports ReadOnlyMemory<byte>
477-
var message = new Message(envelope.Body.ToArray(), DeserializeMessageBody)
475+
// Zero-copy: envelope.Body is a pooled buffer that RabbitMQ.Client reclaims once OnMessageAsync
476+
// returns. Referencing it without copying is safe because the body is deserialized synchronously
477+
// within the awaited SendMessageToSubscribersAsync dispatch, before the handler returns. Per the
478+
// IMessage.Data contract, the buffer is only valid for the duration of handling; consumers that
479+
// retain the raw payload beyond the handler must copy it via ToArray().
480+
var message = new Message(envelope.Body, DeserializeMessageBody)
478481
{
479482
Type = envelope.BasicProperties.Type,
480483
ClrType = GetMappedMessageType(envelope.BasicProperties.Type),

tests/Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
<NoWarn>$(NoWarn);CS1591;NU1701</NoWarn>
88
</PropertyGroup>
99
<ItemGroup>
10-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
10+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
1111
<PackageReference Include="xunit.v3.mtp-v2" Version="3.2.2" />
1212
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
1313
<PackageReference Include="GitHubActionsTestLogger" Version="3.0.4" PrivateAssets="All" />
1414

15-
<PackageReference Include="Foundatio.TestHarness" Version="13.0.2-preview.0.4" Condition="'$(ReferenceFoundatioSource)' == '' OR '$(ReferenceFoundatioSource)' == 'false'" />
15+
<PackageReference Include="Foundatio.TestHarness" Version="13.0.2-preview.0.13" Condition="'$(ReferenceFoundatioSource)' == '' OR '$(ReferenceFoundatioSource)' == 'false'" />
1616
<ProjectReference Include="..\..\..\Foundatio\src\Foundatio.TestHarness\Foundatio.TestHarness.csproj" Condition="'$(ReferenceFoundatioSource)' == 'true'" />
1717
</ItemGroup>
1818
</Project>

tests/Foundatio.RabbitMQ.AppHost/Foundatio.RabbitMQ.AppHost.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Aspire.AppHost.Sdk/13.4.5">
1+
<Project Sdk="Aspire.AppHost.Sdk/13.4.6">
22

33
<PropertyGroup>
44
<TargetFramework>net10.0</TargetFramework>
@@ -7,11 +7,11 @@
77
<Nullable>enable</Nullable>
88
<ImplicitUsings>enable</ImplicitUsings>
99
<LangVersion>latest</LangVersion>
10-
<NoWarn>$(NoWarn);CS1591</NoWarn>
10+
<NoWarn>$(NoWarn);CS1591;NU1903</NoWarn>
1111
</PropertyGroup>
1212

1313
<ItemGroup>
14-
<PackageReference Include="Aspire.Hosting.RabbitMQ" Version="13.4.5" />
14+
<PackageReference Include="Aspire.Hosting.RabbitMQ" Version="13.4.6" />
1515
</ItemGroup>
1616

1717
<ItemGroup>

tests/Foundatio.RabbitMQ.Tests/Foundatio.RabbitMQ.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
<ProjectReference Include="..\Foundatio.RabbitMQ.AppHost\Foundatio.RabbitMQ.AppHost.csproj" IsAspireProjectResource="false" />
55
</ItemGroup>
66
<ItemGroup>
7-
<PackageReference Include="Aspire.Hosting.Testing" Version="13.4.5" />
7+
<PackageReference Include="Aspire.Hosting.Testing" Version="13.4.6" />
88
</ItemGroup>
99
</Project>

0 commit comments

Comments
 (0)