Skip to content

Commit 820820d

Browse files
committed
Significantly Improve the Configuration API for Pools and Connections
## Motivation The API for establishing the configuration of a connection pool had a lot of jargon and properties that developers had issues keeping straight and understanding what each does. This commit provides first-class API support for concepts such as retry strategies, and how the pool handles connection counts. ## Changes - Add: New ConnectionCountBehavior for determining leaky / non-leaky behavior - Add: New ConnectionRetryStrategy for allowing customization of retry behavior - Change: RedisConnection.defaultPort to be a computed property - Change: The logging keys of pool connection retry metadata - Rename: Several configuration properties to drop prefixes or to be combined into new structures ## Result Developers should have a much better experience exploring the available configuration options for pools and connections, being able to understand how each piece works with the underlying system.
1 parent eedf158 commit 820820d

10 files changed

+401
-268
lines changed

Sources/RediStack/ConnectionPool/ConnectionPool.swift

Lines changed: 64 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -48,34 +48,22 @@ internal final class ConnectionPool {
4848
/// The event loop we're on.
4949
private let loop: EventLoop
5050

51-
/// The exponential backoff factor for connection attempts.
52-
internal let backoffFactor: Float32
53-
54-
/// The initial delay for backing off a reconnection attempt.
55-
internal let initialBackoffDelay: TimeAmount
56-
57-
/// The maximum number of connections the pool will preserve. Additional connections will be made available
58-
/// past this limit if `leaky` is set to `true`, but they will not be persisted in the pool once used.
59-
internal let maximumConnectionCount: Int
51+
/// The strategy to use for finding and returning connections when requested.
52+
internal let connectionRetryStrategy: RedisConnectionPool.PoolConnectionRetryStrategy
6053

6154
/// The minimum number of connections the pool will keep alive. If a connection is disconnected while in the
6255
/// pool such that the number of connections drops below this number, the connection will be re-established.
6356
internal let minimumConnectionCount: Int
57+
/// The maximum number of connections the pool will preserve.
58+
internal let maximumConnectionCount: Int
59+
/// The behavior to use for allowing or denying additional connections past the max connection count.
60+
internal let maxConnectionCountBehavior: RedisConnectionPool.ConnectionCountBehavior.MaxConnectionBehavior
6461

6562
/// The number of connection attempts currently outstanding.
6663
private var pendingConnectionCount: Int
67-
6864
/// The number of connections that have been handed out to users and are in active use.
6965
private(set) var leasedConnectionCount: Int
7066

71-
/// Whether this connection pool is "leaky".
72-
///
73-
/// The difference between a leaky and non-leaky connection pool is their behaviour when the pool is currently
74-
/// entirely in-use. For a leaky pool, if a connection is requested and none are available, a new connection attempt
75-
/// will be made and the connection will be passed to the user. For a non-leaky pool, the user will wait for a connection
76-
/// to be returned to the pool.
77-
internal let leaky: Bool
78-
7967
/// The current state of this connection pool.
8068
private var state: State
8169

@@ -85,49 +73,48 @@ internal final class ConnectionPool {
8573
return self.availableConnections.count + self.pendingConnectionCount + self.leasedConnectionCount
8674
}
8775

88-
/// Whether a connection can be added into the availableConnections pool when it's returned. This is true
89-
/// for non-leaky pools if the sum of availableConnections and leased connections is less than max connections,
90-
/// and for leaky pools if the number of availableConnections is less than max connections (as we went to all
91-
/// the effort to create the connection, we may as well keep it).
92-
/// Note that this means connection attempts in flight may not be used for anything. This is ok!
76+
/// Whether a connection can be added into the availableConnections pool when it's returned.
9377
private var canAddConnectionToPool: Bool {
94-
if self.leaky {
78+
switch self.maxConnectionCountBehavior {
79+
// only if the current available count is less than the max
80+
case .elastic:
9581
return self.availableConnections.count < self.maximumConnectionCount
96-
} else {
82+
83+
// only if the total connections count is less than the max
84+
case .strict:
9785
return (self.availableConnections.count + self.leasedConnectionCount) < self.maximumConnectionCount
9886
}
9987
}
10088

10189
internal init(
102-
maximumConnectionCount: Int,
10390
minimumConnectionCount: Int,
104-
leaky: Bool,
91+
maximumConnectionCount: Int,
92+
maxConnectionCountBehavior: RedisConnectionPool.ConnectionCountBehavior.MaxConnectionBehavior,
93+
connectionRetryStrategy: RedisConnectionPool.PoolConnectionRetryStrategy,
10594
loop: EventLoop,
10695
poolLogger: Logger,
107-
connectionBackoffFactor: Float32 = 2,
108-
initialConnectionBackoffDelay: TimeAmount = .milliseconds(100),
10996
connectionFactory: @escaping (EventLoop) -> EventLoopFuture<RedisConnection>
11097
) {
111-
guard minimumConnectionCount <= maximumConnectionCount else {
98+
self.minimumConnectionCount = minimumConnectionCount
99+
self.maximumConnectionCount = maximumConnectionCount
100+
self.maxConnectionCountBehavior = maxConnectionCountBehavior
101+
102+
guard self.minimumConnectionCount <= self.maximumConnectionCount else {
112103
poolLogger.critical("pool's minimum connection count is higher than the maximum")
113-
preconditionFailure("Minimum connection count must not exceed maximum")
104+
preconditionFailure("minimum connection count must not exceed maximum")
114105
}
115106

116-
self.connectionFactory = connectionFactory
107+
self.pendingConnectionCount = 0
108+
self.leasedConnectionCount = 0
117109
self.availableConnections = []
118-
self.availableConnections.reserveCapacity(maximumConnectionCount)
110+
self.availableConnections.reserveCapacity(self.maximumConnectionCount)
119111

120112
// 8 is a good number to skip the first few buffer resizings
121113
self.connectionWaiters = CircularBuffer(initialCapacity: 8)
122114
self.loop = loop
123-
self.backoffFactor = connectionBackoffFactor
124-
self.initialBackoffDelay = initialConnectionBackoffDelay
115+
self.connectionFactory = connectionFactory
116+
self.connectionRetryStrategy = connectionRetryStrategy
125117

126-
self.maximumConnectionCount = maximumConnectionCount
127-
self.minimumConnectionCount = minimumConnectionCount
128-
self.pendingConnectionCount = 0
129-
self.leasedConnectionCount = 0
130-
self.leaky = leaky
131118
self.state = .active
132119
}
133120

@@ -154,12 +141,13 @@ internal final class ConnectionPool {
154141
}
155142
}
156143

157-
func leaseConnection(deadline: NIODeadline, logger: Logger) -> EventLoopFuture<RedisConnection> {
144+
func leaseConnection(logger: Logger, deadline: NIODeadline? = nil) -> EventLoopFuture<RedisConnection> {
145+
let deadline = deadline ?? .now() + self.connectionRetryStrategy.timeout
158146
if self.loop.inEventLoop {
159-
return self._leaseConnection(deadline, logger: logger)
147+
return self._leaseConnection(logger: logger, deadline: deadline)
160148
} else {
161149
return self.loop.flatSubmit {
162-
return self._leaseConnection(deadline, logger: logger)
150+
return self._leaseConnection(logger: logger, deadline: deadline)
163151
}
164152
}
165153
}
@@ -191,12 +179,16 @@ extension ConnectionPool {
191179
RedisLogging.MetadataKeys.connectionCount: "\(neededConnections)"
192180
])
193181
while neededConnections > 0 {
194-
self._createConnection(backoff: self.initialBackoffDelay, startIn: .nanoseconds(0), logger: logger)
182+
self._createConnection(
183+
retryDelay: self.connectionRetryStrategy.initialDelay,
184+
startIn: .nanoseconds(0),
185+
logger: logger
186+
)
195187
neededConnections -= 1
196188
}
197189
}
198190

199-
private func _createConnection(backoff: TimeAmount, startIn delay: TimeAmount, logger: Logger) {
191+
private func _createConnection(retryDelay: TimeAmount, startIn delay: TimeAmount, logger: Logger) {
200192
self.loop.assertInEventLoop()
201193
self.pendingConnectionCount += 1
202194

@@ -212,7 +204,7 @@ extension ConnectionPool {
212204
self.connectionCreationSucceeded(connection, logger: logger)
213205

214206
case .failure(let error):
215-
self.connectionCreationFailed(error, backoff: backoff, logger: logger)
207+
self.connectionCreationFailed(error, retryDelay: retryDelay, logger: logger)
216208
}
217209
}
218210
}
@@ -243,7 +235,7 @@ extension ConnectionPool {
243235
}
244236
}
245237

