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

Information
Class: AsyncResponse.Transports.AzureServiceBus.AzureServiceBusWorkerTransport
Assembly: AsyncResponse.Transports.AzureServiceBus
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/Transports/AsyncResponse.Transports.AzureServiceBus/AzureServiceBusWorkerTransport.cs
Line coverage
100%
Covered lines: 83
Uncovered lines: 0
Coverable lines: 83
Total lines: 179
Line coverage: 100%
Branch coverage
100%
Covered branches: 28
Total branches: 28
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.ctor(...)100%11100%
.ctor(...)100%11100%
GetSenderAsync()100%44100%
PublishAsync()100%1212100%
SendWithRetryAsync()100%44100%
IsTransient(...)100%22100%
RetryDelay(...)100%11100%
DisposeAsync()100%66100%

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/Transports/AsyncResponse.Transports.AzureServiceBus/AzureServiceBusWorkerTransport.cs

#LineLine coverage
 1using Microsoft.Extensions.Options;
 2using Azure.Messaging.ServiceBus;
 3using System.Diagnostics;
 4using System.Text.Json;
 5
 6namespace AsyncResponse.Transports.AzureServiceBus;
 7
 8/// <summary>Publishes <see cref="WorkerJobEnvelope"/> messages to an Azure Service Bus queue.</summary>
 9/// <remarks>
 10/// A Service Bus sender is created lazily and reused for the lifetime of the transport. Send
 11/// operations are explicitly awaited so the method completes only after Service Bus accepts the
 12/// transfer or the Azure SDK reports a failure.
 13/// </remarks>
 14public sealed class AzureServiceBusWorkerTransport : IWorkerTransport, IAsyncDisposable
 15{
 16    private readonly AzureServiceBusAsyncResponseOptions _options;
 17    private readonly IAzureServiceBusClient _client;
 18    private readonly bool _disposeClient;
 319    private readonly SemaphoreSlim _senderGate = new(1, 1);
 20    private IAzureServiceBusSender? _sender;
 21    private int _disposeGate;
 22    private bool _disposed;
 23
 24    /// <summary>Creates a worker transport backed by the configured connection string.</summary>
 25    public AzureServiceBusWorkerTransport(IOptions<AzureServiceBusAsyncResponseOptions> options)
 226        : this(
 227            options,
 228            new AzureServiceBusClientAdapter(new ServiceBusClient(
 229                AzureServiceBusOptionsValidator.Required(options.Value.ConnectionString, nameof(options.Value.Connection
 230                ownsClient: true),
 231            disposeClient: true)
 32    {
 333    }
 34
 35    internal AzureServiceBusWorkerTransport(
 36        IOptions<AzureServiceBusAsyncResponseOptions> options,
 37        IAzureServiceBusClient client)
 338        : this(options, client, disposeClient: false)
 39    {
 340    }
 41
 342    private AzureServiceBusWorkerTransport(
 343        IOptions<AzureServiceBusAsyncResponseOptions> options,
 344        IAzureServiceBusClient client,
 345        bool disposeClient)
 46    {
 347        _options = options.Value;
 348        AzureServiceBusOptionsValidator.ValidateCommon(_options);
 349        _client = client;
 350        _disposeClient = disposeClient;
 351    }
 52
 53    private async Task<IAzureServiceBusSender> GetSenderAsync(CancellationToken cancellationToken)
 54    {
 355        var sender = Volatile.Read(ref _sender);
 356        if (sender is not null)
 357            return sender;
 58
 359        await _senderGate.WaitAsync(cancellationToken).ConfigureAwait(false);
 60        try
 61        {
 362            ObjectDisposedException.ThrowIf(_disposed, this);
 363            _sender ??= _client.CreateSender(_options.WorkerQueue);
 364            return _sender;
 65        }
 66        finally
 67        {
 368            _senderGate.Release();
 69        }
 370    }
 71
 72    /// <summary>Publishes the supplied worker job.</summary>
 73    public async Task PublishAsync(WorkerJobEnvelope job, CancellationToken cancellationToken = default)
 74    {
 375        ArgumentNullException.ThrowIfNull(job);
 76
 377        using var activity = AsyncResponseDiagnostics.StartActivity(
 378            "asyncresponse.worker.publish",
 379            ActivityKind.Producer,
 380            job.CorrelationId);
 381        activity?.SetTag("asyncresponse.transport", "azure_service_bus");
 382        activity?.SetTag("messaging.system", "azure_service_bus");
 383        activity?.SetTag("messaging.destination.name", _options.WorkerQueue);
 384        AsyncResponseDiagnostics.SetReplyTarget(activity, job.ReplyTarget);
 385        AsyncResponseDiagnostics.SetWorker(activity, job.Call);
 86
 87        try
 88        {
 89            // Every message carries a fresh MessageId: MessageId is the duplicate-detection key, so
 90            // reusing the correlation id would silently drop the second job of a flow published on a
 91            // dedup-enabled queue inside its detection window (distinct jobs of one flow share the
 92            // correlation id). The correlation id still travels in the Service Bus CorrelationId
 93            // system property and the configured application property.
 394            var messageId = Guid.NewGuid().ToString("N");
 395            var properties = new Dictionary<string, object?>(StringComparer.Ordinal);
 396            if (!string.IsNullOrWhiteSpace(job.CorrelationId))
 397                properties[_options.CorrelationIdProperty] = job.CorrelationId;
 398            var message = new AzureServiceBusOutboundMessage(
 399                AsyncResponseJson.Serialize(job),
 3100                messageId,
 3101                string.IsNullOrWhiteSpace(job.CorrelationId) ? null : job.CorrelationId,
 3102                properties);
 103
 3104            var sender = await GetSenderAsync(cancellationToken).ConfigureAwait(false);
 3105            await SendWithRetryAsync(sender, message, cancellationToken).ConfigureAwait(false);
 3106            activity?.SetTag("messaging.message.id", messageId);
 3107        }
 2108        catch (Exception ex)
 109        {
 2110            AsyncResponseDiagnostics.SetError(activity, ex);
 3111            throw;
 112        }
 3113    }
 114
 115    private async Task SendWithRetryAsync(
 116        IAzureServiceBusSender sender,
 117        AzureServiceBusOutboundMessage message,
 118        CancellationToken cancellationToken)
 119    {
 3120        for (var attempt = 1; ; attempt++)
 121        {
 122            try
 123            {
 3124                await sender.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
 3125                return;
 126            }
 2127            catch (Exception ex) when (IsTransient(ex) && attempt < _options.PublishMaxAttempts && !cancellationToken.Is
 128            {
 2129                var delay = RetryDelay(attempt);
 2130                await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
 131            }
 132        }
 3133    }
 134
 135    /// <summary>Runs the IsTransient operation.</summary>
 136    internal static bool IsTransient(Exception exception)
 2137        => exception is ServiceBusException { IsTransient: true };
 138
 139    private TimeSpan RetryDelay(int failedAttempt)
 140    {
 2141        var milliseconds = _options.PublishRetryBaseDelay.TotalMilliseconds * Math.Pow(2, failedAttempt - 1);
 3142        return TimeSpan.FromMilliseconds(Math.Min(milliseconds, _options.PublishRetryMaxDelay.TotalMilliseconds));
 143    }
 144
 145    /// <summary>Releases resources held by this instance.</summary>
 146    public async ValueTask DisposeAsync()
 147    {
 3148        if (Interlocked.Exchange(ref _disposeGate, 1) != 0)
 3149            return;
 150
 3151        await _senderGate.WaitAsync().ConfigureAwait(false);
 152        try
 153        {
 3154            _disposed = true;
 3155            if (_sender is not null)
 156            {
 3157                using var cts = new CancellationTokenSource(_options.ShutdownTimeout);
 158                try
 159                {
 3160                    await _sender.CloseAsync(cts.Token).ConfigureAwait(false);
 3161                }
 3162                catch
 163                {
 164                    // Best effort: the sender link may already be closed by broker-side shutdown.
 3165                }
 166
 3167                await _sender.DisposeAsync().ConfigureAwait(false);
 3168            }
 169
 3170            if (_disposeClient)
 2171                await _client.DisposeAsync().ConfigureAwait(false);
 3172        }
 173        finally
 174        {
 3175            _senderGate.Release();
 3176            _senderGate.Dispose();
 177        }
 3178    }
 179}