Hello,
the last couple of years I had to work on numerous iOS projects that had to communicate with different pieces of hardware and different communication protocols. Doing so, my goal was to fully embrace Swift Concurrency. In another post I highlighted some problems I ran into.
While solving these, I could extract two µ-libraries which I've open sourced: swift-concurrency-deadline and swift-concurrency-retry and I think they have now reached a somewhat stable state where others could also profit from using them.
My goal was to achieve this without adding any dependencies, minimizing binary size increases. Neither Retry nor Deadline have any imports [1].
Key features for both of them:
- Uses structured concurrency
- Have actor isolation inheritance
- Have support for custom clocks, enabling precise control in tests
- Have a lot of documentation (but I guess you could always improve docs )
- Have almost 100% test coverage
A quick overview:
Deadline
This library comes with two free functions, one with a generic clock and another one which uses the ContinuousClock
as default. These functions provide a mechanism for enforcing deadlines on asynchronous operations that lack native support.
It creates a TaskGroup
with two concurrent tasks: the provided operation and a sleep task. If the operation doesn’t complete on time, it throws a DeadlineExceededError
.
public func deadline<C, R>(
until instant: C.Instant,
tolerance: C.Instant.Duration? = nil,
clock: C,
isolation: isolated (any Actor)? = #isolation,
operation: @Sendable () async throws -> R
) async throws -> R where C: Clock, R: Sendable { ... }
public func deadline<R>(
until instant: ContinuousClock.Instant,
tolerance: ContinuousClock.Instant.Duration? = nil,
isolation: isolated (any Actor)? = #isolation,
operation: @Sendable () async throws -> R
) async throws -> R where R: Sendable { ... }
A very simple/naive[2] example:
try await deadline(until: .now + .seconds(5)) {
try await URLSession.shared.data(from: url) // Operation to complete before deadline
}
You can check out more documentation and examples at Github: GitHub - ph1ps/swift-concurrency-deadline: A deadline algorithm for Swift Concurrency
Retry
This library, again, comes with two free functions, one with a generic clock and another one which uses the ContinuousClock
as default. These functions perform an asynchronous operation and retry it up to a specified number of attempts if it encounters an error.
You can define a custom retry strategy based on the type of error encountered, and control the delay between attempts with a BackoffStrategy
. It already ships with 4 common backoff strategies (none
, constant
, linear
, exponential
) and two "modifiers" (max
, jitter
).
public func retry<R, E, C>(
maxAttempts: Int = 3,
tolerance: C.Duration? = nil,
clock: C,
isolation: isolated (any Actor)? = #isolation,
operation: () async throws(E) -> sending R,
strategy: (E) -> RetryStrategy<C> = { _ in .backoff(.none) }
) async throws -> R where C: Clock, E: Error { ... }
public func retry<R, E>(
maxAttempts: Int = 3,
tolerance: ContinuousClock.Duration? = nil,
isolation: isolated (any Actor)? = #isolation,
operation: () async throws(E) -> sending R,
strategy: (E) -> RetryStrategy<ContinuousClock> = { _ in .backoff(.none) }
) async throws -> R where E: Error { ... }
Another quick example usage:
let (data, response) = try await retry(maxAttempts: 5) {
try await URLSession.shared.data(from: url) // Retrying the network call
} strategy: { error in
if error.isRecoverable {
return .backoff(.exponential(a: .milliseconds(100), b: 2)) // Exponential backoff
} else {
return .stop // Stop retries for unrecoverable errors
}
}
You can check out more documentation and examples at Github: GitHub - ph1ps/swift-concurrency-retry: A retry algorithm for Swift Concurrency
Take a look, try them out. I’d love to hear your thoughts, feedback, and use cases for these libraries! If you find them useful or have ideas for improvements, feel free to share or contribute on Github.
Thank you for reading, and I hope these libraries help you build more resilient and reliable Swift apps!
Except an internal target which I used for getting
pow
without requiringFoundation
. A really cool trick I saw swift-numerics was using. ↩︎In this example, I use
URLSession
for demonstration. In real-world scenarios, you might use the timeout property ofURLRequest
instead. Showing how you'd usedeadline
inCoreBluetooth
, which would be more realistic, would require a lot of boilerplate code. ↩︎