246-
private func connectionCreationFailed(_ error: Error, backoff: TimeAmount, logger: Logger) {
238+
private func connectionCreationFailed(_ error: Error, retryDelay: TimeAmount, logger: Logger) {
247239
self.loop.assertInEventLoop()
248240

249241
logger.warning("failed to create connection for pool", metadata: [
@@ -260,15 +252,17 @@ extension ConnectionPool {
260252
// for this connection. Waiters can time out: if they do, we can just give up this connection.
261253
// We know folks need this in the following conditions:
262254
//
263-
// 1. For non-leaky buckets, we need this reconnection if there are any waiters AND the number of active connections (which includes
255+
// 1. For non-elastic buckets, we need this reconnection if there are any waiters AND the number of active connections (which includes
264256
// pending connection attempts) is less than max connections
265-
// 2. For leaky buckets, we need this reconnection if connectionWaiters.count is greater than the number of pending connection attempts.
257+
// 2. For elastic buckets, we need this reconnection if connectionWaiters.count is greater than the number of pending connection attempts.
266258
// 3. For either kind, if the number of active connections is less than the minimum.
267259
let shouldReconnect: Bool
268-
if self.leaky {
260+
switch self.maxConnectionCountBehavior {
261+
case .elastic:
269262
shouldReconnect = (self.connectionWaiters.count > self.pendingConnectionCount)
270263
|| (self.minimumConnectionCount > self.activeConnectionCount)
271-
} else {
264+
265+
case .strict:
272266
shouldReconnect = (!self.connectionWaiters.isEmpty && self.maximumConnectionCount > self.activeConnectionCount)
273267
|| (self.minimumConnectionCount > self.activeConnectionCount)
274268
}
@@ -279,12 +273,12 @@ extension ConnectionPool {
279273
}
280274

281275
// Ok, we need the new connection.
282-
let newBackoff = TimeAmount.nanoseconds(Int64(Float32(backoff.nanoseconds) * self.backoffFactor))
276+
let nextRetryDelay = self.connectionRetryStrategy.determineNewDelay(currentDelay: retryDelay)
283277
logger.debug("reconnecting after failed connection attempt", metadata: [
284-
RedisLogging.MetadataKeys.poolConnectionRetryBackoff: "\(backoff)ns",
285-
RedisLogging.MetadataKeys.poolConnectionRetryNewBackoff: "\(newBackoff)ns"
278+
RedisLogging.MetadataKeys.poolConnectionRetryAmount: "\(retryDelay)ns",
279+
RedisLogging.MetadataKeys.poolConnectionRetryNewAmount: "\(nextRetryDelay)ns"
286280
])
287-
self._createConnection(backoff: newBackoff, startIn: backoff, logger: logger)
281+
self._createConnection(retryDelay: nextRetryDelay, startIn: retryDelay, logger: logger)
288282
}
289283

290284
/// A connection that was monitored by this pool has been closed.
@@ -352,7 +346,7 @@ extension ConnectionPool {
352346

353347
/// This is the on-thread implementation for leasing connections out to users. Here we work out how to get a new
354348
/// connection, and attempt to do so.
355-
private func _leaseConnection(_ deadline: NIODeadline, logger: Logger) -> EventLoopFuture<RedisConnection> {
349+
private func _leaseConnection(logger: Logger, deadline: NIODeadline) -> EventLoopFuture<RedisConnection> {
356350
self.loop.assertInEventLoop()
357351

358352
guard case .active = self.state else {
@@ -386,11 +380,22 @@ extension ConnectionPool {
386380
self.connectionWaiters.append(waiter)
387381

388382
// Ok, we have connection targets. If the number of active connections is
389-
// below the max, or the pool is leaky, we can create a new connection. Otherwise, we just have
383+
// below the max, or the pool is elastic, we can create a new connection. Otherwise, we just have
390384
// to wait for a connection to come back.
391-
if self.activeConnectionCount < self.maximumConnectionCount || self.leaky {
385+
386+
let shouldCreateConnection: Bool
387+
switch self.maxConnectionCountBehavior {
388+
case .elastic: shouldCreateConnection = true
389+
case .strict: shouldCreateConnection = false
390+
}
391+
392+
if self.activeConnectionCount < self.maximumConnectionCount || shouldCreateConnection {
392393
logger.trace("creating new connection")
393-
self._createConnection(backoff: self.initialBackoffDelay, startIn: .nanoseconds(0), logger: logger)
394+
self._createConnection(
395+
retryDelay: self.connectionRetryStrategy.initialDelay,
396+
startIn: .nanoseconds(0),
397+
logger: logger
398+
)
394399
}
395400

396401
return waiter.futureResult

0 commit comments

Comments
 (0)