RetryThrottle.swift 5.4 KB

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