Skip to content

CorvidLabs/swift-retry

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

14 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

SwiftRetry

macOS Ubuntu License Version

Pre-1.0 Notice: This library is under active development. The API may change between minor versions until 1.0.

A robust, protocol-oriented retry library for Swift 6 with comprehensive backoff strategies, jitter support, and circuit breaker patterns.

Features

  • Pure Swift 6 with strict concurrency checking
  • Sendable and thread-safe throughout
  • Multiple retry strategies: Constant, Linear, Exponential, Fibonacci
  • Jitter support: Full, Equal, Decorrelated, and None
  • Circuit Breaker pattern to prevent cascading failures
  • Flexible configuration with predicates and timeouts
  • No external dependencies (except swift-docc-plugin)
  • Comprehensive test coverage

Platform Support

  • iOS 15+
  • macOS 12+
  • tvOS 15+
  • watchOS 8+
  • visionOS 1+

Installation

Add to your Package.swift:

dependencies: [
    .package(url: "https://github.com/CorvidLabs/swift-retry.git", from: "0.1.0")
]

Quick Start

Basic Usage

import Retry

let result = try await Retry.execute(maxAttempts: 3) {
    try await fetchData()
}

With Exponential Backoff and Jitter

let result = try await Retry.execute(
    maxAttempts: 5,
    strategy: .exponential(base: 1.0, multiplier: 2.0),
    jitter: .full
) {
    try await networkRequest()
}

With Circuit Breaker

let breaker = CircuitBreaker(failureThreshold: 5, resetTimeout: 60.0)

let result = try await Retry.execute(
    maxAttempts: 3,
    strategy: .fibonacci(base: 1.0),
    circuitBreaker: breaker
) {
    try await externalAPICall()
}

With Configuration

let config = RetryConfiguration(
    maxAttempts: 5,
    maxDelay: 30.0,
    timeout: 120.0,
    shouldRetry: { error in
        // Only retry network errors
        return error is URLError
    }
)

let result = try await Retry.execute(
    configuration: config,
    strategy: .exponential(base: 2.0),
    jitter: .decorrelated()
) {
    try await operation()
}

Retry Strategies

Constant

Fixed delay between retries:

.constant(2.0) // 2 second delay each time

Linear

Linearly increasing delay:

.linear(base: 1.0, increment: 0.5)
// Delays: 1.0, 1.5, 2.0, 2.5...

Exponential

Exponential backoff:

.exponential(base: 1.0, multiplier: 2.0)
// Delays: 1.0, 2.0, 4.0, 8.0...

Fibonacci

Fibonacci sequence delays:

.fibonacci(base: 1.0)
// Delays: 1.0, 1.0, 2.0, 3.0, 5.0, 8.0...

Jitter Types

No Jitter

Uses exact delay from strategy:

jitter: .none

Full Jitter

Random delay between 0 and calculated delay:

jitter: .full

Equal Jitter

Half delay + random half:

jitter: .equal

Decorrelated Jitter

AWS-style decorrelated jitter:

jitter: .decorrelated(base: 1.0)

Circuit Breaker

Prevent cascading failures by opening the circuit after a threshold:

let breaker = CircuitBreaker(
    failureThreshold: 5,  // Open after 5 failures
    resetTimeout: 60.0     // Try again after 60 seconds
)

// Use with retry
let result = try await Retry.execute(
    maxAttempts: 3,
    circuitBreaker: breaker
) {
    try await operation()
}

// Check state
let state = await breaker.currentState // .closed, .open, or .halfOpen

// Reset manually if needed
await breaker.reset()

Error Handling

The library provides specific error types:

do {
    let result = try await Retry.execute(maxAttempts: 3) {
        try await operation()
    }
} catch RetryError.maxAttemptsExceeded(let attempts, let lastError) {
    print("Failed after \(attempts) attempts: \(lastError)")
} catch RetryError.timeout(let duration) {
    print("Timed out after \(duration) seconds")
} catch RetryError.circuitBreakerOpen {
    print("Circuit breaker is open")
} catch {
    print("Operation failed: \(error)")
}

Result-Based API

For non-throwing contexts:

let result = await Retry.executeReturningResult(
    maxAttempts: 3,
    strategy: .exponential(base: 1.0)
) {
    try await operation()
}

switch result {
case .success(let value):
    print("Success: \(value)")
case .failure(let error):
    print("Failed: \(error)")
}

Advanced Configuration

Predefined Configurations

// Default: 3 attempts, no limits
.default

// Conservative: 5 attempts with timeouts
.conservative

// Aggressive: 10 attempts with longer timeouts
.aggressive

Custom Error Filtering

enum APIError: Error, Equatable {
    case rateLimit
    case serverError
    case badRequest
}

let config = RetryConfiguration.forErrors(
    maxAttempts: 5,
    retryableErrors: Set([APIError.rateLimit, APIError.serverError])
)

// Only retries on rateLimit and serverError
try await Retry.execute(configuration: config, strategy: .exponential(base: 2.0)) {
    try await apiCall()
}

Design Philosophy

This library follows protocol-oriented design principles:

  • Protocols over classes: RetryStrategy and Jitter are protocols
  • Value types: Strategies and configurations are structs
  • Composition: Mix and match strategies, jitter, and circuit breakers
  • Type safety: Strong typing prevents runtime errors
  • Sendable: Safe for concurrent use with async/await
  • Clean API: Static member syntax for common cases

Testing

Run tests with:

swift test

Build the package:

swift build

License

MIT

Contributing

Contributions welcome! Please ensure:

  • Swift 6 compatibility
  • Sendable conformance
  • Comprehensive tests
  • Documentation for public APIs