ClientRPCExecutionConfiguration.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. /*
  2. * Copyright 2023, gRPC Authors All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. /// Configuration values for executing an RPC.
  17. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
  18. public struct ClientRPCExecutionConfiguration: Hashable, Sendable {
  19. /// The default timeout for the RPC.
  20. ///
  21. /// If no reply is received in the specified amount of time the request is aborted
  22. /// with an ``RPCError`` with code ``RPCError/Code/deadlineExceeded``.
  23. ///
  24. /// The actual deadline used will be the minimum of the value specified here
  25. /// and the value set by the application by the client API. If either one isn't set
  26. /// then the other value is used. If neither is set then the request has no deadline.
  27. ///
  28. /// The timeout applies to the overall execution of an RPC. If, for example, a retry
  29. /// policy is set then the timeout begins when the first attempt is started and _isn't_ reset
  30. /// when subsequent attempts start.
  31. public var timeout: Duration?
  32. /// The policy determining how many times, and when, the RPC is executed.
  33. ///
  34. /// There are two policy types:
  35. /// 1. Retry
  36. /// 2. Hedging
  37. ///
  38. /// The retry policy allows an RPC to be retried a limited number of times if the RPC
  39. /// fails with one of the configured set of status codes. RPCs are only retried if they
  40. /// fail immediately, that is, the first response part received from the server is a
  41. /// status code.
  42. ///
  43. /// The hedging policy allows an RPC to be executed multiple times concurrently. Typically
  44. /// each execution will be staggered by some delay. The first successful response will be
  45. /// reported to the client. Hedging is only suitable for idempotent RPCs.
  46. public var executionPolicy: ExecutionPolicy?
  47. /// Create an execution configuration.
  48. ///
  49. /// - Parameters:
  50. /// - executionPolicy: The execution policy to use for the RPC.
  51. /// - timeout: The default timeout for the RPC.
  52. public init(
  53. executionPolicy: ExecutionPolicy?,
  54. timeout: Duration?
  55. ) {
  56. self.executionPolicy = executionPolicy
  57. self.timeout = timeout
  58. }
  59. /// Create an execution configuration with a retry policy.
  60. ///
  61. /// - Parameters:
  62. /// - retryPolicy: The policy for retrying the RPC.
  63. /// - timeout: The default timeout for the RPC.
  64. public init(
  65. retryPolicy: RetryPolicy,
  66. timeout: Duration? = nil
  67. ) {
  68. self.executionPolicy = .retry(retryPolicy)
  69. self.timeout = timeout
  70. }
  71. /// Create an execution configuration with a hedging policy.
  72. ///
  73. /// - Parameters:
  74. /// - hedgingPolicy: The policy for hedging the RPC.
  75. /// - timeout: The default timeout for the RPC.
  76. public init(
  77. hedgingPolicy: HedgingPolicy,
  78. timeout: Duration? = nil
  79. ) {
  80. self.executionPolicy = .hedge(hedgingPolicy)
  81. self.timeout = timeout
  82. }
  83. }
  84. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
  85. extension ClientRPCExecutionConfiguration {
  86. /// The execution policy for an RPC.
  87. public enum ExecutionPolicy: Hashable, Sendable {
  88. /// Policy for retrying an RPC.
  89. ///
  90. /// See ``RetryPolicy`` for more details.
  91. case retry(RetryPolicy)
  92. /// Policy for hedging an RPC.
  93. ///
  94. /// See ``HedgingPolicy`` for more details.
  95. case hedge(HedgingPolicy)
  96. }
  97. }
  98. /// Policy for retrying an RPC.
  99. ///
  100. /// gRPC retries RPCs when the first response from the server is a status code which matches
  101. /// one of the configured retryable status codes. If the server begins processing the RPC and
  102. /// first responds with metadata and later responds with a retryable status code then the RPC
  103. /// won't be retried.
  104. ///
  105. /// Execution attempts are limited by ``maximumAttempts`` which includes the original attempt. The
  106. /// maximum number of attempts is limited to five.
  107. ///
  108. /// Subsequent attempts are executed after some delay. The first _retry_, or second attempt, will
  109. /// be started after a randomly chosen delay between zero and ``initialBackoff``. More generally,
  110. /// the nth retry will happen after a randomly chosen delay between zero
  111. /// and `min(initialBackoff * backoffMultiplier^(n-1), maximumBackoff)`.
  112. ///
  113. /// For more information see [gRFC A6 Client
  114. /// Retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md).
  115. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
  116. public struct RetryPolicy: Hashable, Sendable {
  117. /// The maximum number of RPC attempts, including the original attempt.
  118. ///
  119. /// Must be greater than one, values greater than five are treated as five.
  120. public var maximumAttempts: Int {
  121. didSet { self.maximumAttempts = validateMaxAttempts(self.maximumAttempts) }
  122. }
  123. /// The initial backoff duration.
  124. ///
  125. /// The initial retry will occur after a random amount of time up to this value.
  126. ///
  127. /// - Precondition: Must be greater than zero.
  128. public var initialBackoff: Duration {
  129. willSet { Self.validateInitialBackoff(newValue) }
  130. }
  131. /// The maximum amount of time to backoff for.
  132. ///
  133. /// - Precondition: Must be greater than zero.
  134. public var maximumBackoff: Duration {
  135. willSet { Self.validateMaxBackoff(newValue) }
  136. }
  137. /// The multiplier to apply to backoff.
  138. ///
  139. /// - Precondition: Must be greater than zero.
  140. public var backoffMultiplier: Double {
  141. willSet { Self.validateBackoffMultiplier(newValue) }
  142. }
  143. /// The set of status codes which may be retried.
  144. ///
  145. /// - Precondition: Must not be empty.
  146. public var retryableStatusCodes: Set<Status.Code> {
  147. willSet { Self.validateRetryableStatusCodes(newValue) }
  148. }
  149. /// Create a new retry policy.
  150. ///
  151. /// - Parameters:
  152. /// - maximumAttempts: The maximum number of attempts allowed for the RPC.
  153. /// - initialBackoff: The initial backoff period for the first retry attempt. Must be
  154. /// greater than zero.
  155. /// - maximumBackoff: The maximum period of time to wait between attempts. Must be greater than
  156. /// zero.
  157. /// - backoffMultiplier: The exponential backoff multiplier. Must be greater than zero.
  158. /// - retryableStatusCodes: The set of status codes which may be retried. Must not be empty.
  159. /// - Precondition: `maximumAttempts`, `initialBackoff`, `maximumBackoff` and `backoffMultiplier`
  160. /// must be greater than zero.
  161. /// - Precondition: `retryableStatusCodes` must not be empty.
  162. public init(
  163. maximumAttempts: Int,
  164. initialBackoff: Duration,
  165. maximumBackoff: Duration,
  166. backoffMultiplier: Double,
  167. retryableStatusCodes: Set<Status.Code>
  168. ) {
  169. self.maximumAttempts = validateMaxAttempts(maximumAttempts)
  170. Self.validateInitialBackoff(initialBackoff)
  171. self.initialBackoff = initialBackoff
  172. Self.validateMaxBackoff(maximumBackoff)
  173. self.maximumBackoff = maximumBackoff
  174. Self.validateBackoffMultiplier(backoffMultiplier)
  175. self.backoffMultiplier = backoffMultiplier
  176. Self.validateRetryableStatusCodes(retryableStatusCodes)
  177. self.retryableStatusCodes = retryableStatusCodes
  178. }
  179. private static func validateInitialBackoff(_ value: Duration) {
  180. precondition(value.isGreaterThanZero, "initialBackoff must be greater than zero")
  181. }
  182. private static func validateMaxBackoff(_ value: Duration) {
  183. precondition(value.isGreaterThanZero, "maximumBackoff must be greater than zero")
  184. }
  185. private static func validateBackoffMultiplier(_ value: Double) {
  186. precondition(value > 0, "backoffMultiplier must be greater than zero")
  187. }
  188. private static func validateRetryableStatusCodes(_ value: Set<Status.Code>) {
  189. precondition(!value.isEmpty, "retryableStatusCodes mustn't be empty")
  190. }
  191. }
  192. /// Policy for hedging an RPC.
  193. ///
  194. /// Hedged RPCs may execute more than once on a server so only idempotent methods should
  195. /// be hedged.
  196. ///
  197. /// gRPC executes the RPC at most ``maximumAttempts`` times, staggering each attempt
  198. /// by ``hedgingDelay``.
  199. ///
  200. /// For more information see [gRFC A6 Client
  201. /// Retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md).
  202. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
  203. public struct HedgingPolicy: Hashable, Sendable {
  204. /// The maximum number of RPC attempts, including the original attempt.
  205. ///
  206. /// Values greater than five are treated as five.
  207. ///
  208. /// - Precondition: Must be greater than one.
  209. public var maximumAttempts: Int {
  210. didSet { self.maximumAttempts = validateMaxAttempts(self.maximumAttempts) }
  211. }
  212. /// The first RPC will be sent immediately, but each subsequent RPC will be sent at intervals
  213. /// of `hedgingDelay`. Set this to zero to immediately send all RPCs.
  214. public var hedgingDelay: Duration {
  215. willSet { Self.validateHedgingDelay(newValue) }
  216. }
  217. /// The set of status codes which indicate other hedged RPCs may still succeed.
  218. ///
  219. /// If a non-fatal status code is returned by the server, hedged RPCs will continue.
  220. /// Otherwise, outstanding requests will be cancelled and the error returned to the
  221. /// application layer.
  222. public var nonFatalStatusCodes: Set<Status.Code>
  223. /// Create a new hedging policy.
  224. ///
  225. /// - Parameters:
  226. /// - maximumAttempts: The maximum number of attempts allowed for the RPC.
  227. /// - hedgingDelay: The delay between each hedged RPC.
  228. /// - nonFatalStatusCodes: The set of status codes which indicated other hedged RPCs may still
  229. /// succeed.
  230. /// - Precondition: `maximumAttempts` must be greater than zero.
  231. public init(
  232. maximumAttempts: Int,
  233. hedgingDelay: Duration,
  234. nonFatalStatusCodes: Set<Status.Code>
  235. ) {
  236. self.maximumAttempts = validateMaxAttempts(maximumAttempts)
  237. Self.validateHedgingDelay(hedgingDelay)
  238. self.hedgingDelay = hedgingDelay
  239. self.nonFatalStatusCodes = nonFatalStatusCodes
  240. }
  241. private static func validateHedgingDelay(_ value: Duration) {
  242. precondition(
  243. value.isGreaterThanOrEqualToZero,
  244. "hedgingDelay must be greater than or equal to zero"
  245. )
  246. }
  247. }
  248. private func validateMaxAttempts(_ value: Int) -> Int {
  249. precondition(value > 0, "maximumAttempts must be greater than zero")
  250. return min(value, 5)
  251. }
  252. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
  253. extension Duration {
  254. fileprivate var isGreaterThanZero: Bool {
  255. self.components.seconds > 0 || self.components.attoseconds > 0
  256. }
  257. fileprivate var isGreaterThanOrEqualToZero: Bool {
  258. self.components.seconds >= 0 || self.components.attoseconds >= 0
  259. }
  260. }