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

Information
Class: AsyncResponse.Transports.SqlServer.SqlServerTransientFaults
Assembly: AsyncResponse.Transports.SqlServer
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/Transports/AsyncResponse.Transports.SqlServer/SqlServerWorkerTransport.cs
Line coverage
100%
Covered lines: 34
Uncovered lines: 0
Coverable lines: 34
Total lines: 140
Line coverage: 100%
Branch coverage
100%
Covered branches: 6
Total branches: 6
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
IsTransient(...)100%66100%

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/Transports/AsyncResponse.Transports.SqlServer/SqlServerWorkerTransport.cs

#LineLine coverage
 1using Microsoft.Data.SqlClient;
 2using Microsoft.Extensions.Options;
 3using System.Diagnostics;
 4using System.Text.Json;
 5
 6namespace AsyncResponse.Transports.SqlServer;
 7
 8/// <summary>Publishes <see cref="WorkerJobEnvelope"/> messages to the SQL Server worker queue.</summary>
 9public sealed class SqlServerWorkerTransport : IWorkerTransport
 10{
 11    private readonly SqlServerAsyncResponseTransportOptions _options;
 12    private readonly SqlServerTransportStore _store;
 13
 14    /// <summary>Creates a SQL Server worker transport over the configured connection string.</summary>
 15    public SqlServerWorkerTransport(IOptions<SqlServerAsyncResponseTransportOptions> options)
 16        : this(options, new SqlServerTransportStore(options))
 17    {
 18    }
 19
 20    internal SqlServerWorkerTransport(
 21        IOptions<SqlServerAsyncResponseTransportOptions> options,
 22        SqlServerTransportStore store)
 23    {
 24        _options = options.Value;
 25        SqlServerTransportOptionsValidator.ValidateCommon(_options);
 26        _store = store;
 27    }
 28
 29    /// <inheritdoc />
 30    public async Task PublishAsync(WorkerJobEnvelope job, CancellationToken cancellationToken = default)
 31    {
 32        ArgumentNullException.ThrowIfNull(job);
 33
 34        using var activity = AsyncResponseDiagnostics.StartActivity(
 35            "asyncresponse.worker.publish",
 36            ActivityKind.Producer,
 37            job.CorrelationId);
 38        activity?.SetTag("asyncresponse.transport", "sqlserver");
 39        activity?.SetTag("messaging.system", "sqlserver");
 40        activity?.SetTag("messaging.destination.name", _options.WorkerQueue);
 41        AsyncResponseDiagnostics.SetReplyTarget(activity, job.ReplyTarget);
 42        AsyncResponseDiagnostics.SetWorker(activity, job.Call);
 43
 44        try
 45        {
 46            var headers = string.IsNullOrWhiteSpace(job.CorrelationId)
 47                ? null
 48                : new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
 49                {
 50                    [_options.CorrelationIdHeader] = job.CorrelationId!
 51                };
 52
 53            var payload = AsyncResponseJson.Serialize(job);
 54            // Stable id outside the retry loop so a retried publish is idempotent rather than enqueuing
 55            // the same worker job twice.
 56            var messageId = Guid.NewGuid();
 57            await SqlServerTransportRetry.ExecuteAsync(
 58                async token =>
 59                {
 60                    await _store.PublishAsync(messageId, _options.WorkerQueue, payload, headers, token).ConfigureAwait(f
 61                    return true;
 62                },
 63                _options.PublishMaxAttempts,
 64                _options.PublishRetryBaseDelay,
 65                _options.PublishRetryMaxDelay,
 66                cancellationToken).ConfigureAwait(false);
 67        }
 68        catch (Exception ex)
 69        {
 70            AsyncResponseDiagnostics.SetError(activity, ex);
 71            throw;
 72        }
 73    }
 74}
 75
 76internal static class SqlServerTransportRetry
 77{
 78    public static Task<T> ExecuteAsync<T>(
 79        Func<CancellationToken, Task<T>> action,
 80        int maxAttempts,
 81        TimeSpan baseDelay,
 82        TimeSpan maxDelay,
 83        CancellationToken cancellationToken)
 84        => AsyncResponseRetry.ExecuteAsync(action, IsTransient, maxAttempts, baseDelay, maxDelay, cancellationToken);
 85
 86    public static bool IsTransient(Exception exception)
 87        => exception is not OperationCanceledException
 88           && (exception is SqlException sqlException && SqlServerTransientFaults.IsTransient(sqlException)
 89               || exception is TimeoutException);
 90}
 91
 92/// <summary>
 93/// Classifies SQL Server errors worth retrying. <see cref="SqlException"/> exposes no public
 94/// transient flag, so this mirrors the error numbers Microsoft's own retry guidance and the
 95/// SqlClient configurable-retry defaults treat as transient, plus severity ≥ 20 (broken connection).
 96/// </summary>
 97internal static class SqlServerTransientFaults
 98{
 399    private static readonly HashSet<int> TransientErrorNumbers =
 3100    [
 3101        -2,    // client-side command timeout
 3102        20,    // instance does not support encryption
 3103        64,    // connection lost during login
 3104        121,   // transport semaphore timeout
 3105        233,   // no process on the other end of the pipe
 3106        997,   // overlapped I/O in progress
 3107        1204,  // lock resources exhausted
 3108        1205,  // deadlock victim
 3109        1222,  // lock request timeout
 3110        4060,  // database unavailable
 3111        4221,  // readable secondary timeout
 3112        10053, // transport-level connection abort
 3113        10054, // transport-level connection reset
 3114        10060, // network unreachable / connect timeout
 3115        10928, // Azure SQL resource limit reached
 3116        10929, // Azure SQL minimum guarantee exceeded
 3117        40143, // Azure SQL connection failure
 3118        40197, // Azure SQL service processing error
 3119        40501, // Azure SQL service busy
 3120        40540, // Azure SQL service unavailable
 3121        40613, // Azure SQL database unavailable
 3122        49918, // cannot process request, not enough resources
 3123        49919, // cannot process create/update request
 3124        49920  // cannot process request, too many operations
 3125    ];
 126
 127    public static bool IsTransient(SqlException exception)
 128    {
 2129        if (exception.Class >= 20)
 2130            return true;
 131
 2132        foreach (SqlError error in exception.Errors)
 133        {
 2134            if (TransientErrorNumbers.Contains(error.Number))
 2135                return true;
 136        }
 137
 2138        return TransientErrorNumbers.Contains(exception.Number);
 2139    }
 140}