Skip to content

Avoid a double-idle in the idle handler. #875

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions Sources/GRPC/GRPCIdleHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ internal class GRPCIdleHandler: ChannelInboundHandler {
context.fireChannelActive()
}

func handlerRemoved(context: ChannelHandlerContext) {
self.scheduledIdle?.cancel()
}

func channelInactive(context: ChannelHandlerContext) {
self.scheduledIdle?.cancel()
self.scheduledIdle = nil
Expand Down Expand Up @@ -166,18 +170,26 @@ internal class GRPCIdleHandler: ChannelInboundHandler {
}

private func idle(context: ChannelHandlerContext) {
// Don't idle if there are active streams.
guard self.activeStreams == 0 else {
return
}

self.state = .closed
switch self.mode {
case .client(let manager):
manager.idle()
case .server:
switch self.state {
case .notReady, .ready:
self.state = .closed
switch self.mode {
case .client(let manager):
manager.idle()
case .server:
()
}
context.close(mode: .all, promise: nil)

// We need to guard against double closure here. We may go idle as a result of receiving a
// GOAWAY frame or because our scheduled idle timeout fired.
case .closed:
()
}

context.close(mode: .all, promise: nil)
}
}
55 changes: 55 additions & 0 deletions Tests/GRPCTests/ConnectionManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,61 @@ extension ConnectionManagerTests {
self.loop.run()
XCTAssertThrowsError(try channel.wait())
}

func testDoubleIdle() throws {
class CloseDroppingHandler: ChannelOutboundHandler {
typealias OutboundIn = Any
func close(context: ChannelHandlerContext, mode: CloseMode, promise: EventLoopPromise<Void>?) {
promise?.fail(GRPCStatus(code: .unavailable, message: "Purposefully dropping channel close"))
}
}

let channelPromise = self.loop.makePromise(of: Channel.self)
let manager = ConnectionManager.testingOnly(configuration: self.defaultConfiguration, logger: self.logger) {
return channelPromise.futureResult
}

// Start the connection.
let readyChannel: EventLoopFuture<Channel> = self.waitForStateChange(from: .idle, to: .connecting) {
let readyChannel = manager.getChannel()
self.loop.run()
return readyChannel
}

// Setup the real channel and activate it.
let channel = EmbeddedChannel(loop: self.loop)
XCTAssertNoThrow(try channel.pipeline.addHandlers([
CloseDroppingHandler(),
GRPCIdleHandler(mode: .client(manager))
]).wait())
channelPromise.succeed(channel)
self.loop.run()
XCTAssertNoThrow(try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")).wait())

// Write a SETTINGS frame on the root stream.
try self.waitForStateChange(from: .connecting, to: .ready) {
let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([])))
XCTAssertNoThrow(try channel.writeInbound(frame))
}

// The channel should now be ready.
XCTAssertNoThrow(try readyChannel.wait())

// Send a GO_AWAY; the details don't matter. This will cause the connection to go idle and the
// channel to close.
try self.waitForStateChange(from: .ready, to: .idle) {
let goAway = HTTP2Frame(
streamID: .rootStream,
payload: .goAway(lastStreamID: 1, errorCode: .noError, opaqueData: nil)
)
XCTAssertNoThrow(try channel.writeInbound(goAway))
}

// We dropped the close; now wait for the scheduled idle to fire.
//
// Previously doing this this would fail a precondition.
self.loop.advanceTime(by: .minutes(5))
}
}

internal struct Change: Hashable, CustomStringConvertible {
Expand Down
1 change: 1 addition & 0 deletions Tests/GRPCTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ extension ConnectionManagerTests {
("testConnectOnSecondAttempt", testConnectOnSecondAttempt),
("testDoomedOptimisticChannelFromConnecting", testDoomedOptimisticChannelFromConnecting),
("testDoomedOptimisticChannelFromIdle", testDoomedOptimisticChannelFromIdle),
("testDoubleIdle", testDoubleIdle),
("testGoAwayWhenReady", testGoAwayWhenReady),
("testIdleShutdown", testIdleShutdown),
("testIdleTimeoutWhenThereAreActiveStreams", testIdleTimeoutWhenThereAreActiveStreams),
Expand Down