Skip to content

Commit a5db2a6

Browse files
authored
fix event loop hop between registration and activation of accepted channels (#389)
Motivation: Once again, we had an extra event loop hop between a channel registration and its activation. Usually this shows up as `EPOLLHUP` but not so for accepted channels. What happened instead is that we had a small race window after we accepted a channel. It was in a state where it was not marked active _yet_ and therefore we'd not read out its data in case we received a `.readEOF`. That usually leads to a stale connection. Fortunately it doesn't happen very often that the client connects, immediately sends some data and then shuts the write end of the socket. Modifications: prevent the event loop hop between registration and activation Result: will always read out the read buffer on .readEOF
1 parent 5bebbf5 commit a5db2a6

File tree

4 files changed

+52
-9
lines changed

4 files changed

+52
-9
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/bin/bash
2+
##===----------------------------------------------------------------------===##
3+
##
4+
## This source file is part of the SwiftNIO open source project
5+
##
6+
## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
7+
## Licensed under Apache License v2.0
8+
##
9+
## See LICENSE.txt for license information
10+
## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
11+
##
12+
## SPDX-License-Identifier: Apache-2.0
13+
##
14+
##===----------------------------------------------------------------------===##
15+
16+
source defines.sh
17+
18+
token=$(create_token)
19+
start_server "$token"
20+
socket=$(get_socket "$token")
21+
echo -ne 'HTTP/1.1 200 OK\r\ncontent-length: 12\r\n\r\nHello World!' > "$tmp/expected"
22+
for f in $(seq 2000); do
23+
echo -e 'GET / HTTP/1.1\r\n\r\n' | nc -w10 -U "$socket" > "$tmp/actual"
24+
assert_equal_files "$tmp/expected" "$tmp/actual"
25+
done
26+
stop_server "$token"

Sources/NIO/BaseSocketChannel.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,11 @@ class BaseSocketChannel<T: BaseSocket>: SelectableChannel, ChannelCore {
808808
assert(!self.lifecycleManager.hasSeenEOFNotification)
809809
self.lifecycleManager.hasSeenEOFNotification = true
810810

811+
// we can't be not active but still registered here; this would mean that we got a notification about a
812+
// channel before we're ready to receive them.
813+
assert(self.lifecycleManager.isActive || !self.lifecycleManager.isRegistered,
814+
"illegal state: active: \(self.lifecycleManager.isActive), registered: \(self.lifecycleManager.isRegistered)")
815+
811816
self.readEOF0()
812817

813818
assert(!self.interestedEvent.contains(.read))

Sources/NIO/SocketChannel.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -453,13 +453,15 @@ final class ServerSocketChannel: BaseSocketChannel<ServerSocket> {
453453
assert(eventLoop.inEventLoop)
454454

455455
let ch = data.forceAsOther() as SocketChannel
456-
ch.register().thenThrowing {
457-
guard ch.isOpen else {
458-
throw ChannelError.ioOnClosedChannel
456+
ch.eventLoop.execute {
457+
ch.register().thenThrowing {
458+
guard ch.isOpen else {
459+
throw ChannelError.ioOnClosedChannel
460+
}
461+
ch.becomeActive0(promise: nil)
462+
}.whenFailure { error in
463+
ch.close(promise: nil)
459464
}
460-
ch.becomeActive0(promise: nil)
461-
}.whenFailure { error in
462-
ch.close(promise: nil)
463465
}
464466
}
465467

Tests/NIOTests/EventLoopTest.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,23 @@ public class EventLoopTest : XCTestCase {
126126
// to cleanly shut down all the channels before it actually closes. We add a custom channel that we can use
127127
// to wedge the event loop in the "shutting down" state, ensuring that we have plenty of time to attempt the
128128
// registration.
129-
class WedgeOpenHandler: ChannelOutboundHandler {
129+
class WedgeOpenHandler: ChannelDuplexHandler {
130+
typealias InboundIn = Any
130131
typealias OutboundIn = Any
131132
typealias OutboundOut = Any
132133

133134
private let promiseRegisterCallback: (EventLoopPromise<Void>) -> Void
134135

135136
var closePromise: EventLoopPromise<Void>? = nil
137+
private let channelActivePromise: EventLoopPromise<Void>?
136138

137-
init(_ promiseRegisterCallback: @escaping (EventLoopPromise<Void>) -> Void) {
139+
init(channelActivePromise: EventLoopPromise<Void>? = nil, _ promiseRegisterCallback: @escaping (EventLoopPromise<Void>) -> Void) {
138140
self.promiseRegisterCallback = promiseRegisterCallback
141+
self.channelActivePromise = channelActivePromise
142+
}
143+
144+
func channelActive(ctx: ChannelHandlerContext) {
145+
self.channelActivePromise?.succeed(result: ())
139146
}
140147

141148
func close(ctx: ChannelHandlerContext, mode: CloseMode, promise: EventLoopPromise<Void>?) {
@@ -167,9 +174,10 @@ public class EventLoopTest : XCTestCase {
167174
}
168175
let loop = group.next() as! SelectableEventLoop
169176

177+
let serverChannelUp: EventLoopPromise<Void> = group.next().newPromise()
170178
let serverChannel = try ServerBootstrap(group: group)
171179
.childChannelInitializer { channel in
172-
channel.pipeline.add(handler: WedgeOpenHandler { promise in
180+
channel.pipeline.add(handler: WedgeOpenHandler(channelActivePromise: serverChannelUp) { promise in
173181
promiseQueue.sync { promises.append(promise) }
174182
})
175183
}
@@ -196,6 +204,8 @@ public class EventLoopTest : XCTestCase {
196204
// Wait for the connect to complete.
197205
XCTAssertNoThrow(try connectPromise.futureResult.wait())
198206

207+
XCTAssertNoThrow(try serverChannelUp.futureResult.wait())
208+
199209
// Now we're going to start closing the event loop. This should not immediately succeed.
200210
let loopCloseFut = loop.closeGently()
201211

0 commit comments

Comments
 (0)