Skip to content

Instantly share code, notes, and snippets.

@a-voronov
Last active February 27, 2020 05:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save a-voronov/f63726568a27b4650ac33d53ce866c19 to your computer and use it in GitHub Desktop.
Save a-voronov/f63726568a27b4650ac33d53ce866c19 to your computer and use it in GitHub Desktop.
Exponential Backoff Iterator + Retry SignalProducer
struct ExponentialBackoffConfig {
let initialRetries: Int
let initialInterval: TimeInterval
let maxInterval: TimeInterval
let factor: TimeInterval
init(initialRetries: Int = 2, initialInterval: TimeInterval = 0.3, maxInterval: TimeInterval = 120, factor: TimeInterval = 1.6) {
precondition(initialRetries >= 0 && initialInterval >= 0 && maxInterval >= initialInterval && factor > 1)
self.initialRetries = initialRetries
self.initialInterval = initialInterval
self.maxInterval = maxInterval
self.factor = factor
}
}
struct ExponentialBackoffIterator: IteratorProtocol {
let config: ExponentialBackoffConfig
private var attempt: Int = -1
init(config: ExponentialBackoffConfig = .init()) {
self.config = config
}
mutating func next() -> TimeInterval? {
attempt += 1
guard attempt > config.initialRetries else {
return config.initialInterval
}
let interval = pow(config.factor, TimeInterval(attempt - config.initialRetries)) * config.initialInterval
return min(interval, config.maxInterval)
}
}
extension SignalProducer {
func retryWithExponentialBackoff(
upTo count: Int = .max,
while condition: @escaping (Error) -> Bool = { _ in true },
using config: ExponentialBackoffConfig = .init(),
on scheduler: DateScheduler = QueueScheduler()
) -> SignalProducer<Value, Error> {
precondition(count >= 0)
guard count > 0 else {
return producer
}
var retries = count
var iterator = ExponentialBackoffIterator(config: config)
func retry() -> SignalProducer<Value, Error> {
return flatMapError { error in
guard let interval = iterator.next(), retries > 0 && condition(error) else {
return SignalProducer(error: error)
}
retries -= 1
return SignalProducer.empty
.delay(interval, on: scheduler)
.concat(retry())
}
}
return retry()
.on(value: { _ in
retries = count
iterator = ExponentialBackoffIterator(config: config)
})
}
}
@a-voronov
Copy link
Author

Iterator

var i = ExponentialBackoffIterator()
[
    i.next(), i.next(), i.next(), i.next(),
    i.next(), i.next(), i.next(), i.next(),
    i.next(), i.next(), i.next(), i.next(),
    i.next(), i.next(), i.next(), i.next(),
    i.next(), i.next(), i.next(), i.next()
]

Output:

0.3
0.3
0.3
0.48
0.7680000000000001
1.2288000000000003
1.9660800000000003
3.1457280000000005
5.0331648000000015
8.053063680000003
12.884901888000007
20.61584302080001
32.985348833280014
52.77655813324803
84.44249301319685
120.0
120.0
120.0
120.0
120.0

Screen Shot 2019-06-20 at 14 18 54

SignalProducer

enum E: Error { case e }

var now: Date?
let sp = SignalProducer<Never, E> { o, l in
    if let now = now {
        print("\(Date().timeIntervalSince(now))")
    }
    now = Date()
    o.send(error: .e)
}

sp.retryWithExponentialBackoff(upTo: 10).start()

Output

0.3378109931945801
0.3119410276412964
0.32867908477783203
0.5274850130081177
0.7691540718078613
1.350108027458191
2.162986993789673
3.147484064102173
5.035121917724609
8.754245042800903

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment