| | | 1 | | namespace AsyncResponse.Transports.Redis; |
| | | 2 | | |
| | | 3 | | /// <summary> |
| | | 4 | | /// Controls when a Redis stream message is acknowledged relative to AsyncResponse handling. |
| | | 5 | | /// </summary> |
| | | 6 | | public 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> |
| | | 28 | | public sealed class RedisBackgroundFailureContext |
| | | 29 | | { |
| | 2 | 30 | | internal RedisBackgroundFailureContext( |
| | 2 | 31 | | string stream, |
| | 2 | 32 | | string consumerGroup, |
| | 2 | 33 | | string subscriberRole, |
| | 2 | 34 | | string messageId, |
| | 2 | 35 | | string? correlationId, |
| | 2 | 36 | | Exception exception) |
| | | 37 | | { |
| | 3 | 38 | | Stream = stream; |
| | 3 | 39 | | ConsumerGroup = consumerGroup; |
| | 3 | 40 | | SubscriberRole = subscriberRole; |
| | 3 | 41 | | MessageId = messageId; |
| | 3 | 42 | | CorrelationId = correlationId; |
| | 3 | 43 | | Exception = exception; |
| | 3 | 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> |
| | | 66 | | public 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> |
| | | 75 | | public int BatchSize { get; set; } = 16; |
| | | 76 | | |
| | | 77 | | /// <summary>Delay between empty XREADGROUP polls. Default: <c>50ms</c>.</summary> |
| | | 78 | | 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> |
| | | 84 | | 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> |
| | | 90 | | 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> |
| | | 93 | | 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> |
| | | 99 | | 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 |
| | | 114 | | 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 | | { |
| | | 128 | | ArgumentOutOfRangeException.ThrowIfNegativeOrZero(backgroundWorkerCount); |
| | | 129 | | ArgumentOutOfRangeException.ThrowIfNegativeOrZero(backgroundQueueCapacity); |
| | | 130 | | |
| | | 131 | | if (backgroundDrainTimeout is { } timeout && timeout <= TimeSpan.Zero) |
| | | 132 | | throw new ArgumentOutOfRangeException(nameof(backgroundDrainTimeout), timeout, "Drain timeout must be positi |
| | | 133 | | |
| | | 134 | | AckMode = RedisAckMode.AckAfterEnqueue; |
| | | 135 | | BackgroundWorkerCount = backgroundWorkerCount; |
| | | 136 | | BackgroundQueueCapacity = backgroundQueueCapacity; |
| | | 137 | | |
| | | 138 | | if (backgroundDrainTimeout is not null) |
| | | 139 | | BackgroundDrainTimeout = backgroundDrainTimeout.Value; |
| | | 140 | | |
| | | 141 | | return this; |
| | | 142 | | } |
| | | 143 | | } |