Skip to content

Commit 38feec9

Browse files
authored
Soften errors when seeing inactive before active (#376)
Motivation: When we started checking active/inactive ordering, we added code that rejected some channel pipeline state transitions. That code was too strict: it is absolutely possible to see channelInactive before channelActive in well-functioning programs. We'll still call this an error, but we don't need to crash in debug mode when we see it. Modifications: - Made channelInactive before channelActive tolerated. Result: More reliable code
1 parent 8606221 commit 38feec9

File tree

3 files changed

+48
-26
lines changed

3 files changed

+48
-26
lines changed

Sources/NIOHTTP2/HTTP2ChannelHandler.swift

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -222,16 +222,21 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
222222
// call down the pipeline when we added in handlerAdded. That's ok!
223223
// We can safely ignore this.
224224
return
225-
case .activating, .inactiveWhileActivating, .inactive:
225+
case .inactive:
226+
// This means we received channelInactive already. This can happen, unfortunately, usually when
227+
// calling close() from within the completion of a channel connect promise. We tolerate this,
228+
// but fire an error to make sure it's fatal.
229+
context.fireChannelActive()
230+
context.fireErrorCaught(NIOHTTP2Errors.ActivationError(state: .inactive, activating: true))
231+
return
232+
case .activating, .inactiveWhileActivating:
226233
// All of these states are channel pipeline invariant violations, but conceptually possible.
227234
//
228235
// - .activating implies we got another channelActive while we were handling one. That would be
229236
// a violation of pipeline invariants.
230237
// - .inactiveWhileActivating implies a sequence of channelActive, channelInactive, channelActive
231238
// synchronously. This is not just unlikely, but also misuse of the handler or violation of
232239
// channel pipeline invariants.
233-
// - .inactive implies we received channelInactive and then got another active. This is almost certainly
234-
// misuse of the handler.
235240
//
236241
// We'll throw an error and then close. In debug builds, we crash.
237242
self.impossibleActivationStateTransition(
@@ -257,7 +262,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
257262
case .idle, .activated, .inactive:
258263
// These three states should be impossible.
259264
//
260-
// - .idle somehow implies we didn't execute the code above.
265+
// - .idle and .inactive somehow implies we didn't execute the code above.
261266
// - .activated implies that the above code didn't prevent us re-entrantly getting to this point.
262267
// - .inactive implies that somehow we hit channelInactive but didn't enter .inactiveWhileActivating.
263268
self.impossibleActivationStateTransition(
@@ -277,12 +282,12 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
277282
// Huh, we got channelInactive during activation. We need to maintain
278283
// ordering, so we'll temporarily delay this.
279284
self.activationState = .inactiveWhileActivating
280-
case .idle, .inactiveWhileActivating, .inactive:
281-
// This is weird.
282-
//
283-
// .idle implies that somehow we got channelInactive before channelActive, which is probably an error.
284-
// .inactiveWhileActivating and .inactive make this a second channelInactive call, which is also probably
285-
// an error.
285+
case .idle:
286+
// This implies getting channelInactive before channelActive. This is uncommon, but not impossible.
287+
// We'll tolerate it here.
288+
self.activationState = .inactive
289+
case .inactiveWhileActivating, .inactive:
290+
// This is weird. This can only happen if we see channelInactive twice, which is probably an error.
286291
self.impossibleActivationStateTransition(state: self.activationState, activating: false, context: context)
287292
}
288293

Tests/NIOHTTP2Tests/SimpleClientServerTests+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ extension SimpleClientServerTests {
3636
("testHandlingChannelInactiveDuringActive", testHandlingChannelInactiveDuringActive),
3737
("testWritingFromChannelActiveIsntReordered", testWritingFromChannelActiveIsntReordered),
3838
("testChannelActiveAfterAddingToActivePipelineDoesntError", testChannelActiveAfterAddingToActivePipelineDoesntError),
39+
("testChannelInactiveThenChannelActiveErrorsButDoesntTrap", testChannelInactiveThenChannelActiveErrorsButDoesntTrap),
3940
("testImpossibleStateTransitionsThrowErrors", testImpossibleStateTransitionsThrowErrors),
4041
("testDynamicHeaderFieldsArentEmittedWithZeroTableSize", testDynamicHeaderFieldsArentEmittedWithZeroTableSize),
4142
("testDynamicHeaderFieldsArentToleratedWithZeroTableSize", testDynamicHeaderFieldsArentToleratedWithZeroTableSize),

Tests/NIOHTTP2Tests/SimpleClientServerTests.swift

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,29 @@ class SimpleClientServerTests: XCTestCase {
428428
XCTAssertNil(try channel.readOutbound(as: ByteBuffer.self))
429429
}
430430

431+
func testChannelInactiveThenChannelActiveErrorsButDoesntTrap() throws {
432+
let channel = EmbeddedChannel()
433+
let recorder = ErrorRecorder()
434+
try channel.pipeline.syncOperations.addHandlers(
435+
[
436+
NIOHTTP2Handler(mode: .client),
437+
recorder
438+
]
439+
)
440+
441+
XCTAssertEqual(recorder.errors.count, 0)
442+
443+
// Send channelInactive followed by channelActive. This can happen if a user calls close
444+
// from within a connect promise.
445+
channel.pipeline.fireChannelInactive()
446+
XCTAssertEqual(recorder.errors.count, 0)
447+
448+
channel.pipeline.fireChannelActive()
449+
450+
XCTAssertEqual(recorder.errors.count, 1)
451+
XCTAssertTrue(recorder.errors.allSatisfy { $0 is NIOHTTP2Errors.ActivationError })
452+
}
453+
431454
func testImpossibleStateTransitionsThrowErrors() throws {
432455
func setUpChannel() throws -> (EmbeddedChannel, ErrorRecorder) {
433456
let channel = EmbeddedChannel()
@@ -443,22 +466,14 @@ class SimpleClientServerTests: XCTestCase {
443466
return (channel, recorder)
444467
}
445468

446-
// First impossible state transition: channelInactive during idle.
447-
// Doesn't transition state, just errors. Becuase we close during this, we hit
448-
// the error twice!
469+
// First impossible state transition: channelActive on channelActive.
449470
var (channel, recorder) = try setUpChannel()
450-
channel.pipeline.fireChannelInactive()
451-
XCTAssertEqual(recorder.errors.count, 2)
452-
XCTAssertTrue(recorder.errors.allSatisfy { $0 is NIOHTTP2Errors.ActivationError })
453-
454-
// Second impossible state transition: channelActive on channelActive.
455-
(channel, recorder) = try setUpChannel()
456471
try channel.pipeline.syncOperations.addHandler(ActionOnFlushHandler { $0.pipeline.fireChannelActive() }, position: .first)
457472
channel.pipeline.fireChannelActive()
458473
XCTAssertEqual(recorder.errors.count, 1)
459474
XCTAssertTrue(recorder.errors.allSatisfy { $0 is NIOHTTP2Errors.ActivationError })
460475

461-
// Third impossible state transition. Synchronous active/inactive/active. The error causes a close,
476+
// Second impossible state transition. Synchronous active/inactive/active. The error causes a close,
462477
// so we error twice!
463478
(channel, recorder) = try setUpChannel()
464479
try channel.pipeline.syncOperations.addHandler(
@@ -472,17 +487,18 @@ class SimpleClientServerTests: XCTestCase {
472487
XCTAssertEqual(recorder.errors.count, 2)
473488
XCTAssertTrue(recorder.errors.allSatisfy { $0 is NIOHTTP2Errors.ActivationError })
474489

475-
// Fourth impossible state transition: active/inactive/active asynchronously. The error causes a close,
476-
// so we error twice!
490+
// Third impossible state transition: active/inactive/active asynchronously. This error doesn't cause a
491+
// close because we don't distinguish it from the case tested in
492+
// testChannelInactiveThenChannelActiveErrorsButDoesntTrap.
477493
(channel, recorder) = try setUpChannel()
478494
channel.pipeline.fireChannelActive()
479495
channel.pipeline.fireChannelInactive()
480496
XCTAssertEqual(recorder.errors.count, 0)
481497
channel.pipeline.fireChannelActive()
482-
XCTAssertEqual(recorder.errors.count, 2)
498+
XCTAssertEqual(recorder.errors.count, 1)
483499
XCTAssertTrue(recorder.errors.allSatisfy { $0 is NIOHTTP2Errors.ActivationError })
484500

485-
// Fifth impossible state transition: active/inactive/inactive synchronously. The error causes a close,
501+
// Fourth impossible state transition: active/inactive/inactive synchronously. The error causes a close,
486502
// so we error twice!
487503
(channel, recorder) = try setUpChannel()
488504
try channel.pipeline.syncOperations.addHandler(
@@ -496,7 +512,7 @@ class SimpleClientServerTests: XCTestCase {
496512
XCTAssertEqual(recorder.errors.count, 2)
497513
XCTAssertTrue(recorder.errors.allSatisfy { $0 is NIOHTTP2Errors.ActivationError })
498514

499-
// Sixth impossible state transition: active/inactive/inactive asynchronously. The error causes a close,
515+
// Fifth impossible state transition: active/inactive/inactive asynchronously. The error causes a close,
500516
// so we error twice!
501517
(channel, recorder) = try setUpChannel()
502518
channel.pipeline.fireChannelActive()
@@ -506,7 +522,7 @@ class SimpleClientServerTests: XCTestCase {
506522
XCTAssertEqual(recorder.errors.count, 2)
507523
XCTAssertTrue(recorder.errors.allSatisfy { $0 is NIOHTTP2Errors.ActivationError })
508524

509-
// Seventh impossible state transition: adding the handler twice. The error causes a close, so we
525+
// Sixth impossible state transition: adding the handler twice. The error causes a close, so we
510526
// error twice!
511527
(channel, recorder) = try setUpChannel()
512528
try channel.connect(to: SocketAddress(unixDomainSocketPath: "/tmp/ignored"), promise: nil)

0 commit comments

Comments
 (0)