Skip to content

Commit d5519db

Browse files
authored
Fix happy eyeballs races with custom resolver (#2436)
Motivation: The HappyEyeballs connector synchronises state on an event loop but calls out to a 'Resolver' to do DNS lookups. The resolver returns results as a future which may be on a different loop than the connector. The connector does not hop back to its own event loop before processing the results. For client bootstraps, if no resolver is specified then the default resolver uses the same event loop as the connector so in many cases this is not an issue. However, if a custom resolver is used this guarantee is lost and data races are much more likely. Modifications: - Hop back to the connector's event loop after calling the resolver. - Add a test. Result: Fewer data races.
1 parent b22575a commit d5519db

File tree

2 files changed

+38
-2
lines changed

2 files changed

+38
-2
lines changed

Sources/NIOPosix/HappyEyeballs.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -433,8 +433,14 @@ internal class HappyEyeballsConnector {
433433
// The two queries SHOULD be made as soon after one another as possible,
434434
// with the AAAA query made first and immediately followed by the A
435435
// query.
436-
whenAAAALookupComplete(future: resolver.initiateAAAAQuery(host: host, port: port))
437-
whenALookupComplete(future: resolver.initiateAQuery(host: host, port: port))
436+
//
437+
// We hop back to `self.loop` because there's no guarantee the resolver runs
438+
// on our event loop.
439+
let aaaaLookup = self.resolver.initiateAAAAQuery(host: self.host, port: self.port).hop(to: self.loop)
440+
self.whenAAAALookupComplete(future: aaaaLookup)
441+
442+
let aLookup = self.resolver.initiateAQuery(host: self.host, port: self.port).hop(to: self.loop)
443+
self.whenALookupComplete(future: aLookup)
438444
}
439445

440446
/// Called when the A query has completed before the AAAA query.

Tests/NIOPosixTests/HappyEyeballsTest.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,4 +1231,34 @@ public final class HappyEyeballsTest : XCTestCase {
12311231
XCTAssertEqual(channel.state(), .closed)
12321232
}
12331233
}
1234+
1235+
func testResolverOnDifferentEventLoop() throws {
1236+
// Tests a regression where the happy eyeballs connector would update its state on the event
1237+
// loop of the future returned by the resolver (which may be different to its own). Prior
1238+
// to the fix this test would trigger TSAN warnings.
1239+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 2)
1240+
defer {
1241+
XCTAssertNoThrow(try group.syncShutdownGracefully())
1242+
}
1243+
1244+
let server = try ServerBootstrap(group: group)
1245+
.bind(host: "localhost", port: 0)
1246+
.wait()
1247+
1248+
defer {
1249+
XCTAssertNoThrow(try server.close().wait())
1250+
}
1251+
1252+
// Run the resolver and connection on different event loops.
1253+
let resolverLoop = group.next()
1254+
let connectionLoop = group.next()
1255+
XCTAssertNotIdentical(resolverLoop, connectionLoop)
1256+
let resolver = GetaddrinfoResolver(loop: resolverLoop, aiSocktype: .stream, aiProtocol: .tcp)
1257+
let client = try ClientBootstrap(group: connectionLoop)
1258+
.resolver(resolver)
1259+
.connect(host: "localhost", port: server.localAddress!.port!)
1260+
.wait()
1261+
1262+
XCTAssertNoThrow(try client.close().wait())
1263+
}
12341264
}

0 commit comments

Comments
 (0)