ServiceConfig.swift 9.6 KB

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