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

Information
Class: AsyncResponse.Transports.GooglePubSub.GooglePubSubWorkerTransport
Assembly: AsyncResponse.Transports.GooglePubSub
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/Transports/AsyncResponse.Transports.GooglePubSub/GooglePubSubWorkerTransport.cs
Line coverage
100%
Covered lines: 57
Uncovered lines: 0
Coverable lines: 57
Total lines: 139
Line coverage: 100%
Branch coverage
100%
Covered branches: 18
Total branches: 18
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%
GetPublisherAsync()100%44100%
PublishAsync()100%1010100%
DisposeAsync()100%44100%

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/Transports/AsyncResponse.Transports.GooglePubSub/GooglePubSubWorkerTransport.cs

#LineLine coverage
 1using Google.Api.Gax;
 2using Google.Cloud.PubSub.V1;
 3using Google.Protobuf;
 4using Microsoft.Extensions.Options;
 5using System.Diagnostics;
 6using System.Text.Json;
 7
 8namespace AsyncResponse.Transports.GooglePubSub;
 9
 10/// <summary>
 11/// Publishes <see cref="WorkerJobEnvelope"/> messages to a Google Pub/Sub topic.
 12/// </summary>
 13/// <remarks>
 14/// The publisher client is created lazily and re-created on demand: a transient build failure when the
 15/// first job is published does not permanently break the transport (a faulted build attempt is not cached).
 16/// </remarks>
 17public sealed class GooglePubSubWorkerTransport : IWorkerTransport, IAsyncDisposable
 18{
 19    private readonly GooglePubSubAsyncResponseOptions _options;
 20    private readonly Func<Task<IGooglePubSubPublisherClient>> _publisherFactory;
 321    private readonly SemaphoreSlim _publisherGate = new(1, 1);
 22    private IGooglePubSubPublisherClient? _publisher;
 23    private int _disposeGate;
 24    private bool _disposed;
 25
 26    /// <summary>Runs the GooglePubSubWorkerTransport operation.</summary>
 27    public GooglePubSubWorkerTransport(IOptions<GooglePubSubAsyncResponseOptions> options)
 128        : this(options, () => CreatePublisherAsync(options.Value))
 29    {
 330    }
 31
 332    internal GooglePubSubWorkerTransport(
 333        IOptions<GooglePubSubAsyncResponseOptions> options,
 334        Func<Task<IGooglePubSubPublisherClient>> publisherFactory)
 35    {
 336        _options = options.Value;
 337        _ = GooglePubSubOptionsValidator.Required(_options.ProjectId, nameof(_options.ProjectId));
 338        _ = GooglePubSubOptionsValidator.Required(_options.WorkerTopicId, nameof(_options.WorkerTopicId));
 339        _publisherFactory = publisherFactory;
 340    }
 41
 42    private async Task<IGooglePubSubPublisherClient> GetPublisherAsync(CancellationToken cancellationToken)
 43    {
 344        var publisher = Volatile.Read(ref _publisher);
 345        if (publisher is not null)
 346            return publisher;
 47
 348        await _publisherGate.WaitAsync(cancellationToken).ConfigureAwait(false);
 49        try
 50        {
 351            ObjectDisposedException.ThrowIf(_disposed, this);
 352            if (_publisher is not null)
 353                return _publisher;
 54
 55            // Assign only after the await succeeds, so a faulted build attempt is not cached and the next
 56            // publish retries instead of awaiting a permanently faulted task.
 357            var created = await _publisherFactory().ConfigureAwait(false);
 358            _publisher = created;
 359            return created;
 60        }
 61        finally
 62        {
 363            _publisherGate.Release();
 64        }
 365    }
 66
 67    [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
 68    private static async Task<IGooglePubSubPublisherClient> CreatePublisherAsync(
 69        GooglePubSubAsyncResponseOptions options)
 70    {
 71        var projectId = GooglePubSubOptionsValidator.Required(options.ProjectId, nameof(options.ProjectId));
 72        var topicId = GooglePubSubOptionsValidator.Required(options.WorkerTopicId, nameof(options.WorkerTopicId));
 73        var topicName = TopicName.FromProjectTopic(projectId, topicId);
 74        // EmulatorOrProduction honors PUBSUB_EMULATOR_HOST when present (local dev / tests) and uses
 75        // real Google Cloud otherwise — no behavior change in production.
 76        var publisher = await new PublisherClientBuilder
 77        {
 78            TopicName = topicName,
 79            EmulatorDetection = EmulatorDetection.EmulatorOrProduction
 80        }.BuildAsync().ConfigureAwait(false);
 81        return new GooglePubSubPublisherClientAdapter(publisher);
 82    }
 83
 84    /// <summary>Publishes the supplied message.</summary>
 85    public async Task PublishAsync(WorkerJobEnvelope job, CancellationToken cancellationToken = default)
 86    {
 387        ArgumentNullException.ThrowIfNull(job);
 88
 389        using var activity = AsyncResponseDiagnostics.StartActivity(
 390            "asyncresponse.worker.publish",
 391            ActivityKind.Producer,
 392            job.CorrelationId);
 393        activity?.SetTag("asyncresponse.transport", "google_pubsub");
 394        activity?.SetTag("messaging.system", "gcp_pubsub");
 395        activity?.SetTag("messaging.destination.name", _options.WorkerTopicId);
 396        AsyncResponseDiagnostics.SetReplyTarget(activity, job.ReplyTarget);
 397        AsyncResponseDiagnostics.SetWorker(activity, job.Call);
 98
 99        try
 100        {
 3101            var message = new PubsubMessage
 3102            {
 3103                Data = ByteString.CopyFromUtf8(AsyncResponseJson.Serialize(job))
 3104            };
 105
 3106            if (!string.IsNullOrWhiteSpace(job.CorrelationId))
 3107                message.Attributes[_options.CorrelationIdAttribute] = job.CorrelationId;
 108
 3109            var publisher = await GetPublisherAsync(cancellationToken).ConfigureAwait(false);
 3110            var messageId = await publisher.PublishAsync(message).WaitAsync(cancellationToken).ConfigureAwait(false);
 3111            activity?.SetTag("messaging.message.id", messageId);
 3112        }
 2113        catch (Exception ex)
 114        {
 2115            AsyncResponseDiagnostics.SetError(activity, ex);
 3116            throw;
 117        }
 3118    }
 119
 120    /// <summary>Releases resources held by this instance.</summary>
 121    public async ValueTask DisposeAsync()
 122    {
 3123        if (Interlocked.Exchange(ref _disposeGate, 1) != 0)
 3124            return;
 125
 3126        await _publisherGate.WaitAsync().ConfigureAwait(false);
 127        try
 128        {
 3129            _disposed = true;
 3130            if (_publisher is not null)
 3131                await _publisher.ShutdownAsync(_options.ShutdownTimeout).ConfigureAwait(false);
 3132        }
 133        finally
 134        {
 3135            _publisherGate.Release();
 3136            _publisherGate.Dispose();
 137        }
 3138    }
 139}