feat: expose terminal consumer failure hook

Allow consumers to react once after all processing retries are exhausted while preserving the base acknowledgement policy.

Ref: IT-1033
This commit is contained in:
2026-08-04 11:38:24 +03:00
parent c0e83bdb77
commit 33f4b80ff8
3 changed files with 114 additions and 0 deletions
+7
View File
@@ -25,3 +25,10 @@ ownership of acknowledgements and retries.
Override `TryValidateMessage(...)` for application-specific permanent validation.
Returning `false` nacks the delivery without requeue before retry processing begins.
Keep validation errors free of credentials and sensitive payload values.
Override `HandleMessageRetriesExhaustedAsync(...)` when a consumer must react once to a
terminal processing failure, for example by publishing a neutral failure result to the
requesting client. The hook runs after the final handler exception and before the original
delivery is nacked without requeue. Hook failures are logged and do not replace the
original failure or acknowledgement policy. Application shutdown cancellation does not
invoke the hook.
+35
View File
@@ -82,6 +82,20 @@ public abstract class RabbitMqConsumerBase<TMessage, TMessageData> : BackgroundS
return true;
}
/// <summary>
/// Handles a terminal processing failure after all message retries are exhausted.
/// The default implementation is a no-op. Implementations should avoid throwing;
/// failures from this hook are logged and the original message is still nacked.
/// </summary>
protected virtual Task HandleMessageRetriesExhaustedAsync(
TMessage message,
RabbitMqMessageContext context,
Exception exception,
CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await EnsureConnectionAsync(stoppingToken);
@@ -155,6 +169,10 @@ public abstract class RabbitMqConsumerBase<TMessage, TMessageData> : BackgroundS
await _channel!.BasicAckAsync(args.DeliveryTag, multiple: false, cancellationToken: cancellationToken);
return;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex) when (attempt < MaxRetries)
{
_logger.LogWarning(ex,
@@ -169,6 +187,23 @@ public abstract class RabbitMqConsumerBase<TMessage, TMessageData> : BackgroundS
"All {Max} attempts exhausted for message on queue {Queue} [CorrelationId={CorrelationId}] — nacking without requeue",
MaxRetries, QueueName, payloadCorrelationId);
try
{
await HandleMessageRetriesExhaustedAsync(
message,
context,
ex,
cancellationToken);
}
catch (Exception hookException)
{
_logger.LogError(
hookException,
"Terminal failure hook failed for message on queue {Queue} [CorrelationId={CorrelationId}]",
QueueName,
payloadCorrelationId);
}
await NackWithoutRequeueAsync(args.DeliveryTag, cancellationToken);
}
}