Skip to content

Commit 80363d9

Browse files
weissiLukasa
authored andcommitted
fix EventLoops that Bootstrap channel initialiser's call out on (#424)
Motivation: Previously we were not running the (child/server)channelInitializers on the event loop associated to the `Channel` we're initialising. Almost all operations in there are pipeline modifications which are thread safe so it presumably wasn't a massive correctness issue. However it's very expensive to hop threads that often and it is also very unexpected. This addresses this issue. Modifications: made all (child/server)channelInitializers run on the event loop that is associated to their `Channel` Result: - more correctness - less unexpected behaviour
1 parent e94315d commit 80363d9

File tree

6 files changed

+166
-41
lines changed

6 files changed

+166
-41
lines changed

Sources/NIO/Bootstrap.swift

Lines changed: 69 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -205,18 +205,22 @@ public final class ServerBootstrap {
205205
return eventLoop.newFailedFuture(error: error)
206206
}
207207

208-
return serverChannelInit(serverChannel).then {
209-
serverChannel.pipeline.add(handler: AcceptHandler(childChannelInitializer: childChannelInit,
210-
childChannelOptions: childChannelOptions))
211-
}.then {
212-
serverChannelOptions.applyAll(channel: serverChannel)
208+
return eventLoop.submit {
209+
return serverChannelInit(serverChannel).then {
210+
serverChannel.pipeline.add(handler: AcceptHandler(childChannelInitializer: childChannelInit,
211+
childChannelOptions: childChannelOptions))
212+
}.then {
213+
serverChannelOptions.applyAll(channel: serverChannel)
214+
}.then {
215+
register(eventLoop, serverChannel)
216+
}.map {
217+
serverChannel as Channel
218+
}.thenIfError { error in
219+
serverChannel.close0(error: error, mode: .all, promise: nil)
220+
return eventLoop.newFailedFuture(error: error)
221+
}
213222
}.then {
214-
register(eventLoop, serverChannel)
215-
}.map {
216-
serverChannel
217-
}.thenIfError { error in
218-
serverChannel.close0(error: error, mode: .all, promise: nil)
219-
return eventLoop.newFailedFuture(error: error)
223+
$0
220224
}
221225
}
222226

