Skip to content

Commit 6a79d46

Browse files
Lukasaweissi
authored andcommitted
Make pipeline handler behave better when removed (#1080)
Motivation: The HTTPServerPipelineHandler is removed from pipelines in many cases, but the most common case is over upgrade. While the handler is removable, it doesn't make any effort to ensure that it leaves the pipeline in a sensible state, which is pretty awkward. In particular, there are 3 things the pipeline handler may be holding on to that can lead to damage. The first is pipelined requests: if there are any, they should be delivered, as the user may be deliberately allowing pipelining. The second thing is read() calls. The HTTPServerPipelineHandler exerts backpressure on clients that aggressively pipeline by refusing to read from the socket. If that happens, and then the handler is removed from the channel, it will "forget" to restart reading from the socket on the way out. That leaves the channel quietly in a state where no reads will occur ever again, which is pretty uncool. The third thing is quiescing. The HTTPServerPipelineHandler catches quiescing events and allows them to deliver a response before closing a connection. If that has happened when the pipeline handler is removed, it should fall back to the behaviour as though it were not there. Modifications: - Added a handlerRemoved implementation to play event state that should be replayed. - Added a channelInactive implementation to drop data. Result: More graceful handler removal.
1 parent d9dc735 commit 6a79d46

File tree

3 files changed

+279
-8
lines changed

3 files changed

+279
-8
lines changed

Sources/NIOHTTP1/HTTPServerPipelineHandler.swift

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ public final class HTTPServerPipelineHandler: ChannelDuplexHandler, RemovableCha
164164

165165
/// Quiescing and the last request's `.end` has been seen which means we no longer accept any input.
166166
case quiescingLastRequestEndReceived
167+
168+
/// Quiescing and we have issued a channel close. Further I/O here is not expected, and won't be managed.
169+
case quiescingCompleted
167170
}
168171

169172
private var lifecycleState: LifecycleState = .acceptingEvents
@@ -174,8 +177,13 @@ public final class HTTPServerPipelineHandler: ChannelDuplexHandler, RemovableCha
174177
private var nextExpectedOutboundMessage: NextExpectedMessageType?
175178

176179
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
177-
guard self.lifecycleState != .quiescingLastRequestEndReceived else {
180+
switch self.lifecycleState {
181+
case .quiescingLastRequestEndReceived, .quiescingCompleted:
182+
// We're done, no more data for you.
178183
return
184+
case .acceptingEvents, .quiescingWaitingForRequestEnd:
185+
// Still accepting I/O
186+
()
179187
}
180188

181189
if self.eventBuffer.count != 0 || self.state == .responseEndPending {
@@ -187,7 +195,8 @@ public final class HTTPServerPipelineHandler: ChannelDuplexHandler, RemovableCha
187195
}
188196

189197
private func deliverOneMessage(context: ChannelHandlerContext, data: NIOAny) {
190-
assert(self.lifecycleState != .quiescingLastRequestEndReceived,
198+
assert(self.lifecycleState != .quiescingLastRequestEndReceived &&
199+
self.lifecycleState != .quiescingCompleted,
191200
"deliverOneMessage called in lifecycle illegal state \(self.lifecycleState)")
192201
let msg = self.unwrapInboundIn(data)
193202

@@ -216,6 +225,7 @@ public final class HTTPServerPipelineHandler: ChannelDuplexHandler, RemovableCha
216225
self.eventBuffer.removeAll()
217226
}
218227
if self.lifecycleState == .quiescingLastRequestEndReceived && self.state == .idle {
228+
self.lifecycleState = .quiescingCompleted
219229
context.close(promise: nil)
220230
}
221231
case .body:
@@ -248,7 +258,7 @@ public final class HTTPServerPipelineHandler: ChannelDuplexHandler, RemovableCha
248258
self.eventBuffer.removeAll()
249259
case .idle:
250260
// we're completely idle, let's just close
251-
self.lifecycleState = .quiescingLastRequestEndReceived
261+
self.lifecycleState = .quiescingCompleted
252262
self.eventBuffer.removeAll()
253263
context.close(promise: nil)
254264
case .requestEndPending, .requestAndResponseEndPending:
@@ -313,11 +323,17 @@ public final class HTTPServerPipelineHandler: ChannelDuplexHandler, RemovableCha
313323
// we just received the .end that we're missing so we can fall through to closing the connection
314324
fallthrough
315325
case .quiescingLastRequestEndReceived:
326+
self.lifecycleState = .quiescingCompleted
316327
context.write(data).flatMap {
317328
context.close()
318329
}.cascade(to: promise)
319330
case .acceptingEvents, .quiescingWaitingForRequestEnd:
320331
context.write(data, promise: promise)
332+
case .quiescingCompleted:
333+
// Uh, why are we writing more data here? We'll write it, but it should be guaranteed
334+
// to fail.
335+
assertionFailure("Wrote in quiescing completed state")
336+
context.write(data, promise: promise)
321337
}
322338
case .body, .head:
323339
context.write(data, promise: promise)
@@ -331,7 +347,11 @@ public final class HTTPServerPipelineHandler: ChannelDuplexHandler, RemovableCha
331347
}
332348

