Introducing the `swift-retry` package!

Hey, Swift community!

I am excited to introduce swift-retry, my open-source package for retrying operations until successful. Hopefully, you will find the API easy to use because of its sensible defaults, yet flexible enough to accommodate all of your use cases.

I wasn’t able to find a similar package that satisfied all of my requirements, so I decided to publish my own package. Continue reading to learn about some of the design decisions that I made.


Basic Usage

try await retry {
   try await doSomething()
}

See the documentation for examples of more advanced use cases.

Overview

Designed for Swift Concurrency

The retry function is an async function that runs the given async closure repeatedly until it succeeds or until the failure is no longer retryable. The function sleeps in between attempts while respecting task cancellation.

Sensible Defaults

The library uses similar defaults as Amazon Web Services and Google Cloud.

An important but often overlooked default is the choice of backoff algorithm, which determines how long to sleep in between attempts. This library chooses an exponential backoff algorithm by default, which is suitable for most use cases. Most retry use cases involve a resource, such as a server, with potentially many clients where an exponential backoff algorithm would be ideal to avoid DDoSing the resource.

Powerful Flexibility

The API provides several customization points to accommodate any use case:

  • Retries can be selectively enabled or disabled for specific error cases by providing a custom recoverFromFailure closure. Retries can also be selectively enabled or disabled for specific code paths by wrapping thrown errors with Retryable or NotRetryable.
  • The RetryConfiguration type encapsulates the retry behavior so that it can be reused across multiple call sites without duplicating code.
  • The Backoff type represents the choice of algorithm that will be used to determine how long to sleep in between attempts. It has built-in support for common algorithms but can be initialized with a custom BackoffAlgorithm implementation if needed.
  • The clock that is used to sleep in between attempts can be replaced. For example, one might use a fake Clock implementation in automated tests to ensure the tests are deterministic and efficient.

Safe Retries

The module exposes a RetryableRequest protocol to add safe retry methods to a conforming request type. The retry methods in the protocol are similar to the top-level retry functions, but safer. The retry methods in the protocol enforce that the request is idempotent since it is unsafe to retry a non-idempotent request.

To retry HTTP requests, consider using the swift-http-error-handling package, which adds RetryableRequest conformance to the standard HTTPRequest type.


This package could be relevant to the Swift on Server community. Should I propose this package for incubation by the Swift Server Work Group?

13 Likes

Awesome, thanks! I was considering writing something like this myself to do things like try to generate a user's username handle based on their real name and keep adding some extra characters while it's still taken.

1 Like

That’s an intriguing use case. Thanks for sharing!

I made some changes to this package to help with developing the swift-http-error-handling package, which I just announced. The latest release is now 0.2.2.

Nice work :slight_smile:

May I suggest integrating with swift-metrics? A big part of libraries such as hystrix is exposing a lot of metrics about the operations wrapped. This way you can know if you service is in trouble or not -- so retrying should expose the rate of failures, eventually reset the backoff timers etc. Hope Hystrix can spark some ideas for the future of this library :slight_smile:

2 Likes

Thanks, Konrad!


Hystrix looks really useful for building resilient distributed systems. Thanks for sharing! The library looks quite comprehensive, featuring a convenient wrapper for dependencies that enforces resource quotas, publishes metrics, adds a circuit breaker, etc.

My intention was for swift-retry to be a modular package focused solely on retries while being flexible enough to be used as a building block by higher-level libraries like Hystrix.

For example, the RetryConfiguration type encapsulates all of the retry behavior, so it could be exposed in the public configuration API of a hypothetical Hystrix library. The hypothetical Hystrix library could override the recoverFromFailure closure to, for example, customize the handling of a circuit breaker error:

var configuration = configuration
configuration.recoverFromFailure = { error in
  switch error {
  case let error as CircuitBreakerIsOpen:
    // Do not bother retrying until after the circuit breaker
    // transitions from the open state to the half-open state.
    return .retryAfter(error.nextHalfOpenInstant)

  default:
    return configuration.recoverFromFailure(error)
  }
}

Regarding metrics, I’m not sure swift-retry is the right place for them. First, the metrics have per-application, per-operation namespaces and dimensions that the caller would have to provide. Second, different applications might have different requirements (e.g. a metric per error case vs. a single metric for all errors).

Given the variability in requirements, swift-retry wouldn’t be able to provide automatic metrics functionality without also providing additional API to configure that functionality. swift-retry should already be flexible enough for a caller to add their own metrics though:

let startInstant = clock.now
defer {
  let totalDuration = clock.now - startInstant
  timer.record(totalDuration)
}

try await retry {
  do {
    try await doSomething()
    successCounter.increment()
  } catch {
    failureCounter.increment()
    throw error
  }
}

Hopefully, the developer of a hypothetical Hystrix library will find it easy to build on top of swift-retry. I’m happy to try to make swift-retry more flexible as needed!


My swift-http-error-handling package, which is built on top of swift-retry, is also intended to be modular. Check it out if you haven’t already! :grin: