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

Information
Class: AsyncResponse.Transports.Redis.RedisSubscriberOptions
Assembly: AsyncResponse.Transports.Redis
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/Transports/AsyncResponse.Transports.Redis/RedisSubscriberOptions.cs
Line coverage
100%
Covered lines: 17
Uncovered lines: 0
Coverable lines: 17
Total lines: 143
Line coverage: 100%
Branch coverage
100%
Covered branches: 6
Total branches: 6
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%
UseAckAfterEnqueue(...)100%66100%

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/Transports/AsyncResponse.Transports.Redis/RedisSubscriberOptions.cs

#LineLine coverage
 1namespace AsyncResponse.Transports.Redis;
 2
 3/// <summary>
 4/// Controls when a Redis stream message is acknowledged relative to AsyncResponse handling.
 5/// </summary>
 6public enum RedisAckMode
 7{
 8    /// <summary>
 9    /// ACK only after the AsyncResponse handler completes successfully. If the handler throws, the
 10    /// message stays in the Redis pending-entry list and is reclaimed after
 11    /// <see cref="RedisSubscriberOptions.PendingMessageMinIdleTime"/>.
 12    /// </summary>
 13    AckAfterHandlerCompletes = 0,
 14
 15    /// <summary>
 16    /// ACK immediately after the stream entry is accepted into a bounded in-process background
 17    /// queue. Handler failures are logged, reported through
 18    /// <see cref="RedisSubscriberOptions.OnBackgroundFailure"/>, and dead-lettered when enabled
 19    /// because Redis has already been ACKed.
 20    /// </summary>
 21    AckAfterEnqueue = 1
 22}
 23
 24/// <summary>
 25/// Describes a handler failure that happened after a Redis stream entry was already ACKed by
 26/// <see cref="RedisAckMode.AckAfterEnqueue"/>.
 27/// </summary>
 28public sealed class RedisBackgroundFailureContext
 29{
 30    internal RedisBackgroundFailureContext(
 31        string stream,
 32        string consumerGroup,
 33        string subscriberRole,
 34        string messageId,
 35        string? correlationId,
 36        Exception exception)
 37    {
 38        Stream = stream;
 39        ConsumerGroup = consumerGroup;
 40        SubscriberRole = subscriberRole;
 41        MessageId = messageId;
 42        CorrelationId = correlationId;
 43        Exception = exception;
 44    }
 45
 46    /// <summary>The Redis stream the entry came from.</summary>
 47    public string Stream { get; }
 48
 49    /// <summary>The Redis consumer group that received the entry.</summary>
 50    public string ConsumerGroup { get; }
 51
 52    /// <summary>The logical subscriber role, such as <c>Worker</c> or <c>ResponseIngress</c>.</summary>
 53    public string SubscriberRole { get; }
 54
 55    /// <summary>The Redis stream entry id.</summary>
 56    public string MessageId { get; }
 57
 58    /// <summary>The AsyncResponse correlation id, when one was available.</summary>
 59    public string? CorrelationId { get; }
 60
 61    /// <summary>The exception thrown by the background handler.</summary>
 62    public Exception Exception { get; }
 63}
 64
 65/// <summary>Per-stream Redis subscriber behavior.</summary>
 66public sealed class RedisSubscriberOptions
 67{
 68    /// <summary>
 69    /// Controls when a Redis stream entry is ACKed. Defaults to
 70    /// <see cref="RedisAckMode.AckAfterHandlerCompletes"/>.
 71    /// </summary>
 72    public RedisAckMode AckMode { get; set; } = RedisAckMode.AckAfterHandlerCompletes;
 73
 74    /// <summary>Maximum number of new stream entries read per polling command. Default: <c>16</c>.</summary>
 375    public int BatchSize { get; set; } = 16;
 76
 77    /// <summary>Delay between empty XREADGROUP polls. Default: <c>50ms</c>.</summary>
 378    public TimeSpan EmptyPollDelay { get; set; } = TimeSpan.FromMilliseconds(50);
 79
 80    /// <summary>
 81    /// How long a failed pending entry must be idle before another loop claims it for retry.
 82    /// Default: <c>30s</c>.
 83    /// </summary>
 384    public TimeSpan PendingMessageMinIdleTime { get; set; } = TimeSpan.FromSeconds(30);
 85
 86    /// <summary>
 87    /// How often the subscriber scans the pending-entry list for retryable entries.
 88    /// Default: <c>5s</c>.
 89    /// </summary>
 390    public TimeSpan PendingClaimInterval { get; set; } = TimeSpan.FromSeconds(5);
 91
 92    /// <summary>Maximum number of pending entries inspected/claimed per scan. Default: <c>16</c>.</summary>
 393    public int PendingClaimBatchSize { get; set; } = 16;
 94
 95    /// <summary>
 96    /// Maximum number of delivery attempts before a failing message is ACKed and written to the
 97    /// dead-letter stream. <c>0</c> means unlimited retries. Default: <c>5</c>.
 98    /// </summary>
 399    public int MaxDeliveryAttempts { get; set; } = 5;
 100
 101    /// <summary>
 102    /// Number of background workers used by <see cref="RedisAckMode.AckAfterEnqueue"/>.
 103    /// Must be explicitly set to a positive value for early ACK mode.
 104    /// </summary>
 105    public int BackgroundWorkerCount { get; set; }
 106
 107    /// <summary>
 108    /// Maximum number of entries waiting in the background queue for
 109    /// <see cref="RedisAckMode.AckAfterEnqueue"/>. When full, the entry remains pending for retry.
 110    /// </summary>
 111    public int BackgroundQueueCapacity { get; set; }
 112
 113    /// <summary>Maximum time to wait for queued/running background handlers while the hosted subscriber stops.</summary
 3114    public TimeSpan BackgroundDrainTimeout { get; set; } = TimeSpan.FromSeconds(20);
 115
 116    /// <summary>
 117    /// Optional callback invoked when a background handler fails after the Redis entry was already
 118    /// ACKed. Use it to increment operator-visible metrics or alert on already-ACKed work.
 119    /// </summary>
 120    public Func<RedisBackgroundFailureContext, ValueTask>? OnBackgroundFailure { get; set; }
 121
 122    /// <summary>Explicitly opts this subscriber into ACK-after-enqueue behavior.</summary>
 123    public RedisSubscriberOptions UseAckAfterEnqueue(
 124        int backgroundWorkerCount,
 125        int backgroundQueueCapacity,
 126        TimeSpan? backgroundDrainTimeout = null)
 127    {
 3128        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(backgroundWorkerCount);
 3129        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(backgroundQueueCapacity);
 130
 3131        if (backgroundDrainTimeout is { } timeout && timeout <= TimeSpan.Zero)
 3132            throw new ArgumentOutOfRangeException(nameof(backgroundDrainTimeout), timeout, "Drain timeout must be positi
 133
 3134        AckMode = RedisAckMode.AckAfterEnqueue;
 3135        BackgroundWorkerCount = backgroundWorkerCount;
 3136        BackgroundQueueCapacity = backgroundQueueCapacity;
 137
 3138        if (backgroundDrainTimeout is not null)
 3139            BackgroundDrainTimeout = backgroundDrainTimeout.Value;
 140
 3141        return this;
 142    }
 143}