RetryThrottle.swift 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  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. /// A throttle used to rate-limit retries and hedging attempts.
  17. ///
  18. /// gRPC prevents servers from being overloaded by retries and hedging by using a token-based
  19. /// throttling mechanism at the transport level.
  20. ///
  21. /// Each client transport maintains a throttle for the server it is connected to and gRPC records
  22. /// successful and failed RPC attempts. Successful attempts increment the number of tokens
  23. /// by ``tokenRatio`` and failed attempts decrement the available tokens by one. In the context
  24. /// of throttling, a failed attempt is one where the server terminates the RPC with a status code
  25. /// which is retryable or non fatal (as defined by ``RetryPolicy/retryableStatusCodes`` and
  26. /// ``HedgingPolicy/nonFatalStatusCodes``) or when the client receives a pushback response from
  27. /// the server.
  28. ///
  29. /// See also [gRFC A6: client retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md).
  30. public struct RetryThrottle: Sendable {
  31. // Note: only three figures after the decimal point from the original token ratio are used so
  32. // all computation is done a scaled number of tokens (tokens * 1000). This allows us to do all
  33. // computation in integer space.
  34. /// The number of tokens available, multiplied by 1000.
  35. private let scaledTokensAvailable: LockedValueBox<Int>
  36. /// The number of tokens, multiplied by 1000.
  37. private let scaledTokenRatio: Int
  38. /// The maximum number of tokens, multiplied by 1000.
  39. private let scaledMaximumTokens: Int
  40. /// The retry threshold, multiplied by 1000. If ``scaledTokensAvailable`` is above this then
  41. /// retries are permitted.
  42. private let scaledRetryThreshold: Int
  43. /// Returns the throttling token ratio.
  44. ///
  45. /// The number of tokens held by the throttle is incremented by this value for each successful
  46. /// response. In the context of throttling, a successful response is one which:
  47. /// - receives metadata from the server, or
  48. /// - is terminated with a non-retryable or fatal status code.
  49. ///
  50. /// If the response is a pushback response then it is not considered to be successful, even if
  51. /// either of the preceding conditions are met.
  52. public var tokenRatio: Double {
  53. Double(self.scaledTokenRatio) / 1000
  54. }
  55. /// The maximum number of tokens the throttle may hold.
  56. public var maximumTokens: Int {
  57. self.scaledMaximumTokens / 1000
  58. }
  59. /// The number of tokens the throttle currently has.
  60. ///
  61. /// If this value is less than or equal to the retry threshold (defined as `maximumTokens / 2`)
  62. /// then RPCs will not be retried and hedging will be disabled.
  63. public var tokens: Double {
  64. self.scaledTokensAvailable.withLockedValue {
  65. Double($0) / 1000
  66. }
  67. }
  68. /// Returns whether retries and hedging are permitted at this time.
  69. public var isRetryPermitted: Bool {
  70. self.scaledTokensAvailable.withLockedValue {
  71. $0 > self.scaledRetryThreshold
  72. }
  73. }
  74. /// Create a new throttle.
  75. ///
  76. /// - Parameters:
  77. /// - maximumTokens: The maximum number of tokens available. Must be in the range `1...1000`.
  78. /// - tokenRatio: The number of tokens to increment the available tokens by for successful
  79. /// responses. See the documentation on this type for a description of what counts as a
  80. /// successful response. Note that only three decimal places are used from this value.
  81. /// - Precondition: `maximumTokens` must be in the range `1...1000`.
  82. /// - Precondition: `tokenRatio` must be `>= 0.001`.
  83. public init(maximumTokens: Int, tokenRatio: Double) {
  84. precondition(
  85. (1 ... 1000).contains(maximumTokens),
  86. "maximumTokens must be in the range 1...1000 (is \(maximumTokens))"
  87. )
  88. let scaledTokenRatio = Int(tokenRatio * 1000)
  89. precondition(scaledTokenRatio > 0, "tokenRatio must be >= 0.001 (is \(tokenRatio))")
  90. let scaledTokens = maximumTokens * 1000
  91. self.scaledMaximumTokens = scaledTokens
  92. self.scaledRetryThreshold = scaledTokens / 2
  93. self.scaledTokenRatio = scaledTokenRatio
  94. self.scaledTokensAvailable = LockedValueBox(scaledTokens)
  95. }
  96. /// Create a new throttle.
  97. ///
  98. /// - Parameter policy: The policy to use to configure the throttle.
  99. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
  100. public init(policy: ServiceConfig.RetryThrottling) {
  101. self.init(maximumTokens: policy.maxTokens, tokenRatio: policy.tokenRatio)
  102. }
  103. /// Records a success, adding a token to the throttle.
  104. @usableFromInline
  105. func recordSuccess() {
  106. self.scaledTokensAvailable.withLockedValue { value in
  107. value = min(self.scaledMaximumTokens, value &+ self.scaledTokenRatio)
  108. }
  109. }
  110. /// Records a failure, removing tokens from the throttle.
  111. /// - Returns: Whether retries will now be throttled.
  112. @usableFromInline
  113. @discardableResult
  114. func recordFailure() -> Bool {
  115. self.scaledTokensAvailable.withLockedValue { value in
  116. value = max(0, value &- 1000)
  117. return value <= self.scaledRetryThreshold
  118. }
  119. }
  120. }