Skip to content

Commit 677cc30

Browse files
EtherZazarusz
authored andcommitted
#358 Modify ASB properties
Signed-off-by: Richard Pringle <richardpringle@gmail.com>
1 parent 637197e commit 677cc30

File tree

6 files changed

+270
-16
lines changed

6 files changed

+270
-16
lines changed

docs/intro.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1263,5 +1263,5 @@ This allows to recreate missing elements in the infrastructure without restartin
12631263

12641264
## Versions
12651265

1266-
- The v3 release [migration guide](https://github.com/zarusz/SlimMessageBus/tree/release/v3).
1266+
- The v3 release [migration guide](https://github.com/zarusz/SlimMessageBus/releases/tag/3.0.0).
12671267
- The v2 release [migration guide](https://github.com/zarusz/SlimMessageBus/releases/tag/Host.Transport-2.0.0).

docs/provider_azure_servicebus.md

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Please read the [Introduction](intro.md) before reading this provider documentat
1010
- [Default Subscription Name](#default-subscription-name)
1111
- [Consumer context](#consumer-context)
1212
- [Exception Handling for Consumers](#exception-handling-for-consumers)
13+
- [DeadLetter: Application-Level Dead-Lettering](#deadletter-application-level-dead-lettering)
14+
- [Failure: Modify Application Properties on Failure](#failure-modify-application-properties-on-failure)
1315
- [Transport Specific Settings](#transport-specific-settings)
1416
- [Request-Response Configuration](#request-response-configuration)
1517
- [Produce Request Messages](#produce-request-messages)
@@ -19,6 +21,7 @@ Please read the [Introduction](intro.md) before reading this provider documentat
1921
- [Validation of Topology](#validation-of-topology)
2022
- [Trigger Topology Provisioning](#trigger-topology-provisioning)
2123

24+
2225
## Configuration
2326

2427
Azure Service Bus provider requires a connection string:
@@ -193,12 +196,52 @@ This could be useful to extract the message's `CorrelationId` or `ApplicationPro
193196
194197
### Exception Handling for Consumers
195198

196-
In case the consumer was to throw an exception while processing a message, SMB marks the message as abandoned.
197-
This results in a message delivery retry performed by Azure SB (potentially event in another running instance of your service). By default, Azure SB retries 10 times. After last attempt the message Azure SB moves the message to a dead letter queue (DLQ). More information [here](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dead-letter-queues).
199+
In the case where the consumer throws an exception while processing a message, SMB marks the message as abandoned.
200+
This results in a message delivery retry performed by Azure SB (potentially as an event in another running instance of your service). By default, Azure SB retries 10 times. After last attempt, Azure SB will move the message to the [dead letter queue (DLQ)](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dead-letter-queues).
201+
202+
SMB will also add a user property, `SMB.Exception`, on the message with the exception details (just the message, no stack trace). This should be helpful when reviewing messages on the DLQ.
203+
204+
For finer control, a custom error handler can be added by registering an instance of `IConsumerErrorHandler<T>` with the DI. The offending message and the raised exception can then be inspected to determine if the message should be retried (in proceess), failed, or considered as having executed successfully.
205+
206+
In addition to the standard `IConsumerErrorHandler<T>` return types, `ServiceBusConsumerErrorHandler<T>` provides additional, specialized responses for use with the Azure Service Bus transport.
207+
208+
#### DeadLetter: Application-Level Dead-Lettering
209+
210+
[Application-level dead-lettering](https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dead-letter-queues#application-level-dead-lettering) is supported via `DeadLetter(string reason, string description)`. If neither a `reason` nor `description` are supplied, the raised exception type and message will be used as the `reason` and `description`.
198211

199-
If you need to send only selected messages to DLQ, wrap the body of your consumer method in a `try-catch` block and rethrow the exception for only the messages you want to be moved to DLQ (after the retry limit is reached).
212+
```cs
213+
public sealed class SampleConsumerErrorHandler<T> : ServiceBusConsumerErrorHandler<T>
214+
{
215+
public override Task<ProcessResult> OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts)
216+
{
217+
return Task.FromResult(DeadLetter("reason", "description"));
218+
}
219+
}
220+
```
221+
222+
#### Failure: Modify Application Properties on Failure
223+
224+
An overload to `Failure(IReadOnlyDictionary<string, object)` is included to facilitate the modification of application properites on a failed message. This includes the `SMB.Exception` property should alternative detail be required.
225+
226+
```cs
227+
public sealed class SampleConsumerErrorHandler<T> : ServiceBusConsumerErrorHandler<T>
228+
{
229+
public override Task<ProcessResult> OnHandleError(T message, IConsumerContext consumerContext, Exception exception, int attempts)
230+
{
231+
var properties = new Dictionary<string, object>
232+
{
233+
{ "Key", "value" },
234+
{ "Attempts", attempts },
235+
{ "SMB.Exception", exception.ToString().Substring(0, 1000) }
236+
};
237+
238+
return Task.FromResult(Failure(properties));
239+
}
240+
}
241+
```
242+
243+
> By using `IConsumerContext.Properties` (`IConsumerWithContext`) to pass state to the `IConsumerErrorHandler<T>` instance, consumer state can be persisted with the message. This can then be retrieved from `IConsumerContext.Headers` in a subsequent execution to resume processing from a checkpoint, supporting idempotency, especially when distributed transactions are not possible.
200244
201-
SMB will also set a user property `SMB.Exception` on the message with the exception details (just the message, no stack trace). This should be helpful when reviewing messages on the DLQ.
202245

203246
### Transport Specific Settings
204247

@@ -480,4 +523,4 @@ However, in situations when the underlying ASB topology changes (queue / topic i
480523
ITopologyControl ctrl = // injected
481524
482525
await ctrl.ProvisionTopology();
483-
```
526+
```

src/SlimMessageBus.Host.AzureServiceBus/Consumer/AsbBaseConsumer.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ protected async Task ProcessMessageAsyncInternal(
165165
Func<ServiceBusReceivedMessage, string, string, CancellationToken, Task> deadLetterMessage,
166166
CancellationToken token)
167167
{
168+
const string smbException = "SMB.Exception";
169+
168170
// Process the message.
169171
Logger.LogDebug("Received message - Path: {Path}, SubscriptionName: {SubscriptionName}, SequenceNumber: {SequenceNumber}, DeliveryCount: {DeliveryCount}, MessageId: {MessageId}", TopicSubscription.Path, TopicSubscription.SubscriptionName, message.SequenceNumber, message.DeliveryCount, message.MessageId);
170172

@@ -188,16 +190,33 @@ protected async Task ProcessMessageAsyncInternal(
188190
await completeMessage(message, token).ConfigureAwait(false);
189191
return;
190192

191-
case ServiceBusProcessResult.DeadLetterState:
193+
case ServiceBusProcessResult.DeadLetterState deadLetterState:
192194
Logger.LogError(r.Exception, "Dead letter message - Path: {Path}, SubscriptionName: {SubscriptionName}, SequenceNumber: {SequenceNumber}, DeliveryCount: {DeliveryCount}, MessageId: {MessageId}", TopicSubscription.Path, TopicSubscription.SubscriptionName, message.SequenceNumber, message.DeliveryCount, message.MessageId);
193-
await deadLetterMessage(message, r.Exception?.GetType().Name ?? string.Empty, r.Exception?.Message ?? string.Empty, token).ConfigureAwait(false);
195+
196+
var reason = deadLetterState.Reason ?? r.Exception?.GetType().Name ?? string.Empty;
197+
var descripiton = deadLetterState.Description ?? r.Exception?.GetType().Name ?? string.Empty;
198+
await deadLetterMessage(message, reason, descripiton, token).ConfigureAwait(false);
199+
return;
200+
201+
case ServiceBusProcessResult.FailureStateWithProperties withProperties:
202+
var dict = new Dictionary<string, object>(withProperties.Properties.Count + 1);
203+
foreach (var properties in withProperties.Properties)
204+
{
205+
dict.Add(properties.Key, properties.Value);
206+
}
207+
208+
// Set the exception message if it has not been provided
209+
dict.TryAdd(smbException, r.Exception.Message);
210+
211+
Logger.LogError(r.Exception, "Abandon message (exception occurred while processing) - Path: {Path}, SubscriptionName: {SubscriptionName}, SequenceNumber: {SequenceNumber}, DeliveryCount: {DeliveryCount}, MessageId: {MessageId}", TopicSubscription.Path, TopicSubscription.SubscriptionName, message.SequenceNumber, message.DeliveryCount, message.MessageId);
212+
await abandonMessage(message, dict, token).ConfigureAwait(false);
194213
return;
195214

196215
case ProcessResult.FailureState:
197216
var messageProperties = new Dictionary<string, object>();
198217
{
199218
// Set the exception message
200-
messageProperties.Add("SMB.Exception", r.Exception.Message);
219+
messageProperties.Add(smbException, r.Exception.Message);
201220
}
202221

203222
Logger.LogError(r.Exception, "Abandon message (exception occurred while processing) - Path: {Path}, SubscriptionName: {SubscriptionName}, SequenceNumber: {SequenceNumber}, DeliveryCount: {DeliveryCount}, MessageId: {MessageId}", TopicSubscription.Path, TopicSubscription.SubscriptionName, message.SequenceNumber, message.DeliveryCount, message.MessageId);

src/SlimMessageBus.Host.AzureServiceBus/Consumer/IServiceBusConsumerErrorHandler.cs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,19 @@ public interface IServiceBusConsumerErrorHandler<in T> : IConsumerErrorHandler<T
44

55
public abstract class ServiceBusConsumerErrorHandler<T> : ConsumerErrorHandler<T>, IServiceBusConsumerErrorHandler<T>
66
{
7-
public ProcessResult DeadLetter() => ServiceBusProcessResult.DeadLetter;
7+
public ProcessResult DeadLetter(string reason = null, string description = null)
8+
{
9+
return reason == null && description == null
10+
? ServiceBusProcessResult.DeadLetter
11+
: new ServiceBusProcessResult.DeadLetterState(reason, description);
12+
}
13+
14+
public ProcessResult Failure(IReadOnlyDictionary<string, object> properties)
15+
{
16+
return properties != null && properties.Count > 0
17+
? new ServiceBusProcessResult.FailureStateWithProperties(properties)
18+
: Failure();
19+
}
820
}
921

1022
public record ServiceBusProcessResult : ProcessResult
@@ -14,5 +26,25 @@ public record ServiceBusProcessResult : ProcessResult
1426
/// </summary>
1527
public static readonly ProcessResult DeadLetter = new DeadLetterState();
1628

17-
public record DeadLetterState() : ProcessResult();
29+
public record DeadLetterState : ProcessResult
30+
{
31+
public DeadLetterState(string reason = null, string description = null)
32+
{
33+
Reason = reason;
34+
Description = description;
35+
}
36+
37+
public string Reason { get; }
38+
public string Description { get; }
39+
}
40+
41+
public record FailureStateWithProperties : FailureState
42+
{
43+
public FailureStateWithProperties(IReadOnlyDictionary<string, object> properties)
44+
{
45+
Properties = properties;
46+
}
47+
48+
public IReadOnlyDictionary<string, object> Properties { get; }
49+
}
1850
}

src/SlimMessageBus/IConsumerWithContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
namespace SlimMessageBus;
22

33
/// <summary>
4-
/// An extension point for <see cref="IConsumer{TMessage}"/> to recieve provider specific (for current message subject to processing).
4+
/// An extension point for <see cref="IConsumer{TMessage}"/> to receive provider specific (for current message subject to processing).
55
/// </summary>
66
public interface IConsumerWithContext
77
{

0 commit comments

Comments
 (0)