< Summary - AsyncResponse (Release / net8.0+net10.0 / unit+integration)

Information
Class: AsyncResponse.Channels.MongoDB.MongoDbRecoveryStateStore
Assembly: AsyncResponse.Channels.MongoDB
File(s): /_/src/Channels/AsyncResponse.Channels.MongoDB/MongoDbRecoveryStateStore.cs
Line coverage
100%
Covered lines: 75
Uncovered lines: 0
Coverable lines: 75
Total lines: 156
Line coverage: 100%
Branch coverage
90%
Covered branches: 36
Total branches: 40
Branch coverage: 90%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
SaveAsync()62.5%9876.92%
GetAllAsync()100%11100%
TryDeleteAsync(...)50%2280%
ScanAsync()100%44100%
DeserializeStates(...)100%1010100%
.cctor()100%11100%
DeserializeState(...)75%191676.66%

File(s)

/_/src/Channels/AsyncResponse.Channels.MongoDB/MongoDbRecoveryStateStore.cs

#LineLine coverage
 1using Microsoft.Extensions.Logging;
 2using System.Runtime.CompilerServices;
 3using System.Text.Json;
 4using System.Text.Json.Serialization.Metadata;
 5
 6namespace AsyncResponse.Channels.MongoDB;
 7
 8/// <summary>
 9/// MongoDB implementation of <see cref="IRecoveryStateStore"/> and
 10/// <see cref="IRecoveryStateScanner"/>. Entries live in a TTL-indexed collection, so MongoDB itself
 11/// reaps expired registrations.
 12/// </summary>
 38313internal sealed class MongoDbRecoveryStateStore(
 38314    MongoDbChannelStore _store,
 38315    ILogger<MongoDbRecoveryStateStore> _logger) : IRecoveryStateStore, IRecoveryStateScanner
 16{
 17    /// <inheritdoc />
 18    public async Task SaveAsync(string correlationId, RecoveryState state, TimeSpan ttl, CancellationToken cancellationT
 19    {
 39320        ArgumentException.ThrowIfNullOrWhiteSpace(correlationId);
 39321        ArgumentNullException.ThrowIfNull(state);
 39322        if (ttl <= TimeSpan.Zero)
 223            throw new ArgumentOutOfRangeException(nameof(ttl), "TTL must be greater than zero.");
 39124        if (!string.Equals(state.CorrelationId, correlationId, StringComparison.Ordinal))
 425            throw new ArgumentException("The recovery-state correlation id must match the store key.", nameof(state));
 38726        if (state.SchemaVersion != RecoveryStateSchema.Current)
 227            throw new ArgumentException("The recovery state must use the current schema version.", nameof(state));
 28
 38529        cancellationToken.ThrowIfCancellationRequested();
 38530        if (state.RegistrationId == Guid.Empty)
 331            state.RegistrationId = Guid.NewGuid();
 32
 38533        await _store.SaveRecoveryStateAsync(correlationId, state, ttl, cancellationToken).ConfigureAwait(false);
 38534    }
 35
 36    /// <inheritdoc />
 37    public async Task<IReadOnlyList<RecoveryState>> GetAllAsync(string correlationId, CancellationToken cancellationToke
 38    {
 3339        ArgumentException.ThrowIfNullOrWhiteSpace(correlationId);
 3340        cancellationToken.ThrowIfCancellationRequested();
 41
 3342        var jsonStates = await _store.LoadRecoveryStatesAsync(correlationId, cancellationToken).ConfigureAwait(false);
 3343        return DeserializeStates(jsonStates, correlationId);
 3144    }
 45
 46    /// <inheritdoc />
 47    public Task<bool> TryDeleteAsync(string correlationId, Guid registrationId, CancellationToken cancellationToken = de
 48    {
 38749        ArgumentException.ThrowIfNullOrWhiteSpace(correlationId);
 38750        if (registrationId == Guid.Empty)
 251            throw new ArgumentException("Registration id cannot be empty.", nameof(registrationId));
 38552        cancellationToken.ThrowIfCancellationRequested();
 38553        return _store.DeleteRecoveryStateAsync(correlationId, registrationId, cancellationToken);
 54    }
 55
 56    /// <inheritdoc />
 57    public async IAsyncEnumerable<RecoveryState> ScanAsync([EnumeratorCancellation] CancellationToken cancellationToken 
 58    {
 459        await foreach (var json in _store.ScanRecoveryStateJsonAsync(cancellationToken).ConfigureAwait(false))
 60        {
 161            var ignored = 0;
 162            var state = DeserializeState(json, correlationId: null, ref ignored);
 163            if (state is not null)
 164                yield return state;
 65        }
 166    }
 67
 68    private IReadOnlyList<RecoveryState> DeserializeStates(IReadOnlyList<string> jsonStates, string correlationId)
 69    {
 3370        if (jsonStates.Count == 0)
 1571            return [];
 72
 1873        var states = new List<RecoveryState>(jsonStates.Count);
 1874        var unreadable = 0;
 7275        foreach (var json in jsonStates)
 76        {
 1877            var state = DeserializeState(json, correlationId, ref unreadable);
 1878            if (state is not null)
 1679                states.Add(state);
 80        }
 81
 82        // Rows existed and none of them survived materialization. Returning an empty list here told
 83        // the dispatcher "no recovery callback was ever armed", which it answers by acknowledging
 84        // the response — so a corrupt or newer-schema registration silently consumed a terminal
 85        // response its callback never saw. Fail instead, and let redelivery reach a build that can
 86        // read it. A PARTIALLY readable batch deliberately does not throw: see
 87        // RecoveryStateUnreadableException.
 88        // Only rows this build could not INTERPRET count. A row rejected for belonging to another
 89        // correlation id is perfectly readable — it surfaced because a legacy case-insensitive
 90        // collation matched the wrong key, and refusing it is the ordinal re-check doing its job.
 91        // For the id actually asked about, that is absence, not corruption, and absence must stay
 92        // an empty list.
 1893        if (states.Count == 0 && unreadable > 0)
 294            throw new RecoveryStateUnreadableException(correlationId, unreadable);
 95
 1696        return states;
 97    }
 98
 99    /// <summary>The registration's metadata off the library's resolver — case-sensitive matching, as before.</summary>
 3100    private static readonly JsonTypeInfo<RecoveryState> _stateTypeInfo =
 3101        AsyncResponseJson.GetTypeInfo<RecoveryState>(AsyncResponseJson.Default);
 102
 103    private RecoveryState? DeserializeState(string json, string? correlationId, ref int unreadable)
 104    {
 105        try
 106        {
 107            // Through JsonSafety, not the raw reader: the exception logged below is the body-free
 108            // rebuild (size and position). The reader's own appends `Path: $.Context['<key>']`
 109            // built from the stored registration's context keys — tenant and auth baggage — which
 110            // the warning then carried into the application log.
 37111            var state = JsonSafety.SafeDeserialize(json, _stateTypeInfo);
 30112            if (state is null)
 113            {
 2114                unreadable++;
 2115                return null;
 116            }
 117
 28118            if (state.RegistrationId == Guid.Empty || string.IsNullOrWhiteSpace(state.CorrelationId))
 119            {
 4120                _logger.LogWarning(
 4121                    "MongoDB recovery state for correlationId {CorrelationId} has an incomplete identity; rejecting it."
 4122                    correlationId ?? state.CorrelationId);
 4123                unreadable++;
 4124                return null;
 125            }
 126
 24127            if (!RecoveryStateSchema.IsReadable(state.SchemaVersion))
 128            {
 1129                _logger.LogWarning(
 1130                    "MongoDB recovery state for correlationId {CorrelationId} has unsupported schema version {SchemaVers
 1131                    correlationId ?? state.CorrelationId,
 1132                    state.SchemaVersion,
 1133                    RecoveryStateSchema.Current);
 1134                unreadable++;
 1135                return null;
 136            }
 137
 23138            if (!string.IsNullOrWhiteSpace(correlationId)
 23139                && !string.Equals(state.CorrelationId, correlationId, StringComparison.Ordinal))
 140            {
 4141                _logger.LogWarning(
 4142                    "MongoDB recovery state has correlationId {StoredCorrelationId}, expected {CorrelationId}; rejecting
 4143                    state.CorrelationId, correlationId);
 4144                return null;
 145            }
 146
 19147            return state;
 148        }
 7149        catch (Exception ex) when (ex is JsonException or InvalidDataException)
 150        {
 7151            _logger.LogWarning(ex, "Unreadable MongoDB recovery state for correlationId {CorrelationId}; skipping.", cor
 7152            unreadable++;
 7153            return null;
 154        }
 37155    }
 156}