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

Information
Class: AsyncResponse.AsyncResponseIngress
Assembly: AsyncResponse.Core
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/AsyncResponseIngress.cs
Line coverage
100%
Covered lines: 59
Uncovered lines: 0
Coverable lines: 59
Total lines: 124
Line coverage: 100%
Branch coverage
100%
Covered branches: 10
Total branches: 10
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%
HandleResponseMessageAsync()100%44100%
<HandleResponseMessageAsync()100%11100%
HandleWorkerMessageAsync()100%22100%

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/AsyncResponseIngress.cs

#LineLine coverage
 1using Microsoft.Extensions.Logging;
 2using System.Diagnostics;
 3using System.Security.Cryptography;
 4using System.Text;
 5
 6namespace AsyncResponse;
 7
 8/// <summary>
 9/// Transport-neutral ingress implementation. Broker/webhook adapters can feed response payloads
 10/// and worker-job envelopes into this service without depending on a specific response channel.
 11/// </summary>
 312internal sealed class AsyncResponseIngress(
 313    IRawAsyncResponsePublisher _rawPublisher,
 314    IAsyncResponsePublisher _publisher,
 315    WorkerJobExecutor _workerJobExecutor,
 316    AsyncResponseContextPropagation _propagation,
 317    ILogger<AsyncResponseIngress> _logger) : IAsyncResponseIngress
 18{
 19    /// <summary>Handles the delivered message.</summary>
 20    public async Task HandleResponseMessageAsync(string messageJson, string? correlationId)
 21    {
 322        using var activity = AsyncResponseDiagnostics.StartActivity(
 323            "asyncresponse.ingress.response",
 324            ActivityKind.Consumer,
 325            correlationId);
 26
 327        if (string.IsNullOrWhiteSpace(correlationId))
 28        {
 29            // Deliberately acknowledged, not thrown: without a correlation id the message can
 30            // never route, so redelivery would retry it forever (RabbitMQ's default
 31            // MaxDeliveryAttempts = 0 has no cap) or burn dead-letter attempts on brokers that do.
 32            // Error-level log + counter make the drop loud — every occurrence is a producer-side
 33            // contract violation. Only metadata is logged: response payloads may carry PII and
 34            // stay out of logs by policy (docs/security.md); the byte length and hash prefix are
 35            // enough to correlate with broker-side capture tooling.
 236            var payloadBytes = Encoding.UTF8.GetBytes(messageJson);
 237            _logger.LogError(
 238                "Ingress received a response message with no correlation id; it cannot be routed and is acknowledged wit
 239                payloadBytes.Length,
 240                Convert.ToHexString(SHA256.HashData(payloadBytes).AsSpan(0, 8)));
 241            AsyncResponseDiagnostics.SetError(activity, "correlation_id_null", "No correlation id on the inbound respons
 242            AsyncResponseDiagnostics.RecordUnroutableResponse();
 243            return;
 44        }
 45
 46        try
 47        {
 348            _logger.LogDebug("Ingress received inbound response message: {Message}", messageJson);
 49
 50            // A transient infrastructure fault (channel store briefly unreachable, recovery-state
 51            // read hiccup, resume-callback dependency blip) must not finalize the waiter on the
 52            // first attempt — that would convert a recoverable response into a permanent business
 53            // failure. Retry briefly in-process before escalating. Parse failures are excluded:
 54            // an unparseable message never becomes parseable, so it escalates immediately.
 55            // Recovery resume callbacks may be re-invoked by these retries, which matches their
 56            // contract — broker redelivery re-invokes them the same way.
 357            await AsyncResponseRetry.ExecuteAsync(
 358                async _ =>
 359                {
 360                    await _rawPublisher.SetRawResponseJson(messageJson, correlationId).ConfigureAwait(false);
 361                    return true;
 362                },
 363                isTransient: static ex => ex is not (System.Text.Json.JsonException or InvalidDataException),
 364                maxAttempts: 4,
 365                baseDelay: TimeSpan.FromMilliseconds(250),
 366                maxDelay: TimeSpan.FromSeconds(2),
 367                CancellationToken.None).ConfigureAwait(false);
 368        }
 369        catch (Exception ex)
 70        {
 371            _logger.LogError(ex, "Ingress failed to process the inbound response message.");
 372            AsyncResponseDiagnostics.SetError(activity, ex);
 73            try
 74            {
 375                await _publisher.SetException(ex, correlationId).ConfigureAwait(false);
 276            }
 377            catch (Exception innerEx)
 78            {
 379                _logger.LogError(innerEx, "Ingress failed to publish the exception for the inbound message (original err
 80
 81                // Both the publish and the SetException escalation failed, so returning normally
 82                // would ack a response that now exists nowhere. Propagate instead: the transport's
 83                // redelivery/dead-letter policy retries the whole pipeline, and the recovery
 84                // registration stays valid for the redelivered attempt.
 385                throw;
 86            }
 287        }
 388    }
 89
 90    /// <summary>Handles the delivered message.</summary>
 91    public async Task HandleWorkerMessageAsync(string messageJson)
 92    {
 393        using var activity = AsyncResponseDiagnostics.StartActivity(
 394            "asyncresponse.ingress.worker",
 395            ActivityKind.Consumer);
 96
 97        try
 98        {
 399            _logger.LogDebug("Ingress received worker job: {Payload}", messageJson);
 100
 3101            var job = JsonSafety.SafeDeserialize<WorkerJobEnvelope>(messageJson)
 3102                ?? throw new InvalidDataException("Worker message deserialized to null.");
 3103            AsyncResponseDiagnostics.SetCorrelationId(activity, job.CorrelationId);
 3104            AsyncResponseDiagnostics.SetReplyTarget(activity, job.ReplyTarget);
 3105            AsyncResponseDiagnostics.SetWorker(activity, job.Call);
 106
 107            // The job crossed a serialization boundary (broker → ingress): restore any ambient
 108            // context its propagators captured before executing it.
 3109            using (_propagation.Restore(job.Context))
 3110                await _workerJobExecutor.ExecuteAsync(job).ConfigureAwait(false);
 3111        }
 2112        catch (Exception ex)
 113        {
 2114            _logger.LogError(ex, "Ingress worker job execution failed.");
 2115            AsyncResponseDiagnostics.SetError(activity, ex);
 116
 117            // Propagate: the transport dispatcher owns the retry/dead-letter decision for worker
 118            // jobs (per its AckMode and MaxDeliveryAttempts). Swallowing here acknowledged failed
 119            // jobs as successes, which disabled redelivery entirely and left the waiter to burn
 120            // its full timeout.
 2121            throw;
 122        }
 3123    }
 124}