Skip to content

Commit 3275ff7

Browse files
authored
Don't call to user code before reconciling Channel state. (#310)
Motivation: Callouts to user code allows the user to make calls that re-enter channel code. In the case of channel closure, we could call out to the user before the channel knew it was completely closed, which would trigger a safety assertion (in debug mode) or hit a fatalError (in release mode). Modifications: Reconciled channel state before we call out to user code. Added a test for this case. Result: Fewer crashes, better channel state management.
1 parent aee4aad commit 3275ff7

File tree

3 files changed

+36
-2
lines changed

3 files changed

+36
-2
lines changed

Sources/NIO/BaseSocketChannel.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -667,9 +667,13 @@ class BaseSocketChannel<T: BaseSocket>: SelectableChannel, ChannelCore {
667667

668668
// Fail all pending writes and so ensure all pending promises are notified
669669
self.unsetCachedAddressesFromSocket()
670-
self.cancelWritesOnClose(error: error)
671670

672-
self.lifecycleManager.close(promise: p)(self.pipeline)
671+
// Transition our internal state.
672+
let callouts = self.lifecycleManager.close(promise: p)
673+
674+
// Now that our state is sensible, we can call out to user code.
675+
self.cancelWritesOnClose(error: error)
676+
callouts(self.pipeline)
673677

674678
eventLoop.execute {
675679
// ensure this is executed in a delayed fashion as the users code may still traverse the pipeline

Tests/NIOTests/SocketChannelTest+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ extension SocketChannelTest {
3939
("testWriteServerSocketChannel", testWriteServerSocketChannel),
4040
("testWriteAndFlushServerSocketChannel", testWriteAndFlushServerSocketChannel),
4141
("testConnectServerSocketChannel", testConnectServerSocketChannel),
42+
("testCloseDuringWriteFailure", testCloseDuringWriteFailure),
4243
]
4344
}
4445
}

Tests/NIOTests/SocketChannelTest.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,4 +261,33 @@ public class SocketChannelTest : XCTestCase {
261261
try serverChannel.close().wait()
262262
}
263263

264+
public func testCloseDuringWriteFailure() throws {
265+
let group = MultiThreadedEventLoopGroup(numThreads: 1)
266+
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
267+
268+
let serverChannel = try ServerBootstrap(group: group).bind(host: "127.0.0.1", port: 0).wait()
269+
let clientChannel = try ClientBootstrap(group: group).connect(to: serverChannel.localAddress!).wait()
270+
271+
// Put a write in the channel but don't flush it. We're then going to
272+
// close the channel. This should trigger an error callback that will
273+
// re-close the channel, which should fail with `alreadyClosed`.
274+
var buffer = clientChannel.allocator.buffer(capacity: 12)
275+
buffer.write(staticString: "hello")
276+
let writeFut = clientChannel.write(buffer).map {
277+
XCTFail("Must not succeed")
278+
}.thenIfError { error in
279+
XCTAssertEqual(error as? ChannelError, ChannelError.alreadyClosed)
280+
return clientChannel.close()
281+
}
282+
XCTAssertNoThrow(try clientChannel.close().wait())
283+
284+
do {
285+
try writeFut.wait()
286+
XCTFail("Did not throw")
287+
} catch ChannelError.alreadyClosed {
288+
// ok
289+
} catch {
290+
XCTFail("Unexpected error \(error)")
291+
}
292+
}
264293
}

0 commit comments

Comments
 (0)