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

Information
Class: AsyncResponse.Transports.RabbitMQ.RabbitMqWorkerTransport
Assembly: AsyncResponse.Transports.RabbitMQ
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/Transports/AsyncResponse.Transports.RabbitMQ/RabbitMqWorkerTransport.cs
Line coverage
100%
Covered lines: 79
Uncovered lines: 0
Coverable lines: 79
Total lines: 164
Line coverage: 100%
Branch coverage
72%
Covered branches: 16
Total branches: 22
Branch coverage: 72.7%
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%
ValidatePublishOptions(...)100%11100%
GetChannelAsync()83.33%66100%
PublishAsync()50%1010100%
DisposeAsync()100%66100%

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/Transports/AsyncResponse.Transports.RabbitMQ/RabbitMqWorkerTransport.cs

#LineLine coverage
 1using Microsoft.Extensions.Options;
 2using System.Diagnostics;
 3using System.Text;
 4using System.Text.Json;
 5
 6namespace AsyncResponse.Transports.RabbitMQ;
 7
 8/// <summary>
 9/// Publishes <see cref="WorkerJobEnvelope"/> messages to a RabbitMQ exchange.
 10/// </summary>
 11/// <remarks>
 12/// The publish channel is created lazily and re-created on demand: a transient broker outage when the
 13/// first job is published no longer permanently breaks the transport (a faulted connect attempt is not
 14/// cached). The channel is opened with publisher confirmations so <see cref="PublishAsync"/> only completes
 15/// once the broker has accepted the message. A single channel is shared across concurrent publishers;
 16/// RabbitMQ.Client v7 tracks each in-flight confirmation independently, so concurrent publishing is safe.
 17/// </remarks>
 18public sealed class RabbitMqWorkerTransport : IWorkerTransport, IAsyncDisposable
 19{
 20    private readonly RabbitMqAsyncResponseOptions _options;
 21    private readonly IRabbitMqConnectionFactory _connectionFactory;
 322    private readonly SemaphoreSlim _connectionGate = new(1, 1);
 23    private IRabbitMqConnection? _connection;
 24    private IRabbitMqChannel? _channel;
 25    private int _disposeGate;
 26    private bool _disposed;
 27
 28    /// <summary>Runs the RabbitMqWorkerTransport operation.</summary>
 29    public RabbitMqWorkerTransport(IOptions<RabbitMqAsyncResponseOptions> options)
 330        : this(options, new RabbitMqConnectionFactoryAdapter(options.Value))
 31    {
 332    }
 33
 334    internal RabbitMqWorkerTransport(
 335        IOptions<RabbitMqAsyncResponseOptions> options,
 336        IRabbitMqConnectionFactory connectionFactory)
 37    {
 338        _options = options.Value;
 339        ValidatePublishOptions(_options);
 340        _connectionFactory = connectionFactory;
 341    }
 42
 43    private static void ValidatePublishOptions(RabbitMqAsyncResponseOptions options)
 44    {
 345        _ = RabbitMqOptionsValidator.Required(options.WorkerExchange, nameof(options.WorkerExchange));
 346        _ = RabbitMqOptionsValidator.Required(options.WorkerQueue, nameof(options.WorkerQueue));
 347        _ = RabbitMqOptionsValidator.Required(options.WorkerRoutingKey, nameof(options.WorkerRoutingKey));
 348        RabbitMqOptionsValidator.Positive(options.ShutdownTimeout, nameof(options.ShutdownTimeout));
 349    }
 50
 51    private async Task<IRabbitMqChannel> GetChannelAsync(CancellationToken cancellationToken)
 52    {
 353        var channel = Volatile.Read(ref _channel);
 354        if (channel is not null)
 355            return channel;
 56
 357        await _connectionGate.WaitAsync(cancellationToken).ConfigureAwait(false);
 58        try
 59        {
 360            ObjectDisposedException.ThrowIf(_disposed, this);
 361            if (_channel is not null)
 162                return _channel;
 63
 64            // ??= only assigns when the await succeeds, so a failed connect leaves _connection null and
 65            // the next publish retries. A successful connection is reused even if channel/topology setup fails.
 366            _connection ??= await _connectionFactory.CreateConnectionAsync(cancellationToken).ConfigureAwait(false);
 367            var created = await _connection.CreateChannelAsync(publisherConfirmations: true, cancellationToken).Configur
 368            await RabbitMqTopology.EnsureWorkerAsync(created, _options, cancellationToken).ConfigureAwait(false);
 69
 70            // Publish the channel only once it is fully initialized; if anything above threw, _channel stays
 71            // null so a later publish recreates it instead of awaiting a permanently faulted task.
 372            _channel = created;
 373            return created;
 74        }
 75        finally
 76        {
 377            _connectionGate.Release();
 78        }
 379    }
 80
 81    /// <summary>Publishes the supplied message.</summary>
 82    public async Task PublishAsync(WorkerJobEnvelope job, CancellationToken cancellationToken = default)
 83    {
 384        ArgumentNullException.ThrowIfNull(job);
 85
 386        using var activity = AsyncResponseDiagnostics.StartActivity(
 387            "asyncresponse.worker.publish",
 388            ActivityKind.Producer,
 389            job.CorrelationId);
 390        activity?.SetTag("asyncresponse.transport", "rabbitmq");
 391        activity?.SetTag("messaging.system", "rabbitmq");
 392        activity?.SetTag("messaging.destination.name", _options.WorkerExchange);
 393        activity?.SetTag("messaging.rabbitmq.routing_key", _options.WorkerRoutingKey);
 394        AsyncResponseDiagnostics.SetReplyTarget(activity, job.ReplyTarget);
 395        AsyncResponseDiagnostics.SetWorker(activity, job.Call);
 96
 97        try
 98        {
 399            var payload = Encoding.UTF8.GetBytes(AsyncResponseJson.Serialize(job));
 3100            var properties = RabbitMqTopology.CreatePersistentJsonProperties(job.CorrelationId, _options.CorrelationIdHe
 3101            var channel = await GetChannelAsync(cancellationToken).ConfigureAwait(false);
 3102            await channel.BasicPublishAsync(
 3103                _options.WorkerExchange,
 3104                _options.WorkerRoutingKey,
 3105                properties,
 3106                payload,
 3107                cancellationToken).ConfigureAwait(false);
 3108            activity?.SetTag("messaging.message.id", properties.MessageId);
 3109        }
 2110        catch (Exception ex)
 111        {
 2112            AsyncResponseDiagnostics.SetError(activity, ex);
 3113            throw;
 114        }
 3115    }
 116
 117    /// <summary>Releases resources held by this instance.</summary>
 118    public async ValueTask DisposeAsync()
 119    {
 3120        if (Interlocked.Exchange(ref _disposeGate, 1) != 0)
 3121            return;
 122
 3123        await _connectionGate.WaitAsync().ConfigureAwait(false);
 124        try
 125        {
 3126            _disposed = true;
 127
 3128            if (_channel is not null)
 129            {
 3130                using var cts = new CancellationTokenSource(_options.ShutdownTimeout);
 131                try
 132                {
 3133                    await _channel.CloseAsync(cts.Token).ConfigureAwait(false);
 3134                }
 3135                catch
 136                {
 137                    // Best effort: the channel may already be closed by broker-side shutdown.
 3138                }
 139
 3140                await _channel.DisposeAsync().ConfigureAwait(false);
 3141            }
 142
 3143            if (_connection is not null)
 144            {
 3145                using var cts = new CancellationTokenSource(_options.ShutdownTimeout);
 146                try
 147                {
 3148                    await _connection.CloseAsync(_options.ShutdownTimeout, cts.Token).ConfigureAwait(false);
 3149                }
 3150                catch
 151                {
 152                    // Best effort.
 3153                }
 154
 3155                await _connection.DisposeAsync().ConfigureAwait(false);
 3156            }
 3157        }
 158        finally
 159        {
 3160            _connectionGate.Release();
 3161            _connectionGate.Dispose();
 162        }
 3163    }
 164}