CooperativeExecutor.runUntil sleeps until the next timer while runnable jobs are queued

Ouverte Adaptée aux débutants
#92,461 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

Évaluation

Difficulté
2/5
Temps estimé
1-3 heures
Accessibilité débutants
76/100
Type d'issue
Bug
Clarté
Clairement spécifiée
Activité
Active
Stack technique
swift

Piste de recherche

Commencez dans stdlib/public/Concurrency/CooperativeExecutor.swift autour de CooperativeExecutor.runUntil(_:) aux lignes 286-331, puis exécutez la reproduction WASI fournie sur le runtime affecté. La correction est complète lorsque les tâches exécutables s’exécutent avant que l’executor n’attende un minuteur et que les deux scénarios se terminent en quelques millisecondes avec le watchdog annulé ; ajoutez un test de régression si la structure de test environnante le permet.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

triage needed
Description

On wasm32-unknown-wasip1, a task that becomes runnable while another task is inside Task.sleep does not run until that sleep's deadline. This applies to a task that called Task.yield() and to a task whose awaited child has finished.

CooperativeExecutor.runUntil(_:) takes the ready jobs with runQueue.take(), runs them, and then sleeps for the time to the next timer. Jobs enqueued while those ran are in runQueue at that point; runQueue.isEmpty is checked only when no timer is pending.

https://github.com/swiftlang/swift/blob/swift-6.4.0-RELEASE/stdlib/public/Concurrency/CooperativeExecutor.swift#L286-L331

Reproduction
// Package.swift: swift-tools-version 6.3, one executableTarget named repro.
// Sources/repro/main.swift
let sleepSeconds: UInt64 = 5
let clock = ContinuousClock()
let start = clock.now
func stamp(_ what: String) { print("[\(clock.now - start)] \(what)") }

func scenario(_ name: String, yields: Int) async {
    stamp("\(name): start")
    let watchdog = Task {
        try? await Task.sleep(nanoseconds: sleepSeconds * 1_000_000_000)
        stamp("\(name): watchdog woke, cancelled=\(Task.isCancelled)")
    }
    let worker = Task {
        for _ in 0 ..< yields { await Task.yield() }
        stamp("\(name): worker done")
    }
    await worker.value
    watchdog.cancel()
    stamp("\(name): end")
}

await scenario("no-yield", yields: 0)
await scenario("yield", yields: 50)
stamp("all done")
$ swift sdk install https://download.swift.org/swift-6.4.0-release/wasm-sdk/swift-6.4.0-RELEASE/swift-6.4.0-RELEASE_wasm.artifactbundle.tar.gz --checksum f07b7be3c586d92d7a07051fc6d303b87ebea67eadc40640ba59d5a8b79aa86d
$ swift build -c release --swift-sdk swift-6.4.0-RELEASE_wasm
$ wasmkit run .build/out/Products/Release-webassembly-wasm32/repro.wasm
[0.003 seconds] no-yield: start
[0.006 seconds] no-yield: worker done
[5.023 seconds] no-yield: end
[5.024 seconds] yield: start
[5.026 seconds] no-yield: watchdog woke, cancelled=true
[10.036 seconds] yield: watchdog woke, cancelled=false
[10.039 seconds] yield: worker done
[10.039 seconds] yield: end

With no yields the worker is done at 6 ms, yet the task awaiting it resumes at 5 s. With yields the worker does not advance at all until the watchdog's sleep has run its full length, by which time nobody has cancelled it.

Expected behavior

Jobs that became runnable during a pass run before the executor sleeps; it should sleep only when the run queue is empty. Each scenario above should end within milliseconds, with the watchdog cancelled before it wakes.

A possible fix is to guard the sleep: if let toWait, runQueue.isEmpty { _sleep(…) } (or continue when runQueue is not empty). Untested.

Environment

Swift version 6.4 (swift-6.4-RELEASE), Ubuntu 24.04 aarch64 host, Swift SDK swift-6.4.0-RELEASE_wasm (wasm32-unknown-wasip1), run with the WasmKit 0.3.1 the toolchain ships.

Also reproduced with a binary built by Swift 6.3.3 and its Wasm SDK. The same code is present in release/6.3, release/6.4.x, release/6.4.1, swift-6.4.0-RELEASE, and main (as of 2026-09-19).

Additional information

The Swift version makes no difference; whether the WASI runtime's poll_oneoff actually sleeps does. Time for a four-scenario variant of the program (5 s sleep each) to finish, same two binaries on three runtimes:

Binary WasmKit 0.1.6 (ships with 6.3.3) WasmKit 0.3.1 (ships with 6.4.0) wasmtime 30.0.2
built with Swift 6.3.3 + its Wasm SDK 0.01 s 20 s 20 s
built with Swift 6.4.0 + its Wasm SDK 0.01 s 20 s 20 s

Impact: any program on these targets that keeps a timer pending while other tasks make progress — a timeout or watchdog around a body is the common case — runs at the pace of its timers. It surfaced as a test suite whose every deadline-bounded test took exactly its deadline (300 s) under the 6.4.0 toolchain while passing in seconds under 6.3.3: https://github.com/sinoru/swift-synchronization-kit/issues/4

#90096 (enabling Task.sleep in Embedded Swift) removes the #if !$Embedded guards around the same timer queues, so the same stall would presumably appear there once it lands.

Langage dominant
Swift
Étoiles
70.4k
Forks
10.8k
Merge moyen
2 j 2 h
PR mergées (30 j)
461

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de swiftlang/swift

Toutes les issues de swiftlang/swift

Issues similaires

Plus d'issues Swift

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.