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

Information
Class: AsyncResponse.InMemoryFlowStateStore
Assembly: AsyncResponse.Core
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/InMemoryFlowStateStore.cs
Line coverage
100%
Covered lines: 99
Uncovered lines: 0
Coverable lines: 99
Total lines: 204
Line coverage: 100%
Branch coverage
96%
Covered branches: 73
Total branches: 76
Branch coverage: 96%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
TryCreateAsync(...)100%1010100%
LoadAsync(...)100%1212100%
TryUpdateAsync(...)95%2020100%
TryAcquireLeaseAsync(...)100%11100%
TryRenewLeaseAsync(...)100%11100%
ReleaseLeaseAsync(...)100%66100%
TryDeleteAsync(...)100%11100%
TryChangeLeaseAsync(...)90.91%2222100%
CreateEntry(...)100%11100%
ValidateWrite(...)100%66100%
.ctor(...)100%11100%

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/InMemoryFlowStateStore.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2
 3namespace AsyncResponse;
 4
 5/// <summary>Atomic process-local flow-state store for development, tests, and single-process apps.</summary>
 6internal sealed class InMemoryFlowStateStore : IFlowStateStore
 7{
 38    private readonly ConcurrentDictionary<string, Entry> _entries = new(StringComparer.Ordinal);
 9
 10    public Task<bool> TryCreateAsync(
 11        string flowId,
 12        FlowState state,
 13        TimeSpan ttl,
 14        CancellationToken cancellationToken = default)
 15    {
 316        ValidateWrite(flowId, state, ttl);
 317        cancellationToken.ThrowIfCancellationRequested();
 318        if (state.Revision != 0)
 219            throw new ArgumentException("A new flow ledger must start at revision zero.", nameof(state));
 20
 321        var now = DateTime.UtcNow;
 322        var created = CreateEntry(state, now.Add(ttl));
 23        while (true)
 24        {
 325            if (_entries.TryAdd(flowId, created))
 326                return Task.FromResult(true);
 27
 328            if (!_entries.TryGetValue(flowId, out var existing))
 29                continue;
 30
 331            if (existing.ExpiresAtUtc > now)
 332                return Task.FromResult(false);
 33
 234            if (_entries.TryUpdate(flowId, created, existing))
 235                return Task.FromResult(true);
 36        }
 37    }
 38
 39    public Task<FlowState?> LoadAsync(string flowId, CancellationToken cancellationToken = default)
 40    {
 341        ArgumentException.ThrowIfNullOrWhiteSpace(flowId);
 342        cancellationToken.ThrowIfCancellationRequested();
 43
 344        while (_entries.TryGetValue(flowId, out var entry))
 45        {
 346            if (entry.ExpiresAtUtc <= DateTime.UtcNow)
 47            {
 248                _entries.TryRemove(KeyValuePair.Create(flowId, entry));
 249                continue;
 50            }
 51
 352            var state = FlowStateJson.Deserialize(entry.StateJson);
 353            return Task.FromResult(
 354                state is not null
 355                && FlowStateSchema.IsReadable(state.SchemaVersion)
 356                && state.Revision == entry.Revision
 357                && string.Equals(state.FlowId, flowId, StringComparison.Ordinal)
 358                    ? state
 359                    : null);
 60        }
 61
 362        return Task.FromResult<FlowState?>(null);
 63    }
 64
 65    public Task<bool> TryUpdateAsync(
 66        string flowId,
 67        FlowState state,
 68        long expectedRevision,
 69        TimeSpan ttl,
 70        string? leaseId = null,
 71        CancellationToken cancellationToken = default)
 72    {
 373        ValidateWrite(flowId, state, ttl);
 374        cancellationToken.ThrowIfCancellationRequested();
 375        if (expectedRevision < 0)
 276            throw new ArgumentOutOfRangeException(nameof(expectedRevision), "Expected revision cannot be negative.");
 377        if (state.Revision != checked(expectedRevision + 1))
 278            throw new ArgumentException("The new flow-state revision must increment the expected revision by one.", name
 79
 380        while (_entries.TryGetValue(flowId, out var current))
 81        {
 382            var now = DateTime.UtcNow;
 383            if (current.ExpiresAtUtc <= now || current.Revision != expectedRevision)
 284                return Task.FromResult(false);
 385            if (leaseId is not null
 386                && (!string.Equals(current.LeaseId, leaseId, StringComparison.Ordinal)
 387                    || current.LeaseExpiresAtUtc <= now))
 288                return Task.FromResult(false);
 89
 390            var updated = CreateEntry(
 391                state,
 392                now.Add(ttl),
 393                current.LeaseId,
 394                current.LeaseExpiresAtUtc);
 395            if (_entries.TryUpdate(flowId, updated, current))
 396                return Task.FromResult(true);
 97        }
 98
 299        return Task.FromResult(false);
 100    }
 101
 102    public Task<bool> TryAcquireLeaseAsync(
 103        string flowId,
 104        string leaseId,
 105        TimeSpan leaseDuration,
 106        CancellationToken cancellationToken = default)
 3107        => TryChangeLeaseAsync(flowId, leaseId, leaseDuration, acquire: true, cancellationToken);
 108
 109    public Task<bool> TryRenewLeaseAsync(
 110        string flowId,
 111        string leaseId,
 112        TimeSpan leaseDuration,
 113        CancellationToken cancellationToken = default)
 2114        => TryChangeLeaseAsync(flowId, leaseId, leaseDuration, acquire: false, cancellationToken);
 115
 116    public Task ReleaseLeaseAsync(
 117        string flowId,
 118        string leaseId,
 119        CancellationToken cancellationToken = default)
 120    {
 3121        ArgumentException.ThrowIfNullOrWhiteSpace(flowId);
 3122        ArgumentException.ThrowIfNullOrWhiteSpace(leaseId);
 3123        cancellationToken.ThrowIfCancellationRequested();
 124
 3125        while (_entries.TryGetValue(flowId, out var current))
 126        {
 3127            if (!string.Equals(current.LeaseId, leaseId, StringComparison.Ordinal))
 128                break;
 129
 3130            if (_entries.TryUpdate(flowId, current with { LeaseId = null, LeaseExpiresAtUtc = null }, current))
 131                break;
 132        }
 133
 3134        return Task.CompletedTask;
 135    }
 136
 137    public Task<bool> TryDeleteAsync(string flowId, CancellationToken cancellationToken = default)
 138    {
 2139        ArgumentException.ThrowIfNullOrWhiteSpace(flowId);
 2140        cancellationToken.ThrowIfCancellationRequested();
 2141        return Task.FromResult(_entries.TryRemove(flowId, out _));
 142    }
 143
 144    private Task<bool> TryChangeLeaseAsync(
 145        string flowId,
 146        string leaseId,
 147        TimeSpan leaseDuration,
 148        bool acquire,
 149        CancellationToken cancellationToken)
 150    {
 3151        ArgumentException.ThrowIfNullOrWhiteSpace(flowId);
 3152        ArgumentException.ThrowIfNullOrWhiteSpace(leaseId);
 3153        if (leaseDuration <= TimeSpan.Zero)
 2154            throw new ArgumentOutOfRangeException(nameof(leaseDuration), "Lease duration must be greater than zero.");
 3155        cancellationToken.ThrowIfCancellationRequested();
 156
 3157        while (_entries.TryGetValue(flowId, out var current))
 158        {
 3159            var now = DateTime.UtcNow;
 3160            if (current.ExpiresAtUtc <= now)
 2161                return Task.FromResult(false);
 162
 3163            var ownsLease = string.Equals(current.LeaseId, leaseId, StringComparison.Ordinal);
 3164            if (acquire ? current.LeaseId is not null && current.LeaseExpiresAtUtc > now && !ownsLease : !ownsLease || c
 2165                return Task.FromResult(false);
 166
 3167            var updated = current with
 3168            {
 3169                LeaseId = leaseId,
 3170                LeaseExpiresAtUtc = now.Add(leaseDuration)
 3171            };
 3172            if (_entries.TryUpdate(flowId, updated, current))
 3173                return Task.FromResult(true);
 174        }
 175
 2176        return Task.FromResult(false);
 177    }
 178
 179    private static Entry CreateEntry(
 180        FlowState state,
 181        DateTime expiresAtUtc,
 182        string? leaseId = null,
 183        DateTime? leaseExpiresAtUtc = null)
 3184        => new(FlowStateJson.Serialize(state), state.Revision, expiresAtUtc, leaseId, leaseExpiresAtUtc);
 185
 186    private static void ValidateWrite(string flowId, FlowState state, TimeSpan ttl)
 187    {
 3188        ArgumentException.ThrowIfNullOrWhiteSpace(flowId);
 3189        ArgumentNullException.ThrowIfNull(state);
 3190        if (!string.Equals(state.FlowId, flowId, StringComparison.Ordinal))
 2191            throw new ArgumentException("The flow state id must match the store key.", nameof(state));
 3192        if (state.SchemaVersion != FlowStateSchema.Current)
 2193            throw new ArgumentException("The flow state must use the current schema version.", nameof(state));
 3194        if (ttl <= TimeSpan.Zero)
 2195            throw new ArgumentOutOfRangeException(nameof(ttl), "TTL must be greater than zero.");
 3196    }
 197
 3198    private sealed record Entry(
 3199        string StateJson,
 3200        long Revision,
 3201        DateTime ExpiresAtUtc,
 3202        string? LeaseId = null,
 3203        DateTime? LeaseExpiresAtUtc = null);
 204}