Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ let package = Package(
.library(name: "AsyncAlgorithms", targets: ["AsyncAlgorithms"])
],
targets: [
.systemLibrary(name: "_CAsyncSequenceValidationSupport"),
.target(name: "_CPowSupport"),
.target(
name: "AsyncAlgorithms",
dependencies: [
"_CPowSupport",
.product(name: "OrderedCollections", package: "swift-collections"),
.product(name: "DequeModule", package: "swift-collections"),
],
Expand All @@ -33,7 +36,6 @@ let package = Package(
.enableExperimentalFeature("StrictConcurrency=complete")
]
),
.systemLibrary(name: "_CAsyncSequenceValidationSupport"),
.target(
name: "AsyncAlgorithms_XCTest",
dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation"],
Expand Down
198 changes: 198 additions & 0 deletions Sources/AsyncAlgorithms/Retry/Backoff.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import _CPowSupport

#if compiler(<6.2)
@available(iOS 18.0, macCatalyst 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, watchOS 11.0, *)
extension Duration {
@usableFromInline var attoseconds: Int128 {
return Int128(_low: _low, _high: _high)
}
@usableFromInline init(attoseconds: Int128) {
self.init(_high: attoseconds._high, low: attoseconds._low)
}
}
#endif

@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
public protocol BackoffStrategy<Duration> {
associatedtype Duration: DurationProtocol
mutating func duration(_ attempt: Int) -> Duration
mutating func duration(_ attempt: Int, using generator: inout some RandomNumberGenerator) -> Duration
}

@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
extension BackoffStrategy {
@inlinable public mutating func duration(_ attempt: Int, using generator: inout some RandomNumberGenerator) -> Duration {
return duration(attempt)
}
}

@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
@usableFromInline
struct ConstantBackoffStrategy<Duration: DurationProtocol>: BackoffStrategy {
@usableFromInline let c: Duration
@usableFromInline init(c: Duration) {
self.c = c
}
@inlinable func duration(_ attempt: Int) -> Duration {
return c
}
}

@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
@usableFromInline
struct LinearBackoffStrategy<Duration: DurationProtocol>: BackoffStrategy {
@usableFromInline let a: Duration
@usableFromInline let b: Duration
@usableFromInline init(a: Duration, b: Duration) {
self.a = a
self.b = b
}
@inlinable func duration(_ attempt: Int) -> Duration {
return a * attempt + b
}
}

@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
@usableFromInline struct ExponentialBackoffStrategy: BackoffStrategy {
@usableFromInline let a: Duration
@usableFromInline let b: Double
@usableFromInline init(a: Duration, b: Double) {
self.a = a
self.b = b
}
@inlinable func duration(_ attempt: Int) -> Duration {
return a * pow(b, Double(attempt))
}
}

@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
@usableFromInline
struct MinimumBackoffStrategy<Base: BackoffStrategy>: BackoffStrategy {
@usableFromInline var base: Base
@usableFromInline let minimum: Base.Duration
@usableFromInline init(base: Base, minimum: Base.Duration) {
self.base = base
self.minimum = minimum
}
@inlinable mutating func duration(_ attempt: Int) -> Base.Duration {
return max(minimum, base.duration(attempt))
}
@inlinable mutating func duration(_ attempt: Int, using generator: inout some RandomNumberGenerator) -> Base.Duration {
return max(minimum, base.duration(attempt, using: &generator))
}
}

@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
@usableFromInline
struct MaximumBackoffStrategy<Base: BackoffStrategy>: BackoffStrategy {
@usableFromInline var base: Base
@usableFromInline let maximum: Base.Duration
@usableFromInline init(base: Base, maximum: Base.Duration) {
self.base = base
self.maximum = maximum
}
@inlinable mutating func duration(_ attempt: Int) -> Base.Duration {
return min(maximum, base.duration(attempt))
}
@inlinable mutating func duration(_ attempt: Int, using generator: inout some RandomNumberGenerator) -> Base.Duration {
return min(maximum, base.duration(attempt, using: &generator))
}
}

@available(iOS 18.0, macCatalyst 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, watchOS 11.0, *)
@usableFromInline
struct FullJitterBackoffStrategy<Base: BackoffStrategy>: BackoffStrategy where Base.Duration == Swift.Duration {
@usableFromInline var base: Base
@usableFromInline init(base: Base) {
self.base = base
}
@inlinable mutating func duration(_ attempt: Int) -> Base.Duration {
return .init(attoseconds: Int128.random(in: 0...base.duration(attempt).attoseconds))
}
@inlinable mutating func duration(_ attempt: Int, using generator: inout some RandomNumberGenerator) -> Base.Duration {
return .init(attoseconds: Int128.random(in: 0...base.duration(attempt, using: &generator).attoseconds, using: &generator))
}
}

@available(iOS 18.0, macCatalyst 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, watchOS 11.0, *)
@usableFromInline
struct EqualJitterBackoffStrategy<Base: BackoffStrategy>: BackoffStrategy where Base.Duration == Swift.Duration {
@usableFromInline var base: Base
@usableFromInline init(base: Base) {
self.base = base
}
@inlinable mutating func duration(_ attempt: Int) -> Base.Duration {
let halfBase = (base.duration(attempt) / 2).attoseconds
return .init(attoseconds: halfBase + Int128.random(in: 0...halfBase))
}
@inlinable mutating func duration(_ attempt: Int, using generator: inout some RandomNumberGenerator) -> Base.Duration {
let halfBase = (base.duration(attempt, using: &generator) / 2).attoseconds
return .init(attoseconds: halfBase + Int128.random(in: 0...halfBase, using: &generator))
}
}

@available(iOS 18.0, macCatalyst 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, watchOS 11.0, *)
@usableFromInline
struct DecorrelatedJitterBackoffStrategy<Base: BackoffStrategy>: BackoffStrategy where Base.Duration == Swift.Duration {
@usableFromInline var base: Base
@usableFromInline let divisor: Int128
@usableFromInline var previousDuration: Duration?
@usableFromInline init(base: Base, divisor: Int128) {
self.base = base
self.divisor = divisor
}
@inlinable mutating func duration(_ attempt: Int) -> Base.Duration {
let base = base.duration(attempt)
let previousDuration = previousDuration ?? base
self.previousDuration = previousDuration
return .init(attoseconds: Int128.random(in: base.attoseconds...previousDuration.attoseconds / divisor))
}
@inlinable mutating func duration(_ attempt: Int, using generator: inout some RandomNumberGenerator) -> Base.Duration {
let base = base.duration(attempt, using: &generator)
let previousDuration = previousDuration ?? base
self.previousDuration = previousDuration
return .init(attoseconds: Int128.random(in: base.attoseconds...previousDuration.attoseconds / divisor, using: &generator))
}
}

@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
public enum Backoff {
@inlinable public static func constant<Duration: DurationProtocol>(_ c: Duration) -> some BackoffStrategy<Duration> {
return ConstantBackoffStrategy(c: c)
}
@inlinable public static func constant(_ c: Duration) -> some BackoffStrategy<Duration> {
return ConstantBackoffStrategy(c: c)
}
@inlinable public static func linear<Duration: DurationProtocol>(increment a: Duration, initial b: Duration) -> some BackoffStrategy<Duration> {
return LinearBackoffStrategy(a: a, b: b)
}
@inlinable public static func linear(increment a: Duration, initial b: Duration) -> some BackoffStrategy<Duration> {
return LinearBackoffStrategy(a: a, b: b)
}
@inlinable public static func exponential(multiplier b: Double = 2, initial a: Duration) -> some BackoffStrategy<Duration> {
return ExponentialBackoffStrategy(a: a, b: b)
}
}

@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
extension BackoffStrategy {
@inlinable public func minimum(_ minimum: Duration) -> some BackoffStrategy<Duration> {
return MinimumBackoffStrategy(base: self, minimum: minimum)
}
@inlinable public func maximum(_ maximum: Duration) -> some BackoffStrategy<Duration> {
return MaximumBackoffStrategy(base: self, maximum: maximum)
}
}

@available(iOS 18.0, macCatalyst 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, watchOS 11.0, *)
extension BackoffStrategy where Duration == Swift.Duration {
@inlinable public func fullJitter() -> some BackoffStrategy<Duration> {
return FullJitterBackoffStrategy(base: self)
}
@inlinable public func equalJitter() -> some BackoffStrategy<Duration> {
return EqualJitterBackoffStrategy(base: self)
}
@inlinable public func decorrelatedJitter(divisor: Int = 3) -> some BackoffStrategy<Duration> {
return DecorrelatedJitterBackoffStrategy(base: self, divisor: Int128(divisor))
}
}
51 changes: 51 additions & 0 deletions Sources/AsyncAlgorithms/Retry/Retry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
public enum RetryStrategy<Duration: DurationProtocol> {
case backoff(Duration)
case stop
}

@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
@inlinable
public func retry<Result, ErrorType, ClockType>(
maxAttempts: Int = 3,
tolerance: ClockType.Instant.Duration? = nil,
clock: ClockType,
isolation: isolated (any Actor)? = #isolation,
operation: () async throws(ErrorType) -> sending Result,
strategy: (_ attempt: Int, ErrorType) -> RetryStrategy<ClockType.Instant.Duration> = { _, _ in .backoff(.zero) }
) async throws -> Result where ClockType: Clock, ErrorType: Error {
precondition(maxAttempts > 0, "Must have at least one attempt")
for attempt in 0..<maxAttempts - 1 {
do {
return try await operation()
} catch where Task.isCancelled {
throw error
} catch {
switch strategy(attempt, error) {
case .backoff(let duration):
try await Task.sleep(for: duration, tolerance: tolerance, clock: clock)
case .stop:
throw error
}
}
}
return try await operation()
}

@available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, visionOS 1.0, watchOS 9.0, *)
@inlinable
public func retry<Result, ErrorType>(
maxAttempts: Int = 3,
tolerance: ContinuousClock.Instant.Duration? = nil,
isolation: isolated (any Actor)? = #isolation,
operation: () async throws(ErrorType) -> sending Result,
strategy: (_ attempt: Int, ErrorType) -> RetryStrategy<ContinuousClock.Instant.Duration> = { _, _ in .backoff(.zero) }
) async throws -> Result where ErrorType: Error {
return try await retry(
maxAttempts: maxAttempts,
tolerance: tolerance,
clock: ContinuousClock(),
operation: operation,
strategy: strategy
)
}
1 change: 1 addition & 0 deletions Sources/_CPowSupport/_CPowSupport.c
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#include "_CPowSupport.h"
3 changes: 3 additions & 0 deletions Sources/_CPowSupport/include/_CPowSupport.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
static inline __attribute__((__always_inline__)) double pow(double x, double y) {
return __builtin_pow(x, y);
}