diff --git a/README.md b/README.md index b5938f6..eceb8e2 100644 --- a/README.md +++ b/README.md @@ -202,17 +202,19 @@ nursery.launch( nvc.checkAndRethrowException(); }); -nursery.requestCancelOnAll(); nursery.closeAdmission(); +nursery.requestCancelOnAll(); nursery.syncAwaitAllSettlements( sscl::ComponentThread::getSelf()->getIoContext()); ``` Each slot owns a `SyncCancelerForAsyncWork`. `requestCancelOnAll()` only signals cooperative stop; it does not destroy invokers. Invokers are retired when their -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. +completion callbacks run. Call `closeAdmission()` before `requestCancelOnAll()` +so no new work can be admitted after cancel begins, and 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 diff --git a/include/spinscale/co/nonViralTaskNursery.h b/include/spinscale/co/nonViralTaskNursery.h index 824c724..ef2530b 100644 --- a/include/spinscale/co/nonViralTaskNursery.h +++ b/include/spinscale/co/nonViralTaskNursery.h @@ -55,9 +55,6 @@ struct MemberInvoker : MemberInvokerBase * nursery member. The external submitter should add the complete flow to the * nursery and then return; the nursery owns that flow until the flow settles. * - * Call closeAdmission() explicitly before asyncAwaitAllSettlements() or - * syncAwaitAllSettlements(). - * * syncAwaitAllSettlements() runs a nested io_context loop on the calling * thread (AsynchronousBridge). Pass the calling thread's io_context — * typically @@ -240,6 +237,36 @@ public: s.rsrc.admissionOpen = true; } + /** EXPLANATION: + * Stopping a nursery: always closeAdmission() before + * requestCancelOnAll(). requestCancelOnAll() only marks currently + * ACTIVE_UNSETTLED slots; it does not refuse new leases. If cancel + * runs while admission is still open, a concurrent submitter can still + * getNewSlotLease() / launch() after cancel has fanned out, and that + * newly admitted work will not have been cancelled — it races past the + * stop wave and keeps the drain from reaching "all settled" until it + * finishes on its own (or a later cancel). Closing admission first + * seals the nursery so cancel applies to a fixed membership set, then + * drain with asyncAwaitAllSettlements() / syncAwaitAllSettlements() + * (those APIs also require admission already closed). + * + * Preferred stop stack for a daemon/service that enqueues request + * coroutines into the nursery: keep protocol "stop listening / + * disconnect / refuse new connections and requests" separate from + * protocol state destruction. Stop accepting at the protocol level + * first, then nursery closeAdmission(), then requestCancelOnAll(), + * then cancel any awaited I/O owned outside the cancelers, then drain, + * then destroy protocol state. That ordering stops new work at the + * source before admission is sealed. + * + * If the daemon/service cannot disconnect/stop listening separately + * from destruction, the spinscale-using embedding project must handle + * closed-admission failures when it tries to enqueue. For example, + * catch the "admission closed" throw around nursery.launch() (or in + * the factory that calls it) and emit a protocol-specific failure such + * as "connection failed" or "request timed out" instead of letting the + * exception escape the accept/request path unbounded. + */ void closeAdmission() { sscl::SpinLock::Guard guard(s.lock);