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

Implement non-blocking async DNS resolver #2060

Closed
wants to merge 4 commits into from

Conversation

clintonpi
Copy link
Contributor

@clintonpi clintonpi commented Sep 16, 2024

Motivation:

The GRPCHTTP2Core module needs a DNS resolver, as currently, IP addresses have to be passed in manually.

Modifications:

  • Add a new type DNSResolver with a method resolve(host:port:) that calls the getaddrinfo libc function to resolve a hostname and port number to a list of socket addresses. resolve(host:port:) is non-blocking and asynchronous.

Result:

The GRPCHTTP2Core module will have a DNS resolver.

Motivation:

The GRPCHTTP2Core module needs a DNS resolver, as currently, IP addresses have to be passed in manually.

Modifications:

- Add a new type `SimpleAsyncDNSResolver` with a method `resolve(host:port:)` that calls the `getaddrinfo` `libc` function to resolve a hostname and port number to a list of IP addresses and port numbers. `resolve(host:port:)` is non-blocking and asynchronous. As calls to `getaddrinfo` are blocking, `resolve(host:port)` executes `getaddrinfo` in a `DispatchQueue` and uses a `CheckedContinuation` to interface with the execution.

Result:

The GRPCHTTP2Core module will have a DNS resolver.
Copy link
Collaborator

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good start Clinton. The overall shape is okay but we can simplify a few things and restructure things so that they're better structured and easier to maintain.

It also looks like you need to handle Linux better, the CI is failing because a bunch of constants can't be found etc.

#elseif canImport(Musl)
import Musl
#endif
import CNIOLinux
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need CNIOLinux here

Comment on lines 19 to 30
#if canImport(Darwin)
import Darwin
#elseif os(Linux) || os(FreeBSD) || os(Android)
#if canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#endif
import CNIOLinux
#else
#error("The GRPCHTTP2Core module was unable to identify your C library.")
#endif
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The simplest way to do these checks if to just use canImports:

#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#else
#error(...)
#endif

* limitations under the License.
*/

import Dispatch
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add appropriate access levels to the imports? Should be possible for both of these to be private


/// An asynchronous non-blocking DNS resolver built on top of the libc `getaddrinfo` function.
@available(macOS 10.15, *)
package enum SimpleAsyncDNSResolver {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to include Simple or Async in the name, we can just call it DNSResolver

@available(macOS 10.15, *)
package enum SimpleAsyncDNSResolver {
private static let dispatchQueue = DispatchQueue(
label: "io.grpc.SimpleAsyncDNSResolver.dispatchQueue"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to include dispatchQueue in the name: io.grpc.DNSResolver is fine

family: Int32,
length: Int32
) -> String {
var resultingAddressBytes = [Int8](repeating: 0, count: Int(length))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be a bit clearer to call these presentationBytes (it's then more obvious what it is in relation to the function name)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're also passing this to a C function which is expected CChar (which is typealiased to Int8) – let's use CChar here because that's what the C function is expecting.

(resultingAddressBytesPtr: inout UnsafeMutableBufferPointer<Int8>) -> String in

// Convert
inet_ntop(family, addressPtr, resultingAddressBytesPtr.baseAddress!, socklen_t(length))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't ignore the return value from inet_ntop

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, of course.

Comment on lines 148 to 153
return resultingAddressBytesPtr.baseAddress!.withMemoryRebound(
to: UInt8.self,
capacity: Int(length)
) { resultingAddressBytesPtr -> String in
String(cString: resultingAddressBytesPtr)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do this without rebinding: String has an overload which accepts UnsafePointer<CChar>

}

/// `Error` that may be thrown based on the error code returned by `getaddrinfo`.
package enum SimpleAsyncDNSResolverError: Error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we embed this within the DNSResolver type?

Comment on lines 18 to 20
import XCTest

class SimpleAsyncDNSResolverTests: XCTestCase {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than using XCTest can we use swift-testing instead? It'd be good to write new tests using this moving forwards.

There are some docs here and some examples here.

Copy link
Collaborator

@FranzBusch FranzBusch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an overall not I would recommend using task executor preference here backed by a dispatch queue. This way you can avoid all of the continuations while still moving the blocking call to a separate queue.

@glbrntt
Copy link
Collaborator

glbrntt commented Sep 19, 2024

As an overall not I would recommend using task executor preference here backed by a dispatch queue. This way you can avoid all of the continuations while still moving the blocking call to a separate queue.

Agree this would be a nice use case. Let's do this in a follow up though.

Comment on lines +27 to +28
#endif
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use vertical whitespace to separate different things

Suggested change
#endif
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
#endif
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)

Comment on lines +37 to +39
if Task.isCancelled {
return []
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this function now throws we should also throw if the task is cancelled, you can replace this with: try Task.checkCancellation()

var socketAddresses = [SocketAddress]()

while true {
let addressBytes: UnsafeRawPointer = .init(result.pointee.ai_addr)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer using the typename rather than .init

Suggested change
let addressBytes: UnsafeRawPointer = .init(result.pointee.ai_addr)
let addressBytes = UnsafeRawPointer(result.pointee.ai_addr)

Comment on lines +107 to +111
fileprivate static func convertAddressFromNetworkToPresentationFormat<T>(
addressPtr: UnsafePointer<T>,
family: CInt,
length: CInt
) throws -> String {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need the generics here

Suggested change
fileprivate static func convertAddressFromNetworkToPresentationFormat<T>(
addressPtr: UnsafePointer<T>,
family: CInt,
length: CInt
) throws -> String {
fileprivate static func convertAddressFromNetworkToPresentationFormat(
addressPtr: UnsafeRawPointer,
family: CInt,
length: CInt
) throws -> String {

As a caller you can still pass in the UnsafePointer<sockaddr_in> etc. though, it will be implicitly converted.

Comment on lines +231 to +234
var presentationAddress = ""

try withUnsafePointer(to: address.sin_addr) { addressPtr in
presentationAddress = try DNSResolver.convertAddressFromNetworkToPresentationFormat(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can assigned directly back into the string:

let presentationAddress = try withUnsaferPointer(to: ...) { ... }

.ipv4(host: "127.0.0.1", port: 80),
]

let result = try await DNSResolver.resolve(host: "localhost", port: 80)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we can rely on the ordering of the results. Probably worth resolve "::1" and "127.0.0.1" directly and checking we get back what we expect.

@glbrntt
Copy link
Collaborator

glbrntt commented Sep 23, 2024

superseded by grpc/grpc-swift-nio-transport#4

@glbrntt glbrntt closed this Sep 23, 2024
@clintonpi clintonpi deleted the simple-async-dns-resolver branch September 24, 2024 15:27
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

Successfully merging this pull request may close these issues.

3 participants