@@ -240,23 +244,42 @@ public final class ServerBootstrap {
240244

241245
func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
242246
let accepted = self.unwrapInboundIn(data)
243-
let childChannelInit = self.childChannelInit ?? { (_: Channel) in ctx.eventLoop.newSucceededFuture(result: ()) }
244-
245-
self.childChannelOptions.applyAll(channel: accepted).hopTo(eventLoop: ctx.eventLoop).then {
246-
assert(ctx.eventLoop.inEventLoop)
247-
return childChannelInit(accepted)
248-
}.then { () -> EventLoopFuture<Void> in
249-
assert(ctx.eventLoop.inEventLoop)
250-
guard !ctx.pipeline.destroyed else {
251-
return accepted.close().thenThrowing {
252-
throw ChannelError.ioOnClosedChannel
247+
let ctxEventLoop = ctx.eventLoop
248+
let childEventLoop = accepted.eventLoop
249+
let childChannelInit = self.childChannelInit ?? { (_: Channel) in childEventLoop.newSucceededFuture(result: ()) }
250+
251+
@inline(__always)
252+
func setupChildChannel() -> EventLoopFuture<Void> {
253+
return self.childChannelOptions.applyAll(channel: accepted).then { () -> EventLoopFuture<Void> in
254+
assert(childEventLoop.inEventLoop)
255+
return childChannelInit(accepted)
256+
}
257+
}
258+
259+
@inline(__always)
260+
func fireThroughPipeline(_ future: EventLoopFuture<Void>) {
261+
assert(ctxEventLoop.inEventLoop)
262+
future.then { (_) -> EventLoopFuture<Void> in
263+
assert(ctxEventLoop.inEventLoop)
264+
guard !ctx.pipeline.destroyed else {
265+
return accepted.close().thenThrowing {
266+
throw ChannelError.ioOnClosedChannel
267+
}
253268
}
269+
ctx.fireChannelRead(data)
270+
return ctx.eventLoop.newSucceededFuture(result: ())
271+
}.whenFailure { error in
272+
assert(ctx.eventLoop.inEventLoop)
273+
self.closeAndFire(ctx: ctx, accepted: accepted, err: error)
254274
}
255-
ctx.fireChannelRead(data)
256-
return ctx.eventLoop.newSucceededFuture(result: ())
257-
}.whenFailure { error in
258-
assert(ctx.eventLoop.inEventLoop)
259-
self.closeAndFire(ctx: ctx, accepted: accepted, err: error)
275+
}
276+
277+
if childEventLoop === ctxEventLoop {
278+
fireThroughPipeline(setupChildChannel())
279+
} else {
280+
fireThroughPipeline(childEventLoop.submit {
281+
return setupChildChannel()
282+
}.then { $0 }.hopTo(eventLoop: ctxEventLoop))
260283
}
261284
}
262285

@@ -463,18 +486,27 @@ public final class ClientBootstrap {
463486
return promise.futureResult
464487
}
465488

466-
channelInitializer(channel).then {
467-
channelOptions.applyAll(channel: channel)
468-
}.then {
469-
channel.registerAndDoSynchronously(body)
470-
}.map {
471-
channel
472-
}.thenIfError { error in
473-
channel.close0(error: error, mode: .all, promise: nil)
474-
return channel.eventLoop.newFailedFuture(error: error)
475-
}.cascade(promise: promise)
489+
@inline(__always)
490+
func setupChannel() -> EventLoopFuture<Channel> {
491+
assert(eventLoop.inEventLoop)
492+
channelInitializer(channel).then {
493+
channelOptions.applyAll(channel: channel)
494+
}.then {
495+
channel.registerAndDoSynchronously(body)
496+
}.map {
497+
channel
498+
}.thenIfError { error in
499+
channel.close0(error: error, mode: .all, promise: nil)
500+
return channel.eventLoop.newFailedFuture(error: error)
501+
}.cascade(promise: promise)
502+
return promise.futureResult
503+
}
476504

477-
return promise.futureResult
505+
if eventLoop.inEventLoop {
506+
return setupChannel()
507+
} else {
508+
return eventLoop.submit(setupChannel).then { $0 }
509+
}
478510
}
479511
}
480512

Tests/LinuxMain.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import XCTest
3636
testCase(Base64Test.allTests),
3737
testCase(BaseObjectTest.allTests),
3838
testCase(BlockingIOThreadPoolTest.allTests),
39+
testCase(BootstrapTest.allTests),
3940
testCase(ByteBufferTest.allTests),
4041
testCase(ByteToMessageDecoderTest.allTests),
4142
testCase(ChannelNotificationTest.allTests),
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
//
15+
// BootstrapTest+XCTest.swift
16+
//
17+
import XCTest
18+
19+
///
20+
/// NOTE: This file was generated by generate_linux_tests.rb
21+
///
22+
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
23+
///
24+
25+
extension BootstrapTest {
26+
27+
static var allTests : [(String, (BootstrapTest) -> () throws -> Void)] {
28+
return [
29+
("testBootstrapsCallInitializersOnCorrectEventLoop", testBootstrapsCallInitializersOnCorrectEventLoop),
30+
]
31+
}
32+
}
33+

Tests/NIOTests/BootstrapTest.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIO
16+
import XCTest
17+
18+
class BootstrapTest: XCTestCase {
19+
func testBootstrapsCallInitializersOnCorrectEventLoop() throws {
20+
for numThreads in [1 /* everything on one event loop */,
21+
2 /* some stuff has shared event loops */,
22+
5 /* everything on a different event loop */] {
23+
let group = MultiThreadedEventLoopGroup(numThreads: numThreads)
24+
defer {
25+
XCTAssertNoThrow(try group.syncShutdownGracefully())
26+
}
27+
28+
let childChannelDone: EventLoopPromise<Void> = group.next().newPromise()
29+
let serverChannelDone: EventLoopPromise<Void> = group.next().newPromise()
30+
let serverChannel = try ServerBootstrap(group: group)
31+
.childChannelInitializer { channel in
32+
XCTAssert(channel.eventLoop.inEventLoop)
33+
childChannelDone.succeed(result: ())
34+
return channel.eventLoop.newSucceededFuture(result: ())
35+
}
36+
.serverChannelInitializer { channel in
37+
XCTAssert(channel.eventLoop.inEventLoop)
38+
serverChannelDone.succeed(result: ())
39+
return channel.eventLoop.newSucceededFuture(result: ())
40+
}
41+
.bind(host: "localhost", port: 0)
42+
.wait()
43+
defer {
44+
XCTAssertNoThrow(try serverChannel.close().wait())
45+
}
46+
47+
let client = try ClientBootstrap(group: group)
48+
.channelInitializer { channel in
49+
XCTAssert(channel.eventLoop.inEventLoop)
50+
return channel.eventLoop.newSucceededFuture(result: ())
51+
}
52+
.connect(to: serverChannel.localAddress!)
53+
.wait()
54+
defer {
55+
XCTAssertNoThrow(try client.syncCloseAcceptingAlreadyClosed())
56+
}
57+
}
58+
}
59+
}

docker/docker-compose.1404.41.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ services:
1717
image: swift-nio:14.04-4.1
1818
environment:
1919
- MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=47000
20-
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=792100
20+
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=718100
2121
- MAX_ALLOCS_ALLOWED_ping_pong_1000_reqs_1_conn=4600
2222
- MAX_ALLOCS_ALLOWED_bytebuffer_lots_of_rw=3100
2323
- MAX_ALLOCS_ALLOWED_future_lots_of_callbacks=99100
@@ -26,7 +26,7 @@ services:
2626
image: swift-nio:14.04-4.1
2727
environment:
2828
- MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=47000
29-
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=792100
29+
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=718100
3030
- MAX_ALLOCS_ALLOWED_ping_pong_1000_reqs_1_conn=4600
3131
- MAX_ALLOCS_ALLOWED_bytebuffer_lots_of_rw=3100
3232
- MAX_ALLOCS_ALLOWED_future_lots_of_callbacks=99100

docker/docker-compose.1604.41.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ services:
1616
image: swift-nio:16.04-4.1
1717
environment:
1818
- MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=47000
19-
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=793100
19+
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=718100
2020
- MAX_ALLOCS_ALLOWED_ping_pong_1000_reqs_1_conn=4600
2121
- MAX_ALLOCS_ALLOWED_bytebuffer_lots_of_rw=3100
2222
- MAX_ALLOCS_ALLOWED_future_lots_of_callbacks=99100
@@ -25,7 +25,7 @@ services:
2525
image: swift-nio:16.04-4.1
2626
environment:
2727
- MAX_ALLOCS_ALLOWED_1000_reqs_1_conn=47000
28-
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=793100
28+
- MAX_ALLOCS_ALLOWED_1_reqs_1000_conn=718100
2929
- MAX_ALLOCS_ALLOWED_ping_pong_1000_reqs_1_conn=4600
3030
- MAX_ALLOCS_ALLOWED_bytebuffer_lots_of_rw=3100
3131
- MAX_ALLOCS_ALLOWED_future_lots_of_callbacks=99100

0 commit comments

Comments
 (0)