ServiceConfiguration.swift 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. /*
  2. * Copyright 2024, 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. /// Service configuration values.
  17. ///
  18. /// See also: https://github.com/grpc/grpc-proto/blob/master/grpc/service_config/service_config.proto
  19. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
  20. public struct ServiceConfiguration: Hashable, Sendable {
  21. /// Per-method configuration.
  22. public var methodConfiguration: [MethodConfiguration]
  23. /// Load balancing policies.
  24. ///
  25. /// The client iterates through the list in order and picks the first configuration it supports.
  26. /// If no policies are supported then the configuration is considered to be invalid.
  27. public var loadBalancingConfiguration: [LoadBalancingConfiguration]
  28. /// The policy for throttling retries.
  29. ///
  30. /// If a ``RetryThrottlingPolicy`` is provided, gRPC will automatically throttle retry attempts
  31. /// and hedged RPCs when the client's ratio of failures to successes exceeds a threshold.
  32. ///
  33. /// For each server name, the gRPC client will maintain a `token_count` which is initially set
  34. /// to ``maxTokens``. Every outgoing RPC (regardless of service or method invoked) will change
  35. /// `token_count` as follows:
  36. ///
  37. /// - Every failed RPC will decrement the `token_count` by 1.
  38. /// - Every successful RPC will increment the `token_count` by ``tokenRatio``.
  39. ///
  40. /// If `token_count` is less than or equal to `max_tokens / 2`, then RPCs will not be retried
  41. /// and hedged RPCs will not be sent.
  42. public var retryThrottlingPolicy: RetryThrottlingPolicy?
  43. /// Creates a new ``ServiceConfiguration``.
  44. ///
  45. /// - Parameters:
  46. /// - methodConfiguration: Per-method configuration.
  47. /// - loadBalancingConfiguration: Load balancing policies. Clients use the the first supported
  48. /// policy when iterating the list in order.
  49. /// - retryThrottlingPolicy: Policy for throttling retries.
  50. public init(
  51. methodConfiguration: [MethodConfiguration] = [],
  52. loadBalancingConfiguration: [LoadBalancingConfiguration] = [],
  53. retryThrottlingPolicy: RetryThrottlingPolicy? = nil
  54. ) {
  55. self.methodConfiguration = methodConfiguration
  56. self.loadBalancingConfiguration = loadBalancingConfiguration
  57. self.retryThrottlingPolicy = retryThrottlingPolicy
  58. }
  59. }
  60. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
  61. extension ServiceConfiguration: Codable {
  62. private enum CodingKeys: String, CodingKey {
  63. case methodConfig
  64. case loadBalancingConfig
  65. case retryThrottling
  66. }
  67. public init(from decoder: Decoder) throws {
  68. let container = try decoder.container(keyedBy: CodingKeys.self)
  69. let methodConfiguration = try container.decodeIfPresent(
  70. [MethodConfiguration].self,
  71. forKey: .methodConfig
  72. )
  73. self.methodConfiguration = methodConfiguration ?? []
  74. let loadBalancingConfiguration = try container.decodeIfPresent(
  75. [LoadBalancingConfiguration].self,
  76. forKey: .loadBalancingConfig
  77. )
  78. self.loadBalancingConfiguration = loadBalancingConfiguration ?? []
  79. self.retryThrottlingPolicy = try container.decodeIfPresent(
  80. RetryThrottlingPolicy.self,
  81. forKey: .retryThrottling
  82. )
  83. }
  84. public func encode(to encoder: Encoder) throws {
  85. var container = encoder.container(keyedBy: CodingKeys.self)
  86. try container.encode(self.methodConfiguration, forKey: .methodConfig)
  87. try container.encode(self.loadBalancingConfiguration, forKey: .loadBalancingConfig)
  88. try container.encodeIfPresent(self.retryThrottlingPolicy, forKey: .retryThrottling)
  89. }
  90. }
  91. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
  92. extension ServiceConfiguration {
  93. /// Configuration used by clients for load-balancing.
  94. public struct LoadBalancingConfiguration: Hashable, Sendable {
  95. private enum Value: Hashable, Sendable {
  96. case pickFirst(PickFirst)
  97. case roundRobin(RoundRobin)
  98. }
  99. private var value: Value?
  100. private init(_ value: Value) {
  101. self.value = value
  102. }
  103. /// Creates a pick-first load balancing policy.
  104. ///
  105. /// - Parameter shuffleAddressList: Whether resolved addresses should be shuffled before
  106. /// attempting to connect to them.
  107. public static func pickFirst(shuffleAddressList: Bool) -> Self {
  108. Self(.pickFirst(PickFirst(shuffleAddressList: shuffleAddressList)))
  109. }
  110. /// Creates a pick-first load balancing policy.
  111. ///
  112. /// - Parameter pickFirst: The pick-first load balancing policy.
  113. public static func pickFirst(_ pickFirst: PickFirst) -> Self {
  114. Self(.pickFirst(pickFirst))
  115. }
  116. /// Creates a round-robin load balancing policy.
  117. public static var roundRobin: Self {
  118. Self(.roundRobin(RoundRobin()))
  119. }
  120. /// The pick-first policy, if configured.
  121. public var pickFirst: PickFirst? {
  122. get {
  123. switch self.value {
  124. case .pickFirst(let value):
  125. return value
  126. default:
  127. return nil
  128. }
  129. }
  130. set {
  131. self.value = newValue.map { .pickFirst($0) }
  132. }
  133. }
  134. /// The round-robin policy, if configured.
  135. public var roundRobin: RoundRobin? {
  136. get {
  137. switch self.value {
  138. case .roundRobin(let value):
  139. return value
  140. default:
  141. return nil
  142. }
  143. }
  144. set {
  145. self.value = newValue.map { .roundRobin($0) }
  146. }
  147. }
  148. }
  149. }
  150. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
  151. extension ServiceConfiguration.LoadBalancingConfiguration {
  152. /// Configuration for the pick-first load balancing policy.
  153. public struct PickFirst: Hashable, Sendable, Codable {
  154. /// Whether the resolved addresses should be shuffled before attempting to connect to them.
  155. public var shuffleAddressList: Bool
  156. /// Creates a new pick-first load balancing policy.
  157. /// - Parameter shuffleAddressList: Whether the resolved addresses should be shuffled before
  158. /// attempting to connect to them.
  159. public init(shuffleAddressList: Bool = false) {
  160. self.shuffleAddressList = shuffleAddressList
  161. }
  162. public init(from decoder: Decoder) throws {
  163. let container = try decoder.container(keyedBy: CodingKeys.self)
  164. let shuffle = try container.decodeIfPresent(Bool.self, forKey: .shuffleAddressList) ?? false
  165. self.shuffleAddressList = shuffle
  166. }
  167. }
  168. /// Configuration for the round-robin load balancing policy.
  169. public struct RoundRobin: Hashable, Sendable, Codable {
  170. /// Creates a new round-robin load balancing policy.
  171. public init() {}
  172. }
  173. }
  174. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
  175. extension ServiceConfiguration.LoadBalancingConfiguration: Codable {
  176. private enum CodingKeys: String, CodingKey {
  177. case roundRobin = "round_robin"
  178. case pickFirst = "pick_first"
  179. }
  180. public init(from decoder: Decoder) throws {
  181. let container = try decoder.container(keyedBy: CodingKeys.self)
  182. if let value = try container.decodeIfPresent(RoundRobin.self, forKey: .roundRobin) {
  183. self.value = .roundRobin(value)
  184. } else if let value = try container.decodeIfPresent(PickFirst.self, forKey: .pickFirst) {
  185. self.value = .pickFirst(value)
  186. } else {
  187. self.value = nil
  188. }
  189. }
  190. public func encode(to encoder: Encoder) throws {
  191. var container = encoder.container(keyedBy: CodingKeys.self)
  192. switch self.value {
  193. case .pickFirst(let value):
  194. try container.encode(value, forKey: .pickFirst)
  195. case .roundRobin(let value):
  196. try container.encode(value, forKey: .roundRobin)
  197. case .none:
  198. ()
  199. }
  200. }
  201. }
  202. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
  203. extension ServiceConfiguration {
  204. public struct RetryThrottlingPolicy: Hashable, Sendable, Codable {
  205. /// The initial, and maximum number of tokens.
  206. ///
  207. /// - Precondition: Must be greater than zero.
  208. public var maxTokens: Int
  209. /// The amount of tokens to add on each successful RPC.
  210. ///
  211. /// Typically this will be some number between 0 and 1, e.g., 0.1. Up to three decimal places
  212. /// are supported.
  213. ///
  214. /// - Precondition: Must be greater than zero.
  215. public var tokenRatio: Double
  216. /// Creates a new retry throttling policy.
  217. ///
  218. /// - Parameters:
  219. /// - maxTokens: The initial, and maximum number of tokens. Must be greater than zero.
  220. /// - tokenRatio: The amount of tokens to add on each successful RPC. Must be greater
  221. /// than zero.
  222. public init(maxTokens: Int, tokenRatio: Double) throws {
  223. self.maxTokens = maxTokens
  224. self.tokenRatio = tokenRatio
  225. try self.validateMaxTokens()
  226. try self.validateTokenRatio()
  227. }
  228. public init(from decoder: Decoder) throws {
  229. let container = try decoder.container(keyedBy: CodingKeys.self)
  230. self.maxTokens = try container.decode(Int.self, forKey: .maxTokens)
  231. self.tokenRatio = try container.decode(Double.self, forKey: .tokenRatio)
  232. try self.validateMaxTokens()
  233. try self.validateTokenRatio()
  234. }
  235. private func validateMaxTokens() throws {
  236. if self.maxTokens <= 0 {
  237. throw RuntimeError(code: .invalidArgument, message: "maxTokens must be greater than zero")
  238. }
  239. }
  240. private func validateTokenRatio() throws {
  241. if self.tokenRatio <= 0 {
  242. throw RuntimeError(code: .invalidArgument, message: "tokenRatio must be greater than zero")
  243. }
  244. }
  245. }
  246. }