Coverage Report

Created: 2026-10-23 06:54

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}