-
-
Notifications
You must be signed in to change notification settings - Fork 60
Expand file tree
/
Copy pathAWSClient+Waiter.swift
More file actions
171 lines (157 loc) · 6.07 KB
/
Copy pathAWSClient+Waiter.swift
File metadata and controls
171 lines (157 loc) · 6.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
//===----------------------------------------------------------------------===//
//
// This source file is part of the Soto for AWS open source project
//
// Copyright (c) 2017-2022 the Soto project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Soto project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import Dispatch
import NIOCore
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
#if canImport(Darwin)
import Darwin.C
#elseif canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#endif
// MARK: Waiters
extension AWSClient {
/// Waiter state
public enum WaiterState: Sendable {
case success
case retry
case failure
}
/// A waiter is a client side abstraction used to poll a resource until a desired state is reached
public struct Waiter<Input: Sendable, Output: Sendable>: Sendable {
/// An acceptor checks the result of a call and can change the waiter state based on that result
public struct Acceptor: Sendable {
public init(state: AWSClient.WaiterState, matcher: AWSWaiterMatcher) {
self.state = state
self.matcher = matcher
}
let state: WaiterState
let matcher: AWSWaiterMatcher
}
public typealias WaiterCommand = @Sendable (Input, Logger) async throws -> Output
/// Initialize an waiter
/// - Parameters:
/// - acceptors: List of acceptors
/// - minDelayTime: minimum amount of time to wait between API calls
/// - maxDelayTime: maximum amount of time to wait between API calls
/// - command: API call
public init(
acceptors: [AWSClient.Waiter<Input, Output>.Acceptor],
minDelayTime: TimeAmount = .seconds(2),
maxDelayTime: TimeAmount = .seconds(120),
command: @escaping WaiterCommand
) {
self.acceptors = acceptors
self.minDelayTime = minDelayTime
self.maxDelayTime = maxDelayTime
self.command = command
}
let acceptors: [Acceptor]
let minDelayTime: TimeAmount
let maxDelayTime: TimeAmount
let command: WaiterCommand
/// Calculate delay until next API call. This calculation comes from the AWS Smithy documentation
/// https://awslabs.github.io/smithy/1.0/spec/waiters.html#waiter-retries
///
/// - Parameters:
/// - attempt: Attempt number (assumes this starts at 1)
/// - remainingTime: Remaining time available
/// - Returns: Calculate retry time
func calculateRetryWaitTime(attempt: Int, remainingTime: TimeAmount) -> TimeAmount {
assert(attempt >= 1, "Attempt number cannot be less than 1")
let minDelay = Double(self.minDelayTime.nanoseconds) / 1_000_000_000
let maxDelay = Double(self.maxDelayTime.nanoseconds) / 1_000_000_000
let attemptCeiling = (log(maxDelay / minDelay) / log(2)) + 1
let calculatedMaxDelay: Double
if Double(attempt) > attemptCeiling {
calculatedMaxDelay = maxDelay
} else {
calculatedMaxDelay = minDelay * Double(1 << (attempt - 1))
}
let delay = Double.random(in: minDelay...calculatedMaxDelay)
let timeDelay = TimeAmount.nanoseconds(Int64(delay * 1_000_000_000))
if remainingTime - timeDelay < self.minDelayTime {
return remainingTime - self.minDelayTime
}
return timeDelay
}
}
/// Returns when waiter polling returns a success state
/// or returns an error if the polling returns an error or timesout
///
/// - Parameters:
/// - input: Input parameters
/// - waiter: Waiter to wait on
/// - maxWaitTime: Maximum amount of time to wait
/// - logger: Logger used to provide output
public func waitUntil<Input, Output>(
_ input: Input,
waiter: Waiter<Input, Output>,
maxWaitTime: TimeAmount? = nil,
logger: Logger = AWSClient.loggingDisabled
) async throws {
let maxWaitTime = maxWaitTime ?? waiter.maxDelayTime
let deadline: NIODeadline = .now() + maxWaitTime
var attempt = 0
while true {
attempt += 1
let result: Result<Output, Error>
do {
result = try await .success(waiter.command(input, logger))
} catch {
result = .failure(error)
}
var acceptorState: WaiterState?
for acceptor in waiter.acceptors {
if acceptor.matcher.match(result: result.map { $0 }) {
acceptorState = acceptor.state
break
}
}
// if state has not been set then set it based on return of API call
let waiterState: WaiterState
if let state = acceptorState {
waiterState = state
} else if case .failure = result {
waiterState = .failure
} else {
waiterState = .retry
}
// based on state succeed, fail promise or retry
switch waiterState {
case .success:
return
case .failure:
if case .failure(let error) = result {
throw error
} else {
throw ClientError.waiterFailed
}
case .retry:
let wait = waiter.calculateRetryWaitTime(attempt: attempt, remainingTime: deadline - .now())
if wait < .seconds(0) {
throw ClientError.waiterTimeout
} else {
logger.trace("Wait \(wait.nanoseconds / 1_000_000)ms")
try await Task.sleep(nanoseconds: UInt64(wait.nanoseconds))
}
}
}
}
}