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

Information
Class: AsyncResponse.Transports.NATS.NatsWorkerTransport
Assembly: AsyncResponse.Transports.NATS
File(s): /_/src/Transports/AsyncResponse.Transports.NATS/NatsWorkerTransport.cs
Line coverage
100%
Covered lines: 64
Uncovered lines: 0
Coverable lines: 64
Total lines: 127
Line coverage: 100%
Branch coverage
75%
Covered branches: 12
Total branches: 16
Branch coverage: 75%
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%
PublishAsync()66.66%1212100%
EnsureWorkerStreamOnceAsync()100%44100%
<EnsureWorkerStreamOnceAsync()100%11100%

File(s)

/_/src/Transports/AsyncResponse.Transports.NATS/NatsWorkerTransport.cs

#LineLine coverage
 1using Microsoft.Extensions.Options;
 2using NATS.Client.Core;
 3using NATS.Net;
 4using System.Diagnostics;
 5using System.Text.Json;
 6
 7namespace AsyncResponse.Transports.NATS;
 8
 9/// <summary>
 10/// Publishes <see cref="WorkerJobEnvelope"/> messages to a NATS JetStream subject.
 11/// </summary>
 12/// <remarks>
 13/// JetStream provides durable queueing and explicit acknowledgement. The correlation id travels as a
 14/// message header so the consuming side can correlate without parsing the body, and transient NATS
 15/// failures are retried with bounded exponential backoff before the exception is returned to the
 16/// caller.
 17/// </remarks>
 18public sealed class NatsWorkerTransport : IWorkerTransport
 19{
 20    private readonly NatsAsyncResponseTransportOptions _options;
 21    private readonly INatsJetStreamTransport _jetStream;
 22    private readonly NatsTransportSubjectSchema _schema;
 21623    private readonly SemaphoreSlim _ensureStreamGate = new(1, 1);
 24    private bool _streamEnsured;
 25
 26    /// <summary>Runs the NatsWorkerTransport operation.</summary>
 27    public NatsWorkerTransport(
 28        IOptions<NatsAsyncResponseTransportOptions> options,
 29        INatsConnection connection)
 19630        : this(options, new NatsJetStreamTransportAdapter(connection.CreateJetStreamContext(), null, options.Value.Strea
 31    {
 19632    }
 33
 21634    internal NatsWorkerTransport(
 21635        IOptions<NatsAsyncResponseTransportOptions> options,
 21636        INatsJetStreamTransport jetStream)
 37    {
 21638        _options = options.Value;
 21639        NatsTransportOptionsValidator.ValidateCommon(_options);
 21640        _jetStream = jetStream;
 21641        _schema = new NatsTransportSubjectSchema(_options);
 21642    }
 43
 44    /// <summary>Publishes the supplied message.</summary>
 45    public async Task PublishAsync(WorkerJobEnvelope job, CancellationToken cancellationToken = default)
 46    {
 43847        ArgumentNullException.ThrowIfNull(job);
 48
 43649        using var activity = AsyncResponseDiagnostics.StartActivity(
 43650            "asyncresponse.worker.publish",
 43651            ActivityKind.Producer,
 43652            job.CorrelationId);
 43653        activity?.SetTag("asyncresponse.transport", "nats");
 43654        activity?.SetTag("messaging.system", "nats");
 43655        activity?.SetTag("messaging.destination.name", _schema.WorkerSubject);
 43656        AsyncResponseDiagnostics.SetReplyTarget(activity, job.ReplyTarget);
 43657        AsyncResponseDiagnostics.SetWorker(activity, job.Call);
 58
 59        try
 60        {
 43661            if (_options.CreateStreams)
 43662                await EnsureWorkerStreamOnceAsync(cancellationToken).ConfigureAwait(false);
 63
 64            // Stable id outside the retry loop so a retried publish is deduplicated by JetStream
 65            // (Nats-Msg-Id within the stream's duplicate window) rather than enqueuing the same
 66            // worker job twice when a PubAck is lost after the broker already persisted the message.
 43267            var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
 43268            {
 43269                ["Nats-Msg-Id"] = Guid.NewGuid().ToString("N")
 43270            };
 43271            if (!string.IsNullOrWhiteSpace(job.CorrelationId))
 11272                headers[_options.CorrelationIdHeader] = job.CorrelationId!;
 73
 43274            var payload = AsyncResponseJson.Serialize(job);
 43275            var sequence = await NatsTransportRetry.ExecuteAsync(
 43676                token => _jetStream.PublishAsync(_schema.WorkerSubject, payload, headers, token),
 43277                _options.PublishMaxAttempts,
 43278                _options.PublishRetryBaseDelay,
 43279                _options.PublishRetryMaxDelay,
 43280                cancellationToken).ConfigureAwait(false);
 81
 43082            activity?.SetTag("messaging.message.id", sequence);
 43083        }
 684        catch (Exception ex)
 85        {
 686            AsyncResponseDiagnostics.SetError(activity, ex);
 687            throw;
 88        }
 43089    }
 90
 91    private async Task EnsureWorkerStreamOnceAsync(CancellationToken cancellationToken)
 92    {
 43693        if (_streamEnsured)
 22394            return;
 95
 21396        await _ensureStreamGate.WaitAsync(cancellationToken).ConfigureAwait(false);
 97        try
 98        {
 21399            if (!_streamEnsured)
 100            {
 101                // Retried on the SAME terms as the publish below: provisioning is a JetStream API
 102                // request like any other, and it runs when that request is likeliest to time out —
 103                // the first publish after startup, or after any hiccup, since the flag only
 104                // latches on success.
 213105                await NatsTransportRetry.ExecuteAsync(
 213106                    async token =>
 213107                    {
 224108                        await _jetStream.EnsureStreamAsync(
 224109                            _schema.WorkerStream,
 224110                            _schema.WorkerSubject,
 224111                            _options.StreamMaxMessages,
 224112                            token).ConfigureAwait(false);
 209113                        return true;
 209114                    },
 213115                    _options.PublishMaxAttempts,
 213116                    _options.PublishRetryBaseDelay,
 213117                    _options.PublishRetryMaxDelay,
 213118                    cancellationToken).ConfigureAwait(false);
 209119                _streamEnsured = true;
 120            }
 209121        }
 122        finally
 123        {
 213124            _ensureStreamGate.Release();
 125        }
 432126    }
 127}