333349
public func read(context: ChannelHandlerContext) {
334-
if self.lifecycleState != .quiescingLastRequestEndReceived {
350+
switch self.lifecycleState {
351+
case .quiescingLastRequestEndReceived, .quiescingCompleted:
352+
// We swallow all reads now, as we're going to close the connection.
353+
()
354+
case .acceptingEvents, .quiescingWaitingForRequestEnd:
335355
if case .responseEndPending = self.state {
336356
self.readPending = true
337357
} else {
@@ -340,13 +360,75 @@ public final class HTTPServerPipelineHandler: ChannelDuplexHandler, RemovableCha
340360
}
341361
}
342362

363+
public func handlerRemoved(context: ChannelHandlerContext) {
364+
// We're being removed from the pipeline. We need to do a few things:
365+
//
366+
// 1. If we have buffered events, deliver them. While we shouldn't be
367+
// re-entrantly called, we want to ensure that so we take a local copy.
368+
// 2. If we are quiescing, we swallowed a quiescing event from the user: replay it,
369+
// as the user has hopefully added a handler that will do something with this.
370+
// 3. Finally, if we have a read pending, we need to release it.
371+
//
372+
// The basic theory here is that if there is anything we were going to do when we received
373+
// either a request .end or a response .end, we do it now because there is no future for us.
374+
// We also need to ensure we do not drop any data on the floor.
375+
//
376+
// At this stage we are no longer in the pipeline, so all further content should be
377+
// blocked from reaching us. Thus we can avoid mutating our own internal state any
378+
// longer.
379+
let bufferedEvents = self.eventBuffer
380+
for event in bufferedEvents {
381+
switch event {
382+
case .channelRead(let read):
383+
context.fireChannelRead(read)
384+
case .halfClose:
385+
context.fireUserInboundEventTriggered(ChannelEvent.inputClosed)
386+
case .error(let error):
387+
context.fireErrorCaught(error)
388+
}
389+
}
390+
391+
392+
switch self.lifecycleState {
393+
case .quiescingLastRequestEndReceived, .quiescingWaitingForRequestEnd:
394+
context.fireUserInboundEventTriggered(ChannelShouldQuiesceEvent())
395+
case .acceptingEvents, .quiescingCompleted:
396+
// Either we haven't quiesced, or we succeeded in doing it.
397+
()
398+
}
399+
400+
if self.readPending {
401+
context.read()
402+
}
403+
}
404+
405+
public func channelInactive(context: ChannelHandlerContext) {
406+
// Welp, this channel isn't going to work anymore. We may as well drop our pending events here, as we
407+
// cannot be expected to act on them any longer.
408+
//
409+
// Side note: it's important that we drop these. If we don't, handlerRemoved will deliver them all.
410+
// While it's fair to immediately pipeline a channel where the user chose to remove the HTTPPipelineHandler,
411+
// it's deeply unfair to do so to a user that didn't choose to do that, where it happened to them only because
412+
// the channel closed.
413+
//
414+
// We set keepingCapacity to avoid this reallocating a buffer, as we'll just free it shortly anyway.
415+
self.eventBuffer.removeAll(keepingCapacity: true)
416+
context.fireChannelInactive()
417+
}
418+
343419
/// A response has been sent: we can now start passing reads through
344420
/// again if there are no further pending requests, and send any read()
345421
/// call we may have swallowed.
346422
private func startReading(context: ChannelHandlerContext) {
347-
if self.readPending && self.state != .responseEndPending && self.lifecycleState != .quiescingLastRequestEndReceived {
348-
self.readPending = false
349-
context.read()
423+
if self.readPending && self.state != .responseEndPending {
424+
switch self.lifecycleState {
425+
case .quiescingLastRequestEndReceived, .quiescingCompleted:
426+
// No more reads in these states.
427+
()
428+
case .acceptingEvents, .quiescingWaitingForRequestEnd:
429+
self.readPending = false
430+
context.read()
431+
}
350432
}
351433
}
352434

Tests/NIOHTTP1Tests/HTTPServerPipelineHandlerTest+XCTest.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ extension HTTPServerPipelineHandlerTest {
4545
("testQuiescingAfterHavingReceivedOneRequestButBeforeResponseWasSentWithMoreRequestsInTheBuffer", testQuiescingAfterHavingReceivedOneRequestButBeforeResponseWasSentWithMoreRequestsInTheBuffer),
4646
("testParserErrorOnly", testParserErrorOnly),
4747
("testLegitRequestFollowedByParserErrorArrivingWhilstResponseOutstanding", testLegitRequestFollowedByParserErrorArrivingWhilstResponseOutstanding),
48+
("testRemovingWithResponseOutstandingTriggersRead", testRemovingWithResponseOutstandingTriggersRead),
49+
("testRemovingWithPartialResponseOutstandingTriggersRead", testRemovingWithPartialResponseOutstandingTriggersRead),
50+
("testRemovingWithBufferedRequestForwards", testRemovingWithBufferedRequestForwards),
51+
("testQuiescingInAResponseThenRemovedFiresEventAndReads", testQuiescingInAResponseThenRemovedFiresEventAndReads),
52+
("testQuiescingInAResponseThenRemovedFiresEventAndDoesntRead", testQuiescingInAResponseThenRemovedFiresEventAndDoesntRead),
4853
]
4954
}
5055
}

0 commit comments

Comments
 (0)