Skip to content

Commit 605f78e

Browse files
committed
JavaScriptEventLoop: release the isSpinning latch with defer
`runAllJobs()` clears `queueState.isSpinning` only as its final statement, so the flag survives as `true` if a job unwinds. `insertJobQueue` schedules a drain only when `!isSpinning`, so after one unwound job the queue is never drained again: every subsequent `enqueue` appends to a queue nothing will run, for the lifetime of the process. Nothing reports it. The failure is silent and total for asynchronous work, while synchronous calls into the module keep working normally — which makes it present as "async stopped" rather than as a crash. Wrapping the reset in `defer` restores the invariant on every exit path. No behaviour change on the normal path.
1 parent cadafdc commit 605f78e

1 file changed

Lines changed: 9 additions & 2 deletions

File tree

Sources/JavaScriptEventLoop/JobQueue.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ extension JavaScriptEventLoop {
4242

4343
func runAllJobs() {
4444
assert(queueState.isSpinning)
45+
// `defer`, so the latch is released even if a job throws.
46+
//
47+
// `runSynchronously` can unwind — a Swift runtime trap, or (under
48+
// JavaScriptKit specifically) a JS exception crossing back into wasm.
49+
// Clearing `isSpinning` only by falling off the end of the loop leaves
50+
// it latched `true` on that path, and `insertJobQueue` then never
51+
// schedules another drain: the executor is dead for the lifetime of the
52+
// process, silently.
53+
defer { queueState.isSpinning = false }
4554

4655
while let job = self.claimNextFromQueue() {
4756
#if compiler(>=5.9)
@@ -50,8 +59,6 @@ extension JavaScriptEventLoop {
5059
job._runSynchronously(on: self.asUnownedSerialExecutor())
5160
#endif
5261
}
53-
54-
queueState.isSpinning = false
5562
}
5663

5764
func claimNextFromQueue() -> UnownedJob? {

0 commit comments

Comments
 (0)