Skip to content

Commit aa25fda

Browse files
authored
Add an AcceptBackoffHandler to the async server bootstraps (#2782)
# Motivation When encountering a file descriptor limit we are currently closing the server socket when using the async bootstraps since the error caught is delivered via the `inbound` sequence of the server socket async sequence. # Modification This PR adds the `AcceptBackoffHandler` in the async bootstrap case to the pipeline to avoid us closing the server socket channel when we hit the file descriptor limit. # Result We are now gracefully handling file descriptor limits.
1 parent 15618e1 commit aa25fda

File tree

3 files changed

+49
-8
lines changed

3 files changed

+49
-8
lines changed

Sources/NIOCore/ChannelHandlers.swift

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public final class AcceptBackoffHandler: ChannelDuplexHandler, RemovableChannelH
2323

2424
private var nextReadDeadlineNS: Optional<NIODeadline>
2525
private let backoffProvider: (IOError) -> TimeAmount?
26+
private let shouldForwardIOErrorCaught: Bool
2627
private var scheduledRead: Optional<Scheduled<Void>>
2728

2829
/// Default implementation used as `backoffProvider` which delays accept by 1 second.
@@ -38,6 +39,22 @@ public final class AcceptBackoffHandler: ChannelDuplexHandler, RemovableChannelH
3839
self.backoffProvider = backoffProvider
3940
self.nextReadDeadlineNS = nil
4041
self.scheduledRead = nil
42+
self.shouldForwardIOErrorCaught = true
43+
}
44+
45+
/// Create a new instance
46+
///
47+
/// - parameters:
48+
/// - shouldForwardIOErrorCaught: A boolean indicating if a caught IOError should be forwarded.
49+
/// - backoffProvider: returns a `TimeAmount` which will be the amount of time to wait before attempting another `read`.
50+
public init(
51+
shouldForwardIOErrorCaught: Bool,
52+
backoffProvider: @escaping (IOError) -> TimeAmount? = AcceptBackoffHandler.defaultBackoffProvider
53+
) {
54+
self.backoffProvider = backoffProvider
55+
self.nextReadDeadlineNS = nil
56+
self.scheduledRead = nil
57+
self.shouldForwardIOErrorCaught = shouldForwardIOErrorCaught
4158
}
4259

4360
public func read(context: ChannelHandlerContext) {
@@ -59,16 +76,22 @@ public final class AcceptBackoffHandler: ChannelDuplexHandler, RemovableChannelH
5976
}
6077

6178
public func errorCaught(context: ChannelHandlerContext, error: Error) {
62-
if let ioError = error as? IOError {
63-
if let amount = backoffProvider(ioError) {
64-
self.nextReadDeadlineNS = .now() + amount
65-
if let scheduled = self.scheduledRead {
66-
scheduled.cancel()
67-
scheduleRead(at: self.nextReadDeadlineNS!, context: context)
68-
}
79+
guard let ioError = error as? IOError else {
80+
context.fireErrorCaught(error)
81+
return
82+
}
83+
84+
if let amount = backoffProvider(ioError) {
85+
self.nextReadDeadlineNS = .now() + amount
86+
if let scheduled = self.scheduledRead {
87+
scheduled.cancel()
88+
scheduleRead(at: self.nextReadDeadlineNS!, context: context)
6989
}
7090
}
71-
context.fireErrorCaught(error)
91+
92+
if self.shouldForwardIOErrorCaught {
93+
context.fireErrorCaught(error)
94+
}
7295
}
7396

7497
public func channelInactive(context: ChannelHandlerContext) {

Sources/NIOPosix/Bootstrap.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,10 @@ extension ServerBootstrap {
706706
serverChannelInit(serverChannel)
707707
}.flatMap { (_) -> EventLoopFuture<NIOAsyncChannel<ChannelInitializerResult, Never>> in
708708
do {
709+
try serverChannel.pipeline.syncOperations.addHandler(
710+
AcceptBackoffHandler(shouldForwardIOErrorCaught: false),
711+
name: "AcceptBackOffHandler"
712+
)
709713
try serverChannel.pipeline.syncOperations.addHandler(
710714
AcceptHandler(
711715
childChannelInitializer: childChannelInit,

Tests/NIOPosixTests/AcceptBackoffHandlerTest.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import Atomics
1616
import NIOCore
17+
import NIOEmbedded
18+
import NIOTestUtils
1719
import XCTest
1820

1921
@testable import NIOPosix
@@ -300,6 +302,18 @@ public final class AcceptBackoffHandlerTest: XCTestCase {
300302
XCTAssertEqual(2, backoffProviderCalled.load(ordering: .relaxed))
301303
}
302304

305+
func testNotForwardingIOError() throws {
306+
let loop = EmbeddedEventLoop()
307+
let acceptBackOffHandler = AcceptBackoffHandler(shouldForwardIOErrorCaught: false)
308+
let eventCounterHandler = EventCounterHandler()
309+
let channel = EmbeddedChannel(handlers: [acceptBackOffHandler, eventCounterHandler], loop: loop)
310+
311+
channel.pipeline.fireErrorCaught(IOError(errnoCode: 1, reason: "test"))
312+
XCTAssertEqual(eventCounterHandler.errorCaughtCalls, 0)
313+
channel.pipeline.fireErrorCaught(ChannelError.alreadyClosed)
314+
XCTAssertEqual(eventCounterHandler.errorCaughtCalls, 1)
315+
}
316+
303317
private final class ReadCountHandler: ChannelOutboundHandler {
304318
typealias OutboundIn = NIOAny
305319
typealias OutboundOut = NIOAny

0 commit comments

Comments
 (0)