/* * Copyright 2026 Flix Authors * * Use of this source code is governed by the Apache 2.0 license * that can be found in the LICENSE.md file. */////// Functions for retrying HTTP requests.///pubmod Net.Retry {use Math.Randomuse Net.HttpRequestuse Net.HttpResponseuse Net.RetryDecisionuse Net.RetryOutcomeuse Time.Clockuse Time.Durationuse Time.Duration.{nanoseconds, milliseconds, seconds, toMillis, toNanos}////// Returns a strategy that never retries.///pubdefnoRetry(): (Int32, HttpRequest, RetryOutcome) -> RetryDecision = (_, _, _) -> RetryDecision.Stop////// Returns a strategy that retries up to `maxRetries` times with a fixed `delay`.///pubdeflinear(mr: {maxRetries = Int32}, dm: {delay = Duration}): (Int32, HttpRequest, RetryOutcome) -> RetryDecision =letmaxRetries = mr#maxRetries;letdelay = dm#delay; (attempt, _, _) ->if (attempt<maxRetries) RetryDecision.Retry(delay)else RetryDecision.Stop////// Returns a strategy with exponential backoff.////// Retries up to `maxRetries` times with delays: `baseDelay`, `2*base`, `4*base`, …///pubdefexponential(mr: {maxRetries = Int32}, bdm: {baseDelay = Duration}): (Int32, HttpRequest, RetryOutcome) -> RetryDecision =letmaxRetries = mr#maxRetries;letbaseNanos = toNanos(bdm#baseDelay); (attempt, _, _) ->if (attempt<maxRetries)// Clamp shift to 62 to prevent shift overflow, then cap the product at Int64.maxValue to prevent multiplication wrap-around.letdelay = baseNanos*Int64.leftShift(1i64, Int32.min(attempt, 62));letclamped = if (delay<=0i64) Int64.maxValue()elsedelay; RetryDecision.Retry(nanoseconds(clamped))else RetryDecision.Stop////// Returns a strategy that retries up to `maxRetries` times, honouring an integer-seconds/// `Retry-After` response header on 429/503 responses.////// Falls back to `baseDelay` when the header is absent or unparseable.///pubdefretryAfter(mr: {maxRetries = Int32}, bdm: {baseDelay = Duration}): (Int32, HttpRequest, RetryOutcome) -> RetryDecision =letmaxRetries = mr#maxRetries;letbaseDelay = bdm#baseDelay; (attempt, _, outcome) ->if (attempt>=maxRetries) RetryDecision.Stopelseletdelay = matchoutcome {case RetryOutcome.HttpError(resp) =>matchHttpResponse.headerValue("retry-after", resp) {case Some(s) => matchInt64.fromString(s) {// Clamp to Int32 range to guard against absurd Retry-After values from untrusted servers.case Some(secs) => seconds(Int64.clampToInt32(min = 0, max = Int32.maxValue(), secs))case None => baseDelay }case None => baseDelay }case RetryOutcome.TransportError(_) => baseDelay }; RetryDecision.Retry(delay)////// Returns a strategy that retries up to `maxRetries` times with a fixed `delay`,/// but only on transport errors (connection refused, timeout, etc.).////// HTTP-level errors (non-2xx responses) are never retried.///pubdefretryTransportOnly(mr: {maxRetries = Int32}, dm: {delay = Duration}): (Int32, HttpRequest, RetryOutcome) -> RetryDecision =letmaxRetries = mr#maxRetries;letdelay = dm#delay; (attempt, _, outcome) -> matchoutcome {case RetryOutcome.TransportError(_) ifattempt<maxRetries => RetryDecision.Retry(delay)case_ => RetryDecision.Stop }////// Wraps `inner` by adding uniform random jitter in `[0, maxJitter]` to any `Retry` delay.///pubdefwithJitter(mjm: {maxJitter = Duration},inner: (Int32, HttpRequest, RetryOutcome) -> RetryDecision \ ef ): (Int32, HttpRequest, RetryOutcome) -> RetryDecision \ ef + Random =letmaxJitterNanos = toNanos(mjm#maxJitter); (attempt, req, outcome) ->letbase = inner(attempt, req, outcome);matchbase {case RetryDecision.Stop => RetryDecision.Stopcase RetryDecision.Retry(d) =>// Mask the sign bit instead of abs to avoid Int64.minValue() staying negative.letj = Int64.bitwiseAnd(Random.randomInt64(), Int64.maxValue()) `Int64.remainder` (maxJitterNanos+1i64); RetryDecision.Retry(d+nanoseconds(j)) }////// Wraps `inner` and clamps any `Retry` delay to the time remaining before the `deadline`.////// `startMs` should be the result of `Clock.now()` captured before entering the retry loop./// Returns `Stop` if the deadline is already past.///pubdefwithDeadline(dlm: {deadline = Duration},sm: {startMs = Int64},inner: (Int32, HttpRequest, RetryOutcome) -> RetryDecision \ ef ): (Int32, HttpRequest, RetryOutcome) -> RetryDecision \ ef + Clock =letdeadlineMs = toMillis(dlm#deadline);letstartMs = sm#startMs; (attempt, req, outcome) ->letbase = inner(attempt, req, outcome);matchbase {case RetryDecision.Stop => RetryDecision.Stopcase RetryDecision.Retry(d) =>letelapsed = Clock.now()-startMs;if (elapsed>=deadlineMs) RetryDecision.Stopelseletms = Int64.min(toMillis(d), deadlineMs-elapsed); RetryDecision.Retry(milliseconds(Int64.clampToInt32(min = 0, max = Int32.maxValue(), ms))) }}