-
Notifications
You must be signed in to change notification settings - Fork 1
LEGLINK-958: Measure Eval: clean up the resource cache when evaluation or normalization fails #1832
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
smailliwcs
wants to merge
10
commits into
dev
Choose a base branch
from
user/steven.williams/LEGLINK-958
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
5c4c1da
Clean up cache on evaluation failure
smailliwcs d175dd1
Clean up cache on normalization failure
smailliwcs 0323eb8
Fix integration test DI
smailliwcs 2752e6d
LEGLINK-958: Address code review findings on cache cleanup
arianamihailescu 2eee295
fix: retry docker pull steps in .NET Tests CI to handle transient MCRβ¦
Copilot e7b294a
LEGLINK-958: Scope the retry-exhausted purge to acquisition keys only
arianamihailescu ec18a9e
Merge remote-tracking branch 'origin/user/steven.williams/LEGLINK-958β¦
arianamihailescu ad9a7b9
Merge branch 'dev' into user/steven.williams/LEGLINK-958
arianamihailescu 7f6fcf8
LEGLINK-958: Normalization never deletes the correlation key
arianamihailescu 5fb7971
Merge branch 'dev' into user/steven.williams/LEGLINK-958
arianamihailescu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
89 changes: 89 additions & 0 deletions
89
DotNet/Normalization/Application/Error/ResourcesAcquiredRetryDeadLetterHandler.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| 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 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); | ||
| 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(); | ||
| } | ||
| } | ||
91 changes: 91 additions & 0 deletions
91
DotNet/Normalization/Application/Services/ResourceCachePurger.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| using LantanaGroup.Link.Normalization.Application.Models.Messages; | ||
| using LantanaGroup.Link.Shared.Application.Interfaces; | ||
| using LantanaGroup.Link.Shared.Application.Services.Security; | ||
|
|
||
| namespace LantanaGroup.Link.Normalization.Application.Services; | ||
|
|
||
| /// <summary> | ||
| /// Releases the resource cache entries belonging to a <c>ResourcesAcquired</c> message after a | ||
| /// terminal (non-retryable) normalization failure. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Only call this on terminal failures. A message bound for <c>ResourcesAcquired-Retry</c> still | ||
| /// needs its cached resources when it is redelivered, so purging on a transient failure would | ||
| /// guarantee the retry fails too. | ||
| /// <para> | ||
| /// Unlike the success path β which deletes only the per-resource-type acquisition keys and leaves | ||
| /// <c>{correlationId}</c> in place for Measure Eval to read β a terminal failure also removes the | ||
| /// correlation key, because nothing downstream will ever consume it. | ||
| /// </para> | ||
| /// </remarks> | ||
| public interface IResourceCachePurger | ||
| { | ||
| Task PurgeAsync(ResourcesAcquiredValue? value, string reason, CancellationToken cancellationToken = default); | ||
| } | ||
|
|
||
| /// <inheritdoc cref="IResourceCachePurger"/> | ||
| public class ResourceCachePurger : IResourceCachePurger | ||
| { | ||
| private readonly IResourceCache _resourceCache; | ||
| private readonly ILogger<ResourceCachePurger> _logger; | ||
|
|
||
| public ResourceCachePurger(IResourceCache resourceCache, ILogger<ResourceCachePurger> logger) | ||
| { | ||
| _resourceCache = resourceCache ?? throw new ArgumentNullException(nameof(resourceCache)); | ||
| _logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||
| } | ||
|
|
||
| public async Task PurgeAsync(ResourcesAcquiredValue? value, string reason, CancellationToken cancellationToken = default) | ||
| { | ||
| var cacheKeys = value?.CacheKeys?.Where(k => !string.IsNullOrWhiteSpace(k)).ToList(); | ||
|
|
||
| if (cacheKeys == null || cacheKeys.Count == 0) | ||
| { | ||
| _logger.LogWarning( | ||
| "Cannot purge resource cache after terminal failure ({Reason}): the message carries no cache keys. " + | ||
| "Any cached resources for it will be released by the cache expiration policy instead.", | ||
| reason.SanitizeForLog()); | ||
| return; | ||
| } | ||
|
|
||
| // Acquisition keys are "{correlationId}:{ResourceType}"; the correlation key itself holds | ||
| // whatever normalization managed to write before it failed, so it goes too. | ||
| var keysToDelete = new List<string>(cacheKeys); | ||
| keysToDelete.AddRange(cacheKeys | ||
| .Select(ExtractCorrelationId) | ||
| .Where(correlationId => !string.IsNullOrWhiteSpace(correlationId)) | ||
| .Distinct() | ||
| .Where(correlationId => !keysToDelete.Contains(correlationId))); | ||
|
|
||
| try | ||
| { | ||
| await _resourceCache | ||
| .GetImplementation(value!.CacheType) | ||
| .DeleteAsync(keysToDelete, cancellationToken); | ||
|
|
||
| _logger.LogInformation( | ||
| "Purged {KeyCount} resource cache entries after terminal failure ({Reason}). CacheType: {CacheType}, Keys: [{Keys}]", | ||
| keysToDelete.Count, | ||
| reason.SanitizeForLog(), | ||
| value.CacheType, | ||
| string.Join(", ", keysToDelete).SanitizeForLog()); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| // Never let cleanup failure escape: the caller is already handling a failed message, and | ||
| // the cache expiration policy is the backstop for whatever we could not delete here. | ||
| _logger.LogError(ex, | ||
| "Failed to purge resource cache after terminal failure ({Reason}). CacheType: {CacheType}, Keys: [{Keys}]", | ||
| reason.SanitizeForLog(), | ||
| value!.CacheType, | ||
| string.Join(", ", keysToDelete).SanitizeForLog()); | ||
| } | ||
| } | ||
|
|
||
| /// <remarks>Mirrors <c>HybridResourceCache.ExtractCorrelationId</c>.</remarks> | ||
| private static string ExtractCorrelationId(string cacheKey) | ||
| { | ||
| var idx = cacheKey.IndexOf(':'); | ||
| return idx > 0 ? cacheKey[..idx] : cacheKey; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.