| | | 1 | | using System.Text; |
| | | 2 | | |
| | | 3 | | namespace AsyncResponse; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Shared startup check that a transport's worst-case graceful-shutdown spend fits inside the |
| | | 7 | | /// mirrored <c>HostShutdownTimeout</c>. Early-ACK subscribers keep working after the broker ACK, |
| | | 8 | | /// so on shutdown the host must wait for the background drain (plus, on some transports, a bounded |
| | | 9 | | /// connection close/join) — if that budget exceeds what the Generic Host allows, work that was |
| | | 10 | | /// already ACKed is silently lost. Each transport passes only the timeouts it actually spends at |
| | | 11 | | /// shutdown; a single shared comparison keeps the ten hand-copied checks from drifting again. |
| | | 12 | | /// </summary> |
| | | 13 | | internal static class ShutdownBudgetValidator |
| | | 14 | | { |
| | | 15 | | /// <summary> |
| | | 16 | | /// Validates that the summed <paramref name="components"/> fit within |
| | | 17 | | /// <paramref name="hostShutdownTimeout"/>. A <c>null</c> budget opts out of the check |
| | | 18 | | /// (the caller validates it externally); a non-positive budget is a configuration error. |
| | | 19 | | /// </summary> |
| | | 20 | | /// <param name="transportName">Human-readable transport name used in the guidance text (e.g. "Kafka").</param> |
| | | 21 | | /// <param name="hostShutdownTimeoutName">Full option path of the transport's mirror option (e.g. "KafkaAsyncRespons |
| | | 22 | | /// <param name="hostShutdownTimeout">The configured mirror of <c>HostOptions.ShutdownTimeout</c>, or <c>null</c> to |
| | | 23 | | /// <param name="components">Option path + value of each timeout the transport spends during graceful shutdown.</par |
| | | 24 | | public static void Validate( |
| | | 25 | | string transportName, |
| | | 26 | | string hostShutdownTimeoutName, |
| | | 27 | | TimeSpan? hostShutdownTimeout, |
| | | 28 | | params (string Name, TimeSpan Value)[] components) |
| | | 29 | | { |
| | 3 | 30 | | if (hostShutdownTimeout is not { } budget) |
| | 2 | 31 | | return; |
| | | 32 | | |
| | 3 | 33 | | if (budget <= TimeSpan.Zero) |
| | 2 | 34 | | throw new InvalidOperationException($"{hostShutdownTimeoutName} must be positive when set."); |
| | | 35 | | |
| | 3 | 36 | | var total = TimeSpan.Zero; |
| | 3 | 37 | | foreach (var (_, value) in components) |
| | 3 | 38 | | total += value; |
| | | 39 | | |
| | | 40 | | // Equality is allowed: the host grants exactly ShutdownTimeout, so a budget that matches |
| | | 41 | | // it can still complete. Only a strictly larger spend guarantees truncation. |
| | 3 | 42 | | if (total <= budget) |
| | 3 | 43 | | return; |
| | | 44 | | |
| | 2 | 45 | | var itemized = new StringBuilder(); |
| | 2 | 46 | | for (var i = 0; i < components.Length; i++) |
| | | 47 | | { |
| | 2 | 48 | | if (i > 0) |
| | 2 | 49 | | itemized.Append(" plus "); |
| | 2 | 50 | | itemized.Append(components[i].Name).Append(" (").Append(components[i].Value).Append(')'); |
| | | 51 | | } |
| | | 52 | | |
| | 2 | 53 | | throw new InvalidOperationException( |
| | 2 | 54 | | $"{itemized} requires a shutdown budget of {total}, which exceeds " + |
| | 2 | 55 | | $"{hostShutdownTimeoutName} ({budget}). Increase Microsoft.Extensions.Hosting.HostOptions.ShutdownTimeout " |
| | 2 | 56 | | $"and mirror that value in {hostShutdownTimeoutName}, " + |
| | 2 | 57 | | $"or reduce the {transportName} shutdown/drain timeouts."); |
| | | 58 | | } |
| | | 59 | | } |