Skip to content

Commit dca6594

Browse files
authored
Backport: Prevent crash in Happy Eyeballs Resolver (#3003) (#3004)
Motivation: In vanishingly rare situations it is possible for the AAAA results to come in on the same tick as the resolution delay timer completes. In those cases, depending on the ordering of the tasks, we can get situations where the resolution delay timer completion causes a crash. Modifications: Tolerate receiving the resolution delay timer after resolution completes. Result: Fewer crashes (cherry picked from commit 16f19c0)
1 parent a234978 commit dca6594

File tree

2 files changed

+53
-1
lines changed

2 files changed

+53
-1
lines changed

Sources/NIOPosix/HappyEyeballs.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,12 @@ internal final class HappyEyeballsConnector<ChannelBuilderResult> {
464464
// notifications, and can also get late scheduled task callbacks. We want to just quietly
465465
// ignore these, as our transition into the complete state should have already sent
466466
// cleanup messages to all of these things.
467-
case (.complete, .resolverACompleted),
467+
//
468+
// We can also get the resolutionDelayElapsed after allResolved, as it's possible that
469+
// callback was already dequeued in the same tick as the cancellation. That's also fine:
470+
// the resolution delay isn't interesting.
471+
case (.allResolved, .resolutionDelayElapsed),
472+
(.complete, .resolverACompleted),
468473
(.complete, .resolverAAAACompleted),
469474
(.complete, .connectSuccess),
470475
(.complete, .connectFailed),

Tests/NIOPosixTests/HappyEyeballsTest.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,4 +1308,51 @@ public final class HappyEyeballsTest: XCTestCase {
13081308

13091309
XCTAssertNoThrow(try client.close().wait())
13101310
}
1311+
1312+
func testResolutionTimeoutAndResolutionInSameTick() throws {
1313+
var channels: [Channel] = []
1314+
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80) {
1315+
let channelFuture = defaultChannelBuilder(loop: $0, family: $1)
1316+
channelFuture.whenSuccess { channel in
1317+
try! channel.pipeline.addHandler(ConnectionDelayer(), name: CONNECT_DELAYER, position: .first).wait()
1318+
channels.append(channel)
1319+
}
1320+
return channelFuture
1321+
}
1322+
let targetFuture = eyeballer.resolveAndConnect().flatMapThrowing { (channel) -> String? in
1323+
let target = channel.connectTarget()
1324+
_ = try (channel as! EmbeddedChannel).finish()
1325+
return target
1326+
}
1327+
loop.run()
1328+
1329+
// Then, queue a task to resolve the v6 promise after 50ms.
1330+
// Why 50ms? This is the same time as the resolution delay.
1331+
let promise = resolver.v6Promise
1332+
loop.scheduleTask(in: .milliseconds(50)) {
1333+
promise.fail(DummyError())
1334+
}
1335+
1336+
// Kick off the IPv4 resolution. This triggers the timer for the resolution delay.
1337+
resolver.v4Promise.succeed(SINGLE_IPv4_RESULT)
1338+
loop.run()
1339+
1340+
// Advance time 50ms.
1341+
loop.advanceTime(by: .milliseconds(50))
1342+
1343+
// Then complete the connection future.
1344+
XCTAssertEqual(channels.count, 1)
1345+
channels.first!.succeedConnection()
1346+
1347+
// Should be done.
1348+
let target = try targetFuture.wait()
1349+
XCTAssertEqual(target!, "10.0.0.1")
1350+
1351+
// We should have had queries for AAAA and A.
1352+
let expectedQueries: [DummyResolver.Event] = [
1353+
.aaaa(host: "example.com", port: 80),
1354+
.a(host: "example.com", port: 80),
1355+
]
1356+
XCTAssertEqual(resolver.events, expectedQueries)
1357+
}
13111358
}

0 commit comments

Comments
 (0)