Skip to content

Commit 624a8bd

Browse files
authored
Merge pull request #4187 from MikeFreeman-Flagstone/fix/sequence-number-cause-argument-exception
fix(asb): add SequenceNumber to ReservedHeaders to prevent duplicate key on redelivery
2 parents bbb0c8c + 5b36387 commit 624a8bd

3 files changed

Lines changed: 101 additions & 1 deletion

File tree

src/Paramore.Brighter.MessagingGateway.AzureServiceBus/ASBConstants.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ internal static class ASBConstants
2424
public const string TraceParent = "cloudevents:traceparent";
2525
public const string TraceState = "cloudevents:tracestate";
2626

27-
public static readonly string[] ReservedHeaders = [LockTokenHeaderBagKey, MessageTypeHeaderBagKey, HandledCountHeaderBagKey, ReplyToHeaderBagKey, SessionIdKey
27+
public static readonly string[] ReservedHeaders = [LockTokenHeaderBagKey, SequenceNumberBagKey, MessageTypeHeaderBagKey, HandledCountHeaderBagKey, ReplyToHeaderBagKey, SessionIdKey
2828
];
2929
}
3030
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using Paramore.Brighter.AzureServiceBus.Tests.TestDoubles;
6+
using Paramore.Brighter.MessagingGateway.AzureServiceBus;
7+
using Xunit;
8+
9+
namespace Paramore.Brighter.AzureServiceBus.Tests.MessagingGateway;
10+
11+
/// <summary>
12+
/// Covers the bug where DeferMessageAction caused a requeued message to carry
13+
/// SequenceNumber in ApplicationProperties. On redelivery, MapToBrighterMessage
14+
/// would throw ArgumentException (duplicate key) because SequenceNumber was
15+
/// already added from the native broker property.
16+
/// Fix: SequenceNumber is in ASBConstants.ReservedHeaders so it is never written
17+
/// to ApplicationProperties during requeue.
18+
/// </summary>
19+
[Trait("Category", "ASB")]
20+
public class When_A_Deferred_Message_Is_Redelivered
21+
{
22+
private readonly AzureServiceBusMesssageCreator _creator;
23+
24+
public When_A_Deferred_Message_Is_Redelivered()
25+
{
26+
var subscription = new AzureServiceBusSubscription<ASBTestCommand>(
27+
subscriptionName: new SubscriptionName("test-sub"),
28+
channelName: new ChannelName("test-channel"),
29+
routingKey: new RoutingKey("test-topic"),
30+
messagePumpType: MessagePumpType.Reactor);
31+
32+
_creator = new AzureServiceBusMesssageCreator(subscription);
33+
}
34+
35+
[Fact]
36+
public void When_a_deferred_message_is_redelivered_it_does_not_throw_a_duplicate_key_exception()
37+
{
38+
// Arrange — first delivery from ASB
39+
var firstDelivery = new BrokeredMessage
40+
{
41+
MessageBodyValue = Encoding.UTF8.GetBytes("{\"key\":\"value\"}"),
42+
ApplicationProperties = new Dictionary<string, object>
43+
{
44+
{ "MessageType", "MT_COMMAND" }
45+
},
46+
LockToken = Guid.NewGuid().ToString(),
47+
SequenceNumber = 100L,
48+
Id = Guid.NewGuid().ToString(),
49+
CorrelationId = Guid.NewGuid().ToString(),
50+
ContentType = "application/json"
51+
};
52+
53+
// First receive — handler will throw DeferMessageAction, triggering a requeue
54+
var brighterMessage = _creator.MapToBrighterMessage(firstDelivery);
55+
56+
// Simulate requeue: Brighter calls ConvertToServiceBusMessage and republishes.
57+
// SequenceNumber must NOT appear in ApplicationProperties of the requeued message.
58+
var requeued = AzureServiceBusMessagePublisher.ConvertToServiceBusMessage(brighterMessage);
59+
Assert.False(requeued.ApplicationProperties.ContainsKey("SequenceNumber"),
60+
"SequenceNumber must not be written to ApplicationProperties on requeue — " +
61+
"it is a broker-assigned property and must stay in ReservedHeaders.");
62+
63+
// Arrange — second delivery (redelivery of the requeued message with the same sequence number)
64+
var secondDelivery = new BrokeredMessage
65+
{
66+
MessageBodyValue = Encoding.UTF8.GetBytes("{\"key\":\"value\"}"),
67+
ApplicationProperties = requeued.ApplicationProperties
68+
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
69+
LockToken = Guid.NewGuid().ToString(),
70+
SequenceNumber = firstDelivery.SequenceNumber,
71+
Id = firstDelivery.Id,
72+
CorrelationId = firstDelivery.CorrelationId,
73+
ContentType = "application/json"
74+
};
75+
76+
// Act & Assert — must not throw ArgumentException: duplicate key SequenceNumber
77+
var redelivered = _creator.MapToBrighterMessage(secondDelivery);
78+
Assert.Equal(firstDelivery.SequenceNumber, redelivered.Header.Bag["SequenceNumber"]);
79+
}
80+
}

tests/Paramore.Brighter.AzureServiceBus.Tests/MessagingGateway/When_Converting_A_Message_With_Local_Headers.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@ namespace Paramore.Brighter.AzureServiceBus.Tests.MessagingGateway;
77
[Trait("Category", "ASB")]
88
public class AzureServiceBusMessagePublisherLocalHeaderTests
99
{
10+
[Fact]
11+
public void When_Converting_A_Message_The_SequenceNumber_Bag_Entry_Is_Not_Written_To_ApplicationProperties()
12+
{
13+
// SequenceNumber is a broker-assigned system property on every delivery.
14+
// It must not round-trip through ApplicationProperties — if it did, a requeued
15+
// message would arrive back with SequenceNumber in both its native property and
16+
// ApplicationProperties, causing a duplicate-key crash in MapToBrighterMessage.
17+
var header = new MessageHeader(
18+
messageId: Guid.NewGuid().ToString(),
19+
topic: new RoutingKey("test.topic"),
20+
messageType: MessageType.MT_COMMAND);
21+
header.Bag["SequenceNumber"] = 42L;
22+
23+
var message = new Message(header, new MessageBody("body"));
24+
25+
var asbMessage = AzureServiceBusMessagePublisher.ConvertToServiceBusMessage(message);
26+
27+
Assert.False(asbMessage.ApplicationProperties.ContainsKey("SequenceNumber"));
28+
}
29+
1030
[Fact]
1131
public void When_Converting_A_Message_The_ProducerTopic_Local_Header_Is_Stripped()
1232
{

0 commit comments

Comments
 (0)