| | | 1 | | namespace AsiBackbone.Core.Outbox; |
| | | 2 | | |
| | | 3 | | /// <summary> |
| | | 4 | | /// Provides provider-neutral retry timing, poison-message, and optional claim/lease options for outbox drain processing |
| | | 5 | | /// </summary> |
| | | 6 | | /// <remarks> |
| | | 7 | | /// These options control default retry timestamps when a downstream emitter does not provide its own retry-after value. |
| | | 8 | | /// </remarks> |
| | | 9 | | public sealed class GovernanceOutboxOptions |
| | | 10 | | { |
| | | 11 | | /// <summary> |
| | | 12 | | /// Gets the stable default reason code used when the drain dead-letters an entry after the configured retry thresho |
| | | 13 | | /// </summary> |
| | | 14 | | public const string DefaultDeadLetterReasonCode = "outbox.max_retry_attempts_exceeded"; |
| | | 15 | | |
| | | 16 | | /// <summary> |
| | | 17 | | /// Gets the stable default reason message used when the drain dead-letters an entry after the configured retry thre |
| | | 18 | | /// </summary> |
| | | 19 | | public const string DefaultDeadLetterReasonMessage = "Governance outbox entry exceeded the configured maximum retry |
| | | 20 | | |
| | | 21 | | /// <summary> |
| | | 22 | | /// Gets the stable default reason code used when the drain dead-letters an entry after the configured claim thresho |
| | | 23 | | /// </summary> |
| | | 24 | | public const string DefaultMaxClaimAttemptsReasonCode = "outbox.max_claim_attempts_exceeded"; |
| | | 25 | | |
| | | 26 | | /// <summary> |
| | | 27 | | /// Gets the stable default reason message used when the drain dead-letters an entry after the configured claim thre |
| | | 28 | | /// </summary> |
| | | 29 | | public const string DefaultMaxClaimAttemptsReasonMessage = "Governance outbox entry exceeded the configured maximum |
| | | 30 | | |
| | | 31 | | /// <summary> |
| | | 32 | | /// Gets the default number of claimed entries drained under a single lease before the drain claims again. |
| | | 33 | | /// </summary> |
| | | 34 | | public const int DefaultClaimPageSize = 10; |
| | | 35 | | |
| | | 36 | | /// <summary> |
| | | 37 | | /// Gets the default number of claims permitted before the drain dead-letters an entry that never reaches a terminal |
| | | 38 | | /// </summary> |
| | | 39 | | public const int DefaultMaxClaimAttempts = 5; |
| | | 40 | | |
| | | 41 | | /// <summary> |
| | | 42 | | /// Gets the default worker identifier used when claim leases are enabled and the host does not supply one. |
| | | 43 | | /// </summary> |
| | | 44 | | /// <remarks> |
| | | 45 | | /// The value combines the machine name and the current process identifier so that replicas of the same deployment d |
| | | 46 | | /// </remarks> |
| | | 47 | | public static string DefaultClaimWorkerId { get; } = $"{Environment.MachineName}:{Environment.ProcessId}"; |
| | | 48 | | |
| | | 49 | | /// <summary> |
| | | 50 | | /// Gets or sets the default delay applied after a transient emission failure or unexpected emitter exception. |
| | | 51 | | /// </summary> |
| | | 52 | | public TimeSpan RetryDelay { get; set; } = TimeSpan.FromMinutes(1); |
| | | 53 | | |
| | | 54 | | /// <summary> |
| | | 55 | | /// Gets or sets the default delay applied when an emitter returns a pending or deferred result without a retry-afte |
| | | 56 | | /// </summary> |
| | | 57 | | public TimeSpan DeferredDelay { get; set; } = TimeSpan.FromMinutes(1); |
| | | 58 | | |
| | | 59 | | /// <summary> |
| | | 60 | | /// Gets or sets the maximum number of failed emission attempts permitted before the drain applies its poison-messag |
| | | 61 | | /// </summary> |
| | | 62 | | /// <remarks> |
| | | 63 | | /// The threshold counts the failure currently being processed. A value of <c>1</c> dead-letters the first failed at |
| | | 64 | | /// </remarks> |
| | | 65 | | public int MaxRetryAttempts { get; set; } = 5; |
| | | 66 | | |
| | | 67 | | /// <summary> |
| | | 68 | | /// Gets or sets a value indicating whether entries are dead-lettered when <see cref="MaxRetryAttempts" /> is reache |
| | | 69 | | /// </summary> |
| | | 70 | | public bool DeadLetterOnMaxRetryAttempts { get; set; } = true; |
| | | 71 | | |
| | | 72 | | /// <summary> |
| | | 73 | | /// Gets or sets the provider-neutral reason code recorded when the configured retry threshold is reached. |
| | | 74 | | /// </summary> |
| | | 75 | | public string DeadLetterReasonCode { get; set; } = DefaultDeadLetterReasonCode; |
| | | 76 | | |
| | | 77 | | /// <summary> |
| | | 78 | | /// Gets or sets the provider-neutral reason message recorded when the configured retry threshold is reached. |
| | | 79 | | /// </summary> |
| | | 80 | | public string DeadLetterReasonMessage { get; set; } = DefaultDeadLetterReasonMessage; |
| | | 81 | | |
| | | 82 | | /// <summary> |
| | | 83 | | /// Gets or sets a value indicating whether the drain should claim outbox entries before provider emission when the |
| | | 84 | | /// </summary> |
| | | 85 | | /// <remarks> |
| | | 86 | | /// This is enabled by default because the alternative path allows two hosts draining the same durable outbox to emi |
| | | 87 | | /// </remarks> |
| | | 88 | | public bool UseClaimLeases { get; set; } = true; |
| | | 89 | | |
| | | 90 | | /// <summary> |
| | | 91 | | /// Gets or sets the worker, process, node, or partition identifier used when <see cref="UseClaimLeases" /> is enabl |
| | | 92 | | /// </summary> |
| | | 93 | | /// <remarks> |
| | | 94 | | /// Defaults to <see cref="DefaultClaimWorkerId" />. Setting this to <see langword="null" /> or whitespace while <se |
| | | 95 | | /// </remarks> |
| | | 96 | | public string? ClaimWorkerId { get; set; } = DefaultClaimWorkerId; |
| | | 97 | | |
| | | 98 | | /// <summary> |
| | | 99 | | /// Gets or sets the lease duration used when <see cref="UseClaimLeases" /> is enabled. |
| | | 100 | | /// </summary> |
| | | 101 | | public TimeSpan ClaimLeaseDuration { get; set; } = GovernanceOutboxClaimRequest.DefaultLeaseDuration; |
| | | 102 | | |
| | | 103 | | /// <summary> |
| | | 104 | | /// Gets or sets the maximum number of claimed entries drained under a single lease before the drain claims again. |
| | | 105 | | /// </summary> |
| | | 106 | | /// <remarks> |
| | | 107 | | /// A drain pass claims entries in pages of this size rather than leasing an entire batch at once, so a slow emitter |
| | | 108 | | /// </remarks> |
| | | 109 | | public int ClaimPageSize { get; set; } = DefaultClaimPageSize; |
| | | 110 | | |
| | | 111 | | /// <summary> |
| | | 112 | | /// Gets or sets the number of claims permitted before the drain dead-letters an entry that has never reached a term |
| | | 113 | | /// </summary> |
| | | 114 | | /// <remarks> |
| | | 115 | | /// An emitter that hangs or is killed mid-emission leaves the entry claimed but not failed, so its retry count neve |
| | | 116 | | /// </remarks> |
| | | 117 | | public int MaxClaimAttempts { get; set; } = DefaultMaxClaimAttempts; |
| | | 118 | | |
| | | 119 | | /// <summary> |
| | | 120 | | /// Gets or sets a value indicating whether entries are dead-lettered when <see cref="MaxClaimAttempts" /> is reache |
| | | 121 | | /// </summary> |
| | | 122 | | /// <remarks> |
| | | 123 | | /// When disabled, an entry that exceeds the claim threshold is still drained normally, which restores the unbounded |
| | | 124 | | /// </remarks> |
| | | 125 | | public bool DeadLetterOnMaxClaimAttempts { get; set; } = true; |
| | | 126 | | |
| | | 127 | | /// <summary> |
| | | 128 | | /// Gets or sets the provider-neutral reason code recorded when the configured claim threshold is reached. |
| | | 129 | | /// </summary> |
| | | 130 | | public string MaxClaimAttemptsReasonCode { get; set; } = DefaultMaxClaimAttemptsReasonCode; |
| | | 131 | | |
| | | 132 | | /// <summary> |
| | | 133 | | /// Gets or sets the provider-neutral reason message recorded when the configured claim threshold is reached. |
| | | 134 | | /// </summary> |
| | | 135 | | public string MaxClaimAttemptsReasonMessage { get; set; } = DefaultMaxClaimAttemptsReasonMessage; |
| | | 136 | | |
| | | 137 | | /// <summary> |
| | | 138 | | /// Validates the configured outbox retry, poison-message, timing, and claim options. |
| | | 139 | | /// </summary> |
| | | 140 | | /// <exception cref="InvalidOperationException">Thrown when a retry threshold, reason, delay, or claim lease option |
| | | 141 | | public void Validate() |
| | | 142 | | { |
| | 119 | 143 | | ValidateDelay(RetryDelay, nameof(RetryDelay)); |
| | 116 | 144 | | ValidateDelay(DeferredDelay, nameof(DeferredDelay)); |
| | | 145 | | |
| | 113 | 146 | | if (MaxRetryAttempts <= 0) |
| | | 147 | | { |
| | 1 | 148 | | throw new InvalidOperationException($"{nameof(MaxRetryAttempts)} must be greater than zero."); |
| | | 149 | | } |
| | | 150 | | |
| | 112 | 151 | | if (string.IsNullOrWhiteSpace(DeadLetterReasonCode)) |
| | | 152 | | { |
| | 1 | 153 | | throw new InvalidOperationException($"{nameof(DeadLetterReasonCode)} is required."); |
| | | 154 | | } |
| | | 155 | | |
| | 111 | 156 | | if (string.IsNullOrWhiteSpace(DeadLetterReasonMessage)) |
| | | 157 | | { |
| | 1 | 158 | | throw new InvalidOperationException($"{nameof(DeadLetterReasonMessage)} is required."); |
| | | 159 | | } |
| | | 160 | | |
| | 110 | 161 | | if (ClaimLeaseDuration <= TimeSpan.Zero) |
| | | 162 | | { |
| | 2 | 163 | | throw new InvalidOperationException($"{nameof(ClaimLeaseDuration)} must be greater than TimeSpan.Zero."); |
| | | 164 | | } |
| | | 165 | | |
| | 108 | 166 | | if (ClaimPageSize <= 0) |
| | | 167 | | { |
| | 1 | 168 | | throw new InvalidOperationException($"{nameof(ClaimPageSize)} must be greater than zero."); |
| | | 169 | | } |
| | | 170 | | |
| | 107 | 171 | | if (MaxClaimAttempts <= 0) |
| | | 172 | | { |
| | 1 | 173 | | throw new InvalidOperationException($"{nameof(MaxClaimAttempts)} must be greater than zero."); |
| | | 174 | | } |
| | | 175 | | |
| | 106 | 176 | | if (string.IsNullOrWhiteSpace(MaxClaimAttemptsReasonCode)) |
| | | 177 | | { |
| | 0 | 178 | | throw new InvalidOperationException($"{nameof(MaxClaimAttemptsReasonCode)} is required."); |
| | | 179 | | } |
| | | 180 | | |
| | 106 | 181 | | if (string.IsNullOrWhiteSpace(MaxClaimAttemptsReasonMessage)) |
| | | 182 | | { |
| | 0 | 183 | | throw new InvalidOperationException($"{nameof(MaxClaimAttemptsReasonMessage)} is required."); |
| | | 184 | | } |
| | | 185 | | |
| | 106 | 186 | | if (UseClaimLeases && string.IsNullOrWhiteSpace(ClaimWorkerId)) |
| | | 187 | | { |
| | 3 | 188 | | throw new InvalidOperationException($"{nameof(ClaimWorkerId)} is required when {nameof(UseClaimLeases)} is e |
| | | 189 | | } |
| | 103 | 190 | | } |
| | | 191 | | |
| | | 192 | | private static void ValidateDelay(TimeSpan delay, string propertyName) |
| | | 193 | | { |
| | 235 | 194 | | if (delay < TimeSpan.Zero) |
| | | 195 | | { |
| | 6 | 196 | | throw new InvalidOperationException($"{propertyName} must be greater than or equal to TimeSpan.Zero."); |
| | | 197 | | } |
| | 229 | 198 | | } |
| | | 199 | | } |