-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: Add waiter client runtime components (#450) * feat: Waiter extension & methods (#478) * feat: Add WaiterTypedError type, conform operation errors to it (#491) * feat: Code-generate Acceptors for waiters (#483) * fix: Use correct var name in AND expression (#493) * fix: Correct Swift compile errors for comparison expressions (#494)
- Loading branch information
Showing
41 changed files
with
3,002 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
// | ||
// Copyright Amazon.com Inc. or its affiliates. | ||
// All Rights Reserved. | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
|
||
extension WaiterConfiguration { | ||
/// `Acceptor` is a Swift-native equivalent of Smithy acceptors: | ||
/// https://awslabs.github.io/smithy/2.0/additional-specs/waiters.html#acceptor-structure | ||
/// An `Acceptor` defines a condition (its `matcher`) that will cause the wait operation to transition to | ||
/// a given state (its `state`). | ||
public struct Acceptor { | ||
|
||
public typealias Matcher = (Input, Result<Output, Error>) -> Bool | ||
|
||
/// The possible states of a Smithy waiter during the waiting process. | ||
public enum State { | ||
/// The waiter has succeeded if this state is reached, and should conclude waiting. | ||
case success | ||
/// The waiter should repeat the operation after a delay if this state is reached. | ||
case retry | ||
/// The waiter has failed if this state is reached, and should conclude waiting. | ||
case failure | ||
} | ||
|
||
/// Used as the root value of an `inputOutput` acceptor, which has the `input` and `output` fields | ||
/// as its two top level properties. | ||
/// | ||
/// Even though `input` and `output` are both guaranteed to be present when this type is created, | ||
/// these properties are optional because `InputOutput` is handled like any other Smithy model object, | ||
/// and smithy-swift currently does not support `@required` properties on Smithy models. | ||
/// | ||
/// In the future, if smithy-swift is updated to support `@required` properties, these may be made | ||
/// non-optional and the corresponding Smithy model's members for `input` and `output` should be | ||
/// marked with `@required` as well. | ||
public struct InputOutput { | ||
public let input: Input? | ||
public let output: Output? | ||
|
||
public init(input: Input, output: Output) { | ||
self.input = input | ||
self.output = output | ||
} | ||
} | ||
|
||
/// The state that the `Waiter` enters when this `Acceptor` matches the operation response. | ||
public let state: State | ||
|
||
/// A closure that determines if this `Acceptor` matches the operation response. | ||
public let matcher: Matcher | ||
|
||
/// Creates a new `Acceptor` that will cause the waiter to enter `state` when `Matcher` is true. | ||
public init( | ||
state: State, | ||
matcher: @escaping Matcher | ||
) { | ||
self.state = state | ||
self.matcher = matcher | ||
} | ||
|
||
/// Determines if the `Acceptor` matches for the supplied parameters, and returns a | ||
/// `Acceptor.Match` value which can be used to conclude the wait or initiate retry. | ||
func evaluate( | ||
input: Input, | ||
result: Result<Output, Error> | ||
) -> Match? { | ||
guard matcher(input, result) else { return nil } | ||
switch (state, result) { | ||
case (.retry, _): | ||
return .retry | ||
case (.success, let result): | ||
return .success(result) | ||
case (.failure, let result): | ||
return .failure(result) | ||
} | ||
} | ||
|
||
/// `Acceptor.Match` encapsulates the action required by an `Acceptor` that matches the | ||
/// operation's response. | ||
public enum Match { | ||
/// An `Acceptor` with `success` state matched an operation, and the associated value | ||
/// is that operation's result. | ||
case success(Result<Output, Error>) | ||
/// An `Acceptor` with `retry` state matched an operation. | ||
case retry | ||
/// An `Acceptor` with `failure` state matched an operation, and the associated value | ||
/// is that operation's result. | ||
case failure(Result<Output, Error>) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
// | ||
// Copyright Amazon.com Inc. or its affiliates. | ||
// All Rights Reserved. | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
|
||
import Foundation | ||
|
||
/// Utility functions for performing comparisons between values in JMESPath expressions. | ||
/// | ||
/// `Bool` may be compared for equality & inequality. | ||
/// | ||
/// `String` and a `RawRepresentable where RawValue == String` may be interchangeable compared for equality and inequality. | ||
/// | ||
/// `Int` and `Double` may be interchangeably compared for equality, inequality, and order. | ||
/// | ||
/// When one of the values in an order comparison is `nil`, the result is `false`. | ||
public enum JMESUtils { | ||
|
||
// Function for comparing Bool to Bool. | ||
|
||
public static func compare(_ lhs: Bool?, _ comparator: (Bool?, Bool?) -> Bool, _ rhs: Bool?) -> Bool { | ||
return comparator(lhs, rhs) | ||
} | ||
|
||
// Functions for comparing Double to Double. | ||
|
||
public static func compare(_ lhs: Double?, _ comparator: (Double?, Double?) -> Bool, _ rhs: Double?) -> Bool { | ||
comparator(lhs, rhs) | ||
} | ||
|
||
public static func compare(_ lhs: Double?, _ comparator: (Double, Double) -> Bool, _ rhs: Double?) -> Bool { | ||
guard let lhs = lhs, let rhs = rhs else { return false } | ||
return comparator(lhs, rhs) | ||
} | ||
|
||
// Functions for comparing Int to Int. Double comparators are used since Int has | ||
// extra overloads on `==` that prevent it from resolving correctly, and Ints compared | ||
// to Doubles are already compared as Doubles anyway. | ||
|
||
public static func compare(_ lhs: Int?, _ comparator: (Double?, Double?) -> Bool, _ rhs: Int?) -> Bool { | ||
comparator(lhs.map { Double($0) }, rhs.map { Double($0) }) | ||
} | ||
|
||
public static func compare(_ lhs: Int?, _ comparator: (Double, Double) -> Bool, _ rhs: Int?) -> Bool { | ||
guard let lhs = lhs, let rhs = rhs else { return false } | ||
return comparator(Double(lhs), Double(rhs)) | ||
} | ||
|
||
// Function for comparing String to String. | ||
|
||
public static func compare(_ lhs: String?, _ comparator: (String?, String?) -> Bool, _ rhs: String?) -> Bool { | ||
comparator(lhs, rhs) | ||
} | ||
|
||
// Function for comparing two types that are each raw representable by String. | ||
|
||
public static func compare<L: RawRepresentable, R: RawRepresentable>( | ||
_ lhs: L?, | ||
_ comparator: (String?, String?) -> Bool, | ||
_ rhs: R? | ||
) -> Bool where L.RawValue == String, R.RawValue == String { | ||
comparator(lhs?.rawValue, rhs?.rawValue) | ||
} | ||
|
||
// Extensions for comparing Int and / or Double. | ||
|
||
public static func compare(_ lhs: Int?, _ comparator: (Double?, Double?) -> Bool, _ rhs: Double?) -> Bool { | ||
comparator(lhs.map { Double($0) }, rhs) | ||
} | ||
|
||
public static func compare(_ lhs: Double?, _ comparator: (Double?, Double?) -> Bool, _ rhs: Int?) -> Bool { | ||
comparator(lhs, rhs.map { Double($0) }) | ||
} | ||
|
||
public static func compare(_ lhs: Int?, _ comparator: (Double, Double) -> Bool, _ rhs: Double?) -> Bool { | ||
guard let lhs = lhs, let rhs = rhs else { return false } | ||
return comparator(Double(lhs), rhs) | ||
} | ||
|
||
public static func compare(_ lhs: Double?, _ comparator: (Double, Double) -> Bool, _ rhs: Int?) -> Bool { | ||
guard let lhs = lhs, let rhs = rhs else { return false } | ||
return comparator(lhs, Double(rhs)) | ||
} | ||
|
||
// Extensions for comparing String with types having raw value of String. | ||
|
||
public static func compare<T: RawRepresentable>( | ||
_ lhs: T?, | ||
_ comparator: (String?, String?) -> Bool, | ||
_ rhs: String? | ||
) -> Bool where T.RawValue == String { | ||
comparator(lhs?.rawValue, rhs) | ||
} | ||
|
||
public static func compare<T: RawRepresentable>( | ||
_ lhs: String?, | ||
_ comparator: (String?, String?) -> Bool, | ||
_ rhs: T? | ||
) -> Bool where T.RawValue == String { | ||
comparator(lhs, rhs?.rawValue) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
// | ||
// Copyright Amazon.com Inc. or its affiliates. | ||
// All Rights Reserved. | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
|
||
import Foundation | ||
|
||
/// An object used to wait on an operation until a desired state is reached, or until it is determined that | ||
/// the desired state will never be reached. | ||
/// Intended to be a generic type for use when waiting on any Smithy `operation`. | ||
/// May be reused for multiple waits, including concurrent operations. | ||
public class Waiter<Input, Output> { | ||
|
||
/// The configuration this waiter was created with. | ||
public let config: WaiterConfiguration<Input, Output> | ||
|
||
/// The operation that this waiter will call one or more times while waiting on the success condition. | ||
public let operation: (Input) async throws -> Output | ||
|
||
let retryer: WaiterRetryer<Input, Output> | ||
|
||
/// Block that creates a `WaiterScheduler` with the supplied params. | ||
/// May be replaced with a different block for testing. | ||
var schedulerFactory = { (minDelay: TimeInterval, maxDelay: TimeInterval, maxWaitTime: TimeInterval) | ||
-> WaiterScheduler in | ||
return WaiterScheduler(minDelay: minDelay, maxDelay: maxDelay, maxWaitTime: maxWaitTime) | ||
} | ||
|
||
/// Creates a `waiter` object with the supplied config and operation. | ||
/// - Parameters: | ||
/// - config: An instance of `WaiterConfiguration` that defines the default behavior of this waiter. | ||
/// - operation: A closure that is called one or more times to perform the waiting operation; | ||
/// takes an `Input` as its sole param & returns an `Output` asynchronously. | ||
/// The `operation` closure throws an error if the operation cannot be performed or the | ||
/// operation completes with an error. | ||
public convenience init( | ||
config: WaiterConfiguration<Input, Output>, | ||
operation: @escaping (Input) async throws -> Output | ||
) { | ||
self.init(config: config, operation: operation, retryer: WaiterRetryer<Input, Output>()) | ||
} | ||
|
||
/// The designated initializer for this class. See public / convenience initializer for more details. | ||
/// | ||
/// Allows for creation with a custom retryer. | ||
/// - Parameters: | ||
/// - config: An instance of `WaiterConfiguration` that defines the default behavior of this waiter. | ||
/// - operation: A closure that is called one or more times to perform the waiting operation; | ||
/// takes an `Input` as its sole param & returns an `Output` asynchronously. | ||
/// The `operation` closure throws an error if the operation cannot be performed or the | ||
/// operation completes with an error. | ||
/// - retryer: The `WaiterRetryer` to be used when polling the operation. | ||
init( | ||
config: WaiterConfiguration<Input, Output>, | ||
operation: @escaping (Input) async throws -> Output, | ||
retryer: WaiterRetryer<Input, Output> | ||
) { | ||
self.config = config | ||
self.operation = operation | ||
self.retryer = retryer | ||
} | ||
|
||
/// Initiates waiting, retrying the operation if necessary until the wait succeeds, fails, or times out. | ||
/// Returns a `WaiterOutcome` asynchronously on waiter success, throws an error asynchronously on | ||
/// waiter failure or timeout. | ||
/// - Parameters: | ||
/// - options: `WaiterOptions` to be used to configure this wait. | ||
/// - input: The `Input` object to be used as a parameter when performing the operation. | ||
/// - Returns: A `WaiterOutcome` with the result of the final, successful performance of the operation. | ||
/// - Throws: `WaiterFailureError` if the waiter fails due to matching an `Acceptor` with state `failure` | ||
/// or there is an error not handled by any `Acceptor.` | ||
/// | ||
/// `WaiterTimeoutError` if the waiter times out. | ||
@discardableResult | ||
public func waitUntil( | ||
options: WaiterOptions, | ||
input: Input | ||
) async throws -> WaiterOutcome<Output> { | ||
let minDelay = options.minDelay ?? config.minDelay | ||
let maxDelay = options.maxDelay ?? config.maxDelay | ||
let maxWaitTime = options.maxWaitTime | ||
let scheduler = schedulerFactory(minDelay, maxDelay, maxWaitTime) | ||
|
||
while !scheduler.isExpired { | ||
if let result = try await retryer.waitThenRequest(scheduler: scheduler, | ||
input: input, | ||
config: config, | ||
operation: operation) { | ||
return result | ||
} | ||
} | ||
// Waiting has expired, throw an error back to the caller | ||
throw WaiterTimeoutError(attempts: scheduler.attempts) | ||
} | ||
} |
Oops, something went wrong.