Skip to content

Commit

Permalink
Merge pull request #82 from malcommac/new/71-retry-delay-parameter
Browse files Browse the repository at this point in the history
new #71: Added support for delay parameter in retry operation
  • Loading branch information
malcommac authored Sep 15, 2020
2 parents 6c9611e + 53cd0cd commit cdfa97e
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 50 deletions.
4 changes: 4 additions & 0 deletions Sources/Hydra/Promise+Defer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ public extension Promise {
/// - seconds: delay time in seconds; execution time is `.now()+seconds`
/// - Returns: the Promise to resolve to after the delay
func `defer`(in context: Context? = nil, _ seconds: TimeInterval) -> Promise<Value> {
guard seconds > 0 else {
return self
}

let ctx = context ?? .background
return self.then(in: ctx, { value in
return Promise<Value> { resolve, _, _ in
Expand Down
14 changes: 11 additions & 3 deletions Sources/Hydra/Promise+Retry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,17 @@
import Foundation

public extension Promise {

func retry(_ attempts: Int = 3, _ condition: @escaping ((Int, Error) throws -> Bool) = { _,_ in true }) -> Promise<Value> {
return retryWhen(attempts) { (remainingAttempts, error) -> Promise<Bool> in

/// Retry the operation of the promise.
///
/// - Parameters:
/// - attempts: number of attempts.
/// - delay: delay between each attempts (starting when failed the first time).
/// - condition: condition for delay.
/// - Returns: Promise<Value>
func retry(_ attempts: Int = 3, delay: TimeInterval = 0,
_ condition: @escaping ((Int, Error) throws -> Bool) = { _,_ in true }) -> Promise<Value> {
return retryWhen(attempts, delay: delay) { (remainingAttempts, error) -> Promise<Bool> in
do {
return Promise<Bool>(resolved: try condition(remainingAttempts, error))
} catch (_) {
Expand Down
7 changes: 4 additions & 3 deletions Sources/Hydra/Promise+RetryWhen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ import Foundation

public extension Promise {

func retryWhen(_ attempts: Int = 3, _ condition: @escaping ((Int, Error) -> Promise<Bool>) = { _,_ in Promise<Bool>(resolved: true) }) -> Promise<Value> {
func retryWhen(_ attempts: Int = 3, delay: TimeInterval = 0,
_ condition: @escaping ((Int, Error) -> Promise<Bool>) = { _,_ in Promise<Bool>(resolved: true) }) -> Promise<Value> {
guard attempts >= 1 else {
// Must be a valid attempts number
return Promise<Value>(rejected: PromiseError.invalidInput)
Expand All @@ -44,7 +45,7 @@ public extension Promise {
// We'll create a next promise which will be resolved when attempts to resolve self (source promise)
// is reached (with a fulfill or a rejection).
let nextPromise = Promise<Value>(in: self.context, token: self.invalidationToken) { (resolve, reject, operation) in
innerPromise = self.recover(in: self.context) { [unowned self] (error) -> (Promise<Value>) in
innerPromise = self.defer(delay).recover(in: self.context) { [unowned self] (error) -> (Promise<Value>) in
// If promise is rejected we'll decrement the attempts counter
remainingAttempts -= 1
guard remainingAttempts >= 1 else {
Expand All @@ -64,7 +65,7 @@ public extension Promise {
self.resetState()
// Re-execute the body of the source promise to re-execute the async operation
self.runBody()
self.retryWhen(remainingAttempts, condition).then(in: self.context) { (result) in
self.retryWhen(remainingAttempts, delay: delay, condition).then(in: self.context) { (result) in
resolve(result)
}.catch { (retriedError) in
reject(retriedError)
Expand Down
121 changes: 77 additions & 44 deletions Tests/HydraTests/HydraTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,22 +226,22 @@ class HydraTestThen: XCTestCase {
/// and return another Promise with the same value of the previous promise as output.
// In this test we have tried to recover a bad call by executing a resolving promise.
// Test is passed if recover works and we get a valid result into the final `then`.
func test_recoverPromise() {
let exp = expectation(description: "test_recoverPromise")
let expResult = 5
intPromise(expResult).then { value in
self.toStringErrorPromise(value)
}.recover { err -> Promise<String> in
return self.toStringPromise("\(expResult)")
}.then { string in
if ("\(expResult)" != string) {
XCTFail()
}
exp.fulfill()
}
waitForExpectations(timeout: expTimeout, handler: nil)
}
func test_recoverPromise() {
let exp = expectation(description: "test_recoverPromise")
let expResult = 5
intPromise(expResult).then { value in
self.toStringErrorPromise(value)
}.recover { err -> Promise<String> in
return self.toStringPromise("\(expResult)")
}.then { string in
if ("\(expResult)" != string) {
XCTFail()
}
exp.fulfill()
}
waitForExpectations(timeout: expTimeout, handler: nil)
}
/// If return rejected promise in `recover` operator, chain to next as its error.
func test_recover_failure() {
let exp = expectation(description: "test_recover_failure")
Expand Down Expand Up @@ -650,34 +650,67 @@ class HydraTestThen: XCTestCase {
}

//MARK: Retry Test

func test_retry() {
let exp = expectation(description: "test_retry")

let retryAttempts = 3
let successOnAttempt = 3
var currentAttempt = 0
Promise<Int> { (resolve, reject, _) in
currentAttempt += 1
if currentAttempt < successOnAttempt {
print("attempt is \(currentAttempt)... reject")
reject(TestErrors.anotherError)
} else {
print("attempt is \(currentAttempt)... resolve")
resolve(5)
}
}.retry(retryAttempts).then { value in
print("value \(value) at attempt \(currentAttempt)")
XCTAssertEqual(currentAttempt, 3)
exp.fulfill()
}.catch { err in
print("failed \(err) at attempt \(currentAttempt)")
XCTFail()
}

waitForExpectations(timeout: expTimeout, handler: nil)
}


func test_retry() {
let exp = expectation(description: "test_retry")

let retryAttempts = 3
let successOnAttempt = 3
var currentAttempt = 0
Promise<Int> { (resolve, reject, _) in
currentAttempt += 1
if currentAttempt < successOnAttempt {
print("attempt is \(currentAttempt)... reject")
reject(TestErrors.anotherError)
} else {
print("attempt is \(currentAttempt)... resolve")
resolve(5)
}
}.retry(retryAttempts).then { value in
print("value \(value) at attempt \(currentAttempt)")
XCTAssertEqual(currentAttempt, 3)
exp.fulfill()
}.catch { err in
print("failed \(err) at attempt \(currentAttempt)")
XCTFail()
}

waitForExpectations(timeout: expTimeout, handler: nil)
}

func test_retryWithDelay() {
let exp = expectation(description: "test_retryWithDelay")

let retryAttempts = 3
let successOnAttempt = 3
var currentAttempt = 0
var lastFailureDate = Date.distantPast
let retryDelay: TimeInterval = 0

Promise<Int> { (resolve, reject, _) in
currentAttempt += 1
if currentAttempt < successOnAttempt {
print("attempt is \(currentAttempt)... reject")
lastFailureDate = Date()
reject(TestErrors.anotherError)
} else {
print("attempt is \(currentAttempt)... resolve")
resolve(5)
}
}.retry(retryAttempts, delay: retryDelay).then { value in
let passed = Date().timeIntervalSince(lastFailureDate)
XCTAssertGreaterThanOrEqual(passed, retryDelay)
print("value \(value) at attempt \(currentAttempt)")
XCTAssertEqual(currentAttempt, 3)
exp.fulfill()
}.catch { err in
print("failed \(err) at attempt \(currentAttempt)")
XCTFail()
}

waitForExpectations(timeout: 30, handler: nil)
}

func test_retry_allFailure() {
let exp = expectation(description: "test_retry_allFailure")

Expand Down

0 comments on commit cdfa97e

Please sign in to comment.