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

Information
Class: AsyncResponse.Transports.SubscriberSupervisor
Assembly: AsyncResponse.Transports.GooglePubSub
File(s): /_/src/Transports/Shared/SubscriberSupervisor.cs
Line coverage
87%
Covered lines: 14
Uncovered lines: 2
Coverable lines: 16
Total lines: 77
Line coverage: 87.5%
Branch coverage
80%
Covered branches: 8
Total branches: 10
Branch coverage: 80%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
RunAsync()80%101087.5%

File(s)

/_/src/Transports/Shared/SubscriberSupervisor.cs

#LineLine coverage
 1namespace AsyncResponse.Transports;
 2
 3// Shared source for every transport's outer BackgroundService.ExecuteAsync supervise-and-retry
 4// loop: each csproj pulls this file in via <Compile Include="..\Shared\SubscriberSupervisor.cs" />,
 5// so it compiles INTO each provider assembly. Per-transport setup that must run once before the
 6// loop starts (topology warnings, resolving a queue/subscription name) stays in the caller, ahead
 7// of the call into RunAsync below; only the retry-with-backoff loop shape is shared.
 8
 9/// <summary>
 10/// Runs a subscriber's connect-and-consume operation with retry-with-backoff on failure. Any
 11/// exception observed once host shutdown has been requested exits quietly — the cancellation
 12/// itself, and equally the ObjectDisposed/connection-closed errors broker clients raise when the
 13/// stop tears a connection down mid-consume; any other exception — including a cancellation NOT
 14/// caused by host shutdown, e.g. a transport-internal timeout — increments the failure count, asks
 15/// the caller-supplied delay policy how long to wait, reports the retry through the caller-supplied
 16/// callback, and waits before trying again. The count is of CONSECUTIVE failures: a run that stayed
 17/// up at least as long as the policy's saturated delay before failing starts the count over.
 18/// </summary>
 19internal static class SubscriberSupervisor
 20{
 21    /// <summary>
 22    /// Runs <paramref name="run"/> until it completes or <paramref name="stoppingToken"/> requests
 23    /// shutdown. <paramref name="delayPolicy"/> receives the 1-based consecutive-failure count and
 24    /// returns how long to wait before the next attempt; <paramref name="logRetry"/> renders the
 25    /// per-transport log line for that wait. The policy must saturate: it is also asked for
 26    /// <see cref="int.MaxValue"/> failures, and that answer — its longest delay — is the healthy-run
 27    /// threshold past which a failed run no longer counts as consecutive with the one before it.
 28    /// <paramref name="timeProvider"/> clocks both the run and the wait (a test seam; the system
 29    /// clock when omitted).
 30    /// </summary>
 31    public static async Task RunAsync(
 32        Func<CancellationToken, Task> run,
 33        CancellationToken stoppingToken,
 34        Func<int, TimeSpan> delayPolicy,
 35        Action<Exception, TimeSpan> logRetry,
 36        TimeProvider? timeProvider = null)
 37    {
 41438        var clock = timeProvider ?? TimeProvider.System;
 41439        var failures = 0;
 41440        TimeSpan? healthyRun = null;
 46541        while (!stoppingToken.IsCancellationRequested)
 42        {
 46543            var startedAt = clock.GetTimestamp();
 44            try
 45            {
 46546                await run(stoppingToken).ConfigureAwait(false);
 41247                return;
 48            }
 5349            catch (Exception) when (stoppingToken.IsCancellationRequested)
 50            {
 51                // Shutdown. Not only the OperationCanceledException: a client whose connection the
 52                // stop just closed throws its own ObjectDisposed/AlreadyClosed/connection error
 53                // instead, and with no arm for that shape it escaped ExecuteAsync — faulting the
 54                // BackgroundService (a critical "BackgroundService failed" log, and StopApplication
 55                // under the default behavior) on every clean redeploy.
 056                return;
 57            }
 58            catch (Exception ex)
 59            {
 60                // Consecutive, not lifetime: a subscriber runs for weeks, and with a count that only
 61                // ever grew, a handful of unrelated blips spread over that time pinned EVERY later
 62                // reconnect at the policy's longest delay — a one-second broker failover then cost
 63                // the full maximum on every replica, for the rest of the process's life. A run that
 64                // outlived the longest delay the policy can impose was healthy, so this failure
 65                // starts a new streak. (A run that merely took that long to fail — a black-holed
 66                // connect — resets too, harmlessly: its own duration already paces the retries.)
 5367                if (failures > 0 && clock.GetElapsedTime(startedAt) >= (healthyRun ??= delayPolicy(int.MaxValue)))
 068                    failures = 0;
 69
 5370                failures++;
 5371                var retryDelay = delayPolicy(failures);
 5372                logRetry(ex, retryDelay);
 5373                await Task.Delay(retryDelay, clock, stoppingToken).ConfigureAwait(false);
 74            }
 75        }
 41276    }
 77}

Methods/Properties

RunAsync()