You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Index + minor-findings umbrella for the 2026-07-10 systematic concurrency audit of the doeff execution substrate (scheduler / async bridges / Rust VM), triggered by #490. Three parallel reviewers read scheduler.py (all 819 lines), handlers.py/doeff-time handlers, and both VM crates at 8526c105; every executed finding was then independently re-verified with a second repro run (most also on the #491 branch — all reproduce post-#491).
Prior art from the same effort: #490 (wait_external stall) + PR #491 (fix, verified 905 passed/0 failed).
Minor findings (this issue tracks them; split out on demand)
Scheduler:
CompletePromise/FailPromise lack the already-resolved guard the drain path has (scheduler.py:747-765 vs 352/566): double-complete silently rewrites result after waiters were woken with the old value; an internal CompletePromise on an external promise is accepted, after which the foreign thread's completion is silently ignored.
wrap_task catches Exception, not BaseException (scheduler.py:336): KeyboardInterrupt/SystemExit in a task bypasses TaskCompleted, leaving status "running" and waiters unwoken while unwinding into the dispatching scope.
"suspended" status is checked (scheduler.py:734) but never assigned — dead branch/doc mismatch.
Late ExternalPromise.fail() after run end is silently lost (queue outlives the run).
Resume order does not preserve cross-promise completion order (drain wake at NORMAL can jump a priority-5 entry resolved earlier) — do not build event sequencing on Await resume order.
Bridges / doeff-time:
7. WaitUntil/ScheduleAt compute the wall-clock delta once and sleep on the loop's monotonic clock with no re-check after wake (async_time.py:42-45, 52-62): NTP steps / host suspend slip the wake against the target; a re-check loop is cheap insurance for auction-timing use.
8. Delay(0) is not a cheap yield: it round-trips the background loop + external queue (ms-scale, full #493 exposure).
9. Await(asyncio.to_thread(...)) shares the loop's default ThreadPoolExecutor (cap min(32, cpu+4)) with every memo/cache sqlite op (storage/sqlite.py:114-128) — >32 concurrent blocking ops silently serialize; one accidentally-blocking coroutine freezes all timers process-wide. SQLiteStorage.close() closes only the calling thread's connection; pool-thread thread-local connections (sqlite.py:49-64) never close (up to ~32 leaked WAL-holding connections per storage).
10. make_memo_rewriter has no in-flight dedup (memo_handlers.py:204-251): concurrent misses on the same key compute (and bill) twice.
11. Latent, blocked on #499 verification: SimTimeRuntime instance reuse across scheduled() runs → stale _driver_running/TimeQueue; cross-run promise-ID collision can complete the WRONG promise (both runs' fresh_id start at 0; unguarded promises[pid] at scheduler.py:749). Handler-caching (Hy defhandler+lazy-var) makes reuse likely.
12. Sim _forwarding_tell reentrancy flag is per-runtime, not per-task (sim_time.py:44-45, 81-89, 129-134): concurrent Tells can skip sim-timestamp formatting.
VM:
13. A PyK passed as a plain value (e.g. Pure(k), Resume(k2, k1)) is destructively take()n with no one-shot error (python_generator_stream.rs:932-940); identity changes on round-trip; a dropped Value::Continuation is a silent stall. Effect fields are safe (stay Opaque).
14. Resuming an exhausted generator stream returns Done(Unit) → silent None instead of a loud error (python_generator_stream.rs:303-306); raw IRStream re-use shares one generator across wrappers.
15. Error path skips end_active_run_session (pyvm.rs:65-66) — only matters for embedders holding a PyVM.
16. Latent LexicalScope top-frame spin in step_send (step.rs:92-96) — unreachable today (Var subsystem decided-dead, invariants.md D15); becomes a 100% CPU stall if re-exposed before deletion.
17. Value::clone() panics on Continuation/Callable/Stream (value.rs:94-101) — latent; Python effects are always Opaque.
18. _RESUME_ANALYSIS_CACHE_KEEPALIVE (do.py:181) grows unboundedly when handlers are constructed from freshly compiled code objects (Hy runtime eval).
19. Dead code: EvalReturnContinuation::{ResumeToContinuation, ReturnToContinuation, EvalInScopeReturn, TailResumeReturn}, Frame::{MapReturn, FlatMapBind*} processed but never constructed (step.rs:97-105, 832-857).
Verified-clean areas (for the record)
One-shot continuation enforcement is loud on every consumption path (dispatch.rs:122-141; repro'd RuntimeError); no scheduler-side double-resume path found (gather/race resolved flags + waiter removal correct; cancel-then-complete safe).
Index + minor-findings umbrella for the 2026-07-10 systematic concurrency audit of the doeff execution substrate (scheduler / async bridges / Rust VM), triggered by #490. Three parallel reviewers read
scheduler.py(all 819 lines),handlers.py/doeff-timehandlers, and both VM crates at8526c105; every executed finding was then independently re-verified with a second repro run (most also on the #491 branch — all reproduce post-#491).Filed issues (severity-ordered)
Critical
pending_handler_k_handle— wrong-scope exception delivery + result substitution (repro'd)High
__traverse__/__clear__— permanent uncollectable cycles (repro'd)Medium
Race()leaks the caller continuation (repro'd; scheduler: unawaited Spawn task failures are silently dropped; wrap_task try also wraps the Ok TaskCompleted perform #485 family)Prior art from the same effort: #490 (wait_external stall) + PR #491 (fix, verified 905 passed/0 failed).
Minor findings (this issue tracks them; split out on demand)
Scheduler:
CompletePromise/FailPromiselack the already-resolved guard the drain path has (scheduler.py:747-765 vs 352/566): double-complete silently rewritesresultafter waiters were woken with the old value; an internal CompletePromise on an external promise is accepted, after which the foreign thread's completion is silently ignored.wrap_taskcatchesException, notBaseException(scheduler.py:336): KeyboardInterrupt/SystemExit in a task bypasses TaskCompleted, leaving status "running" and waiters unwoken while unwinding into the dispatching scope."suspended"status is checked (scheduler.py:734) but never assigned — dead branch/doc mismatch.external_queue.get()has no timeout (scheduler.py:351): on Windows the hang shapes in scheduler: unresolvable waits hang silently in external_queue.get(); semaphore deadlock detection is masked by any live external waiter and unreachable behind wait_external entries #495 also eat Ctrl-C.ExternalPromise.fail()after run end is silently lost (queue outlives the run).Bridges / doeff-time:
7.
WaitUntil/ScheduleAtcompute the wall-clock delta once and sleep on the loop's monotonic clock with no re-check after wake (async_time.py:42-45, 52-62): NTP steps / host suspend slip the wake against the target; a re-check loop is cheap insurance for auction-timing use.8.
Delay(0)is not a cheap yield: it round-trips the background loop + external queue (ms-scale, full #493 exposure).9.
Await(asyncio.to_thread(...))shares the loop's default ThreadPoolExecutor (cap min(32, cpu+4)) with every memo/cache sqlite op (storage/sqlite.py:114-128) — >32 concurrent blocking ops silently serialize; one accidentally-blocking coroutine freezes all timers process-wide.SQLiteStorage.close()closes only the calling thread's connection; pool-thread thread-local connections (sqlite.py:49-64) never close (up to ~32 leaked WAL-holding connections per storage).10.
make_memo_rewriterhas no in-flight dedup (memo_handlers.py:204-251): concurrent misses on the same key compute (and bill) twice.11. Latent, blocked on #499 verification: SimTimeRuntime instance reuse across
scheduled()runs → stale_driver_running/TimeQueue; cross-run promise-ID collision can complete the WRONG promise (both runs'fresh_idstart at 0; unguardedpromises[pid]at scheduler.py:749). Handler-caching (Hydefhandler+lazy-var) makes reuse likely.12. Sim
_forwarding_tellreentrancy flag is per-runtime, not per-task (sim_time.py:44-45, 81-89, 129-134): concurrent Tells can skip sim-timestamp formatting.VM:
13. A
PyKpassed as a plain value (e.g.Pure(k),Resume(k2, k1)) is destructivelytake()n with no one-shot error (python_generator_stream.rs:932-940); identity changes on round-trip; a droppedValue::Continuationis a silent stall. Effect fields are safe (stay Opaque).14. Resuming an exhausted generator stream returns
Done(Unit)→ silentNoneinstead of a loud error (python_generator_stream.rs:303-306); raw IRStream re-use shares one generator across wrappers.15. Error path skips
end_active_run_session(pyvm.rs:65-66) — only matters for embedders holding a PyVM.16. Latent LexicalScope top-frame spin in
step_send(step.rs:92-96) — unreachable today (Var subsystem decided-dead, invariants.md D15); becomes a 100% CPU stall if re-exposed before deletion.17.
Value::clone()panics on Continuation/Callable/Stream (value.rs:94-101) — latent; Python effects are always Opaque.18.
_RESUME_ANALYSIS_CACHE_KEEPALIVE(do.py:181) grows unboundedly when handlers are constructed from freshly compiled code objects (Hy runtime eval).19. Dead code:
EvalReturnContinuation::{ResumeToContinuation, ReturnToContinuation, EvalInScopeReturn, TailResumeReturn},Frame::{MapReturn, FlatMapBind*}processed but never constructed (step.rs:97-105, 832-857).Verified-clean areas (for the record)
resolvedflags + waiter removal correct; cancel-then-complete safe).unsafein either crate; GIL held for the whole step loop; no VM-level blocking.