{"id":21668493,"url":"https://github.com/ph1ps/swift-concurrency-retry","last_synced_at":"2025-05-08T07:53:31.046Z","repository":{"id":261793922,"uuid":"885362352","full_name":"ph1ps/swift-concurrency-retry","owner":"ph1ps","description":"A retry algorithm for Swift Concurrency","archived":false,"fork":false,"pushed_at":"2025-04-11T09:53:59.000Z","size":78,"stargazers_count":54,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-08T07:53:24.394Z","etag":null,"topics":["backoff","concurrency","retry","swift","swift-concurrency"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ph1ps.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-11-08T12:48:46.000Z","updated_at":"2025-04-16T15:58:00.000Z","dependencies_parsed_at":"2025-01-25T08:41:14.998Z","dependency_job_id":"64e61cea-6a6a-4574-a3a3-0087ad175788","html_url":"https://github.com/ph1ps/swift-concurrency-retry","commit_stats":null,"previous_names":["ph1ps/swift-concurrency-retry"],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ph1ps%2Fswift-concurrency-retry","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ph1ps%2Fswift-concurrency-retry/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ph1ps%2Fswift-concurrency-retry/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ph1ps%2Fswift-concurrency-retry/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ph1ps","download_url":"https://codeload.github.com/ph1ps/swift-concurrency-retry/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253025337,"owners_count":21842409,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backoff","concurrency","retry","swift","swift-concurrency"],"created_at":"2024-11-25T12:16:06.450Z","updated_at":"2025-05-08T07:53:31.039Z","avatar_url":"https://github.com/ph1ps.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Retry\nA retry algorithm for Swift Concurrency\n\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fph1ps%2Fswift-concurrency-retry%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/ph1ps/swift-concurrency-retry)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fph1ps%2Fswift-concurrency-retry%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/ph1ps/swift-concurrency-retry)\n\n## Details\n\nThe library comes with two free functions, one with a generic clock and another one which uses the `ContinuousClock` as default.\n```swift\npublic func retry\u003cR, E, C\u003e(\n  maxAttempts: Int = 3,\n  tolerance: C.Duration? = nil,\n  clock: C,\n  isolation: isolated (any Actor)? = #isolation,\n  operation: () async throws(E) -\u003e sending R,\n  strategy: (E) -\u003e RetryStrategy\u003cC\u003e = { _ in .backoff(.none) }\n) async throws -\u003e R where C: Clock, E: Error { ... }\n\npublic func retry\u003cR, E\u003e(\n  maxAttempts: Int = 3,\n  tolerance: ContinuousClock.Duration? = nil,\n  isolation: isolated (any Actor)? = #isolation,\n  operation: () async throws(E) -\u003e sending R,\n  strategy: (E) -\u003e RetryStrategy\u003cContinuousClock\u003e = { _ in .backoff(.none) }\n) async throws -\u003e R where E: Error { ... }\n```\n\nThe `retry` function performs an asynchronous operation and retries 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`.\n\n- Parameters:\n  - `maxAttempts`: The maximum number of attempts to retry the operation. Defaults to 3.\n  - `tolerance`: An optional tolerance for the delay duration to account for clock imprecision. Defaults to `nil`.\n  - `isolation`: The inherited actor isolation.\n  - `operation`: The asynchronous operation to perform. This function will retry the operation in case of error, based on the retry strategy provided.\n  - `strategy`: A closure that determines the `RetryStrategy` for handling retries based on the error type. Defaults to `.backoff(.none)`, meaning no delay between retries.\n\n- Returns: The result of the operation, if successful within the allowed number of attempts.\n- Throws: Rethrows the last encountered error if all retry attempts fail or if the retry strategy specifies stopping retries or any error thrown by `clock`\n- Precondition: `maxAttempts` must be greater than 0.\n\n### Backoff\nThis library ships with 7 prebuilt backoff strategies:\n\n#### None\nA backoff strategy with no delay between attempts.\n\nThis strategy enforces a zero-duration delay, making retries immediate. It’s suitable for situations where retries should happen as soon as possible without any waiting period.\n\n$`f(x) = 0`$ where `x` is the current attempt.\n```swift\nextension BackoffStrategy {\n  public static var none: Self { ... }\n}\n```\n\n#### Constant\nA backoff strategy with a constant delay between each attempt.\n\nThis strategy applies a fixed, unchanging delay between retries, regardless of attempt count. It’s ideal for retry patterns where uniform intervals between attempts are desired.\n\n$`f(x) = c`$ where `x` is the current attempt.\n```swift\nextension BackoffStrategy {\n  public static func constant(_ c: C.Duration) -\u003e Self { ... }\n}\n```\n\n#### Linear\nA backoff strategy with a linearly increasing delay between attempts.\n\nThis strategy gradually increases the delay after each retry, beginning with an initial delay and scaling linearly. Useful for scenarios where delays need to increase consistently over time.\n\n$`f(x) = ax + b`$ where `x` is the current attempt.\n```swift\nextension BackoffStrategy {\n  public static func linear(a: C.Duration, b: C.Duration) -\u003e Self { ... }\n}\n```\n\n#### Exponential\nA backoff strategy with an exponentially increasing delay between attempts.\n\nThis strategy grows the delay exponentially, starting with an initial duration and applying a multiplicative factor after each retry. Suitable for cases where retries should become increasingly sparse.\n\n$`f(x) = a * b^x`$ where `x` is the current attempt.\n\n```swift\nextension BackoffStrategy where C.Duration == Duration {\n  public static func exponential(a: C.Duration, b: Double) -\u003e Self { ... }\n}\n```\n\n#### Minimum\nEnforces a minimum delay duration for this backoff strategy.\n\nThis method ensures the backoff delay is never shorter than the defined minimum duration, helping to maintain a baseline wait time between retries. This retains the original backoff pattern but raises durations below the specified threshold to the minimum value.\n\n$`g(x) = max(f(x), m)`$ where `x` is the current attempt and `f(x)` the base backoff strategy.\n```swift\nextension BackoffStrategy {\n  public func min(_ m: C.Duration) -\u003e Self { ... }\n}\n```\n\n#### Maximum\nLimits the maximum delay duration for this backoff strategy.\n\nThis method ensures the backoff delay does not exceed the defined maximum duration, helping to avoid overly long wait times between retries. This retains the original backoff pattern up to the specified cap.\n\n$`g(x) = min(f(x), M)`$ where `x` is the current attempt and `f(x)` the base backoff strategy.\n```swift\nextension BackoffStrategy {\n  public func max(_ M: C.Duration) -\u003e Self { ... }\n}\n```\n\n#### Jitter\nApplies jitter to the delay duration, introducing randomness into the backoff interval.\n\nThis method randomizes the delay for each retry attempt within a range from zero up to the base duration. Jitter can help reduce contention when multiple sources retry concurrently in distributed systems.\n\n$`g(x) = random[0, f(x)[`$ where `x` is the current attempt and `f(x)` the base backoff strategy.\n```swift\n@available(iOS 18.0, macOS 15.0, macCatalyst 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)\nextension BackoffStrategy where C.Duration == Duration {\n  public func jitter\u003cT\u003e(using generator: T = SystemRandomNumberGenerator()) -\u003e Self where T: RandomNumberGenerator { ... }\n}\n```\n\n## Examples\nTo fully understand this, let's illustrate 3 customizations of this function with `URLSession.shared.data(from: url)` as example operation:\n\n### Customization 1\nUse the defaults:\n```swift\nlet (data, response) = try await retry {\n  try await URLSession.shared.data(from: url)\n}\n```\nIf the operation succeeds, the result is returned immediately.\nHowever if any error occurs, the operation will be tried 2 more times, as the default maximum attempts are 3.\nBetween the consecutive attempts, there will be no delay, as, again, the default retry strategy is to not add any backoff.\n\n### Customization 2\nUse a custom backoff strategy and custom amount of attempts:\n```swift\nlet (data, response) = try await retry(maxAttempts: 5) {\n  try await URLSession.shared.data(from: url)\n} strategy: { _ in\n  return .backoff(.constant(.seconds(2)))\n}\n```\nSimilarly to the first customization, if any error occurs, the operation will be tried 4 more times.\nHowever, between consecutive attempts, it will wait for 2 seconds until it will run operation again.\n\n### Customization 3\nUse a custom backoff and retry strategy:\n```swift\nstruct TooManyRequests: Error {\n  let retryAfter: Double\n}\n\nlet (data, response) = try await retry {\n  let (data, response) = try await URLSession.shared.data(from: url)\n\n  if\n    let response = response as? HTTPURLResponse,\n    let retryAfter = response.value(forHTTPHeaderField: \"Retry-After\").flatMap(Double.init),\n    response.statusCode == 429\n  {\n    throw TooManyRequests(retryAfter: retryAfter)\n  }\n\n  return (data, response)\n} strategy: { error in\n  if let error = error as? TooManyRequests {\n    return .backoff(.constant(.seconds(error.retryAfter)))\n  } else {\n    return .stop\n  }\n}\n```\nA common behavior for servers is to return an HTTP status code 429 with a recommended backoff if the load gets too high.\nContrary to the first two examples, we only retry if the error is of type `TooManyRequests`, any other errors will be rethrown and retrying is stopped.\nThe constant backoff is dynamically fetched from the custom error and passed as seconds.\n\n## Improvements\n- Only have one free function with a default expression of `ContinuousClock` for the `clock` parameter.\n  - Blocked by: https://github.com/swiftlang/swift/issues/72199\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fph1ps%2Fswift-concurrency-retry","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fph1ps%2Fswift-concurrency-retry","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fph1ps%2Fswift-concurrency-retry/lists"}