Skip to content
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

Update the Resolver interface #1487

Open
Lukasa opened this issue Apr 21, 2020 · 1 comment
Open

Update the Resolver interface #1487

Lukasa opened this issue Apr 21, 2020 · 1 comment

Comments

@Lukasa
Copy link
Contributor

Lukasa commented Apr 21, 2020

NIO has spent the majority of its lifetime with a pluggable DNS resolver via the Resolver protocol:

/// A protocol that covers an object that does DNS lookups.
///
/// In general the rules for the resolver are relatively broad: there are no specific requirements on how
/// it operates. However, the rest of the code assumes that it obeys RFC 6724, particularly section 6 on
/// ordering returned addresses. That is, the IPv6 and IPv4 responses should be ordered by the destination
/// address ordering rules from that RFC. This specification is widely implemented by getaddrinfo
/// implementations, so any implementation based on getaddrinfo will work just fine. In the future, a custom
/// resolver will need also to implement these sorting rules.
public protocol Resolver {
/// Initiate a DNS A query for a given host.
///
/// - parameters:
/// - host: The hostname to do an A lookup on.
/// - port: The port we'll be connecting to.
/// - returns: An `EventLoopFuture` that fires with the result of the lookup.
func initiateAQuery(host: String, port: Int) -> EventLoopFuture<[SocketAddress]>
/// Initiate a DNS AAAA query for a given host.
///
/// - parameters:
/// - host: The hostname to do an AAAA lookup on.
/// - port: The port we'll be connecting to.
/// - returns: An `EventLoopFuture` that fires with the result of the lookup.
func initiateAAAAQuery(host: String, port: Int) -> EventLoopFuture<[SocketAddress]>
/// Cancel all outstanding DNS queries.
///
/// This method is called whenever queries that have not completed no longer have their
/// results needed. The resolver should, if possible, abort any outstanding queries and
/// clean up their state.
///
/// This method is not guaranteed to terminate the outstanding queries.
func cancelQueries()
}

This resolver is a vital part of our connection establishment logic which uses Happy Eyeballs v2 to race outbound TCP connections against each other. The goal is to allow an arbitrary DNS resolver to inform NIO of DNS resolution asynchronously, which we will use to manage racing outbound TCP connections. The forward-looking goal was to allow NIO to be easily updated to use asynchronous DNS where available.

This Resolver interface bakes has two major limitations. The first is that it bakes in a strong knowledge of DNS: it's phrased in terms of A/AAAA. The second is that it does not allow result streaming: there are only two opportunities to deliver results.

These limitations manage to constrain the current Resolver interface both in terms of how good it is for DNS and how general it can be. More general service discovery use-cases have to awkwardly contort themselves into the shape of DNS. However, truly streaming DNS resolvers such as dns-sd are required to force themselves to give all-or-nothing answers.

We should update the Resolver interface to be substantially more general. Here's what the shape would likely be:

protocol NIOStreamingResolver {
    func resolve(name: String, destinationPort: Int, session: NIONameResolutionSession)

    func cancel(_ session: NIONameResolutionSession)
}

class NIONameResolutionSession {
    var hints: Hints

    func deliverResults(_: [SocketAddress])

    func resolutionComplete(_: Result<Void, Error>)

    struct Hints {
        var localAddress: SocketAddress?
    }
}

There are three parts to this new interface. The first is the NIOStreamingResolver protocol. This is the protocol that new resolvers will adopt. It is now phrased entirely in terms of resolving names to socket addresses, without any implication that DNS is the only way to do this. This unlocks powerful new tools such as the ability to plug in arbitrary service discovery tools into the resolver interface. For example, you could teach NIO to use Redis for service discovery, and the Happy Eyeballs implementation would seamlessly transition to using results from Redis. The destination port is still provided because it is necessary to properly construct a SocketAddress, and it allows the resolver to potentially override that port choice if needed.

The second is NIONameResolutionSession. This is the session associated with the resolution. It is used by the NIOStreamingResolver to deliver results, as well as to inform about the completion (or failure!) of DNS resolution. This expresses no constraints on address family, the number of times deliverResults is called, or anything else. Thus, simple resolvers like getaddrinfo no longer need to synthesise two calls, while complex resolvers can deliver information as they get it rather than being forced to hold on to and aggregate them. To ease implementation this interface will be thread-safe: callers will not be required to re-establish the event loop context in order to call into it, allowing for complex callback-based resolvers to be easily integrated.

The third is Hints. This is an extensible way for NIO to communicate extra constraints on the results that it would like to see, or to provide extra information that the resolver may need. The immediately-supported case will be localAddress. When set, this will be NIO requesting that if possible the resolver try to return only addresses that are reachable from that local address.

Note that because Hints is extensible it is non-binding. NIO is required to tolerate a failure by the resolver to abide by them. In the case of localAddress the consequence of failing to respect it will be excessive channel creation for channels that will immediately fail to connect due to routing issues.


In addition to updating the resolver interface, we will need to refresh the happy eyeballs implementation to tolerate receiving results multiple times. This enhancement is covered in RFC 6724, so shouldn't be too difficult.


Finally, to support backwards compatibility we'll need to define a NIOStreamingResolver that can accept an arbitrary Resolver. This is not too hard: the existing interface is a strict subset of this one, and easily translated.

@Lukasa
Copy link
Contributor Author

Lukasa commented Oct 21, 2020

One thing we should aim to provide is an implementation that maps an arbitrary ServiceDiscovery implementation to a new Resolver.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant