ServiceConfig.swift 9.1 KB

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