-
Notifications
You must be signed in to change notification settings - Fork 24
Make DNSSD queries cancellable #34
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
base: main
Are you sure you want to change the base?
Conversation
072927e
to
610463d
Compare
if result == 0 { | ||
continue | ||
} | ||
if result == 1 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it always 1 and not any positive number?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to poll
man page, it's the number of elements in the pollFDs
array that are ready for I/O. The array being of size 1, it could not be anything other than 1 if it is a positive number.
I can change it to >=
, but that should never happen.
var pollFDs = [pollfd(fd: serviceSockFD, events: Int16(POLLIN), revents: 0)] | ||
while true { | ||
guard !Task.isCancelled else { | ||
return continuation.finish(throwing: CancellationError()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it would be more useful to throw AsyncDNSResolver.Error
here? (we would need to add .cancelled
to the enum)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I thought that CancellationError was the standard way to throw when a task is cancelled, but apparently it's not a guarantee and an AsyncDNSResolver.Error might be more specific and useful.
@swift-server-bot add to allowlist |
Add AsyncDNSResolver.Error.cancelled and error messages when polling the DNSSD service socket
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a kernel thread blocking function in the middle of an async
hronous function
return continuation.finish(throwing: AsyncDNSResolver.Error(code: .internalError, message: "Failed to access the DNSSD service socket")) | ||
} | ||
|
||
var pollFDs = [pollfd(fd: serviceSockFD, events: Int16(POLLIN), revents: 0)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
poll
is blocking until this is ready so we can't do this here. If you want async I/O you'd need to use SwiftNIO here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We did work to remove the SwiftNIO dependency earlier (#20), so I don't think we want to add it back.
The intention here is to allow checking for task cancellation, because calling DNSServiceProcessResult
alone just blocks and doesn't seem to give us the chance to do that at all.
The docs for DNSServiceProcessResult
says:
This call will block until the daemon's response is received. Use DNSServiceRefSockFD() in
conjunction with a run loop or select() to determine the presence of a response from the
server before calling this function to process the reply without blocking.
That's why we went with this approach here. It doesn't sound like we must do async I/O to see improvement, but I could be wrong of course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If poll
isn't called with a timeout, it'll block. If we mustn't use NIO, prefer DispatchSource
s.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use DNSServiceRefSockFD() in conjunction with a run loop
^^ that's exactly what NIO can do for you.
What's the goal with removing the NIO dependency? Every adopter will have a NIO dependency anyway, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the goal with removing the NIO dependency?
Only a couple of methods on ByteBuffer
were being used for parsing so pulling in NIO was quite heavy handed. Replacing with ArraySlice<UInt8>
was relatively straightforward. There's no requirement to be free of NIO.
Every adopter will have a NIO dependency anyway, right?
Not necessarily although I'm sure many will.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If poll isn't called with a timeout, it'll block.
We are passing 0
as timeout I believe (poll(&pollFDs, 1, 0)
), so it should return immediately according to the man page:
Specifying a negative value in timeout means an infinite timeout.
Specifying a timeout of zero causes poll() to return immediately,
even if no file descriptors are ready.
I wonder if it should sleep a bit before calling poll
again though.
that's exactly what NIO can do for you
Is there an example that we can follow? I found these threads:
- https://forums.swift.org/t/socket-library-built-with-swift-concurrency/56505
- Think about adding a poll-loop mode where Selectable EventLoop can be driven from other threads without taking them over swift-nio#2074
But I didn't find anything obvious.
I don't doubt that SwiftNIO can do this more efficiently, but at the end of the day we need to look at the trade-offs. Does doing it without SwiftNIO give us a "good enough" solution? Is it worthwhile to add SwiftNIO, which is a heavier dependency, for this? (I don't have problem with adding the SwiftNIO dependency FWIW.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hot-looping on poll is not the right choice: it's not meaningfully different than just sleeping the thread, as we'll never yield it.
Doing this with NIO directly is gonna be a little painful as you'd have to write a Channel
to intercept the readFromSocket
function. Not the end of the world, but not as easy as you might like. I continue to think that DispatchSource is going to be the easiest way to achieve this goal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yes, if you don't actually need to do anything with the data in there, then DispacthSource is good. It's pretty bad if you need to actually read the data but if you just need to be told that there is something to read, DispatchSource.makeReadSource
is fine
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your feedback, I'm going to try implementing this with DispatchSource then, if it's ok for everyone.
Convert DNSSD to class to use deinit on the pointers we initialized for dns-sd C functions The Dispatch Source event handler is called when data can be read from the DNSSD service socket Remove the unchecked Sendable subtyping for the DNSServiceRef pointer as it's no longer needed Remove the cancelled case from the error code enum as it's no longer needed
Hi, sorry for the wait, I updated the implementation to use DispatchReadSource to process the query results. I converted the DNSSD struct to a class to use the deinit and deinitialize the pointers when we are done. The current implementation deinitialized the pointers at the end of the Other than that I only removed code that was no longer necessary. |
Don't bother reviewing for now as the code is actually not correct, we only have one reply handler pointer so concurrent requests cause a crash. Trying to fix it. |
Convert DNSSD back to struct A different Query class is created for every query, so concurrent queries no longer cause an issue
I added a I have tested with many concurrent requests and it seems to be working correctly. This implementation uses DispatchSource, as stated above. Let me know if there is any changes I should make! |
Thanks @victorherrerod. @weissi @Lukasa @ktoso Can you please review the latest changes when you have time? Thanks in advance. |
byteCount: MemoryLayout<QueryReplyHandler>.stride, | ||
alignment: MemoryLayout<QueryReplyHandler>.alignment | ||
) | ||
init(handler: QueryReplyHandler) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: add newline before
let replyHandlerPointer = UnsafeMutableRawPointer.allocate( | ||
byteCount: MemoryLayout<QueryReplyHandler>.stride, | ||
alignment: MemoryLayout<QueryReplyHandler>.alignment | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a difference in initializing serviceRefPtr
and replyHandlerPointer
here vs. inside the initializer? Does anyone have any preference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slight preference towards doing it in the init
so the allocate
and initializeMemory
calls are next to each other.
let replyHandlerPointer = UnsafeMutableRawPointer.allocate( | ||
byteCount: MemoryLayout<QueryReplyHandler>.stride, | ||
alignment: MemoryLayout<QueryReplyHandler>.alignment | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slight preference towards doing it in the init
so the allocate
and initializeMemory
calls are next to each other.
return continuation.finish(throwing: AsyncDNSResolver.Error(code: .internalError, message: "Failed to access the DNSSD service socket")) | ||
} | ||
|
||
let readSource = DispatchSource.makeReadSource(fileDescriptor: serviceSockFD) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we be passing our own queue here rather than using the default?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- security relevant use after free bug
- plus: either file descriptor leak or use after free
|
||
continuation.onTermination = { _ in | ||
readSource.cancel() | ||
DNSServiceRefDeallocate(query.serviceRefPtr.pointee) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
security relevant use after free bug here, you could be concurrently deallocating this as well as using it in the event handler
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The deallocation is now done in the dispatch source cancel handler so that should no longer happen
// Streaming done | ||
continuation.finish() | ||
// Streaming done | ||
continuation.finish() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should cancel the read source
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied in latest commit
return continuation.finish(throwing: AsyncDNSResolver.Error(code: .internalError, message: "Failed to access the DNSSD service socket")) | ||
} | ||
|
||
let readSource = DispatchSource.makeReadSource(fileDescriptor: serviceSockFD) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't this file descriptor leaked?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I close it now in the cancellation handler
return continuation.finish(throwing: AsyncDNSResolver.Error(code: .internalError, message: "Failed to access the DNSSD service socket")) | ||
} | ||
|
||
let readSource = DispatchSource.makeReadSource(fileDescriptor: serviceSockFD) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you'll need .setCancelHandler
here and manage the resources from there.
DispatchSources that use file descriptors and don't use setCancelHandler
are guaranteed to be incorrect. Also see https://developer.apple.com/documentation/dispatch/1385648-dispatch_source_set_cancel_handl?language=objc#discussion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, thanks for the feedback! I implemented it in the latest commit
DNSServiceRefDeallocate is called in the DispatchSource cancellation handler to be sure it's not used in the event handler after being freed Remove Query class as it is not needed for pointer management Close DNSServiceRefSockFD in DispatchSource cancellation handler to avoid any FD leak
Thanks everyone for the feedback, I followed @weissi advice and implemented a cancellation handler for the dispatch source that manages the ressources. I also deleted the I have tested concurrent queries and everything seems to work correctly. Unit tests all passed too. |
DNSServiceRefDeallocate(serviceRefPtr.pointee) | ||
let serviceSockFD = DNSServiceRefSockFD(serviceRefPointer.pointee) | ||
guard serviceSockFD != -1 else { | ||
return continuation.finish(throwing: AsyncDNSResolver.Error(code: .internalError, message: "Failed to access the DNSSD service socket")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't we leaking the serviceRefPointer
and replyHandlerPointer
in this branch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed the code so that the deallocates are also called in this branch and the other error branch just above.
Hi, could someone review the changes? We'd like to use this in a project to update our SRV resolver to use async/await, and being able to timeout the requests by cancelling them is necessary. I've tried to answer and fix most of the issues, let me know if there is anything else that's needed. |
|
||
// Wrap 'handler' into a pointer so we can pass it to DNSServiceQueryRecord | ||
let replyHandlerPointer = UnsafeMutablePointer<QueryReplyHandler>.allocate(capacity: 1) | ||
replyHandlerPointer.initialize(repeating: handler, count: 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's avoid initialize(repeating:)
, it's not needed. Just initialize(to:)
is fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in latest commit
@@ -173,6 +180,12 @@ struct DNSSD { | |||
|
|||
return try replyHandler.generateReply(records: records) | |||
} | |||
|
|||
private func deallocatePointers(serviceRefPointer: UnsafeMutablePointer<DNSServiceRef?>, replyHandlerPointer: UnsafeMutablePointer<DNSSD.QueryReplyHandler>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making this static
would make the code a lot easier to follow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made it static in the latest commit
…reply handler pointer
Anything new for this PR? It would be nice to be able to use it in a project, and with the requests being uncancellable and the timeout being so long, it's kind of difficult to use. |
Hey all any updates to this one? Would love to use this in some projects but the un-cancellable 30 second timeout is brutal. |
Resolves #32