Repository navigation
core: client-side stop plumbing: a stopped co_await stops its call; execute(action, StopToken) - #866
Merged
Merged
Conversation
Yaraslaut
force-pushed
the
lane/stop-plumbing
branch
from
October 4, 2026 11:36
2730860 to
1f18553
Compare
CompletionAwaiter now links its scope to the awaited call's stop source through CompletionState::linkStop when it attaches, as a scope-gated continuation already does. A stop that withdraws the await, or the frame destroyed while suspended, therefore also asks the call to stop: a Task handler on a LocalBackend sees OperationCancelled at its next stop-aware await. linkStop alone was not the whole fix. A stop that lands before the handlers are attached (a token already stopped at co_await, or a stop racing the stop-callback registration) never reaches attach(), so the awaiter now relays that stop to the call on the completion's executor. The cancellation policy gains a measured G2 row for a stopped co_await; the coroutine spec's Cancellation section says what the stop now does to the call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n) (#863) A stop on the token before the call settles rejects the Completion with core::async::OperationCancelled, posted from the thread that requested the stop, and requests stop on the call's StopSource, so a Task handler on a LocalBackend sees it. A synchronous handler runs to its end; on a remote backend the call is only abandoned, since no cancel crosses the wire yet. A token already stopped rejects the call without dispatching. The link is CompletionState::linkCancel, held with the scope links in stopLinks and released by deliver(). Like the execute deadline it settles the state, not the sink, so the call stays counted pending until the backend's own reply. The cancellation policy gains a measured G2 row for the verb, stating the remote limitation; completion.md and bridge.md no longer say that no work cancellation exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yaraslaut
force-pushed
the
lane/stop-plumbing
branch
from
October 4, 2026 12:06
1f18553 to
ad79b11
Compare
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…deliver into them "a promotion reply after the handler or the bridge is gone is a no-op" declared each Outcome inside the scope it was destroying, but attached ungated then/onError handlers that capture it. A reply settling inside a later runFor() is delivered into it, so whether the test read a destroyed stack object depended on whether the pool's settle landed inside the 5 ms window. clang-asan caught it once as a stack-use-after-scope at test_async_registration.cpp:556. Both outcomes now live at test scope. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Client-side stop plumbing: two tickets, one commit each.
Closes #862
Closes #863
#862: a stopped
co_awaitstops the call (be7a14f)CompletionAwaiter::attachlinks the awaiter's scope to the awaited call's stop source withCompletionState::linkStop, the same way a scope-gated continuation does.linkStopwas not the whole fix, as the issue suspected and as noted on core: a stopped co_await ends the call, not only the await (link CompletionAwaiter to the call's stop) #862. A stop that lands beforeattachruns never reaches the call: a token already stopped atco_await, or a stop that races the registration of the stop callback. In both casesstopUnattachedrelays the stop to the call on the completion's executor.coroutines.mdnow says what the stop does to the call.#863:
BridgeHandler::execute(action, core::async::StopToken)(2730860)OperationCancelled. The rejection is posted from the thread that requested the stop. The stop is also requested on the call'sStopSource, so a Task handler on aLocalBackendsees it. A token already stopped rejects the call without dispatching it.CompletionState::linkCancel, held instopLinkswith the scope links and released bydeliver(). It settles first and then stops, soOperationCancelledwins over the outcome the stopped handler produces. Like the execute deadline, it settles the state and not the sink, so the call stays counted as pending until the backend's own reply arrives.cancelcrosses the wire until core, wire: a hello-negotiated cancel envelope that RemoteServer maps onto the run's StopSource #864/net, qt: SocketBackend and QtWebSocketBackend send cancel when a call's stop is requested #865, so the server's handler runs to its end and its reply is dropped. That case is measured withSimulatedRemoteBackend.completion.mdandbridge.mdno longer claim that no work cancellation exists.execute(Action)andexecuteJson(...). A trailingStopTokenis the core-cpp andstd::jthreadconvention, adds one overload without touching any existing call, and leaves room for an options struct later if per-call options grow. The verb is onBridgeHandlerbecause that is the caller-facing surface the spec documents.Bridge::executeViaand its variants take aHandlerBindingand are the handler's plumbing.executeJsongets no token variant here.Mutation checks (real output)
#862: with
state.linkStop(token.stopToken())removed fromattach:The case with the token already stopped stayed green under that mutation, because it goes through
stopUnattached. That relay was mutated separately, in the same build as the #863 mutation below, and its test went red (:727, samesleeper().cancelledassertion).#863: with the token ignored (
execute(action, stop)forwards toexecute(action)):The settle assertion is a
REQUIRE, so it aborts the case before the handler-stop assertion runs. With the token ignored, nothing could stop the handler anyway.Review, done inline
CancelRelayruns on whichever thread requests the stop.setExceptionis already safe from any thread, as the deadline relies on. It reads the stop source through aweak_ptrcaptured on the owner, so it never readsstopSourceoff the owner.stopLinks.deliver()drops a link while its callback is running on another thread, the drop waits for that callback, as the existing scope links already do. The callback never blocks on the owner, so this cannot deadlock.deliver()keeps the links alive until it returns, so a coroutine that resumes inline and finishes there can request stop on a call that has already settled. That is harmless, since the call has nothing left to stop, but it is visible in a trace.#includeshiftsbridge.hpp, so thebranch_partial_allowlist.jsonhint moves from 1772 to 1773. All 13 entries'sourcelines were re-checked and match.Verification
morph_tests, full run:test cases: 1750 | 1749 passed | 1 failed as expected.[cancel-policy]: 25 runs, 25 passed, 0 failed.clang-tidy-diff.pywith local clang-tidy 23.1 (CI pins 22): two findings fixed (readability-trailing-comma,misc-const-correctness). One remains,misc-const-correctnessonerrCodeinside the glaze expansion ofBRIDGE_REGISTER_ACTION(CPModel, CPSleep, ...). The identical registration lines core: stamp the verified principal on deregister, release only held references; spec + tests for the cancellation policy (#855, #858, #846) #859 added passed CI's clang-tidy 22, so I expect 22 not to raise it. That is inferred, not run.doctarget exits 0 with no warnings.Not verified:
MORPH_BUILD_NETandMORPH_BUILD_QTbuilds. Those remote backends carry no client-side stop source, so for them both verbs only abandon or withdraw.morph_tests(journal skew, client-only, canary), which need targets I did not build.Separate commit: a test's stack-use-after-scope (not part of either ticket)
On the rebased run,
Linux / clang-asanreported a stack-use-after-scope attests/test_async_registration.cpp:556, in "Bridge::assignHandlerPrimary: a promotion reply after the handler or the bridge is gone is a no-op". The test declared eachOutcomeinside the scope it was destroying, but attached ungatedthen/onErrorhandlers that capture it. Whether a late reply was delivered into the dead object depended on whether the pool's settle landed inside a 5 msrunForwindow. The same test passed ASan on #867's run. Neither ticket's code is involved, since the test passes noStopToken. Fixed in place, as AGENTS.md asks for tests: both outcomes now live at test scope. The ASan failure was not reproduced locally. The fix holds by construction: the outcomes outlive every pump.🤖 Generated with Claude Code