Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 0 additions & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ opt_in_rules: # some rules are only opt-in
- empty_count
- empty_string
- empty_xctest_method
- enum_case_associated_values_count
- explicit_init
- fallthrough
- fatal_error_message
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Change Log
All notable changes to this project will be documented in this file.

## [Unreleased]

## Added
- Implement exponential backoff with jitter
- Added in Pull Request [#4](https://github.com/space-code/typhoon/pull/4).

#### 1.x Releases
- `1.0.x` Releases - [1.0.0](#100)

Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,17 @@

## Usage

`Typhoon` provides two retry policy strategies:
`Typhoon` provides three retry policy strategies:

```swift
/// A retry strategy with a constant number of attempts and fixed duration between retries.
case constant(retry: Int, duration: DispatchTimeInterval)

/// A retry strategy with an exponential increase in duration between retries.
case exponential(retry: Int, multiplier: Double, duration: DispatchTimeInterval)
case exponential(retry: Int, multiplier: Double = 2.0, duration: DispatchTimeInterval)

/// A retry strategy with exponential increase in duration between retries and added jitter.
case exponentialWithJitter(retry: Int, jitterFactor: Double = 0.1, maxInterval: UInt64? = 60, multiplier: Double = 2.0, duration: DispatchTimeInterval)
```

Create a `RetryPolicyService` instance and pass a desired strategy like this:
Expand Down Expand Up @@ -87,4 +90,4 @@ Please feel free to help out with this project! If you see something that could
Nikita Vasilev, nv3212@gmail.com

## License
typhoon is available under the MIT license. See the LICENSE file for more info.
typhoon is available under the MIT license. See the LICENSE file for more info.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ struct RetryIterator: IteratorProtocol {
let value = duration * pow(multiplier, Double(retries))
return UInt64(value * .nanosec)
}
case let .exponentialWithJitter(_, jitterFactor, maxInterval, multiplier, duration):
if let duration = duration.double {
let exponentialBackoff = duration * pow(multiplier, Double(retries))
let jitter = Double.random(in: -jitterFactor * exponentialBackoff ... jitterFactor * exponentialBackoff)
let value = max(0, exponentialBackoff + jitter)
return min(maxInterval ?? UInt64.max, UInt64(value * .nanosec))
}
}

return 0
Expand Down
31 changes: 30 additions & 1 deletion Sources/Typhoon/Classes/Strategy/RetryPolicyStrategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,35 @@ import Foundation
/// A strategy used to define different retry policies.
public enum RetryPolicyStrategy {
/// A retry strategy with a constant number of attempts and fixed duration between retries.
///
/// - Parameters:
/// - retry: The number of retry attempts.
/// - duration: The initial duration between retries.
case constant(retry: Int, duration: DispatchTimeInterval)

/// A retry strategy with an exponential increase in duration between retries.
case exponential(retry: Int, multiplier: Double, duration: DispatchTimeInterval)
///
/// - Parameters:
/// - retry: The number of retry attempts.
/// - multiplier: The multiplier for calculating the exponential backoff duration (default is 2).
/// - duration: The initial duration between retries.
case exponential(retry: Int, multiplier: Double = 2, duration: DispatchTimeInterval)

/// A retry strategy with exponential increase in duration between retries and added jitter.
///
/// - Parameters:
/// - retry: The number of retry attempts.
/// - jitterFactor: The factor to control the amount of jitter (default is 0.1).
/// - maxInterval: The maximum allowed interval between retries (default is 60 seconds).
/// - multiplier: The multiplier for calculating the exponential backoff duration (default is 2).
/// - duration: The initial duration between retries.
case exponentialWithJitter(
retry: Int,
jitterFactor: Double = 0.1,
maxInterval: UInt64? = 60,
multiplier: Double = 2,
duration: DispatchTimeInterval
)

/// The number of retry attempts based on the strategy.
public var retries: Int {
Expand All @@ -20,6 +45,8 @@ public enum RetryPolicyStrategy {
return retry
case let .exponential(retry, _, _):
return retry
case let .exponentialWithJitter(retry, _, _, _, _):
return retry
}
}

Expand All @@ -30,6 +57,8 @@ public enum RetryPolicyStrategy {
return duration
case let .exponential(_, _, duration):
return duration
case let .exponentialWithJitter(_, _, _, _, duration):
return duration
}
}
}
69 changes: 65 additions & 4 deletions Tests/TyphoonTests/UnitTests/RetrySequenceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,84 @@ final class RetrySequenceTests: XCTestCase {
let result: [UInt64] = sequence.map { $0 }

// then
XCTAssertEqual(result, [1, 1, 1, 1, 1, 1])
XCTAssertEqual(result, [1, 1, 1, 1, 1, 1, 1, 1])
}

func test_thatRetrySequenceCreatesASequence_whenStrategyIsExponential() {
// given
let sequence = RetrySequence(strategy: .exponential(retry: .retry, multiplier: 2, duration: .nanoseconds(1)))
let sequence = RetrySequence(strategy: .exponential(retry: .retry, duration: .nanoseconds(1)))

// when
let result: [UInt64] = sequence.map { $0 }

// then
XCTAssertEqual(result, [1, 2, 4, 8, 16, 32])
XCTAssertEqual(result, [1, 2, 4, 8, 16, 32, 64, 128])
}

func test_thatRetrySequenceCreatesASequence_whenStrategyIsExponentialWithJitter() {
// given
let sequence = RetrySequence(
strategy: .exponentialWithJitter(
retry: .retry,
jitterFactor: .jitterFactor,
maxInterval: .maxInterval,
duration: .nanoseconds(1)
)
)

// when
let result: [UInt64] = sequence.map { $0 }

// then
XCTAssertEqual(result.count, 8)
XCTAssertEqual(result[0], 1, accuracy: 1)
XCTAssertEqual(result[1], 2, accuracy: 1)
XCTAssertEqual(result[2], 4, accuracy: 1)
XCTAssertEqual(result[3], 8, accuracy: 1)
XCTAssertEqual(result[4], 16, accuracy: 2)
XCTAssertEqual(result[5], 32, accuracy: 4)
XCTAssertEqual(result[6], 64, accuracy: 7)
XCTAssertEqual(result[7], .maxInterval)
}

func test_thatRetrySequenceDoesNotLimitASequence_whenStrategyIsExponentialWithJitterAndMaxIntervalIsNil() {
// given
let sequence = RetrySequence(
strategy: .exponentialWithJitter(
retry: .retry,
jitterFactor: .jitterFactor,
maxInterval: nil,
duration: .nanoseconds(1)
)
)

// when
let result: [UInt64] = sequence.map { $0 }

// then
XCTAssertEqual(result.count, 8)
XCTAssertEqual(result[0], 1, accuracy: 1)
XCTAssertEqual(result[1], 2, accuracy: 1)
XCTAssertEqual(result[2], 4, accuracy: 1)
XCTAssertEqual(result[3], 8, accuracy: 1)
XCTAssertEqual(result[4], 16, accuracy: 2)
XCTAssertEqual(result[5], 32, accuracy: 4)
XCTAssertEqual(result[6], 64, accuracy: 8)
XCTAssertEqual(result[7], 128, accuracy: 13)
}
}

// MARK: - Constant

private extension Int {
static let retry: Int = 6
static let retry: Int = 8
}

private extension UInt64 {
static let maxInterval: UInt64 = 60
}

private extension Double {
static let multiplier = 2.0
static let jitterFactor = 0.1
}