Skip to content

Commit dbf417f

Browse files
authored
feat: Smithy Waiters (#463)
* 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)
1 parent 581320d commit dbf417f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+3002
-3
lines changed

Packages/.swiftlint.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ disabled_rules:
2020
- file_length
2121
- syntactic_sugar
2222
- unused_capture_list
23+
- nesting
24+
- large_tuple
2325
- type_body_length
2426

2527
opt_in_rules:

Packages/ClientRuntime/Sources/Networking/Http/UnknownHttpServiceError.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
/// General Service Error structure used when exact error could not be deduced from the `HttpResponse`
77
public struct UnknownHttpServiceError: HttpServiceError, Swift.Equatable {
8+
public var _errorType: String?
9+
810
public var _isThrottling: Bool = false
911

1012
public var _statusCode: HttpStatusCode?
@@ -19,9 +21,23 @@ public struct UnknownHttpServiceError: HttpServiceError, Swift.Equatable {
1921
}
2022

2123
extension UnknownHttpServiceError {
22-
public init(httpResponse: HttpResponse, message: String? = nil) {
24+
25+
/// Creates an `UnknownHttpServiceError` from a HTTP response.
26+
/// - Parameters:
27+
/// - httpResponse: The `HttpResponse` for this error.
28+
/// - message: The message associated with this error. Defaults to `nil`.
29+
/// - errorType: The error type associated with this error. Defaults to `nil`.
30+
public init(httpResponse: HttpResponse, message: String? = nil, errorType: String? = nil) {
2331
self._statusCode = httpResponse.statusCode
2432
self._headers = httpResponse.headers
2533
self._message = message
34+
self._errorType = errorType
2635
}
2736
}
37+
38+
extension UnknownHttpServiceError: WaiterTypedError {
39+
40+
/// The Smithy identifier, without namespace, for the type of this error, or `nil` if the
41+
/// error has no known type.
42+
public var waiterErrorType: String? { _errorType }
43+
}

Packages/ClientRuntime/Sources/Networking/SdkError.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,19 @@ public enum SdkError<E>: Error {
1414
case unknown(Error?)
1515

1616
}
17+
18+
extension SdkError: WaiterTypedError {
19+
20+
/// The Smithy identifier, without namespace, for the type of this error, or `nil` if the
21+
/// error has no known type.
22+
public var waiterErrorType: String? {
23+
switch self {
24+
case .service(let error, _):
25+
return (error as? WaiterTypedError)?.waiterErrorType
26+
case .client(let error, _):
27+
return (error as? WaiterTypedError)?.waiterErrorType
28+
case .unknown(let error):
29+
return (error as? WaiterTypedError)?.waiterErrorType
30+
}
31+
}
32+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
extension WaiterConfiguration {
9+
/// `Acceptor` is a Swift-native equivalent of Smithy acceptors:
10+
/// https://awslabs.github.io/smithy/2.0/additional-specs/waiters.html#acceptor-structure
11+
/// An `Acceptor` defines a condition (its `matcher`) that will cause the wait operation to transition to
12+
/// a given state (its `state`).
13+
public struct Acceptor {
14+
15+
public typealias Matcher = (Input, Result<Output, Error>) -> Bool
16+
17+
/// The possible states of a Smithy waiter during the waiting process.
18+
public enum State {
19+
/// The waiter has succeeded if this state is reached, and should conclude waiting.
20+
case success
21+
/// The waiter should repeat the operation after a delay if this state is reached.
22+
case retry
23+
/// The waiter has failed if this state is reached, and should conclude waiting.
24+
case failure
25+
}
26+
27+
/// Used as the root value of an `inputOutput` acceptor, which has the `input` and `output` fields
28+
/// as its two top level properties.
29+
///
30+
/// Even though `input` and `output` are both guaranteed to be present when this type is created,
31+
/// these properties are optional because `InputOutput` is handled like any other Smithy model object,
32+
/// and smithy-swift currently does not support `@required` properties on Smithy models.
33+
///
34+
/// In the future, if smithy-swift is updated to support `@required` properties, these may be made
35+
/// non-optional and the corresponding Smithy model's members for `input` and `output` should be
36+
/// marked with `@required` as well.
37+
public struct InputOutput {
38+
public let input: Input?
39+
public let output: Output?
40+
41+
public init(input: Input, output: Output) {
42+
self.input = input
43+
self.output = output
44+
}
45+
}
46+
47+
/// The state that the `Waiter` enters when this `Acceptor` matches the operation response.
48+
public let state: State
49+
50+
/// A closure that determines if this `Acceptor` matches the operation response.
51+
public let matcher: Matcher
52+
53+
/// Creates a new `Acceptor` that will cause the waiter to enter `state` when `Matcher` is true.
54+
public init(
55+
state: State,
56+
matcher: @escaping Matcher
57+
) {
58+
self.state = state
59+
self.matcher = matcher
60+
}
61+
62+
/// Determines if the `Acceptor` matches for the supplied parameters, and returns a
63+
/// `Acceptor.Match` value which can be used to conclude the wait or initiate retry.
64+
func evaluate(
65+
input: Input,
66+
result: Result<Output, Error>
67+
) -> Match? {
68+
guard matcher(input, result) else { return nil }
69+
switch (state, result) {
70+
case (.retry, _):
71+
return .retry
72+
case (.success, let result):
73+
return .success(result)
74+
case (.failure, let result):
75+
return .failure(result)
76+
}
77+
}
78+
79+
/// `Acceptor.Match` encapsulates the action required by an `Acceptor` that matches the
80+
/// operation's response.
81+
public enum Match {
82+
/// An `Acceptor` with `success` state matched an operation, and the associated value
83+
/// is that operation's result.
84+
case success(Result<Output, Error>)
85+
/// An `Acceptor` with `retry` state matched an operation.
86+
case retry
87+
/// An `Acceptor` with `failure` state matched an operation, and the associated value
88+
/// is that operation's result.
89+
case failure(Result<Output, Error>)
90+
}
91+
}
92+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
10+
/// Utility functions for performing comparisons between values in JMESPath expressions.
11+
///
12+
/// `Bool` may be compared for equality & inequality.
13+
///
14+
/// `String` and a `RawRepresentable where RawValue == String` may be interchangeable compared for equality and inequality.
15+
///
16+
/// `Int` and `Double` may be interchangeably compared for equality, inequality, and order.
17+
///
18+
/// When one of the values in an order comparison is `nil`, the result is `false`.
19+
public enum JMESUtils {
20+
21+
// Function for comparing Bool to Bool.
22+
23+
public static func compare(_ lhs: Bool?, _ comparator: (Bool?, Bool?) -> Bool, _ rhs: Bool?) -> Bool {
24+
return comparator(lhs, rhs)
25+
}
26+
27+
// Functions for comparing Double to Double.
28+
29+
public static func compare(_ lhs: Double?, _ comparator: (Double?, Double?) -> Bool, _ rhs: Double?) -> Bool {
30+
comparator(lhs, rhs)
31+
}
32+
33+
public static func compare(_ lhs: Double?, _ comparator: (Double, Double) -> Bool, _ rhs: Double?) -> Bool {
34+
guard let lhs = lhs, let rhs = rhs else { return false }
35+
return comparator(lhs, rhs)
36+
}
37+
38+
// Functions for comparing Int to Int. Double comparators are used since Int has
39+
// extra overloads on `==` that prevent it from resolving correctly, and Ints compared
40+
// to Doubles are already compared as Doubles anyway.
41+
42+
public static func compare(_ lhs: Int?, _ comparator: (Double?, Double?) -> Bool, _ rhs: Int?) -> Bool {
43+
comparator(lhs.map { Double($0) }, rhs.map { Double($0) })
44+
}
45+
46+
public static func compare(_ lhs: Int?, _ comparator: (Double, Double) -> Bool, _ rhs: Int?) -> Bool {
47+
guard let lhs = lhs, let rhs = rhs else { return false }
48+
return comparator(Double(lhs), Double(rhs))
49+
}
50+
51+
// Function for comparing String to String.
52+
53+
public static func compare(_ lhs: String?, _ comparator: (String?, String?) -> Bool, _ rhs: String?) -> Bool {
54+
comparator(lhs, rhs)
55+
}
56+
57+
// Function for comparing two types that are each raw representable by String.
58+
59+
public static func compare<L: RawRepresentable, R: RawRepresentable>(
60+
_ lhs: L?,
61+
_ comparator: (String?, String?) -> Bool,
62+
_ rhs: R?
63+
) -> Bool where L.RawValue == String, R.RawValue == String {
64+
comparator(lhs?.rawValue, rhs?.rawValue)
65+
}
66+
67+
// Extensions for comparing Int and / or Double.
68+
69+
public static func compare(_ lhs: Int?, _ comparator: (Double?, Double?) -> Bool, _ rhs: Double?) -> Bool {
70+
comparator(lhs.map { Double($0) }, rhs)
71+
}
72+
73+
public static func compare(_ lhs: Double?, _ comparator: (Double?, Double?) -> Bool, _ rhs: Int?) -> Bool {
74+
comparator(lhs, rhs.map { Double($0) })
75+
}
76+
77+
public static func compare(_ lhs: Int?, _ comparator: (Double, Double) -> Bool, _ rhs: Double?) -> Bool {
78+
guard let lhs = lhs, let rhs = rhs else { return false }
79+
return comparator(Double(lhs), rhs)
80+
}
81+
82+
public static func compare(_ lhs: Double?, _ comparator: (Double, Double) -> Bool, _ rhs: Int?) -> Bool {
83+
guard let lhs = lhs, let rhs = rhs else { return false }
84+
return comparator(lhs, Double(rhs))
85+
}
86+
87+
// Extensions for comparing String with types having raw value of String.
88+
89+
public static func compare<T: RawRepresentable>(
90+
_ lhs: T?,
91+
_ comparator: (String?, String?) -> Bool,
92+
_ rhs: String?
93+
) -> Bool where T.RawValue == String {
94+
comparator(lhs?.rawValue, rhs)
95+
}
96+
97+
public static func compare<T: RawRepresentable>(
98+
_ lhs: String?,
99+
_ comparator: (String?, String?) -> Bool,
100+
_ rhs: T?
101+
) -> Bool where T.RawValue == String {
102+
comparator(lhs, rhs?.rawValue)
103+
}
104+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
10+
/// An object used to wait on an operation until a desired state is reached, or until it is determined that
11+
/// the desired state will never be reached.
12+
/// Intended to be a generic type for use when waiting on any Smithy `operation`.
13+
/// May be reused for multiple waits, including concurrent operations.
14+
public class Waiter<Input, Output> {
15+
16+
/// The configuration this waiter was created with.
17+
public let config: WaiterConfiguration<Input, Output>
18+
19+
/// The operation that this waiter will call one or more times while waiting on the success condition.
20+
public let operation: (Input) async throws -> Output
21+
22+
let retryer: WaiterRetryer<Input, Output>
23+
24+
/// Block that creates a `WaiterScheduler` with the supplied params.
25+
/// May be replaced with a different block for testing.
26+
var schedulerFactory = { (minDelay: TimeInterval, maxDelay: TimeInterval, maxWaitTime: TimeInterval)
27+
-> WaiterScheduler in
28+
return WaiterScheduler(minDelay: minDelay, maxDelay: maxDelay, maxWaitTime: maxWaitTime)
29+
}
30+
31+
/// Creates a `waiter` object with the supplied config and operation.
32+
/// - Parameters:
33+
/// - config: An instance of `WaiterConfiguration` that defines the default behavior of this waiter.
34+
/// - operation: A closure that is called one or more times to perform the waiting operation;
35+
/// takes an `Input` as its sole param & returns an `Output` asynchronously.
36+
/// The `operation` closure throws an error if the operation cannot be performed or the
37+
/// operation completes with an error.
38+
public convenience init(
39+
config: WaiterConfiguration<Input, Output>,
40+
operation: @escaping (Input) async throws -> Output
41+
) {
42+
self.init(config: config, operation: operation, retryer: WaiterRetryer<Input, Output>())
43+
}
44+
45+
/// The designated initializer for this class. See public / convenience initializer for more details.
46+
///
47+
/// Allows for creation with a custom retryer.
48+
/// - Parameters:
49+
/// - config: An instance of `WaiterConfiguration` that defines the default behavior of this waiter.
50+
/// - operation: A closure that is called one or more times to perform the waiting operation;
51+
/// takes an `Input` as its sole param & returns an `Output` asynchronously.
52+
/// The `operation` closure throws an error if the operation cannot be performed or the
53+
/// operation completes with an error.
54+
/// - retryer: The `WaiterRetryer` to be used when polling the operation.
55+
init(
56+
config: WaiterConfiguration<Input, Output>,
57+
operation: @escaping (Input) async throws -> Output,
58+
retryer: WaiterRetryer<Input, Output>
59+
) {
60+
self.config = config
61+
self.operation = operation
62+
self.retryer = retryer
63+
}
64+
65+
/// Initiates waiting, retrying the operation if necessary until the wait succeeds, fails, or times out.
66+
/// Returns a `WaiterOutcome` asynchronously on waiter success, throws an error asynchronously on
67+
/// waiter failure or timeout.
68+
/// - Parameters:
69+
/// - options: `WaiterOptions` to be used to configure this wait.
70+
/// - input: The `Input` object to be used as a parameter when performing the operation.
71+
/// - Returns: A `WaiterOutcome` with the result of the final, successful performance of the operation.
72+
/// - Throws: `WaiterFailureError` if the waiter fails due to matching an `Acceptor` with state `failure`
73+
/// or there is an error not handled by any `Acceptor.`
74+
///
75+
/// `WaiterTimeoutError` if the waiter times out.
76+
@discardableResult
77+
public func waitUntil(
78+
options: WaiterOptions,
79+
input: Input
80+
) async throws -> WaiterOutcome<Output> {
81+
let minDelay = options.minDelay ?? config.minDelay
82+
let maxDelay = options.maxDelay ?? config.maxDelay
83+
let maxWaitTime = options.maxWaitTime
84+
let scheduler = schedulerFactory(minDelay, maxDelay, maxWaitTime)
85+
86+
while !scheduler.isExpired {
87+
if let result = try await retryer.waitThenRequest(scheduler: scheduler,
88+
input: input,
89+
config: config,
90+
operation: operation) {
91+
return result
92+
}
93+
}
94+
// Waiting has expired, throw an error back to the caller
95+
throw WaiterTimeoutError(attempts: scheduler.attempts)
96+
}
97+
}

0 commit comments

Comments
 (0)