rastrillo / native Public

Clone
git clone https://amadan.net/rastrillo/native

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2
3/// Retry delays for sends and stream reconnects: 0.5 s doubling to 10 s, each
4/// with equal jitter (a random point in [d/2, d]), and Retry-After honoured up
5/// to 5 minutes. These are callscore's numbers, so a suite of apps retrying
6/// against one server spreads out instead of arriving in lockstep.
7///
8/// A delay is only a timer. It must never run while the network path is down:
9/// wait on `NativeConnectivity` instead, so an offline phone makes no timed
10/// attempts at all.
11public struct Backoff: Sendable {
12 public static let initial: TimeInterval = 0.5
13 public static let cap: TimeInterval = 10
14 public static let retryAfterCap: TimeInterval = 300
15 /// A connection that lived longer than this was working; the next failure
16 /// starts again from `initial` rather than where the last outage left off.
17 public static let stableConnection: TimeInterval = 5
18
19 private var attempt = 0
20 private let random: @Sendable (ClosedRange<Double>) -> Double
21
22 public init(random: @escaping @Sendable (ClosedRange<Double>) -> Double = { Double.random(in: $0) }) {
23 self.random = random
24 }
25
26 /// The delay before the next attempt. A server's Retry-After is a floor.
27 public mutating func next(retryAfter: TimeInterval? = nil) -> TimeInterval {
28 let delay = min(Self.cap, Self.initial * pow(2, Double(attempt)))
29 attempt = min(attempt + 1, 32)
30 let jittered = random((delay / 2)...delay)
31 guard let retryAfter, retryAfter > 0 else { return jittered }
32 return max(jittered, min(retryAfter, Self.retryAfterCap))
33 }
34
35 /// After a success, a path change or a return to the foreground.
36 public mutating func reset() { attempt = 0 }
37
38 /// A stream was accepted: its `onOpen` ran. `at` is any monotonic time in
39 /// seconds, such as `ProcessInfo.processInfo.systemUptime`.
40 public mutating func opened(at time: TimeInterval) { openedAt = time }
41
42 /// The attempt is over. Resets only if a stream opened and then stayed
43 /// open longer than `stableConnection`. A slow refusal, a connect that
44 /// timed out, or an access check after a failure never opened anything,
45 /// so however long it took it is not evidence the server was working.
46 public mutating func ended(at time: TimeInterval) {
47 if let openedAt, time - openedAt > Self.stableConnection { reset() }
48 openedAt = nil
49 }
50
51 private var openedAt: TimeInterval?
52}
53