Repository navigation
Conversation
|
I think I will pass the review baton to @alexcrichton on this one if that's OK -- I'm not familiar enough with the innards of |
Several scheduler paths in `concurrent.rs` moved a live fiber out of store-owned state (into a `WaitMode`, a `WorkItem`, or a local) and then ran a fallible step before it reached its next owner. If that step failed, the unfinished fiber was dropped and its `Drop` impl panicked, turning a trap or `bail_bug!` into a host panic or process abort. Instead of guarding each handoff, keep every resting guest fiber in its `GuestThreadState`, and the reusable worker fiber in `ConcurrentState::worker`. Wait sets, work queues, and switch slots now carry only thread ids, and a fiber is taken out of its thread only when a work item resumes it, after checking that the thread is in the expected state. A new `FiberKind` records why the fiber is at rest so that `thread.resume*` and promotion see the same states as before. Restoring the current thread leaves a `Waiting` or `Scheduled` fiber in place instead of overwriting it with `Running`. `thread-wait-resume.wast` checks that a thread blocked in `waitable-set.wait` still cannot be resumed.
While `resume_fiber` runs a fiber, that fiber is outside the store's state. If restoring the previous thread or storing the suspended fiber fails, or if the resume future is dropped before it completes, the fiber would be dropped unfinished. Hold it in a small `ResumingFiber` guard together with the store for that interval, so that it is disposed of through the store unless it is put back in its thread or the worker slot. `run_on_worker` now checks `worker_item` before taking the worker fiber.
`take_fibers_and_futures` and `trace_fiber_roots` are where fibers are found when a store is dropped or traced. Destructure `ConcurrentState` and match `GuestThreadState` exhaustively in both, as suggested in the bytecodealliance#14146 review, so that a new field or variant that can hold a fiber has to be handled there.
If an internal invariant were broken, a few state transitions could overwrite or delete a slot that still owns a fiber, or lose a pending switch. Check first and `bail_bug!` instead: - `resume_fiber` checks that the target thread does not already own a fiber, and that the wait set or `next_switch_item` is free, before storing the fiber. - `cleanup_thread` refuses to clean up a thread that still owns a fiber. - The callback wait path checks for an existing waiter before setting `wake_on_cancel`. - `restore_next_switch_item` refuses to overwrite a `next_switch_item` that was set while the saved one was stashed.
Add `fiber_ownership_tests`, which set up broken states directly and check that the original error (or the `bail_bug!` panic in debug builds) comes back and that the fiber is either still owned by the store or has been disposed of. They cover invalid wait set, thread, and subtask handles when a fiber suspends, an invalid waiter in `mark_ready`, an occupied switch slot, `set_thread_running` and `cleanup_thread` on a thread that still owns a fiber, and dropping a resume future before it is polled. One more test checks the resume and promote rules for each `FiberKind`. Fixes bytecodealliance#14241.
b6da114 to
b9fee6c
Compare
|
It's been a bit of a busy week for me and this is a large enough change that I want to make sure I've got sufficient time to sit down and review this, so mostly wanted to say I haven't forgotten this @Byte-Naut just taking some time to review it. |
Thanks for letting me know, no rush at all. Happy to adjust whatever you'd like once you get to it. |
alexcrichton
left a comment
There was a problem hiding this comment.
Ok I've gotten a chance to read this now, thanks for your patience. Overall I'm a bit fearful of how this turned out. Whenever we add state to async things it become quite difficult to reconcile that new state space with all the preexisting state spaces and is often the source of bugs. For example adding FiberKind to the mix here seems like it's multiplying the state space further. I'm finding it personally pretty difficult to follow the refactor here to understand all of these state transitions.
My inclination of "only have fibers live in the store" might just be flat-out wrong here. One example from this PR is that the change to resume_fiber looks correct to me (along with the ResumingFiber abstraction). Otherwise though I'm fearful of the additional state being a bit too complicated to manage.
I don't know how best to resolve the original issue myself. Do you have ideas/opinions yourself?
Thanks for reviewing. I agree that this grew into a larger scheduler refactor than the original ownership bug warrants. I would keep the If we continue with fibers resting in their threads, one option is to use direct My preference is to pursue the smaller ownership repair first and assess the representation change separately (the local checks support these distinctions, but I have not implemented or run the full Wasmtime suites for either proposed simplification now). |
|
I'd personally be amenable to seeing how things looked, but I don't have a great vision in my head of what you're describing insofar as I don't feel "yeah for sure we want that" or "no I don't think that'll work". If you're up to experiment though it'd be appreciated! |
Thanks for the guidance, happy to help sort this out. I've put together an implementation of the smaller path and opened it as #14620. The scheduler representation is unchanged — Happy to hear if the representation or the scope looks off — and glad to adjust, do more testing, or answer questions whenever you have time. |
|
Closing in favor of #14620. |
Fixes #14241.
This supersedes #14385 and follows @alexcrichton's suggestion there: rather than guarding each handoff, keep fibers at rest in the table and only take them out while they're being resumed.
Motivation
Several scheduler paths in
concurrent.rsmoved a live fiber out of store-owned state (into aWaitMode, aWorkItem, or a local) and then ran a fallible step before it reached its next owner. These only fail once an internal invariant is already broken, but when they do the unfinished fiber is dropped,StoreFiber'sDroppanics, and a trap orbail_bug!turns into a host panic or process abort. #14382 added two more such paths (next_switch_iteminYieldingToSubtask, and theotherarm insubtask_cancel).Changes
GuestThreadState::Fiber { fiber, kind }, and the reusable worker fiber stays inConcurrentState::worker.WaitMode::FiberandWorkItem::ResumeFiberno longer hold a fiber, so wait sets, work queues,switch_item,next_switch_item, and the saved switch items in the table only carry thread ids.handle_work_item, after checking the expectedkind, and handed straight toresume_fiber.resume_fiberholds it together with the store in a small private guard until it is back in its thread or the worker slot, so a failed lookup, or dropping the future before it is polled, disposes of it through the store.take_fibers_and_futuresandtrace_fiber_rootsdestructureConcurrentStateand matchGuestThreadStateexhaustively, as suggested in the align multithreading and trap behavior with CM spec #14146 review, so a new field or variant has to be handled there. Neither needs to look in wait sets or work items for fibers anymore.bail_bug!instead.restore_next_switch_itemdoes the same ifnext_switch_itemwas set again while the saved one was stashed, since overwriting it would silently lose that switch.No public API changes, no new locks, no per-suspension allocations, and no new
ConcurrentStatefields. About 270 of the added lines are tests.Design
FiberKindkeeps the distinctions the thread intrinsics relied on when the fiber's location implied the state:Running, blocked in a waitable setWaitMode::FiberFiberKind::WaitingRunning, queued asResumeFiberor yielding to a subtasknext_switch_itemFiberKind::ScheduledSuspendedFiberKind::SuspendedReady, queued asResumeThreadFiberKind::ReadyGuestThreadState::can_resumeapplies the same rules as before: onlySuspended(or a not-yet-started explicit thread) can be resumed, and onlyReadycan be promoted.WaitingandScheduledbehave likeRunning.resume_work_item_fiberchecks that aResumeFiberitem findsScheduledand aResumeThreaditem findsReadybefore taking the fiber.Two things worth a look in review:
set_thread_runningleaves aScheduledorWaitingfiber in place. Before, that write set a thread whose fiber lived elsewhere toRunning; now the fiber stays in the thread state, which already means "running" for the intrinsics.resume_fiberis the only place a guest fiber is outside the table, and it covers exactly the resume interval.None of the details here are fixed. If other names for
FiberKind's variants, or fewer or differently placed tests, would be easier to review, I'm happy to rework it.Testing
thread-wait-resume.wast(new) blocks a thread inwaitable-set.waitwith no pending event, then callsthread.resume-laterorthread.suspend-then-resumeon it and expectscannot resume thread which is not suspended. It guards against a waiting thread being treated as suspended now that its fiber lives in the thread state.The unit tests in
concurrent.rs(fiber_ownership_tests) set up broken states directly: invalid wait set, thread, or subtask handles when a fiber suspends; an invalid waiter inmark_ready; an occupied switch slot;set_thread_runningandcleanup_threadon a thread that still owns a fiber; and dropping a resume future before it's polled. Each checks that the original error (orbail_bug!panic in debug builds) comes back and that the fiber is still owned by the store or has been disposed of. One more test checks the resume and promote rules for eachFiberKind.Locally: fmt, clippy for
wasmtime, the fullwasmtimelib tests, the async component-model.wasttests on Cranelift and Winch, thecomponent_modeltests intests/all, the focused tests with debug assertions disabled, andno_std/ non-async builds all pass.Reviewing
The change is split into five commits that each build on their own:
FiberKind,GuestThreadState::Fiber, id-only wait sets and work items, andthread-wait-resume.wast. This is the design choice worth checking first.resume_fiber, which covers only the resume interval. Most of the diff is re-indentation into theasyncblock, so "Hide whitespace" helps.bail_bug!s for broken invariants.3 and 4 are hardening on top of the fix. If you'd rather keep this smaller, I'm happy to drop them or move them to a follow-up.
Developed with assistance from Claude (Anthropic). Per the Bytecode Alliance AI Tool Use Policy, I'm the author and accountable for this change.