Repository navigation
fix(os): avoid recursive signal cancellation lock - #908
stephenlclarke wants to merge 1 commit into
Conversation
b4cc9e3 to
471b580
Compare
| struct AsyncSignalHandlerTests { | ||
| @Test | ||
| func cancelFinishesStreamsWithoutRecursivelyLocking() async { | ||
| let handler = AsyncSignalHandler.create(notify: [SIGUSR1]) |
There was a problem hiding this comment.
Do you think installing a signal handler in the test process could cause issues/flakes?
There was a problem hiding this comment.
Rebased onto current upstream 6adea5663427b4068108e9268e6125fc139b6045 and updated the regression in 19eb7f900324b7999678f0174b1af4474169b45c. The test now uses notify: []: it exercises both stream termination callbacks and repeated cancellation without installing a process-wide signal disposition in the shared test runner.
A standalone executable compiled from the exact production file reproduces the original failure after entering cancel() and before returning; the updated implementation returns, drains both streams, and tolerates repeated cancellation. This is isolated / focused cancellation evidence.
19eb7f9 to
afa1fc6
Compare
afa1fc6 to
abddf8c
Compare
Summary
Avoid recursively acquiring
AsyncSignalHandler's state mutex during cancellation.cancel()previously calledAsyncStream.Continuation.finish()while holding the lock. Finishing invokes the continuation'sonTerminationcallback, which callscancel()again and can deadlock on the same non-recursive mutex.Drain the continuations and dispatch sources under the lock, clear the shared state, then finish and cancel those resources after releasing the lock. This keeps repeated cancellation idempotent while removing the recursive lock path.
Testing
swift test --filter AsyncSignalHandlerTestsResult: 1 focused regression passed. It creates multiple signal streams, cancels them, verifies both finish, and then cancels again.
Compatibility
No public API changes. Signal sources and streams are still cancelled by the same call; only the lock boundary changes.