using System.Net; namespace ClawdDotNet.Core.Api; /// /// Entscheidet, ob ein fehlgeschlagener API-Aufruf wiederholt wird und wie lange /// vorher gewartet wird. /// /// Hintergrund (B12): Der Client warf bei jedem Nicht-2xx sofort. Ein einzelnes /// HTTP 429 beendete damit einen kompletten geplanten Lauf — bei OpenRouter sind /// 429 und 5xx im Normalbetrieb aber zu erwarten. /// /// Die Logik ist bewusst frei von Wartezeiten und Zufall an der Aufrufstelle, damit /// sie ohne echtes Warten testbar bleibt. /// public sealed class RetryPolicy { /// Gesamtzahl der Versuche, inklusive des ersten. public int MaxAttempts { get; init; } = 4; public TimeSpan BaseDelay { get; init; } = TimeSpan.FromSeconds(1); public TimeSpan MaxDelay { get; init; } = TimeSpan.FromSeconds(30); /// Anteil zufälliger Streuung, damit parallele Agenten nicht im Gleichtakt erneut anfragen. public double JitterFactor { get; init; } = 0.25; public static RetryPolicy Default { get; } = new(); /// Kein Wiederholen — für Tests und Sonderfälle. public static RetryPolicy None { get; } = new() { MaxAttempts = 1 }; public bool ShouldRetry(HttpStatusCode status) => (int)status switch { 408 => true, // Request Timeout 409 => false, 425 => true, // Too Early 429 => true, // Rate Limit — der häufigste Fall >= 500 and <= 599 => true, // Serverseitige Störungen _ => false }; /// /// Wartezeit vor dem nächsten Versuch. ist 1-basiert: /// 1 bedeutet, dass der erste Versuch fehlgeschlagen ist. /// /// Ein vom Server gesendetes Retry-After hat Vorrang — es ist die verlässlichste /// Angabe und zu ignorieren führt nur zu weiteren Ablehnungen. /// public TimeSpan GetDelay(int attempt, TimeSpan? retryAfter = null, double? jitterSample = null) { if (retryAfter is { } serverHint && serverHint > TimeSpan.Zero) return serverHint > MaxDelay ? MaxDelay : serverHint; // Exponentiell: 1s, 2s, 4s, 8s … var exponent = Math.Max(0, attempt - 1); var scaled = BaseDelay.TotalMilliseconds * Math.Pow(2, exponent); if (scaled > MaxDelay.TotalMilliseconds) scaled = MaxDelay.TotalMilliseconds; // Streuung im Bereich ±JitterFactor var sample = jitterSample ?? Random.Shared.NextDouble(); var jitter = 1.0 + (sample * 2 - 1) * JitterFactor; var result = scaled * jitter; if (result < 0) result = 0; return TimeSpan.FromMilliseconds(Math.Min(result, MaxDelay.TotalMilliseconds)); } }