< Summary

Information
Class: AsiBackbone.AspNetCore.Outbox.AsiBackboneGovernanceOutboxDrainHostedService
Assembly: AsiBackbone.AspNetCore
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.AspNetCore/Outbox/AsiBackboneGovernanceOutboxDrainHostedService.cs
Line coverage
82%
Covered lines: 65
Uncovered lines: 14
Coverable lines: 79
Total lines: 145
Line coverage: 82.2%
Branch coverage
77%
Covered branches: 14
Total branches: 18
Branch coverage: 77.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%66100%
.cctor()100%11100%
StopAsync()100%8660%
ExecuteAsync()83.33%8665%
DrainOnceAsync()100%11100%
DelayAsync()100%1180%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.AspNetCore/Outbox/AsiBackboneGovernanceOutboxDrainHostedService.cs

#LineLine coverage
 1using AsiBackbone.Core.Outbox;
 2using Microsoft.Extensions.DependencyInjection;
 3using Microsoft.Extensions.Hosting;
 4using Microsoft.Extensions.Logging;
 5using Microsoft.Extensions.Options;
 6
 7namespace AsiBackbone.AspNetCore.Outbox;
 8
 9/// <summary>
 10/// Runs the provider-neutral governance outbox drain from an ASP.NET Core or generic-host background worker.
 11/// </summary>
 12/// <remarks>
 13/// Hosting remains outside Core. The worker resolves the drain through a scoped service provider so durable providers t
 14/// </remarks>
 1015public sealed class AsiBackboneGovernanceOutboxDrainHostedService(
 1016    IServiceScopeFactory scopeFactory,
 1017    IOptionsMonitor<AsiBackboneGovernanceOutboxDrainWorkerOptions> optionsMonitor,
 1018    ILogger<AsiBackboneGovernanceOutboxDrainHostedService> logger) : BackgroundService
 19{
 220    private static readonly Action<ILogger, Exception?> LogShutdownDrainCanceled = LoggerMessage.Define(
 221        LogLevel.Debug,
 222        new EventId(19801, nameof(LogShutdownDrainCanceled)),
 223        "Governance outbox shutdown drain was canceled.");
 24
 225    private static readonly Action<ILogger, Exception?> LogShutdownDrainFailed = LoggerMessage.Define(
 226        LogLevel.Warning,
 227        new EventId(19802, nameof(LogShutdownDrainFailed)),
 228        "Governance outbox shutdown drain failed.");
 29
 230    private static readonly Action<ILogger, Exception?> LogWorkerDisabled = LoggerMessage.Define(
 231        LogLevel.Debug,
 232        new EventId(19803, nameof(LogWorkerDisabled)),
 233        "Governance outbox drain worker is disabled.");
 34
 235    private static readonly Action<ILogger, int, Exception?> LogDrainAttempted = LoggerMessage.Define<int>(
 236        LogLevel.Debug,
 237        new EventId(19804, nameof(LogDrainAttempted)),
 238        "Governance outbox drain attempted {DrainedCount} entries.");
 39
 240    private static readonly Action<ILogger, Exception?> LogWorkerFailed = LoggerMessage.Define(
 241        LogLevel.Warning,
 242        new EventId(19805, nameof(LogWorkerFailed)),
 243        "Governance outbox drain worker failed before the next polling interval.");
 44
 1045    private readonly IServiceScopeFactory scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFac
 1046    private readonly IOptionsMonitor<AsiBackboneGovernanceOutboxDrainWorkerOptions> optionsMonitor = optionsMonitor ?? t
 1047    private readonly ILogger<AsiBackboneGovernanceOutboxDrainHostedService> logger = logger ?? throw new ArgumentNullExc
 48
 49    /// <inheritdoc />
 50    public override async Task StopAsync(CancellationToken cancellationToken)
 51    {
 852        await base.StopAsync(cancellationToken).ConfigureAwait(false);
 53
 854        AsiBackboneGovernanceOutboxDrainWorkerOptions options = optionsMonitor.CurrentValue;
 55
 856        if (options.Enabled && options.DrainOnShutdown && !cancellationToken.IsCancellationRequested)
 57        {
 258            using var shutdownDrainCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
 259            shutdownDrainCancellation.CancelAfter(options.ShutdownDrainTimeout);
 60
 61            try
 62            {
 263                _ = await DrainOnceAsync(options, shutdownDrainCancellation.Token).ConfigureAwait(false);
 264            }
 065            catch (OperationCanceledException) when (shutdownDrainCancellation.IsCancellationRequested)
 66            {
 067                LogShutdownDrainCanceled(logger, null);
 068            }
 069            catch (Exception ex)
 70            {
 071                LogShutdownDrainFailed(logger, ex);
 072            }
 273        }
 874    }
 75
 76    /// <inheritdoc />
 77    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 78    {
 879        AsiBackboneGovernanceOutboxDrainWorkerOptions startupOptions = optionsMonitor.CurrentValue;
 80
 881        if (!startupOptions.Enabled)
 82        {
 283            LogWorkerDisabled(logger, null);
 284            return;
 85        }
 86
 1287        while (!stoppingToken.IsCancellationRequested)
 88        {
 689            AsiBackboneGovernanceOutboxDrainWorkerOptions options = optionsMonitor.CurrentValue;
 90
 691            if (!options.Enabled)
 92            {
 093                await DelayAsync(options.PollingInterval, stoppingToken).ConfigureAwait(false);
 094                continue;
 95            }
 96
 97            try
 98            {
 699                int drainedCount = await DrainOnceAsync(options, stoppingToken).ConfigureAwait(false);
 6100                LogDrainAttempted(logger, drainedCount, null);
 6101                await DelayAsync(options.PollingInterval, stoppingToken).ConfigureAwait(false);
 6102            }
 0103            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
 104            {
 0105                break;
 106            }
 0107            catch (Exception ex)
 108            {
 0109                LogWorkerFailed(logger, ex);
 0110                await DelayAsync(options.FailureDelay, stoppingToken).ConfigureAwait(false);
 111            }
 6112        }
 8113    }
 114
 115    private async ValueTask<int> DrainOnceAsync(
 116        AsiBackboneGovernanceOutboxDrainWorkerOptions options,
 117        CancellationToken cancellationToken)
 118    {
 8119        options.Validate();
 8120        cancellationToken.ThrowIfCancellationRequested();
 121
 8122        using IServiceScope scope = scopeFactory.CreateScope();
 8123        AsiBackboneGovernanceOutboxDrain drain = scope.ServiceProvider.GetRequiredService<AsiBackboneGovernanceOutboxDra
 8124        DateTimeOffset retryUtc = options.RetryClock().ToUniversalTime();
 8125        IReadOnlyList<GovernanceOutboxEntry> drainedEntries = await drain.DrainAsync(
 8126            retryUtc,
 8127            options.BatchSize,
 8128            cancellationToken)
 8129            .ConfigureAwait(false);
 130
 8131        return drainedEntries.Count;
 8132    }
 133
 134    private static async ValueTask DelayAsync(TimeSpan delay, CancellationToken cancellationToken)
 135    {
 136        try
 137        {
 6138            await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
 0139        }
 6140        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 141        {
 142            // Host shutdown cancels the delay. The outer loop handles termination.
 6143        }
 6144    }
 145}