Skip to content

Commit 1056b62

Browse files
Merge pull request #4 from SwiftPackageIndex/current-try-parameter
Pass the current try (0 based) to the retry closure
2 parents 4fba683 + 74783fa commit 1056b62

File tree

2 files changed

+107
-28
lines changed

2 files changed

+107
-28
lines changed

Sources/Retry/Retry.swift

Lines changed: 90 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,35 +25,41 @@ public enum Retry {
2525
(pow(2, max(0, attempt - 1)) * Decimal(baseDelay) as NSDecimalNumber).uint32Value
2626
}
2727

28+
public struct State {
29+
public var retriesLeft: Int
30+
public var currentTry = 0
31+
public var lastError: Swift.Error?
32+
33+
public var isFirstIteration: Bool { currentTry == 0 }
34+
public var hasRetriesLeft: Bool { retriesLeft > 0 }
35+
mutating func advance() {
36+
currentTry += 1
37+
retriesLeft -= 1
38+
}
39+
}
40+
2841
@discardableResult
2942
public static func attempt<T>(_ label: String,
3043
delay: Double = 5,
3144
retries: Int = 5,
3245
logger: RetryLogging = DefaultLogger(),
3346
_ block: () throws -> T) throws -> T {
34-
var retriesLeft = retries
35-
var currentTry = 1
36-
var lastError: Swift.Error?
47+
var state = State(retriesLeft: retries)
3748
while true {
38-
if currentTry > 1 {
39-
logger.onStartOfRetry(label: label, attempt: currentTry)
49+
if !state.isFirstIteration {
50+
logger.onStartOfRetry(label: label, attempt: state.currentTry)
4051
}
4152
do {
4253
return try block()
4354
} catch let Error.abort(with: error) {
4455
throw Error.abort(with: error)
4556
} catch {
46-
logger.onError(label: label, error: error)
47-
lastError = error
48-
guard retriesLeft > 0 else { break }
49-
let delay = backedOffDelay(baseDelay: delay, attempt: currentTry)
50-
logger.onStartOfDelay(label: label, delay: Double(delay))
51-
sleep(delay)
52-
currentTry += 1
53-
retriesLeft -= 1
57+
if catchHandler(label, delay: delay, logger: logger, error: error, state: &state) {
58+
break
59+
}
5460
}
5561
}
56-
throw Error.retryLimitExceeded(lastError: lastError)
62+
throw Error.retryLimitExceeded(lastError: state.lastError)
5763
}
5864

5965
@discardableResult
@@ -62,29 +68,85 @@ public enum Retry {
6268
retries: Int = 5,
6369
logger: RetryLogging = DefaultLogger(),
6470
_ block: () async throws -> T) async throws -> T {
65-
var retriesLeft = retries
66-
var currentTry = 1
67-
var lastError: Swift.Error?
71+
var state = State(retriesLeft: retries)
6872
while true {
69-
if currentTry > 1 {
70-
logger.onStartOfRetry(label: label, attempt: currentTry)
73+
if !state.isFirstIteration {
74+
logger.onStartOfRetry(label: label, attempt: state.currentTry)
7175
}
7276
do {
7377
return try await block()
7478
} catch let Error.abort(with: error) {
7579
throw Error.abort(with: error)
7680
} catch {
77-
logger.onError(label: label, error: error)
78-
lastError = error
79-
guard retriesLeft > 0 else { break }
80-
let delay = backedOffDelay(baseDelay: delay, attempt: currentTry)
81-
logger.onStartOfDelay(label: label, delay: Double(delay))
82-
sleep(delay)
83-
currentTry += 1
84-
retriesLeft -= 1
81+
if catchHandler(label, delay: delay, logger: logger, error: error, state: &state) {
82+
break
83+
}
84+
}
85+
}
86+
throw Error.retryLimitExceeded(lastError: state.lastError)
87+
}
88+
89+
@discardableResult
90+
public static func attempt<T>(_ label: String,
91+
delay: Double = 5,
92+
retries: Int = 5,
93+
logger: RetryLogging = DefaultLogger(),
94+
_ block: (State) throws -> T) throws -> T {
95+
var state = State(retriesLeft: retries)
96+
while true {
97+
if !state.isFirstIteration {
98+
logger.onStartOfRetry(label: label, attempt: state.currentTry)
99+
}
100+
do {
101+
return try block(state)
102+
} catch let Error.abort(with: error) {
103+
throw Error.abort(with: error)
104+
} catch {
105+
if catchHandler(label, delay: delay, logger: logger, error: error, state: &state) {
106+
break
107+
}
108+
}
109+
}
110+
throw Error.retryLimitExceeded(lastError: state.lastError)
111+
}
112+
113+
@discardableResult
114+
public static func attempt<T>(_ label: String,
115+
delay: Double = 5,
116+
retries: Int = 5,
117+
logger: RetryLogging = DefaultLogger(),
118+
_ block: (State) async throws -> T) async throws -> T {
119+
var state = State(retriesLeft: retries)
120+
while true {
121+
if !state.isFirstIteration {
122+
logger.onStartOfRetry(label: label, attempt: state.currentTry)
123+
}
124+
do {
125+
return try await block(state)
126+
} catch let Error.abort(with: error) {
127+
throw Error.abort(with: error)
128+
} catch {
129+
if catchHandler(label, delay: delay, logger: logger, error: error, state: &state) {
130+
break
131+
}
85132
}
86133
}
87-
throw Error.retryLimitExceeded(lastError: lastError)
134+
throw Error.retryLimitExceeded(lastError: state.lastError)
135+
}
136+
137+
static func catchHandler(_ label: String,
138+
delay: Double,
139+
logger: RetryLogging,
140+
error: Swift.Error,
141+
state: inout State) -> Bool {
142+
logger.onError(label: label, error: error)
143+
state.lastError = error
144+
guard state.hasRetriesLeft else { return true }
145+
let delay = backedOffDelay(baseDelay: delay, attempt: state.currentTry + 1)
146+
logger.onStartOfDelay(label: label, delay: Double(delay))
147+
sleep(delay)
148+
state.advance()
149+
return false
88150
}
89151
}
90152

Tests/RetryTests/RetryTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,23 @@ final class RetryTests: XCTestCase {
5353
XCTAssertEqual(called, 3)
5454
}
5555

56+
func test_attempt_state() throws {
57+
var called = 0
58+
struct Error: Swift.Error { }
59+
60+
// MUT
61+
try Retry.attempt("", delay: 0, retries: 3) { state in
62+
XCTAssertEqual(state.currentTry, called)
63+
called += 1
64+
if called < 3 {
65+
throw Error()
66+
}
67+
}
68+
69+
// validation
70+
XCTAssertEqual(called, 3)
71+
}
72+
5673
func test_attempt_retryLimitExceeded() throws {
5774
var called = 0
5875
struct Error: Swift.Error, CustomStringConvertible {

0 commit comments

Comments
 (0)