Repository navigation
Conversation
Cancelling a FlyingSocks stream or datagram endpoint's run() cancels its socket pool, which stops its event queue under the wait in progress. On Darwin that wait can then fail with EBADF instead of ending as a cancellation: a race, lost in most runs. When it was lost, the endpoint logged a critical "server error ... kqueue kevent" and threw that error. It also skipped removing itself from its device, so it stayed registered. When the task has been cancelled, close the socket, remove the endpoint and throw CancellationError, whatever the pool threw. That covers the FlyingFox we resolve (0.27.1) as well as swhitty/FlyingFox#242, which makes the pool itself throw CancellationError. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Fq7ie1LzJERrh9uRuZZWE
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #242 +/- ##
=======================================
Coverage 93.76% 93.77%
=======================================
Files 72 72
Lines 3866 3872 +6
=======================================
+ Hits 3625 3631 +6
Misses 241 241 ☔ View full report in Codecov by Harness. |
|
The Linux Swift 6.0 and 6.2 failures here are in |
c622d6e to
2414491
Compare
Cancelling SocketPool.run() while it waits on a kqueue closes the queue under kevent, which returns EBADF, and run() throws that instead of CancellationError (swhitty/FlyingFox#242). The monitor then warned on every disconnect. Keep quiet when the task was cancelled; the pool is rebuilt either way. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CckctnB2YrMHhEKeRJDij
2414491 to
6205e96
Compare
|
Rebased on main. The #243 commit has dropped out, since it landed via #247, and so has the change making A heads-up in case CI trips on it: since #252, |
|
Thanks, this is a good catch, and the write-up made it easy to follow. I merged it onto current Two small changes before merging: 1. In } catch {
if !Task.isCancelled {
logger.logCritical("server error: \(error.localizedDescription)")
}2. Linux is affected too. Could you update the comment and description? Not blocking: the new test adds about 1s (5 × 200 ms). A For the release notes: there's no public API change, but when cancelled while waiting, Separately, and outside this PR: the underlying issue is that |
Cancelling SocketPool.run() while it waits on its event queue made it
throw the error of the interrupted wait instead of CancellationError. On
Darwin, getNotifications()'s cancellation handler stops the queue, which
closes the kqueue under the kevent call in progress. That call fails with
EBADF, and run() threw SocketError.makeFailed("kqueue kevent"). Throw
CancellationError when the wait fails after the task has been cancelled.
HTTPServer.run() logged every error as a critical server error, including
this one, so each cancelled server logged "server error: ...kqueue
kevent...". A cancellation is not a server error; don't log it as one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Fq7ie1LzJERrh9uRuZZWE
6205e96 to
2d01eb6
Compare
Summary
Cancelling
SocketPool.run()while the pool waits on its event queue throws the interrupted wait's error instead ofCancellationError, andHTTPServer.run()then logs it as a critical server error. The result is that every cancelledHTTPServeron Darwin logs:Cause
getNotifications()runs the blocking wait on its dispatch queue, and its cancellation handler callsstopQueue()to break that wait. ForkQueue,stop()closes the kqueue whilekeventis still waiting on it. SokeventreturnsEBADF,getNotifications()throwsSocketError.makeFailed("kqueue kevent"), and that becomesrun()'s error.Changes
SocketPool.getNotifications(): if the wait fails after the task has been cancelled, throwCancellationError.HTTPServer.run(): don't log aCancellationErroras a critical server error. Other errors are still logged.On Linux, as far as I can tell from reading the code, the
ePollqueue already comes out asCancellationError:stop()wakes the wait through the canary eventfd, and the nextgetNotifications()call'sTask.checkCancellation()throws. So only the logging change applies there.Reproducing
It's a race, and whether it happens depends on how far the pool has got when it's cancelled. The existing
taskCanBeCancelledcancels as soon as the server is listening, before the pool is waiting on its queue, so it doesn't see this. I cancelled a server 200 ms after it started listening, three times per build:SocketError(kqueue kevent,EBADF)main(bd32d43)Tests
HTTPServerTests.taskCanBeCancelled_WhileWaitingForConnections. It does that 5 times and expectsCancellationErroreach time.SocketPoolchange it failed every time I ran it, with 5 or 6 issues per run, eachexpected error of type CancellationError, but ".failed(type: "kqueue kevent", errno: 9, ...)" of type SocketError was thrown instead.The full suite passes on macOS (Swift 6.3.3). In 5 runs, one hung in
HTTPClientTests.client_sends_request(), which never cancels a server while it runs. Unmodifiedmainhangs the same way, in 1 of 6 runs, there inhandlerError_ResponseIncludesDateHeader(). So that's an existing intermittent hang, not something this change introduces. I haven't run the suite on Linux.We found this in SwiftOCA, whose device endpoints use
SocketPoolthe same wayHTTPServerdoes.🤖 Generated with Claude Code
https://claude.ai/code/session_012Fq7ie1LzJERrh9uRuZZWE