Skip to content

Commit e481d56

Browse files
authored
Allow clients to set a max age for connections (#2235)
Motivation: Some load balancers reject new streams when a client certificate has expired but allow existing ones to continue. This is problematic because there's no signal that the connection can't be used anymore so new RPCs will continue to fail. Modifications: Allow clients to configure a max age for connections, after which time the connection will shutdown gracefully. This allows new connections to be established. Result: Client connections can be configured to age out
1 parent 532dfc6 commit e481d56

9 files changed

+324
-6
lines changed

Sources/GRPC/ClientConnection.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,14 @@ extension ClientConnection {
423423
/// Defaults to 30 minutes.
424424
public var connectionIdleTimeout: TimeAmount = .minutes(30)
425425

426+
/// The maximum allowed age of a connection.
427+
///
428+
/// If set, no new RPCs will be started on the connection after the connection has been opened
429+
/// for this period of time. Existing RPCs will be allowed to continue and the connection will
430+
/// close once all RPCs on the connection have finished. If this isn't set then connections have
431+
/// no limit on their lifetime.
432+
public var connectionMaxAge: TimeAmount? = nil
433+
426434
/// The behavior used to determine when an RPC should start. That is, whether it should wait for
427435
/// an active connection or fail quickly if no connection is currently available.
428436
///
@@ -635,6 +643,7 @@ extension ChannelPipeline.SynchronousOperations {
635643
connectionManager: ConnectionManager,
636644
connectionKeepalive: ClientConnectionKeepalive,
637645
connectionIdleTimeout: TimeAmount,
646+
connectionMaxAge: TimeAmount?,
638647
httpTargetWindowSize: Int,
639648
httpMaxFrameSize: Int,
640649
httpMaxResetStreams: Int,
@@ -672,6 +681,7 @@ extension ChannelPipeline.SynchronousOperations {
672681
connectionManager: connectionManager,
673682
multiplexer: h2Multiplexer,
674683
idleTimeout: connectionIdleTimeout,
684+
maxAge: connectionMaxAge,
675685
keepalive: connectionKeepalive,
676686
logger: logger
677687
)

Sources/GRPC/ConnectionManagerChannelProvider.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider {
6060
internal var connectionKeepalive: ClientConnectionKeepalive
6161
@usableFromInline
6262
internal var connectionIdleTimeout: TimeAmount
63+
@usableFromInline
64+
internal var connectionMaxAge: TimeAmount?
6365

6466
@usableFromInline
6567
internal var tlsMode: TLSMode
@@ -100,6 +102,7 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider {
100102
connectionTarget: ConnectionTarget,
101103
connectionKeepalive: ClientConnectionKeepalive,
102104
connectionIdleTimeout: TimeAmount,
105+
connectionMaxAge: TimeAmount?,
103106
tlsMode: TLSMode,
104107
tlsConfiguration: GRPCTLSConfiguration?,
105108
httpTargetWindowSize: Int,
@@ -113,6 +116,7 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider {
113116
connectionTarget: connectionTarget,
114117
connectionKeepalive: connectionKeepalive,
115118
connectionIdleTimeout: connectionIdleTimeout,
119+
connectionMaxAge: connectionMaxAge,
116120
tlsMode: tlsMode,
117121
tlsConfiguration: tlsConfiguration,
118122
httpTargetWindowSize: httpTargetWindowSize,
@@ -131,6 +135,7 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider {
131135
connectionTarget: ConnectionTarget,
132136
connectionKeepalive: ClientConnectionKeepalive,
133137
connectionIdleTimeout: TimeAmount,
138+
connectionMaxAge: TimeAmount?,
134139
tlsMode: TLSMode,
135140
tlsConfiguration: GRPCTLSConfiguration?,
136141
httpTargetWindowSize: Int,
@@ -142,6 +147,7 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider {
142147
self.connectionTarget = connectionTarget
143148
self.connectionKeepalive = connectionKeepalive
144149
self.connectionIdleTimeout = connectionIdleTimeout
150+
self.connectionMaxAge = connectionMaxAge
145151

146152
self.tlsMode = tlsMode
147153
self.tlsConfiguration = tlsConfiguration
@@ -182,6 +188,7 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider {
182188
connectionTarget: configuration.target,
183189
connectionKeepalive: configuration.connectionKeepalive,
184190
connectionIdleTimeout: configuration.connectionIdleTimeout,
191+
connectionMaxAge: configuration.connectionMaxAge,
185192
tlsMode: tlsMode,
186193
tlsConfiguration: configuration.tlsConfiguration,
187194
httpTargetWindowSize: configuration.httpTargetWindowSize,
@@ -264,6 +271,7 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider {
264271
connectionManager: connectionManager,
265272
connectionKeepalive: self.connectionKeepalive,
266273
connectionIdleTimeout: self.connectionIdleTimeout,
274+
connectionMaxAge: self.connectionMaxAge,
267275
httpTargetWindowSize: self.httpTargetWindowSize,
268276
httpMaxFrameSize: self.httpMaxFrameSize,
269277
httpMaxResetStreams: self.httpMaxResetStreams,

Sources/GRPC/ConnectionPool/GRPCChannelPool.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,14 @@ extension GRPCChannelPool {
156156
/// If a connection becomes idle, starting a new RPC will automatically create a new connection.
157157
public var idleTimeout = TimeAmount.minutes(30)
158158

159+
/// The maximum allowed age of a connection.
160+
///
161+
/// If set, no new RPCs will be started on the connection after the connection has been opened
162+
/// for this period of time. Existing RPCs will be allowed to continue and the connection will
163+
/// close once all RPCs on the connection have finished. If this isn't set then connections have
164+
/// no limit on their lifetime.
165+
public var maxConnectionAge: TimeAmount? = nil
166+
159167
/// The connection keepalive configuration.
160168
public var keepalive = ClientConnectionKeepalive()
161169

Sources/GRPC/ConnectionPool/PooledChannel.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ internal final class PooledChannel: GRPCChannel {
8686
connectionTarget: configuration.target,
8787
connectionKeepalive: configuration.keepalive,
8888
connectionIdleTimeout: configuration.idleTimeout,
89+
connectionMaxAge: configuration.maxConnectionAge,
8990
tlsMode: tlsMode,
9091
tlsConfiguration: configuration.transportSecurity.tlsConfiguration,
9192
httpTargetWindowSize: configuration.http2.targetWindowSize,
@@ -100,6 +101,7 @@ internal final class PooledChannel: GRPCChannel {
100101
connectionTarget: configuration.target,
101102
connectionKeepalive: configuration.keepalive,
102103
connectionIdleTimeout: configuration.idleTimeout,
104+
connectionMaxAge: configuration.maxConnectionAge,
103105
tlsMode: tlsMode,
104106
tlsConfiguration: configuration.transportSecurity.tlsConfiguration,
105107
httpTargetWindowSize: configuration.http2.targetWindowSize,
@@ -114,6 +116,7 @@ internal final class PooledChannel: GRPCChannel {
114116
connectionTarget: configuration.target,
115117
connectionKeepalive: configuration.keepalive,
116118
connectionIdleTimeout: configuration.idleTimeout,
119+
connectionMaxAge: configuration.maxConnectionAge,
117120
tlsMode: tlsMode,
118121
tlsConfiguration: configuration.transportSecurity.tlsConfiguration,
119122
httpTargetWindowSize: configuration.http2.targetWindowSize,

Sources/GRPC/GRPCIdleHandler.swift

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,18 @@ internal final class GRPCIdleHandler: ChannelInboundHandler {
2727
/// If nil, then we shouldn't schedule idle tasks.
2828
private let idleTimeout: TimeAmount?
2929

30+
/// The maximum amount of time the connection is allowed to live before quiescing.
31+
private let maxAge: TimeAmount?
32+
3033
/// The ping handler.
3134
private var pingHandler: PingHandler
3235

36+
/// The scheduled task which will close the connection gently after the max connection age
37+
/// has been reached.
38+
private var scheduledMaxAgeClose: Scheduled<Void>?
39+
3340
/// The scheduled task which will close the connection after the keep-alive timeout has expired.
34-
private var scheduledClose: Scheduled<Void>?
41+
private var scheduledKeepAliveClose: Scheduled<Void>?
3542

3643
/// The scheduled task which will ping.
3744
private var scheduledPing: RepeatedTask?
@@ -75,6 +82,7 @@ internal final class GRPCIdleHandler: ChannelInboundHandler {
7582
connectionManager: ConnectionManager,
7683
multiplexer: HTTP2StreamMultiplexer,
7784
idleTimeout: TimeAmount,
85+
maxAge: TimeAmount?,
7886
keepalive configuration: ClientConnectionKeepalive,
7987
logger: Logger
8088
) {
@@ -95,6 +103,7 @@ internal final class GRPCIdleHandler: ChannelInboundHandler {
95103
minimumSentPingIntervalWithoutData: configuration.minimumSentPingIntervalWithoutData
96104
)
97105
self.creationTime = .now()
106+
self.maxAge = maxAge
98107
}
99108

100109
init(
@@ -116,6 +125,7 @@ internal final class GRPCIdleHandler: ChannelInboundHandler {
116125
maximumPingStrikes: configuration.maximumPingStrikes
117126
)
118127
self.creationTime = .now()
128+
self.maxAge = nil
119129
}
120130

121131
private func perform(operations: GRPCIdleHandlerStateMachine.Operations) {
@@ -218,8 +228,8 @@ internal final class GRPCIdleHandler: ChannelInboundHandler {
218228
)
219229

220230
case .cancelScheduledTimeout:
221-
self.scheduledClose?.cancel()
222-
self.scheduledClose = nil
231+
self.scheduledKeepAliveClose?.cancel()
232+
self.scheduledKeepAliveClose = nil
223233

224234
case let .schedulePing(delay, timeout):
225235
self.schedulePing(in: delay, timeout: timeout)
@@ -267,7 +277,7 @@ internal final class GRPCIdleHandler: ChannelInboundHandler {
267277
}
268278

269279
private func scheduleClose(in timeout: TimeAmount) {
270-
self.scheduledClose = self.context?.eventLoop.scheduleTask(in: timeout) {
280+
self.scheduledKeepAliveClose = self.context?.eventLoop.scheduleTask(in: timeout) {
271281
self.stateMachine.logger.debug("keepalive timer expired")
272282
self.perform(operations: self.stateMachine.shutdownNow())
273283
}
@@ -334,22 +344,35 @@ internal final class GRPCIdleHandler: ChannelInboundHandler {
334344
remote: context.remoteAddress
335345
)
336346

347+
// If a max age has been set then start a timer. This will only be cancelled when it fires or when
348+
// the channel eventually becomes inactive.
349+
if let maxAge = self.maxAge {
350+
assert(self.scheduledMaxAgeClose == nil)
351+
self.scheduledMaxAgeClose = context.eventLoop.scheduleTask(in: maxAge) {
352+
let operations = self.stateMachine.reachedMaxAge()
353+
self.perform(operations: operations)
354+
}
355+
}
356+
337357
// No state machine action here.
338358
switch self.mode {
339359
case let .client(connectionManager, multiplexer):
340360
connectionManager.channelActive(channel: context.channel, multiplexer: multiplexer)
341361
case .server:
342362
()
343363
}
364+
344365
context.fireChannelActive()
345366
}
346367

347368
func channelInactive(context: ChannelHandlerContext) {
348369
self.perform(operations: self.stateMachine.channelInactive())
349370
self.scheduledPing?.cancel()
350-
self.scheduledClose?.cancel()
371+
self.scheduledKeepAliveClose?.cancel()
372+
self.scheduledMaxAgeClose?.cancel()
351373
self.scheduledPing = nil
352-
self.scheduledClose = nil
374+
self.scheduledKeepAliveClose = nil
375+
self.scheduledMaxAgeClose = nil
353376
context.fireChannelInactive()
354377
}
355378

Sources/GRPC/GRPCIdleHandlerStateMachine.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,13 @@ struct GRPCIdleHandlerStateMachine {
465465
return operations
466466
}
467467

468+
/// The connection has reached it's max allowable age. Let existing RPCs continue, but don't
469+
/// allow any new ones.
470+
mutating func reachedMaxAge() -> Operations {
471+
// Treat this as if the other side sent us a GOAWAY: gently shutdown the connection.
472+
self.receiveGoAway()
473+
}
474+
468475
/// We've received a GOAWAY frame from the remote peer. Either the remote peer wants to close the
469476
/// connection or they're responding to us shutting down the connection.
470477
mutating func receiveGoAway() -> Operations {

Tests/GRPCTests/ConnectionManagerTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ extension ConnectionManagerTests {
165165
connectionManager: manager,
166166
multiplexer: h2mux,
167167
idleTimeout: .minutes(5),
168+
maxAge: nil,
168169
keepalive: .init(),
169170
logger: self.logger
170171
)
@@ -217,6 +218,7 @@ extension ConnectionManagerTests {
217218
connectionManager: manager,
218219
multiplexer: h2mux,
219220
idleTimeout: .minutes(5),
221+
maxAge: nil,
220222
keepalive: .init(),
221223
logger: self.logger
222224
)
@@ -273,6 +275,7 @@ extension ConnectionManagerTests {
273275
inboundStreamInitializer: nil
274276
),
275277
idleTimeout: .minutes(5),
278+
maxAge: nil,
276279
keepalive: .init(),
277280
logger: self.logger
278281
)
@@ -322,6 +325,7 @@ extension ConnectionManagerTests {
322325
inboundStreamInitializer: nil
323326
),
324327
idleTimeout: .minutes(5),
328+
maxAge: nil,
325329
keepalive: .init(),
326330
logger: self.logger
327331
)
@@ -350,6 +354,7 @@ extension ConnectionManagerTests {
350354
inboundStreamInitializer: nil
351355
),
352356
idleTimeout: .minutes(5),
357+
maxAge: nil,
353358
keepalive: .init(),
354359
logger: self.logger
355360
)
@@ -391,6 +396,7 @@ extension ConnectionManagerTests {
391396
connectionManager: manager,
392397
multiplexer: h2mux,
393398
idleTimeout: .minutes(5),
399+
maxAge: nil,
394400
keepalive: .init(),
395401
logger: self.logger
396402
)
@@ -464,6 +470,7 @@ extension ConnectionManagerTests {
464470
connectionManager: manager,
465471
multiplexer: h2mux,
466472
idleTimeout: .minutes(5),
473+
maxAge: nil,
467474
keepalive: .init(),
468475
logger: self.logger
469476
)
@@ -536,6 +543,7 @@ extension ConnectionManagerTests {
536543
connectionManager: manager,
537544
multiplexer: h2mux,
538545
idleTimeout: .minutes(5),
546+
maxAge: nil,
539547
keepalive: .init(),
540548
logger: self.logger
541549
)
@@ -654,6 +662,7 @@ extension ConnectionManagerTests {
654662
connectionManager: manager,
655663
multiplexer: h2mux,
656664
idleTimeout: .minutes(5),
665+
maxAge: nil,
657666
keepalive: .init(),
658667
logger: self.logger
659668
)
@@ -730,6 +739,7 @@ extension ConnectionManagerTests {
730739
connectionManager: manager,
731740
multiplexer: h2mux,
732741
idleTimeout: .minutes(5),
742+
maxAge: nil,
733743
keepalive: .init(),
734744
logger: self.logger
735745
)
@@ -807,6 +817,7 @@ extension ConnectionManagerTests {
807817
connectionManager: manager,
808818
multiplexer: firstH2mux,
809819
idleTimeout: .minutes(5),
820+
maxAge: nil,
810821
keepalive: .init(),
811822
logger: self.logger
812823
)
@@ -855,6 +866,7 @@ extension ConnectionManagerTests {
855866
connectionManager: manager,
856867
multiplexer: secondH2mux,
857868
idleTimeout: .minutes(5),
869+
maxAge: nil,
858870
keepalive: .init(),
859871
logger: self.logger
860872
)
@@ -905,6 +917,7 @@ extension ConnectionManagerTests {
905917
connectionManager: manager,
906918
multiplexer: h2mux,
907919
idleTimeout: .minutes(5),
920+
maxAge: nil,
908921
keepalive: .init(),
909922
logger: self.logger
910923
)
@@ -1063,6 +1076,7 @@ extension ConnectionManagerTests {
10631076
connectionManager: manager,
10641077
multiplexer: h2mux,
10651078
idleTimeout: .minutes(5),
1079+
maxAge: nil,
10661080
keepalive: .init(),
10671081
logger: self.logger
10681082
)
@@ -1120,6 +1134,7 @@ extension ConnectionManagerTests {
11201134
connectionManager: manager,
11211135
multiplexer: h2mux,
11221136
idleTimeout: .minutes(5),
1137+
maxAge: nil,
11231138
keepalive: .init(),
11241139
logger: self.logger
11251140
)
@@ -1201,6 +1216,7 @@ extension ConnectionManagerTests {
12011216
connectionManager: manager,
12021217
multiplexer: multiplexer,
12031218
idleTimeout: .minutes(5),
1219+
maxAge: nil,
12041220
keepalive: ClientConnectionKeepalive(),
12051221
logger: self.logger
12061222
)
@@ -1314,6 +1330,7 @@ extension ConnectionManagerTests {
13141330
connectionManager: connectionManager,
13151331
multiplexer: multiplexer,
13161332
idleTimeout: .minutes(60),
1333+
maxAge: nil,
13171334
keepalive: .init(),
13181335
logger: self.clientLogger
13191336
)

Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,7 @@ extension ChannelController: ConnectionManagerChannelProvider {
12991299
connectionManager: connectionManager,
13001300
multiplexer: multiplexer,
13011301
idleTimeout: .minutes(5),
1302+
maxAge: nil,
13021303
keepalive: ClientConnectionKeepalive(),
13031304
logger: logger
13041305
)

0 commit comments

Comments
 (0)