Skip to content

Commit 4dd5ee2

Browse files
authored
Add a server-idle handler (#818)
Motivation: Connections should be dropped to save resources if there are not RPCs for an idle timeout. This was added for clients in #798; this adds similart functionality to the server. Modifications: - Rename a `ClientConnectivityHandler` to `GRPCIdleHandler` - Add a 'mode' to the idle handler Note: timeout configuration will be added for client and server in a followup PR. Result: - Server's will drop connections if the client isn't doing anything. - Resolves #706
1 parent f71049e commit 4dd5ee2

File tree

4 files changed

+72
-33
lines changed

4 files changed

+72
-33
lines changed

Sources/GRPC/ClientConnection.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ extension Channel {
346346
}.flatMap { _ in
347347
return self.pipeline.handler(type: NIOHTTP2Handler.self).flatMap { http2Handler in
348348
self.pipeline.addHandler(
349-
ClientConnectivityHandler(connectionManager: connectionManager),
349+
GRPCIdleHandler(mode: .client(connectionManager)),
350350
position: .after(http2Handler)
351351
)
352352
}.flatMap {

Sources/GRPC/ClientConnectivityHandler.swift renamed to Sources/GRPC/GRPCIdleHandler.swift

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,29 @@
1616
import NIO
1717
import NIOHTTP2
1818

19-
internal class ClientConnectivityHandler: ChannelInboundHandler {
19+
internal class GRPCIdleHandler: ChannelInboundHandler {
2020
typealias InboundIn = HTTP2Frame
2121

22-
private var connectionManager: ConnectionManager
22+
/// The amount of time to wait before closing the channel when there are no active streams.
2323
private let idleTimeout: TimeAmount
2424

25+
/// The number of active streams.
2526
private var activeStreams = 0
27+
28+
/// The scheduled task which will close the channel.
2629
private var scheduledIdle: Scheduled<Void>? = nil
30+
31+
/// Client and server have slightly different behaviours; track which we are following.
32+
private var mode: Mode
33+
34+
/// The mode of operation: the client tracks additional connection state in the connection
35+
/// manager.
36+
internal enum Mode {
37+
case client(ConnectionManager)
38+
case server
39+
}
40+
41+
/// The current connection state.
2742
private var state: State = .notReady
2843

2944
private enum State {
@@ -37,8 +52,8 @@ internal class ClientConnectivityHandler: ChannelInboundHandler {
3752
case closed
3853
}
3954

40-
init(connectionManager: ConnectionManager, idleTimeout: TimeAmount = .minutes(5)) {
41-
self.connectionManager = connectionManager
55+
init(mode: Mode, idleTimeout: TimeAmount = .minutes(5)) {
56+
self.mode = mode
4257
self.idleTimeout = idleTimeout
4358
}
4459

@@ -67,10 +82,15 @@ internal class ClientConnectivityHandler: ChannelInboundHandler {
6782
}
6883

6984
func channelActive(context: ChannelHandlerContext) {
70-
switch self.state {
71-
case .notReady:
72-
self.connectionManager.channelActive(channel: context.channel)
73-
case .ready, .closed:
85+
switch (self.mode, self.state) {
86+
// The client should become active: we'll only schedule the idling when the channel
87+
// becomes 'ready'.
88+
case (.client(let manager), .notReady):
89+
manager.channelActive(channel: context.channel)
90+
91+
case (.server, .notReady),
92+
(_, .ready),
93+
(_, .closed):
7494
()
7595
}
7696

@@ -81,11 +101,16 @@ internal class ClientConnectivityHandler: ChannelInboundHandler {
81101
self.scheduledIdle?.cancel()
82102
self.scheduledIdle = nil
83103

84-
switch self.state {
85-
case .notReady, .ready:
86-
self.connectionManager.channelInactive()
87-
case .closed:
104+
switch (self.mode, self.state) {
105+
case (.client(let manager), .notReady),
106+
(.client(let manager), .ready):
107+
manager.channelInactive()
108+
109+
case (.server, .notReady),
110+
(.server, .ready),
111+
(_, .closed):
88112
()
113+
89114
}
90115

91116
context.fireChannelInactive()
@@ -100,18 +125,24 @@ internal class ClientConnectivityHandler: ChannelInboundHandler {
100125
case (.notReady, .settings):
101126
self.state = .ready
102127

103-
let remoteAddressDescription = context.channel.remoteAddress.map { "\($0)" } ?? "n/a"
104-
self.connectionManager.logger.info("gRPC connection ready", metadata: [
105-
"remote_address": "\(remoteAddressDescription)",
106-
"event_loop": "\(context.eventLoop)"
107-
])
128+
switch self.mode {
129+
case .client(let manager):
130+
let remoteAddressDescription = context.channel.remoteAddress.map { "\($0)" } ?? "n/a"
131+
manager.logger.info("gRPC connection ready", metadata: [
132+
"remote_address": "\(remoteAddressDescription)",
133+
"event_loop": "\(context.eventLoop)"
134+
])
135+
136+
// Let the manager know we're ready.
137+
manager.ready()
138+
139+
case .server:
140+
()
141+
}
108142

109143
// Start the idle timeout.
110144
self.scheduleIdleTimeout(context: context)
111145

112-
// Let the manager know we're ready.
113-
self.connectionManager.ready()
114-
115146
case (.notReady, .goAway),
116147
(.ready, .goAway):
117148
self.idle(context: context)
@@ -140,7 +171,13 @@ internal class ClientConnectivityHandler: ChannelInboundHandler {
140171
}
141172

142173
self.state = .closed
143-
self.connectionManager.idle()
174+
switch self.mode {
175+
case .client(let manager):
176+
manager.idle()
177+
case .server:
178+
()
179+
}
180+
144181
context.close(mode: .all, promise: nil)
145182
}
146183
}

Sources/GRPC/HTTPProtocolSwitcher.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,10 @@ extension HTTPProtocolSwitcher: ChannelInboundHandler, RemovableChannelHandler {
144144
) { (streamChannel, streamID) in
145145
streamChannel.pipeline.addHandler(HTTP2ToHTTP1ServerCodec(streamID: streamID, normalizeHTTPHeaders: true))
146146
.flatMap { self.handlersInitializer(streamChannel) }
147+
}.flatMap { multiplexer in
148+
// Add an idle handler between the two HTTP2 handlers.
149+
context.channel.pipeline.addHandler(GRPCIdleHandler(mode: .server), position: .before(multiplexer))
147150
}
148-
.map { _ in }
149151
.cascade(to: pipelineConfigured)
150152
}
151153

Tests/GRPCTests/ConnectionManagerTests.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ extension ConnectionManagerTests {
116116

117117
// Setup the real channel and activate it.
118118
let channel = EmbeddedChannel(
119-
handler: ClientConnectivityHandler(connectionManager: manager),
119+
handler: GRPCIdleHandler(mode: .client(manager)),
120120
loop: self.loop
121121
)
122122
channelPromise.succeed(channel)
@@ -150,7 +150,7 @@ extension ConnectionManagerTests {
150150

151151
// Setup the channel.
152152
let channel = EmbeddedChannel(
153-
handler: ClientConnectivityHandler(connectionManager: manager),
153+
handler: GRPCIdleHandler(mode: .client(manager)),
154154
loop: self.loop
155155
)
156156
channelPromise.succeed(channel)
@@ -193,7 +193,7 @@ extension ConnectionManagerTests {
193193

194194
// Setup the channel.
195195
let channel = EmbeddedChannel(
196-
handler: ClientConnectivityHandler(connectionManager: manager),
196+
handler: GRPCIdleHandler(mode: .client(manager)),
197197
loop: self.loop
198198
)
199199
channelPromise.succeed(channel)
@@ -249,7 +249,7 @@ extension ConnectionManagerTests {
249249

250250
// Setup the channel.
251251
let channel = EmbeddedChannel(
252-
handler: ClientConnectivityHandler(connectionManager: manager),
252+
handler: GRPCIdleHandler(mode: .client(manager)),
253253
loop: self.loop
254254
)
255255
channelPromise.succeed(channel)
@@ -305,7 +305,7 @@ extension ConnectionManagerTests {
305305

306306
// Setup the actual channel and complete the promise.
307307
let channel = EmbeddedChannel(
308-
handler: ClientConnectivityHandler(connectionManager: manager),
308+
handler: GRPCIdleHandler(mode: .client(manager)),
309309
loop: self.loop
310310
)
311311
channelPromise.succeed(channel)
@@ -401,7 +401,7 @@ extension ConnectionManagerTests {
401401

402402
// Prepare the channel
403403
let channel = EmbeddedChannel(
404-
handler: ClientConnectivityHandler(connectionManager: manager),
404+
handler: GRPCIdleHandler(mode: .client(manager)),
405405
loop: self.loop
406406
)
407407
channelPromise.succeed(channel)
@@ -461,7 +461,7 @@ extension ConnectionManagerTests {
461461

462462
// Prepare the channel
463463
let firstChannel = EmbeddedChannel(
464-
handler: ClientConnectivityHandler(connectionManager: manager),
464+
handler: GRPCIdleHandler(mode: .client(manager)),
465465
loop: self.loop
466466
)
467467
channelPromise.succeed(firstChannel)
@@ -521,7 +521,7 @@ extension ConnectionManagerTests {
521521

522522
// Prepare the first channel
523523
let firstChannel = EmbeddedChannel(
524-
handler: ClientConnectivityHandler(connectionManager: manager),
524+
handler: GRPCIdleHandler(mode: .client(manager)),
525525
loop: self.loop
526526
)
527527
firstChannelPromise.succeed(firstChannel)
@@ -548,7 +548,7 @@ extension ConnectionManagerTests {
548548

549549
// Prepare the second channel
550550
let secondChannel = EmbeddedChannel(
551-
handler: ClientConnectivityHandler(connectionManager: manager),
551+
handler: GRPCIdleHandler(mode: .client(manager)),
552552
loop: self.loop
553553
)
554554
secondChannelPromise.succeed(secondChannel)
@@ -582,7 +582,7 @@ extension ConnectionManagerTests {
582582

583583
// Setup the channel.
584584
let channel = EmbeddedChannel(
585-
handler: ClientConnectivityHandler(connectionManager: manager),
585+
handler: GRPCIdleHandler(mode: .client(manager)),
586586
loop: self.loop
587587
)
588588
channelPromise.succeed(channel)

0 commit comments

Comments
 (0)