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

Information
Class: AsyncResponse.Transports.RabbitMQ.RabbitMqSubscriberService
Assembly: AsyncResponse.Transports.RabbitMQ
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/Transports/AsyncResponse.Transports.RabbitMQ/RabbitMqSubscriberServices.cs
Line coverage
88%
Covered lines: 60
Uncovered lines: 8
Coverable lines: 68
Total lines: 210
Line coverage: 88.2%
Branch coverage
87%
Covered branches: 7
Total branches: 8
Branch coverage: 87.5%
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%
ExecuteAsync()75%9872.41%
RunSubscriberAsync()100%1196.43%

File(s)

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

#LineLine coverage
 1using Microsoft.Extensions.Hosting;
 2using Microsoft.Extensions.Logging;
 3using Microsoft.Extensions.Options;
 4using System.Text;
 5
 6namespace AsyncResponse.Transports.RabbitMQ;
 7
 8internal abstract class RabbitMqSubscriberService : BackgroundService
 9{
 10    private readonly IRabbitMqConnectionFactory _connectionFactory;
 11
 12    /// <summary>Runs the RabbitMqSubscriberService operation.</summary>
 13    protected RabbitMqSubscriberService(
 14        IOptions<RabbitMqAsyncResponseOptions> options,
 15        ILogger logger)
 316        : this(options, logger, new RabbitMqConnectionFactoryAdapter(options.Value))
 17    {
 318    }
 19
 20    /// <summary>Runs the RabbitMqSubscriberService operation.</summary>
 321    protected RabbitMqSubscriberService(
 322        IOptions<RabbitMqAsyncResponseOptions> options,
 323        ILogger logger,
 324        IRabbitMqConnectionFactory connectionFactory)
 25    {
 326        Options = options.Value;
 327        Logger = logger;
 328        _connectionFactory = connectionFactory;
 329    }
 30
 31    protected RabbitMqAsyncResponseOptions Options { get; }
 32    protected ILogger Logger { get; }
 33
 34    protected abstract string QueueName { get; }
 35    protected abstract RabbitMqSubscriberOptions SubscriberOptions { get; }
 36    protected abstract RabbitMqSubscriberRole SubscriberRole { get; }
 37    /// <summary>Ensures the required resource exists.</summary>
 38    protected abstract Task EnsureTopologyAsync(IRabbitMqChannel channel, CancellationToken cancellationToken);
 39    /// <summary>Handles the delivered message.</summary>
 40    protected abstract Task HandleMessageAsync(RabbitMqDelivery delivery, CancellationToken cancellationToken);
 41
 42    /// <summary>Runs this background operation until cancellation is requested.</summary>
 43    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 44    {
 345        var queue = QueueName;
 346        RabbitMqMessageDispatcher.ValidateOptions(Options, SubscriberOptions, SubscriberRole);
 47
 48        // basic.nack requeue does not increment the x-death header, so the resolved attempt for a plain
 49        // requeued delivery never exceeds 2. Warn once at startup instead of silently never enforcing the cap.
 350        if (SubscriberOptions.AckMode is RabbitMqAckMode.AckAfterHandlerCompletes
 351            && SubscriberOptions.MaxDeliveryAttempts > 2)
 52        {
 053            Logger.LogWarning(
 054                "RabbitMQ {OptionName} is {MaxDeliveryAttempts} for queue {Queue} ({Role}), but attempts beyond 2 cannot
 055                + "basic.nack requeue does not increment x-death, so the cap only takes effect once a TTL-retry dead-let
 056                + "re-delivers the message through a dead-letter exchange.",
 057                nameof(RabbitMqSubscriberOptions.MaxDeliveryAttempts),
 058                SubscriberOptions.MaxDeliveryAttempts,
 059                queue,
 060                SubscriberRole);
 61        }
 62
 363        while (!stoppingToken.IsCancellationRequested)
 64        {
 65            try
 66            {
 367                await RunSubscriberAsync(queue, stoppingToken).ConfigureAwait(false);
 368                return;
 69            }
 370            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
 71            {
 372                return;
 73            }
 374            catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
 75            {
 276                var retryDelay = Options.NetworkRecoveryInterval > TimeSpan.Zero
 277                    ? Options.NetworkRecoveryInterval
 278                    : TimeSpan.FromSeconds(5);
 279                Logger.LogWarning(
 280                    ex,
 281                    "RabbitMQ subscriber could not start for queue {Queue} ({Role}); retrying in {RetryDelay}.",
 282                    queue,
 283                    SubscriberRole,
 284                    retryDelay);
 285                await Task.Delay(retryDelay, stoppingToken).ConfigureAwait(false);
 86            }
 87        }
 388    }
 89
 90    private async Task RunSubscriberAsync(string queue, CancellationToken stoppingToken)
 91    {
 392        await using var connection = await _connectionFactory.CreateConnectionAsync(stoppingToken).ConfigureAwait(false)
 393        await using var channel = await connection.CreateChannelAsync(cancellationToken: stoppingToken).ConfigureAwait(f
 394        await EnsureTopologyAsync(channel, stoppingToken).ConfigureAwait(false);
 395        await channel.BasicQosAsync(SubscriberOptions.PrefetchCount, stoppingToken).ConfigureAwait(false);
 96
 397        await using var dispatcher = RabbitMqMessageDispatcher.Create(
 398            HandleMessageAsync,
 399            Options,
 3100            SubscriberOptions,
 3101            Logger,
 3102            queue,
 3103            SubscriberRole);
 104
 3105        Logger.LogInformation(
 3106            "RabbitMQ subscriber started. Queue: {Queue}. Role: {Role}. AckMode: {AckMode}.",
 3107            queue,
 3108            SubscriberRole,
 3109            SubscriberOptions.AckMode);
 110
 3111        var consumerTag = await channel.BasicConsumeAsync(
 3112            queue,
 3113            delivery => dispatcher.HandleAsync(delivery, channel, stoppingToken),
 3114            stoppingToken).ConfigureAwait(false);
 115
 116        try
 117        {
 3118            await Task.Delay(Timeout.InfiniteTimeSpan, stoppingToken).ConfigureAwait(false);
 1119        }
 3120        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
 121        {
 3122            using var shutdown = new CancellationTokenSource(Options.ShutdownTimeout);
 3123            await channel.BasicCancelAsync(consumerTag, shutdown.Token).ConfigureAwait(false);
 2124            await channel.CloseAsync(shutdown.Token).ConfigureAwait(false);
 2125            await connection.CloseAsync(Options.ShutdownTimeout, shutdown.Token).ConfigureAwait(false);
 3126        }
 3127    }
 128}
 129
 130internal sealed class RabbitMqWorkerSubscriber : RabbitMqSubscriberService
 131{
 132    private readonly IAsyncResponseIngress _ingress;
 133
 134    /// <summary>Runs the RabbitMqWorkerSubscriber operation.</summary>
 135    public RabbitMqWorkerSubscriber(
 136        IOptions<RabbitMqAsyncResponseOptions> options,
 137        IAsyncResponseIngress ingress,
 138        ILogger<RabbitMqWorkerSubscriber> logger)
 139        : base(options, logger)
 140    {
 141        _ingress = ingress;
 142    }
 143
 144    internal RabbitMqWorkerSubscriber(
 145        IOptions<RabbitMqAsyncResponseOptions> options,
 146        IAsyncResponseIngress ingress,
 147        ILogger<RabbitMqWorkerSubscriber> logger,
 148        IRabbitMqConnectionFactory connectionFactory)
 149        : base(options, logger, connectionFactory)
 150    {
 151        _ingress = ingress;
 152    }
 153
 154    protected override string QueueName
 155        => RabbitMqOptionsValidator.Required(Options.WorkerQueue, nameof(Options.WorkerQueue));
 156
 157    protected override RabbitMqSubscriberOptions SubscriberOptions => Options.WorkerSubscriber;
 158    protected override RabbitMqSubscriberRole SubscriberRole => RabbitMqSubscriberRole.Worker;
 159
 160    /// <summary>Ensures the required resource exists.</summary>
 161    protected override Task EnsureTopologyAsync(IRabbitMqChannel channel, CancellationToken cancellationToken)
 162        => RabbitMqTopology.EnsureWorkerAsync(channel, Options, cancellationToken);
 163
 164    /// <summary>Handles the delivered message.</summary>
 165    protected override Task HandleMessageAsync(RabbitMqDelivery delivery, CancellationToken cancellationToken)
 166        => _ingress.HandleWorkerMessageAsync(Encoding.UTF8.GetString(delivery.Body.Span));
 167}
 168
 169internal sealed class RabbitMqResponseIngressSubscriber : RabbitMqSubscriberService
 170{
 171    private readonly IAsyncResponseIngress _ingress;
 172
 173    /// <summary>Runs the RabbitMqResponseIngressSubscriber operation.</summary>
 174    public RabbitMqResponseIngressSubscriber(
 175        IOptions<RabbitMqAsyncResponseOptions> options,
 176        IAsyncResponseIngress ingress,
 177        ILogger<RabbitMqResponseIngressSubscriber> logger)
 178        : base(options, logger)
 179    {
 180        _ingress = ingress;
 181    }
 182
 183    internal RabbitMqResponseIngressSubscriber(
 184        IOptions<RabbitMqAsyncResponseOptions> options,
 185        IAsyncResponseIngress ingress,
 186        ILogger<RabbitMqResponseIngressSubscriber> logger,
 187        IRabbitMqConnectionFactory connectionFactory)
 188        : base(options, logger, connectionFactory)
 189    {
 190        _ingress = ingress;
 191    }
 192
 193    protected override string QueueName
 194        => RabbitMqOptionsValidator.Required(Options.ResponseQueue, nameof(Options.ResponseQueue));
 195
 196    protected override RabbitMqSubscriberOptions SubscriberOptions => Options.ResponseSubscriber;
 197    protected override RabbitMqSubscriberRole SubscriberRole => RabbitMqSubscriberRole.ResponseIngress;
 198
 199    /// <summary>Ensures the required resource exists.</summary>
 200    protected override Task EnsureTopologyAsync(IRabbitMqChannel channel, CancellationToken cancellationToken)
 201        => RabbitMqTopology.EnsureResponseAsync(channel, Options, cancellationToken);
 202
 203    /// <summary>Handles the delivered message.</summary>
 204    protected override Task HandleMessageAsync(RabbitMqDelivery delivery, CancellationToken cancellationToken)
 205    {
 206        var messageJson = Encoding.UTF8.GetString(delivery.Body.Span);
 207        var correlationId = RabbitMqCorrelationIdExtractor.Extract(delivery, messageJson, Options);
 208        return _ingress.HandleResponseMessageAsync(messageJson, correlationId);
 209    }
 210}