-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathResourcesAcquiredRetryDeadLetterHandler.cs
More file actions
90 lines (81 loc) · 4.19 KB
/
Copy pathResourcesAcquiredRetryDeadLetterHandler.cs
File metadata and controls
90 lines (81 loc) · 4.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
using Confluent.Kafka;
using LantanaGroup.Link.Normalization.Application.Models.Messages;
using LantanaGroup.Link.Normalization.Application.Services;
using LantanaGroup.Link.Shared.Application.Error.Handlers;
using LantanaGroup.Link.Shared.Application.Error.Interfaces;
using LantanaGroup.Link.Shared.Application.Interfaces;
using LantanaGroup.Link.Shared.Application.Listeners;
using LantanaGroup.Link.Shared.Application.Models;
using LantanaGroup.Link.Shared.Application.Services.Security;
using System.Text.Json;
namespace LantanaGroup.Link.Normalization.Application.Error;
/// <summary>
/// Dead letter handler for <see cref="RetryListener"/> that also releases the resource cache.
/// </summary>
/// <remarks>
/// A <c>ResourcesAcquired</c> message that fails transiently is republished to
/// <c>ResourcesAcquired-Retry</c> and redelivered on a schedule. Once the retry count is exhausted,
/// <see cref="RetryListener"/> — shared, service-agnostic code with no knowledge of the resource
/// cache — dead-letters it to <c>ResourcesAcquired-Error</c>. That is the second and final terminal
/// path for the message (the first being a <c>DeadLetterException</c> raised directly in
/// <c>ResourcesAcquiredListener</c>), so it is where the cached resources have to be released.
/// <para>
/// Normalization registers <see cref="RetryListener"/> for <c>ResourcesAcquired-Retry</c> only, so
/// every message reaching this handler is a <see cref="ResourcesAcquiredValue"/>.
/// </para>
/// </remarks>
public class ResourcesAcquiredRetryDeadLetterHandler : DeadLetterExceptionHandler<RetryListener, string, string>
{
private static readonly JsonSerializerOptions DeserializerOptions = new()
{
// Mirrors JsonWithFhirMessageDeserializer, which is how the value was read off the topic.
PropertyNameCaseInsensitive = true,
AllowTrailingCommas = true
};
private readonly IResourceCachePurger _resourceCachePurger;
private readonly ILogger<ResourcesAcquiredRetryDeadLetterHandler> _logger;
public ResourcesAcquiredRetryDeadLetterHandler(
IKafkaProducerFactory<string, string> producerFactory,
IKafkaProducerFactory<string, string> nullConsumeResultProducerFactory,
ServiceInformation serviceInformation,
IExceptionLogger<RetryListener> exceptionHandler,
IResourceCachePurger resourceCachePurger,
ILogger<ResourcesAcquiredRetryDeadLetterHandler> logger)
: base(producerFactory, nullConsumeResultProducerFactory, serviceInformation, exceptionHandler)
{
_resourceCachePurger = resourceCachePurger ?? throw new ArgumentNullException(nameof(resourceCachePurger));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <remarks>
/// Both <c>HandleException</c> overloads funnel through here. The dead letter is produced first so
/// that the durable record of the failure is never at the mercy of the cache purge.
/// </remarks>
public override void ProduceDeadLetter(ConsumeResult<string, string> consumeResult, string exceptionMessage)
{
base.ProduceDeadLetter(consumeResult, exceptionMessage);
PurgeResourceCache(consumeResult, exceptionMessage);
}
private void PurgeResourceCache(ConsumeResult<string, string> consumeResult, string exceptionMessage)
{
ResourcesAcquiredValue? value;
try
{
value = JsonSerializer.Deserialize<ResourcesAcquiredValue>(
consumeResult.Message.Value, DeserializerOptions);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Could not deserialize a retry-exhausted message from {Topic} to determine its resource cache keys. " +
"Any cached resources for it will be released by the cache expiration policy instead.",
consumeResult.Topic.SanitizeForLog());
return;
}
// RetryListener runs its consume callback on a background thread with no synchronization
// context, so blocking here cannot deadlock. PurgeAsync does not throw.
_resourceCachePurger
.PurgeAsync(value, $"retry count exhausted: {exceptionMessage}")
.GetAwaiter()
.GetResult();
}
}