Skip to content

Commit a17234a

Browse files
authored
feat(retry): introduce fibonacci delay strategy (#74)
1 parent 2e4929c commit a17234a

6 files changed

Lines changed: 96 additions & 1 deletion

File tree

.swiftlint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ identifier_name:
9898
excluded:
9999
- id
100100
- URL
101-
101+
- n
102102
analyzer_rules:
103103
- unused_import
104104
- unused_declaration

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ case constant(retry: Int, duration: DispatchTimeInterval)
104104
/// A retry strategy with a linearly increasing delay.
105105
case linear(retry: UInt, duration: DispatchTimeInterval)
106106

107+
/// A retry strategy with a Fibonacci-based delay progression.
108+
case fibonacci(retry: UInt, duration: DispatchTimeInterval)
109+
107110
/// A retry strategy with exponential increase in duration between retries and added jitter.
108111
case exponential(
109112
retry: Int,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//
2+
// Typhoon
3+
// Copyright © 2026 Space Code. All rights reserved.
4+
//
5+
6+
import Foundation
7+
8+
/// A retry delay strategy that increases the delay
9+
/// following the Fibonacci sequence.
10+
///
11+
/// The delay is calculated as:
12+
/// `baseDuration * fibonacci(retryIndex + 1)`
13+
struct FibonacciDelayStrategy: IRetryDelayStrategy {
14+
// MARK: - Properties
15+
16+
/// The base delay interval.
17+
///
18+
/// Each retry multiplies this value by
19+
/// the corresponding Fibonacci number.
20+
let duration: DispatchTimeInterval
21+
22+
// MARK: - IRetryDelayStrategy
23+
24+
/// Calculates a delay based on the Fibonacci sequence.
25+
///
26+
/// - Parameter retries: The current retry attempt index (starting from `0`).
27+
/// - Returns: The delay in nanoseconds.
28+
func delay(forRetry retries: UInt) -> UInt64? {
29+
guard let seconds = duration.double else { return .zero }
30+
31+
let fib = fibonacci(retries + 1)
32+
let delay = seconds * Double(fib)
33+
34+
return (delay * .nanosec).safeUInt64
35+
}
36+
37+
// MARK: - Private
38+
39+
/// Returns the Fibonacci number for a given index.
40+
///
41+
/// Uses an iterative approach to avoid recursion overhead.
42+
private func fibonacci(_ n: UInt) -> UInt {
43+
guard n > 1 else { return 1 }
44+
45+
var previous: UInt = 1
46+
var current: UInt = 1
47+
48+
for _ in 2 ..< n {
49+
let next = previous + current
50+
previous = current
51+
current = next
52+
}
53+
54+
return current
55+
}
56+
}

Sources/Typhoon/Classes/Strategy/RetryPolicyStrategy.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ public enum RetryPolicyStrategy: Sendable {
2727
/// the linear backoff interval.
2828
case linear(retry: UInt, duration: DispatchTimeInterval)
2929

30+
/// A retry strategy with a Fibonacci-based delay progression.
31+
///
32+
/// The delay grows according to the Fibonacci sequence:
33+
/// `duration * fibonacci(retryIndex + 1)`.
34+
///
35+
/// - Parameters:
36+
/// - retry: The maximum number of retry attempts.
37+
/// - duration: The base delay used to calculate
38+
/// the Fibonacci backoff interval.
39+
case fibonacci(retry: UInt, duration: DispatchTimeInterval)
40+
3041
/// A retry strategy with exponential increase in duration between retries and added jitter.
3142
///
3243
/// - Parameters:
@@ -52,6 +63,8 @@ public enum RetryPolicyStrategy: Sendable {
5263
retry
5364
case let .linear(retry, _):
5465
retry
66+
case let .fibonacci(retry, _):
67+
retry
5568
}
5669
}
5770

@@ -64,6 +77,8 @@ public enum RetryPolicyStrategy: Sendable {
6477
duration
6578
case let .linear(_, duration):
6679
duration
80+
case let .fibonacci(_, duration):
81+
duration
6782
}
6883
}
6984
}
@@ -82,6 +97,8 @@ extension RetryPolicyStrategy {
8297
ConstantDelayStrategy(duration: duration)
8398
case let .linear(_, duration):
8499
LinearDelayStrategy(duration: duration)
100+
case let .fibonacci(_, duration):
101+
FibonacciDelayStrategy(duration: duration)
85102
}
86103
}
87104
}

Tests/TyphoonTests/UnitTests/RetryPolicyStrategyTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ final class RetryPolicyStrategyTests: XCTestCase {
4242
// then
4343
XCTAssertEqual(duration, .second)
4444
}
45+
46+
func test_thatRetryPolicyStrategyReturnsDuration_whenTypeIsFibonacci() {
47+
// when
48+
let duration = RetryPolicyStrategy.fibonacci(retry: .retry, duration: .second).duration
49+
50+
// then
51+
XCTAssertEqual(duration, .second)
52+
}
4553
}
4654

4755
// MARK: Constants

Tests/TyphoonTests/UnitTests/RetrySequenceTests.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ final class RetrySequenceTests: XCTestCase {
3333
XCTAssertEqual(result, [1, 2, 3, 4, 5, 6, 7, 8])
3434
}
3535

36+
func test_thatRetrySequenceCreatesASequence_whenStrategyIsFibonacci() {
37+
// given
38+
let sequence = RetrySequence(strategy: .fibonacci(retry: .retry, duration: .nanosecond))
39+
40+
// when
41+
let result: [UInt64] = sequence.map { $0 }
42+
43+
// then
44+
XCTAssertEqual(result, [1, 1, 2, 3, 5, 8, 13, 21])
45+
}
46+
3647
func test_thatRetrySequenceCreatesASequence_whenStrategyIsExponential() {
3748
// given
3849
let sequence = RetrySequence(strategy: .exponential(retry: .retry, jitterFactor: .zero, duration: .nanosecond))

0 commit comments

Comments
 (0)