diff --git a/README.md b/README.md index e146973..b5938f6 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,8 @@ nursery.launch( nursery.requestCancelOnAll(); nursery.closeAdmission(); -nursery.syncAwaitAllSettlements(ioContext); +nursery.syncAwaitAllSettlements( + sscl::ComponentThread::getSelf()->getIoContext()); ``` Each slot owns a `SyncCancelerForAsyncWork`. `requestCancelOnAll()` only signals @@ -213,6 +214,14 @@ completion callbacks run. Call `closeAdmission()` explicitly before `asyncAwaitAllSettlements()` or `syncAwaitAllSettlements()`; those APIs wait until all slots have retired naturally and throw if admission is still open. +`syncAwaitAllSettlements()` runs a nested `io_context` loop on the **calling +thread** (it blocks in `run_one()` until every slot has retired). Pass the +caller thread's `io_context` — usually `ComponentThread::getSelf()->getIoContext()` +— not some other thread's context. While the caller is blocked pumping another +thread's queue, handlers posted to the caller's own `io_context` are abandoned +and the drain can deadlock even when in-flight work has already completed on a +different thread. + `launch(factory, onSettledHook)` registers a non-null hook before `fillSlot()`. Omit the hook (default `nullptr`) when no settlement callback is needed. `Slot::Lease` is commit-required: an uncommitted lease removes its diff --git a/include/spinscale/co/nonViralTaskNursery.h b/include/spinscale/co/nonViralTaskNursery.h index a351b6a..c6c19f3 100644 --- a/include/spinscale/co/nonViralTaskNursery.h +++ b/include/spinscale/co/nonViralTaskNursery.h @@ -45,9 +45,15 @@ struct MemberInvoker : MemberInvokerBase * SyncCancelerForAsyncWork, and provides drain APIs. * * Call closeAdmission() explicitly before asyncAwaitAllSettlements() or - * syncAwaitAllSettlements(). syncAwaitAllSettlements() caller must pass the - * io_context where non-viral posting completions will land, and must ensure - * that io_context is prepared to run (e.g. not left stopped without restart). + * syncAwaitAllSettlements(). + * + * syncAwaitAllSettlements() runs a nested io_context loop on the calling + * thread (AsynchronousBridge). Pass the calling thread's io_context — + * typically + * ComponentThread::getSelf()->getIoContext() — not another thread's + * io_context. If the caller pumps a different thread's queue while blocked, + * completions posted back to the caller's own io_context are never executed + * and the drain can deadlock even after cooperative cancel. */ class NonViralTaskNursery { @@ -307,6 +313,9 @@ public: } } + /** Nested drain: blocks the calling thread in run_one() on @p ioContext until + * all slots retire. @p ioContext must be the caller thread's io_context. + */ void syncAwaitAllSettlements(boost::asio::io_context &ioContext) { if (admissionIsOpen()) diff --git a/include/spinscale/cps/asynchronousBridge.h b/include/spinscale/cps/asynchronousBridge.h index f7ec3b1..c2493a2 100644 --- a/include/spinscale/cps/asynchronousBridge.h +++ b/include/spinscale/cps/asynchronousBridge.h @@ -25,6 +25,10 @@ public: boost::asio::post(io_context, []{}); } + /** Blocks the calling thread in run_one() on the bridge's io_context. + * Used by syncAwaitAllSettlements(); that io_context must be the caller + * thread's own queue so posted completions on the caller are not starved. + */ void waitForAsyncOperationCompleteOrIoContextStopped(void) { for (;;)