< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.WebApi.Retention.ScheduledPassService
Assembly: ArturRios.Heimdall.WebApi
File(s): /home/ci/agent/workspace/heimdall-api-docs_main/app/src/Presentation/ArturRios.Heimdall.WebApi/Retention/ScheduledPassService.cs
Line coverage
100%
Covered lines: 27
Uncovered lines: 0
Coverable lines: 27
Total lines: 119
Line coverage: 100%
Branch coverage
100%
Covered branches: 6
Total branches: 6
Branch coverage: 100%
Method coverage
100%
Covered methods: 4
Fully covered methods: 4
Total methods: 4
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ExecuteAsync()100%22100%
SafeWaitForNextTickAsync()100%11100%
RunOnceAsync()100%44100%

File(s)

/home/ci/agent/workspace/heimdall-api-docs_main/app/src/Presentation/ArturRios.Heimdall.WebApi/Retention/ScheduledPassService.cs

#LineLine coverage
 1using ArturRios.Mediator.Command;
 2
 3namespace ArturRios.Heimdall.WebApi.Retention;
 4
 5/// <summary>
 6///     Runs one scheduled pass on an interval. Subclasses supply the pass; everything about
 7///     <em>when</em> it runs, and what happens when it fails, lives here.
 8/// </summary>
 9/// <remarks>
 10///     <para>
 11///         The first tick is one whole interval after start-up, which is deliberate: it keeps the
 12///         passes clear of migrations and seeding, and it means a container that restarts repeatedly
 13///         never turns start-up into write pressure on the tables the login path uses.
 14///     </para>
 15///     <para>
 16///         A failed run — one that returns a failure or throws — is logged at Error with
 17///         <see cref="FailureMarker" /> and the loop continues. These are background tasks, not part of
 18///         serving a request — a database blip must not take the service down, and the next tick
 19///         retries whatever was missed, because a row past its retention period stays past it.
 20///     </para>
 21///     <para>
 22///         Every instance runs its own copy of every pass, which is what NFR-06 asks for: an
 23///         instance that assumed another was doing the work would stop doing it the moment it ran
 24///         alone. Each pass documents why overlapping runs need no coordination.
 25///     </para>
 26/// </remarks>
 387927public abstract class ScheduledPassService(
 387928    IServiceScopeFactory scopeFactory,
 387929    TimeSpan interval,
 387930    ILogger logger) : BackgroundService
 31{
 32    /// <summary>
 33    ///     The marker a failed pass's log line carries, at Error level. Stable and greppable: an alert
 34    ///     should match on it rather than on the prose, which is written for a human and will change.
 35    /// </summary>
 36    public const string FailureMarker = "SCHEDULED_PASS_FAILED";
 37
 38    /// <summary>What the service writes at start-up to say what it will do and how often.</summary>
 39    protected abstract string StartupDescription { get; }
 40
 41    /// <summary>
 42    ///     The pass's stable name: the <c>pass</c> label on its metrics and the name beside the failure
 43    ///     marker. Lower-case and hyphenated, and not to be changed once an alert depends on it.
 44    /// </summary>
 45    public abstract string PassName { get; }
 46
 47    /// <summary>
 48    ///     Dispatches the pass and returns what it did — a summary for the log (or <c>null</c> when
 49    ///     there is nothing worth saying, so an hourly no-op does not bury the runs that matter), whether
 50    ///     it failed, and how many rows it processed.
 51    /// </summary>
 52    /// <param name="scope">
 53    ///     A fresh dependency-injection scope. The pass resolves what it needs from it — most
 54    ///     resolve <see cref="CommandMediator" />, the security detector a <c>QueryMediator</c>,
 55    ///     because it reads rather than writes.
 56    /// </param>
 57    protected abstract Task<ScheduledPassResult> RunAsync(IServiceProvider scope);
 58
 59    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 60    {
 386961        logger.LogInformation("{Description}", StartupDescription);
 62
 386963        using var timer = new PeriodicTimer(interval);
 64
 388265        while (await SafeWaitForNextTickAsync(timer, stoppingToken))
 66        {
 1367            await RunOnceAsync();
 68        }
 386969    }
 70
 71    /// <summary>
 72    ///     Waits for the next tick, reporting shutdown as a stop rather than letting the
 73    ///     cancellation surface as an exception the host logs as a fault.
 74    /// </summary>
 75    private static async Task<bool> SafeWaitForNextTickAsync(PeriodicTimer timer, CancellationToken stoppingToken)
 76    {
 77        try
 78        {
 388279            return await timer.WaitForNextTickAsync(stoppingToken);
 80        }
 386981        catch (OperationCanceledException)
 82        {
 386983            return false;
 84        }
 388285    }
 86
 87    private async Task RunOnceAsync()
 88    {
 89        try
 90        {
 91            // The mediator is scoped, and so is the DbContext behind it, so the run needs a scope of
 92            // its own — this service is a singleton and has none.
 1393            using var scope = scopeFactory.CreateScope();
 94
 1395            var result = await RunAsync(scope.ServiceProvider);
 96
 1297            ScheduledPassMetrics.RowsProcessed(PassName, result.RowsProcessed);
 98
 1299            if (result.Failed)
 100            {
 101                // Error, not Information: an alert on Warning or above has to fire when a retention
 102                // obligation (NFR-19..21) stops being met, and the metric below deliberately stays put.
 3103                logger.LogError("{Marker} {Pass}: {Summary}", FailureMarker, PassName, result.Summary);
 3104                return;
 105            }
 106
 9107            ScheduledPassMetrics.Succeeded(PassName);
 108
 9109            if (result.Summary is not null)
 110            {
 6111                logger.LogInformation("{Summary}", result.Summary);
 112            }
 9113        }
 1114        catch (Exception exception)
 115        {
 1116            logger.LogError(exception, "{Marker} {Pass}: the pass threw", FailureMarker, PassName);
 1117        }
 13118    }
 119}