Skip to content

Commit c75fc64

Browse files
committed
add retry&backoff
1 parent c537393 commit c75fc64

File tree

5 files changed

+256
-1
lines changed

5 files changed

+256
-1
lines changed

Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ let package = Package(
1616
.library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"])
1717
],
1818
targets: [
19+
.systemLibrary(name: "_CAsyncSequenceValidationSupport"),
20+
.target(name: "_CPowSupport"),
1921
.target(
2022
name: "AsyncAlgorithms",
2123
dependencies: [
24+
"_CPowSupport",
2225
.product(name: "OrderedCollections", package: "swift-collections"),
2326
.product(name: "DequeModule", package: "swift-collections"),
2427
],
@@ -33,7 +36,6 @@ let package = Package(
3336
.enableExperimentalFeature("StrictConcurrency=complete")
3437
]
3538
),
36-
.systemLibrary(name: "_CAsyncSequenceValidationSupport"),
3739
.target(
3840
name: "AsyncAlgorithms_XCTest",
3941
dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"],
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import _CPowSupport
2+
3+
#if compiler(<6.2)
4+
@available(iOS 18.0, macCatalyst 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, watchOS 11.0, *)
5+
extension Duration {
6+
@usableFromInline var attoseconds: Int128 {
7+
return Int128(_low: _low, _high: _high)
8+
}
9+
@usableFromInline init(attoseconds: Int128) {
10+
self.init(_high: attoseconds._high, low: attoseconds._low)
11+
}
12+
}
13+
#endif
14+
15+
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
16+
public protocol BackoffStrategy<Duration> {
17+
associatedtype Duration: DurationProtocol
18+
mutating func duration(_ attempt: Int) -> Duration
19+
mutating func duration(_ attempt: Int, using generator: inout some RandomNumberGenerator) -> Duration
20+
}
21+
22+
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
23+
extension BackoffStrategy {
24+
public mutating func duration(_ attempt: Int, using generator: inout some RandomNumberGenerator) -> Duration {
25+
return duration(attempt)
26+
}
27+
}
28+
29+
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
30+
@usableFromInline
31+
struct ConstantBackoffStrategy<Duration: DurationProtocol>: BackoffStrategy {
32+
@usableFromInline let c: Duration
33+
@usableFromInline init(c: Duration) {
34+
self.c = c
35+
}
36+
@inlinable func duration(_ attempt: Int) -> Duration {
37+
return c
38+
}
39+
}
40+
41+
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
42+
@usableFromInline
43+
struct LinearBackoffStrategy<Duration: DurationProtocol>: BackoffStrategy {
44+
@usableFromInline let a: Duration
45+
@usableFromInline let b: Duration
46+
@usableFromInline init(a: Duration, b: Duration) {
47+
self.a = a
48+
self.b = b
49+
}
50+
@inlinable func duration(_ attempt: Int) -> Duration {
51+
return a * attempt + b
52+
}
53+
}
54+
55+
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
56+
@usableFromInline struct ExponentialBackoffStrategy: BackoffStrategy {
57+
@usableFromInline let a: Duration
58+
@usableFromInline let b: Double
59+
@usableFromInline init(a: Duration, b: Double) {
60+
self.a = a
61+
self.b = b
62+
}
63+
@inlinable func duration(_ attempt: Int) -> Duration {
64+
return a * pow(b, Double(attempt))
65+
}
66+
}
67+
68+
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
69+
@usableFromInline
70+
struct MinimumBackoffStrategy<Base: BackoffStrategy>: BackoffStrategy {
71+
@usableFromInline var base: Base
72+
@usableFromInline let minimum: Base.Duration
73+
@usableFromInline init(base: Base, minimum: Base.Duration) {
74+
self.base = base
75+
self.minimum = minimum
76+
}
77+
@inlinable mutating func duration(_ attempt: Int) -> Base.Duration {
78+
return max(minimum, base.duration(attempt))
79+
}
80+
@inlinable mutating func duration(_ attempt: Int, using generator: inout some RandomNumberGenerator) -> Base.Duration {
81+
return max(minimum, base.duration(attempt, using: &generator))
82+
}
83+
}
84+
85+
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
86+
@usableFromInline
87+
struct MaximumBackoffStrategy<Base: BackoffStrategy>: BackoffStrategy {
88+
@usableFromInline var base: Base
89+
@usableFromInline let maximum: Base.Duration
90+
@usableFromInline init(base: Base, maximum: Base.Duration) {
91+
self.base = base
92+
self.maximum = maximum
93+
}
94+
@inlinable mutating func duration(_ attempt: Int) -> Base.Duration {
95+
return min(maximum, base.duration(attempt))
96+
}
97+
@inlinable mutating func duration(_ attempt: Int, using generator: inout some RandomNumberGenerator) -> Base.Duration {
98+
return min(maximum, base.duration(attempt, using: &generator))
99+
}
100+
}
101+
102+
@available(iOS 18.0, macCatalyst 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, watchOS 11.0, *)
103+
@usableFromInline
104+
struct FullJitterBackoffStrategy<Base: BackoffStrategy>: BackoffStrategy where Base.Duration == Swift.Duration {
105+
@usableFromInline var base: Base
106+
@usableFromInline init(base: Base) {
107+
self.base = base
108+
}
109+
@inlinable mutating func duration(_ attempt: Int) -> Base.Duration {
110+
return .init(attoseconds: Int128.random(in: 0...base.duration(attempt).attoseconds))
111+
}
112+
@inlinable mutating func duration(_ attempt: Int, using generator: inout some RandomNumberGenerator) -> Base.Duration {
113+
return .init(attoseconds: Int128.random(in: 0...base.duration(attempt, using: &generator).attoseconds, using: &generator))
114+
}
115+
}
116+
117+
@available(iOS 18.0, macCatalyst 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, watchOS 11.0, *)
118+
@usableFromInline
119+
struct EqualJitterBackoffStrategy<Base: BackoffStrategy>: BackoffStrategy where Base.Duration == Swift.Duration {
120+
@usableFromInline var base: Base
121+
@usableFromInline init(base: Base) {
122+
self.base = base
123+
}
124+
@inlinable mutating func duration(_ attempt: Int) -> Base.Duration {
125+
let halfBase = (base.duration(attempt) / 2).attoseconds
126+
return .init(attoseconds: halfBase + Int128.random(in: 0...halfBase))
127+
}
128+
@inlinable mutating func duration(_ attempt: Int, using generator: inout some RandomNumberGenerator) -> Base.Duration {
129+
let halfBase = (base.duration(attempt, using: &generator) / 2).attoseconds
130+
return .init(attoseconds: halfBase + Int128.random(in: 0...halfBase, using: &generator))
131+
}
132+
}
133+
134+
@available(iOS 18.0, macCatalyst 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, watchOS 11.0, *)
135+
@usableFromInline
136+
struct DecorrelatedJitterBackoffStrategy<Base: BackoffStrategy>: BackoffStrategy where Base.Duration == Swift.Duration {
137+
@usableFromInline var base: Base
138+
@usableFromInline let divisor: Int128
139+
@usableFromInline var previousDuration: Duration?
140+
@usableFromInline init(base: Base, divisor: Int128) {
141+
self.base = base
142+
self.divisor = divisor
143+
}
144+
@inlinable mutating func duration(_ attempt: Int) -> Base.Duration {
145+
let base = base.duration(attempt)
146+
let previousDuration = previousDuration ?? base
147+
self.previousDuration = previousDuration
148+
return .init(attoseconds: Int128.random(in: base.attoseconds...previousDuration.attoseconds / divisor))
149+
}
150+
@inlinable mutating func duration(_ attempt: Int, using generator: inout some RandomNumberGenerator) -> Base.Duration {
151+
let base = base.duration(attempt, using: &generator)
152+
let previousDuration = previousDuration ?? base
153+
self.previousDuration = previousDuration
154+
return .init(attoseconds: Int128.random(in: base.attoseconds...previousDuration.attoseconds / divisor, using: &generator))
155+
}
156+
}
157+
158+
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
159+
public enum Backoff {
160+
@inlinable public static func constant<Duration: DurationProtocol>(_ c: Duration) -> some BackoffStrategy<Duration> {
161+
return ConstantBackoffStrategy(c: c)
162+
}
163+
@inlinable public static func constant(_ c: Duration) -> some BackoffStrategy<Duration> {
164+
return ConstantBackoffStrategy(c: c)
165+
}
166+
@inlinable public static func linear<Duration: DurationProtocol>(increment a: Duration, initial b: Duration) -> some BackoffStrategy<Duration> {
167+
return LinearBackoffStrategy(a: a, b: b)
168+
}
169+
@inlinable public static func linear(increment a: Duration, initial b: Duration) -> some BackoffStrategy<Duration> {
170+
return LinearBackoffStrategy(a: a, b: b)
171+
}
172+
@inlinable public static func exponential(multiplier b: Double = 2, initial a: Duration) -> some BackoffStrategy<Duration> {
173+
return ExponentialBackoffStrategy(a: a, b: b)
174+
}
175+
}
176+
177+
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
178+
extension BackoffStrategy {
179+
@inlinable public func minimum(_ minimum: Duration) -> some BackoffStrategy<Duration> {
180+
return MinimumBackoffStrategy(base: self, minimum: minimum)
181+
}
182+
@inlinable public func maximum(_ maximum: Duration) -> some BackoffStrategy<Duration> {
183+
return MaximumBackoffStrategy(base: self, maximum: maximum)
184+
}
185+
}
186+
187+
@available(iOS 18.0, macCatalyst 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, watchOS 11.0, *)
188+
extension BackoffStrategy where Duration == Swift.Duration {
189+
@inlinable public func fullJitter() -> some BackoffStrategy<Duration> {
190+
return FullJitterBackoffStrategy(base: self)
191+
}
192+
@inlinable public func equalJitter() -> some BackoffStrategy<Duration> {
193+
return EqualJitterBackoffStrategy(base: self)
194+
}
195+
@inlinable public func decorrelatedJitter(divisor: Int = 3) -> some BackoffStrategy<Duration> {
196+
return DecorrelatedJitterBackoffStrategy(base: self, divisor: Int128(divisor))
197+
}
198+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
2+
public enum RetryStrategy<Duration: DurationProtocol> {
3+
case backoff(Duration)
4+
case stop
5+
}
6+
7+
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
8+
@inlinable
9+
public func retry<Result, ErrorType, ClockType>(
10+
maxAttempts: Int = 3,
11+
tolerance: ClockType.Instant.Duration? = nil,
12+
clock: ClockType,
13+
isolation: isolated (any Actor)? = #isolation,
14+
operation: () async throws(ErrorType) -> sending Result,
15+
strategy: (_ attempt: Int, ErrorType) -> RetryStrategy<ClockType.Instant.Duration> = { _, _ in .backoff(.zero) }
16+
) async throws -> Result where ClockType: Clock, ErrorType: Error {
17+
precondition(maxAttempts >= 0, "Must have at least one attempt")
18+
for attempt in 0..<maxAttempts - 1 {
19+
do {
20+
return try await operation()
21+
} catch where Task.isCancelled {
22+
throw error
23+
} catch {
24+
switch strategy(attempt, error) {
25+
case .backoff(let duration):
26+
try await Task.sleep(for: duration, tolerance: tolerance, clock: clock)
27+
case .stop:
28+
throw error
29+
}
30+
}
31+
}
32+
return try await operation()
33+
}
34+
35+
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
36+
@inlinable
37+
public func retry<Result, ErrorType>(
38+
maxAttempts: Int = 3,
39+
tolerance: ContinuousClock.Instant.Duration? = nil,
40+
isolation: isolated (any Actor)? = #isolation,
41+
operation: () async throws(ErrorType) -> sending Result,
42+
strategy: (_ attempt: Int, ErrorType) -> RetryStrategy<ContinuousClock.Instant.Duration> = { _, _ in .backoff(.zero) }
43+
) async throws -> Result where ErrorType: Error {
44+
return try await retry(
45+
maxAttempts: maxAttempts,
46+
tolerance: tolerance,
47+
clock: ContinuousClock(),
48+
operation: operation,
49+
strategy: strategy
50+
)
51+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
#include "_CPowSupport.h"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
static inline __attribute__((__always_inline__)) double pow(double x, double y) {
2+
return __builtin_pow(x, y);
3+
}

0 commit comments

Comments
 (0)