< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.WebApi.Retention.ScheduledPassService
Assembly: ArturRios.Heimdall.WebApi
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Retention/ScheduledPassService.cs
Line coverage
54%
Covered lines: 12
Uncovered lines: 10
Coverable lines: 22
Total lines: 93
Line coverage: 54.5%
Branch coverage
25%
Covered branches: 1
Total branches: 4
Branch coverage: 25%
Method coverage
75%
Covered methods: 3
Fully covered methods: 2
Total methods: 4
Method coverage: 75%
Full method coverage: 50%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ExecuteAsync()50%2280%
SafeWaitForNextTickAsync()100%11100%
RunOnceAsync()0%620%

File(s)

/home/runner/work/heimdall-api/heimdall-api/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 is logged and the loop continues. These are background tasks, not part of
 17///         serving a request — a database blip must not take the service down, and the next tick
 18///         retries whatever was missed, because a row past its retention period stays past it.
 19///     </para>
 20///     <para>
 21///         Every instance runs its own copy of every pass, which is what NFR-06 asks for: an
 22///         instance that assumed another was doing the work would stop doing it the moment it ran
 23///         alone. Each pass documents why overlapping runs need no coordination.
 24///     </para>
 25/// </remarks>
 359626public abstract class ScheduledPassService(
 359627    IServiceScopeFactory scopeFactory,
 359628    TimeSpan interval,
 359629    ILogger logger) : BackgroundService
 30{
 31    /// <summary>What the service writes at start-up to say what it will do and how often.</summary>
 32    protected abstract string StartupDescription { get; }
 33
 34    /// <summary>
 35    ///     Dispatches the pass and returns what it did, for the log — or <c>null</c> when there is
 36    ///     nothing worth saying, so an hourly no-op does not bury the runs that matter.
 37    /// </summary>
 38    /// <param name="scope">
 39    ///     A fresh dependency-injection scope. The pass resolves what it needs from it — most
 40    ///     resolve <see cref="CommandMediator" />, the security detector a <c>QueryMediator</c>,
 41    ///     because it reads rather than writes.
 42    /// </param>
 43    protected abstract Task<string?> RunAsync(IServiceProvider scope);
 44
 45    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 46    {
 359647        logger.LogInformation("{Description}", StartupDescription);
 48
 359649        using var timer = new PeriodicTimer(interval);
 50
 359651        while (await SafeWaitForNextTickAsync(timer, stoppingToken))
 52        {
 053            await RunOnceAsync();
 54        }
 359655    }
 56
 57    /// <summary>
 58    ///     Waits for the next tick, reporting shutdown as a stop rather than letting the
 59    ///     cancellation surface as an exception the host logs as a fault.
 60    /// </summary>
 61    private static async Task<bool> SafeWaitForNextTickAsync(PeriodicTimer timer, CancellationToken stoppingToken)
 62    {
 63        try
 64        {
 359665            return await timer.WaitForNextTickAsync(stoppingToken);
 66        }
 359667        catch (OperationCanceledException)
 68        {
 359669            return false;
 70        }
 359671    }
 72
 73    private async Task RunOnceAsync()
 74    {
 75        try
 76        {
 77            // The mediator is scoped, and so is the DbContext behind it, so the run needs a scope of
 78            // its own — this service is a singleton and has none.
 079            using var scope = scopeFactory.CreateScope();
 80
 081            var summary = await RunAsync(scope.ServiceProvider);
 82
 083            if (summary is not null)
 84            {
 085                logger.LogInformation("{Summary}", summary);
 86            }
 087        }
 088        catch (Exception exception)
 89        {
 090            logger.LogError(exception, "A scheduled pass threw");
 091        }
 092    }
 93}