/src/grpc-swift/Sources/GRPC/ConnectionPool/PoolManager.swift
Line | Count | Source |
1 | | /* |
2 | | * Copyright 2021, 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 | | import Logging |
17 | | import NIOConcurrencyHelpers |
18 | | import NIOCore |
19 | | |
20 | | // Unchecked because all mutable state is protected by a lock. |
21 | | extension PooledChannel: @unchecked Sendable {} |
22 | | |
23 | | @usableFromInline |
24 | | internal final class PoolManager { |
25 | | /// Configuration used for each connection pool. |
26 | | @usableFromInline |
27 | | internal struct PerPoolConfiguration { |
28 | | /// The maximum number of connections per pool. |
29 | | @usableFromInline |
30 | | var maxConnections: Int |
31 | | |
32 | | /// The maximum number of waiters per pool. |
33 | | @usableFromInline |
34 | | var maxWaiters: Int |
35 | | |
36 | | /// The minimum number of connections to keep open per pool. |
37 | | /// This number of connections will never go idle and be closed. |
38 | | @usableFromInline |
39 | | var minConnections: Int |
40 | | |
41 | | /// A load threshold in the range `0.0 ... 1.0` beyond which another connection will be started |
42 | | /// (assuming there is a connection available to start). |
43 | | @usableFromInline |
44 | | var loadThreshold: Double |
45 | | |
46 | | /// The assumed value of HTTP/2 'SETTINGS_MAX_CONCURRENT_STREAMS'. |
47 | | @usableFromInline |
48 | | var assumedMaxConcurrentStreams: Int |
49 | | |
50 | | /// The assumed maximum number of streams concurrently available in the pool. |
51 | | @usableFromInline |
52 | 0 | var assumedStreamCapacity: Int { |
53 | 0 | return self.maxConnections * self.assumedMaxConcurrentStreams |
54 | 0 | } |
55 | | |
56 | | @usableFromInline |
57 | | var connectionBackoff: ConnectionBackoff |
58 | | |
59 | | /// A `Channel` provider. |
60 | | @usableFromInline |
61 | | var channelProvider: DefaultChannelProvider |
62 | | |
63 | | @usableFromInline |
64 | | var delegate: GRPCConnectionPoolDelegate? |
65 | | |
66 | | @usableFromInline |
67 | | var statsPeriod: TimeAmount? |
68 | | |
69 | | @usableFromInline |
70 | | internal init( |
71 | | maxConnections: Int, |
72 | | maxWaiters: Int, |
73 | | minConnections: Int, |
74 | | loadThreshold: Double, |
75 | | assumedMaxConcurrentStreams: Int = 100, |
76 | | connectionBackoff: ConnectionBackoff, |
77 | | channelProvider: DefaultChannelProvider, |
78 | | delegate: GRPCConnectionPoolDelegate?, |
79 | | statsPeriod: TimeAmount? |
80 | 0 | ) { |
81 | 0 | self.maxConnections = maxConnections |
82 | 0 | self.maxWaiters = maxWaiters |
83 | 0 | self.minConnections = minConnections |
84 | 0 | self.loadThreshold = loadThreshold |
85 | 0 | self.assumedMaxConcurrentStreams = assumedMaxConcurrentStreams |
86 | 0 | self.connectionBackoff = connectionBackoff |
87 | 0 | self.channelProvider = channelProvider |
88 | 0 | self.delegate = delegate |
89 | 0 | self.statsPeriod = statsPeriod |
90 | 0 | } |
91 | | } |
92 | | |
93 | | /// Logging metadata keys |
94 | | private enum Metadata { |
95 | | /// The ID of the pool manager. |
96 | | static let id = "poolmanager.id" |
97 | | /// The number of managed connection pools. |
98 | | static let poolCount = "poolmanager.pools.count" |
99 | | /// The maximum number of connections per pool. |
100 | | static let connectionsPerPool = "poolmanager.pools.conns_per_pool" |
101 | | /// The maximum number of waiters per pool. |
102 | | static let waitersPerPool = "poolmanager.pools.waiters_per_pool" |
103 | | } |
104 | | |
105 | | /// The current state of the pool manager, `lock` must be held when accessing or |
106 | | /// modifying `state`. |
107 | | @usableFromInline |
108 | | internal var _state: PoolManagerStateMachine |
109 | | |
110 | | @usableFromInline |
111 | | internal var _pools: [ConnectionPool] |
112 | | |
113 | | @usableFromInline |
114 | 0 | internal let lock = NIOLock() |
115 | | |
116 | | /// The `EventLoopGroup` providing `EventLoop`s for connection pools. Once initialized the manager |
117 | | /// will hold as many pools as there are loops in this `EventLoopGroup`. |
118 | | @usableFromInline |
119 | | internal let group: EventLoopGroup |
120 | | |
121 | | @usableFromInline |
122 | | internal let id: GRPCConnectionPoolID |
123 | | |
124 | | /// Make a new pool manager and initialize it. |
125 | | /// |
126 | | /// The pool manager manages one connection pool per event loop in the provided `EventLoopGroup`. |
127 | | /// Each connection pool is configured using the `perPoolConfiguration`. |
128 | | /// |
129 | | /// - Parameters: |
130 | | /// - group: The `EventLoopGroup` providing `EventLoop`s to connections managed by the pool |
131 | | /// manager. |
132 | | /// - perPoolConfiguration: Configuration used by each connection pool managed by the manager. |
133 | | /// - logger: A logger. |
134 | | /// - Returns: An initialized pool manager. |
135 | | @usableFromInline |
136 | | internal static func makeInitializedPoolManager( |
137 | | using group: EventLoopGroup, |
138 | | perPoolConfiguration: PerPoolConfiguration, |
139 | | logger: Logger |
140 | 0 | ) -> PoolManager { |
141 | 0 | let manager = PoolManager(privateButUsableFromInline_group: group) |
142 | 0 | manager.initialize(perPoolConfiguration: perPoolConfiguration, logger: logger) |
143 | 0 | return manager |
144 | 0 | } |
145 | | |
146 | | @usableFromInline |
147 | 0 | internal init(privateButUsableFromInline_group group: EventLoopGroup) { |
148 | 0 | self._state = PoolManagerStateMachine(.inactive) |
149 | 0 | self._pools = [] |
150 | 0 | self.group = group |
151 | 0 | self.id = .next() |
152 | 0 |
|
153 | 0 | // The pool relies on the identity of each `EventLoop` in the `EventLoopGroup` being unique. In |
154 | 0 | // practice this is unlikely to happen unless a custom `EventLoopGroup` is constructed, because |
155 | 0 | // of that we'll only check when running in debug mode. |
156 | 0 | debugOnly { |
157 | 0 | let eventLoopIDs = group.makeIterator().map { ObjectIdentifier($0) } |
158 | 0 | let uniqueEventLoopIDs = Set(eventLoopIDs) |
159 | 0 | assert( |
160 | 0 | eventLoopIDs.count == uniqueEventLoopIDs.count, |
161 | 0 | "'group' contains non-unique event loops" |
162 | 0 | ) |
163 | 0 | } |
164 | 0 | } |
165 | | |
166 | 0 | deinit { |
167 | 0 | self.lock.withLock { |
168 | 0 | assert( |
169 | 0 | self._state.isShutdownOrShuttingDown, |
170 | 0 | "The pool manager (\(self.id)) must be shutdown before going out of scope." |
171 | 0 | ) |
172 | 0 | } |
173 | 0 | } |
174 | | |
175 | | /// Initialize the pool manager, create and initialize one connection pool per event loop in the |
176 | | /// pools `EventLoopGroup`. |
177 | | /// |
178 | | /// - Important: Must only be called once. |
179 | | /// - Parameters: |
180 | | /// - configuration: The configuration used for each connection pool. |
181 | | /// - logger: A logger. |
182 | | private func initialize( |
183 | | perPoolConfiguration configuration: PerPoolConfiguration, |
184 | | logger: Logger |
185 | 0 | ) { |
186 | 0 | var logger = logger |
187 | 0 | logger[metadataKey: Metadata.id] = "\(self.id)" |
188 | 0 |
|
189 | 0 | let pools = self.makePools(perPoolConfiguration: configuration, logger: logger) |
190 | 0 |
|
191 | 0 | logger.debug( |
192 | 0 | "initializing connection pool manager", |
193 | 0 | metadata: [ |
194 | 0 | Metadata.poolCount: "\(pools.count)", |
195 | 0 | Metadata.connectionsPerPool: "\(configuration.maxConnections)", |
196 | 0 | Metadata.waitersPerPool: "\(configuration.maxWaiters)", |
197 | 0 | ] |
198 | 0 | ) |
199 | 0 |
|
200 | 0 | // The assumed maximum number of streams concurrently available in each pool. |
201 | 0 | let assumedCapacity = configuration.assumedStreamCapacity |
202 | 0 |
|
203 | 0 | // The state machine stores the per-pool state keyed by the pools `EventLoopID` and tells the |
204 | 0 | // pool manager about which pool to use/operate via the pools index in `self.pools`. |
205 | 0 | let poolKeys = pools.indices.map { index in |
206 | 0 | return ConnectionPoolKey( |
207 | 0 | index: ConnectionPoolIndex(index), |
208 | 0 | eventLoopID: pools[index].eventLoop.id |
209 | 0 | ) |
210 | 0 | } |
211 | 0 |
|
212 | 0 | let statsTask: RepeatedTask? |
213 | 0 | if let period = configuration.statsPeriod, let delegate = configuration.delegate { |
214 | 0 | let loop = self.group.next() |
215 | 0 | statsTask = loop.scheduleRepeatedTask(initialDelay: period, delay: period) { _ in |
216 | 0 | self.emitStats(delegate: delegate) |
217 | 0 | } |
218 | 0 | } else { |
219 | 0 | statsTask = nil |
220 | 0 | } |
221 | 0 |
|
222 | 0 | self.lock.withLock { |
223 | 0 | assert(self._pools.isEmpty) |
224 | 0 | self._pools = pools |
225 | 0 |
|
226 | 0 | // We'll blow up if we've already been initialized, that's fine, we don't allow callers to |
227 | 0 | // call `initialize` directly. |
228 | 0 | self._state.activatePools( |
229 | 0 | keyedBy: poolKeys, |
230 | 0 | assumingPerPoolCapacity: assumedCapacity, |
231 | 0 | statsTask: statsTask |
232 | 0 | ) |
233 | 0 | } |
234 | 0 |
|
235 | 0 | for pool in pools { |
236 | 0 | pool.initialize(connections: configuration.maxConnections) |
237 | 0 | } |
238 | 0 | } |
239 | | |
240 | | /// Make one pool per `EventLoop` in the pool's `EventLoopGroup`. |
241 | | /// - Parameters: |
242 | | /// - configuration: The configuration to make each pool with. |
243 | | /// - logger: A logger. |
244 | | /// - Returns: An array of `ConnectionPool`s. |
245 | | private func makePools( |
246 | | perPoolConfiguration configuration: PerPoolConfiguration, |
247 | | logger: Logger |
248 | 0 | ) -> [ConnectionPool] { |
249 | 0 | let eventLoops = self.group.makeIterator() |
250 | 0 | return eventLoops.map { eventLoop in |
251 | 0 | // We're creating a retain cycle here as each pool will reference the manager and the per-pool |
252 | 0 | // state will hold the pool which will in turn be held by the pool manager. That's okay: when |
253 | 0 | // the pool is shutdown the per-pool state and in turn each connection pool will be dropped. |
254 | 0 | // and we'll break the cycle. |
255 | 0 | return ConnectionPool( |
256 | 0 | eventLoop: eventLoop, |
257 | 0 | maxWaiters: configuration.maxWaiters, |
258 | 0 | minConnections: configuration.minConnections, |
259 | 0 | reservationLoadThreshold: configuration.loadThreshold, |
260 | 0 | assumedMaxConcurrentStreams: configuration.assumedMaxConcurrentStreams, |
261 | 0 | connectionBackoff: configuration.connectionBackoff, |
262 | 0 | channelProvider: configuration.channelProvider, |
263 | 0 | streamLender: self, |
264 | 0 | delegate: configuration.delegate, |
265 | 0 | logger: logger |
266 | 0 | ) |
267 | 0 | } |
268 | 0 | } |
269 | | |
270 | | // MARK: Stream Creation |
271 | | |
272 | | /// A future for a `Channel` from a managed connection pool. The `eventLoop` indicates the loop |
273 | | /// that the `Channel` is running on and therefore which event loop the RPC will use for its |
274 | | /// transport. |
275 | | @usableFromInline |
276 | | internal struct PooledStreamChannel { |
277 | | @inlinable |
278 | 0 | internal init(futureResult: EventLoopFuture<Channel>) { |
279 | 0 | self.futureResult = futureResult |
280 | 0 | } |
281 | | |
282 | | /// The future `Channel`. |
283 | | @usableFromInline |
284 | | var futureResult: EventLoopFuture<Channel> |
285 | | |
286 | | /// The `EventLoop` that the `Channel` is using. |
287 | | @usableFromInline |
288 | 0 | var eventLoop: EventLoop { |
289 | 0 | return self.futureResult.eventLoop |
290 | 0 | } |
291 | | } |
292 | | |
293 | | /// Make a stream and initialize it. |
294 | | /// |
295 | | /// - Parameters: |
296 | | /// - preferredEventLoop: The `EventLoop` that the stream should be created on, if possible. If |
297 | | /// a pool exists running this `EventLoop` then it will be chosen over all other pools, |
298 | | /// irregardless of the load on the pool. If no pool exists on the preferred `EventLoop` or |
299 | | /// no preference is given then the pool with the most streams available will be selected. |
300 | | /// The `EventLoop` of the selected pool will be the same as the `EventLoop` of |
301 | | /// the `EventLoopFuture<Channel>` returned from this call. |
302 | | /// - deadline: The point in time by which the stream must have been selected. If this deadline |
303 | | /// is passed then the returned `EventLoopFuture` will be failed. |
304 | | /// - logger: A logger. |
305 | | /// - initializer: A closure to initialize the `Channel` with. |
306 | | /// - Returns: A `PoolStreamChannel` indicating the future channel and `EventLoop` as that the |
307 | | /// `Channel` is using. The future will be failed if the pool manager has been shutdown, |
308 | | /// the deadline has passed before a stream was created or if the selected connection pool |
309 | | /// is unable to create a stream (if there is too much demand on that pool, for example). |
310 | | @inlinable |
311 | | internal func makeStream( |
312 | | preferredEventLoop: EventLoop?, |
313 | | deadline: NIODeadline, |
314 | | logger: Logger, |
315 | | streamInitializer initializer: @escaping @Sendable (Channel) -> EventLoopFuture<Void> |
316 | 0 | ) -> PooledStreamChannel { |
317 | 0 | let preferredEventLoopID = preferredEventLoop.map { EventLoopID($0) } |
318 | 0 | let reservedPool = self.lock.withLock { |
319 | 0 | return self._state.reserveStream(preferringPoolWithEventLoopID: preferredEventLoopID).map { |
320 | 0 | return self._pools[$0.value] |
321 | 0 | } |
322 | 0 | } |
323 | 0 |
|
324 | 0 | switch reservedPool { |
325 | 0 | case let .success(pool): |
326 | 0 | let channel = pool.makeStream(deadline: deadline, logger: logger, initializer: initializer) |
327 | 0 | return PooledStreamChannel(futureResult: channel) |
328 | 0 |
|
329 | 0 | case let .failure(error): |
330 | 0 | let eventLoop = preferredEventLoop ?? self.group.next() |
331 | 0 | return PooledStreamChannel(futureResult: eventLoop.makeFailedFuture(error)) |
332 | 0 | } |
333 | 0 | } |
334 | | |
335 | | // MARK: Shutdown |
336 | | |
337 | | /// Shutdown the pool manager and all connection pools it manages. |
338 | | @usableFromInline |
339 | 0 | internal func shutdown(mode: ConnectionManager.ShutdownMode, promise: EventLoopPromise<Void>) { |
340 | 0 | let (action, pools): (PoolManagerStateMachine.ShutdownAction, [ConnectionPool]?) = self.lock |
341 | 0 | .withLock { |
342 | 0 | let action = self._state.shutdown(promise: promise) |
343 | 0 |
|
344 | 0 | switch action { |
345 | 0 | case .shutdownPools: |
346 | 0 | // Clear out the pools; we need to shut them down. |
347 | 0 | let pools = self._pools |
348 | 0 | self._pools.removeAll(keepingCapacity: true) |
349 | 0 | return (action, pools) |
350 | 0 |
|
351 | 0 | case .alreadyShutdown, .alreadyShuttingDown: |
352 | 0 | return (action, nil) |
353 | 0 | } |
354 | 0 | } |
355 | 0 |
|
356 | 0 | switch (action, pools) { |
357 | 0 | case let (.shutdownPools(statsTask), .some(pools)): |
358 | 0 | statsTask?.cancel(promise: nil) |
359 | 0 | promise.futureResult.whenComplete { _ in self.shutdownComplete() } |
360 | 0 | EventLoopFuture.andAllSucceed(pools.map { $0.shutdown(mode: mode) }, promise: promise) |
361 | 0 |
|
362 | 0 | case let (.alreadyShuttingDown(future), .none): |
363 | 0 | promise.completeWith(future) |
364 | 0 |
|
365 | 0 | case (.alreadyShutdown, .none): |
366 | 0 | promise.succeed(()) |
367 | 0 |
|
368 | 0 | case (.shutdownPools, .none), |
369 | 0 | (.alreadyShuttingDown, .some), |
370 | 0 | (.alreadyShutdown, .some): |
371 | 0 | preconditionFailure() |
372 | 0 | } |
373 | 0 | } |
374 | | |
375 | 0 | private func shutdownComplete() { |
376 | 0 | self.lock.withLock { |
377 | 0 | self._state.shutdownComplete() |
378 | 0 | } |
379 | 0 | } |
380 | | |
381 | | // MARK: - Stats |
382 | | |
383 | 0 | private func emitStats(delegate: GRPCConnectionPoolDelegate) { |
384 | 0 | let pools = self.lock.withLock { self._pools } |
385 | 0 | if pools.isEmpty { return } |
386 | 0 |
|
387 | 0 | let statsFutures = pools.map { $0.stats() } |
388 | 0 | EventLoopFuture.whenAllSucceed(statsFutures, on: self.group.any()).whenSuccess { stats in |
389 | 0 | delegate.connectionPoolStats(stats, id: self.id) |
390 | 0 | } |
391 | 0 | } |
392 | | } |
393 | | |
394 | | // MARK: - Connection Pool to Pool Manager |
395 | | |
396 | | extension PoolManager: StreamLender { |
397 | | @usableFromInline |
398 | 0 | internal func returnStreams(_ count: Int, to pool: ConnectionPool) { |
399 | 0 | self.lock.withLock { |
400 | 0 | self._state.returnStreams(count, toPoolOnEventLoopWithID: pool.eventLoop.id) |
401 | 0 | } |
402 | 0 | } |
403 | | |
404 | | @usableFromInline |
405 | 0 | internal func changeStreamCapacity(by delta: Int, for pool: ConnectionPool) { |
406 | 0 | self.lock.withLock { |
407 | 0 | self._state.changeStreamCapacity(by: delta, forPoolOnEventLoopWithID: pool.eventLoop.id) |
408 | 0 | } |
409 | 0 | } |
410 | | } |
411 | | |
412 | | @usableFromInline |
413 | | internal enum PoolManagerError: Error { |
414 | | /// The pool manager has not been initialized yet. |
415 | | case notInitialized |
416 | | |
417 | | /// The pool manager has been shutdown or is in the process of shutting down. |
418 | | case shutdown |
419 | | } |