From d73f01ecff58ed73a82029b11197f0690b44f3cc Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 4 Oct 2026 16:32:59 +0200 Subject: [PATCH] docs: the cancellation policy once a cancel crosses the wire The policy table still said no cancel crosses the wire. Now a Task handler on a server that advertised `cancel` is asked to stop by SocketBackend/QtWebSocketBackend cancelPending and by the execute deadline (measured over a real loopback, net and Qt); the caller-token and co_await rows say the stop is relayed as a cancel over those backends (read, not measured for those verbs). The "No cancel crosses the wire" bullet goes, and backend.md's interim copy of the rows becomes a pointer to where they are measured. Co-Authored-By: Claude Opus 5.5 --- docs/spec/concurrency_and_lifetimes.md | 13 +++++-------- docs/spec/core/backend.md | 25 +++++-------------------- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/docs/spec/concurrency_and_lifetimes.md b/docs/spec/concurrency_and_lifetimes.md index 0ce457e23..290b42eff 100644 --- a/docs/spec/concurrency_and_lifetimes.md +++ b/docs/spec/concurrency_and_lifetimes.md @@ -403,15 +403,15 @@ the "work" column below is for a synchronous handler unless it says otherwise. | `LocalBackend::cancelPending` | **G2** | The error is posted to each pending call's `cbExec`; the action's later result reaches nobody | Not waited for: a synchronous action runs to its end. A Task handler is asked to stop | M | | `SimulatedRemoteBackend::cancelPending` | **G2** | As above | The server's handler runs to its end; nothing crosses the wire | M | | `SynchronousBackendAdapter::cancelPending` | **G2** for the binds and promotes it produced; **G0** on return for the wrapped backend's calls | Its own completions are rejected at once. The wrapped backend's `cancelPending` is queued on the adapter's control strand, behind whatever was queued before it | A bind still queued never reaches the wrapped backend; one already running is not recalled | M | -| `SocketBackend::cancelPending` | **G0** on return; **G2** once the I/O loop runs it | Posted to the loop, which settles both pending tables | The server keeps executing; its reply finds no pending entry | R | -| `QtWebSocketBackend::cancelPending` | **G2** | On the Qt thread: executes, control calls and never-sent registrations are settled; fire-and-forget deregisters are dropped, having no one to tell | The server keeps executing | R | +| `SocketBackend::cancelPending` | **G0** on return; **G2** once the I/O loop runs it | Posted to the loop, which settles both pending tables | A Task handler on a server that advertised `cancel` is asked to stop (measured, through `~Bridge`, over a real loopback); otherwise the server keeps executing and its reply finds no pending entry | R (G); M (work) | +| `QtWebSocketBackend::cancelPending` | **G2** | On the Qt thread: executes, control calls and never-sent registrations are settled; fire-and-forget deregisters are dropped, having no one to tell | A Task handler on a server that advertised `cancel` is asked to stop (measured, through `~Bridge`, against a real `QtWebSocketServer`); otherwise the server keeps executing | R (G); M (work) | | `~Bridge` | **G2** for its calls | Calls waiting for a bind and the backend's pending calls end with `BridgeDestroyedError`, posted | **G4** with a `LocalBackend`: the backend is destroyed with the bridge, and drains its strands, so the destructor returns only once the running action has ended. A Task handler is asked to stop first | M | | `Bridge::switchBackend` | **G2** for the outgoing backend's calls | `BackendChangedError`, posted | The outgoing backend is destroyed inside the switch: with a `LocalBackend`, the switch waits for its running actions | M | -| `Bridge::setExecuteDeadline` | **G0** for calls already made | Applies to calls made after it. When a call's deadline fires, the call ends as in G2, with `ClientTimeoutError` | A synchronous action runs to its end; a Task handler is asked to stop | M | -| A stop on `BridgeHandler::execute(action, stop)`'s token | **G2**, from any thread | `OperationCancelled` is posted to the call's `cbExec` by the thread that requested the stop; the call's own outcome, later, reaches nobody. A token already stopped rejects the call the same way without dispatching it | Asked to stop, not waited for: a Task handler on a `LocalBackend` sees the stop. A synchronous action runs to its end. **On a remote backend the call is only abandoned**: no `cancel` crosses the wire, so the server's handler runs to its end and its reply is dropped (measured with `SimulatedRemoteBackend`). The call stays counted as pending until that reply, as for a fired deadline | M | +| `Bridge::setExecuteDeadline` | **G0** for calls already made | Applies to calls made after it. When a call's deadline fires, the call ends as in G2, with `ClientTimeoutError` | A synchronous action runs to its end; a Task handler is asked to stop — over `SocketBackend` or `QtWebSocketBackend` too, through a `cancel`, when the server advertised it (measured over both) | M | +| A stop on `BridgeHandler::execute(action, stop)`'s token | **G2**, from any thread | `OperationCancelled` is posted to the call's `cbExec` by the thread that requested the stop; the call's own outcome, later, reaches nobody. A token already stopped rejects the call the same way without dispatching it | Asked to stop, not waited for: a Task handler on a `LocalBackend` sees the stop. A synchronous action runs to its end. **On a remote backend that sends no `cancel` the call is only abandoned**: the server's handler runs to its end and its reply is dropped (measured with `SimulatedRemoteBackend`). Over `SocketBackend` or `QtWebSocketBackend`, against a server that advertised `cancel`, the stop on the call's stop source is relayed as a `cancel` (read, not measured for this verb). The call stays counted as pending until that reply, as for a fired deadline | M | | `BridgeHandler::unsubscribe()` | **G1** | A delivery already posted still runs after it returns; no new one is scheduled. Pair it with a `CallbackScope` for G3 | n/a | M | | `CallbackScope::requestStop()`, `reset()`, `~CallbackScope` | **G3** on the delivery executor; **G1** from another thread | A gated callback posted but not yet run is refused when it runs. From another thread the check and the body are not atomic, so a callback that has passed its check still runs — advisory, and not measured as a race | Never waits for a callback in its body (measured). Stops every call a gated continuation is attached to | M | -| A stop on a coroutine suspended in `co_await completion` | **G2** for the await | The coroutine's resumption with `OperationCancelled` is posted to the executor it suspended on; the await's handlers are refused, so the call's own outcome, later, does not reach it | The call is asked to stop, through the awaiter's scope linked to its stop source: a Task handler on a `LocalBackend` sees the stop (measured). Not waited for. A synchronous action runs to its end; a remote server's handler runs on | M | +| A stop on a coroutine suspended in `co_await completion` | **G2** for the await | The coroutine's resumption with `OperationCancelled` is posted to the executor it suspended on; the await's handlers are refused, so the call's own outcome, later, does not reach it | The call is asked to stop, through the awaiter's scope linked to its stop source: a Task handler on a `LocalBackend` sees the stop (measured). Not waited for. A synchronous action runs to its end. A remote server's handler runs on, unless the backend relays the call's stop as a `cancel` (read, not measured for this verb) | M | | `TimeoutScheduler::cancel` | **G0** on return; **G3** for a callback not yet started, once the loop has run the cancel | The cancel is posted to the loop: on return the callback still holds its captures | Never waits for a callback already running | M | | `~TimeoutScheduler` | **G4** | Every pending callback is dropped unfired | Returns only once no callback of the scheduler is running | M | | `LimitPolicy::executeTimeout` (`RemoteServer`) | **G2** for the reply | `err "timeout"` is sent; the handler's own reply, later, is dropped — one reply per call | A synchronous handler runs to its end; a Task handler is asked to stop | M | @@ -424,9 +424,6 @@ the "work" column below is for a synchronous handler unless it says otherwise. ### What no verb does today -- **No cancel crosses the wire.** A remote call is settled locally only; the - server finishes the handler and its reply is dropped. There is no `cancel` - envelope. - **`Completion` has no cancel API of its own.** A call is cancelled through the token given to `execute(action, stop)`, a stopped scope a continuation is gated by, a stopped `co_await`, or the execute deadline. diff --git a/docs/spec/core/backend.md b/docs/spec/core/backend.md index 1641cd04d..ddeb1dd81 100644 --- a/docs/spec/core/backend.md +++ b/docs/spec/core/backend.md @@ -1627,30 +1627,15 @@ when it does (measured on both backends). `QtWebSocketBackend` sends nothing fro destructor's sweep — its socket is already aborted there — so a backend destroyed without its owner's `cancelPending` cancels nothing. -### Cancellation-policy rows for the remote backends +### Where the remote backends' cancel rows are measured The policy table in [concurrency_and_lifetimes.md](../concurrency_and_lifetimes.md#every-cancel-verb-measured) -marks the remote backends' rows read, not measured, and gives "the server -keeps executing" for the work they abandon. What those rows are, and which -parts are measured, is: - -- `SocketBackend::cancelPending` — **G0** on return; **G2** once the I/O loop - runs it (unchanged). Work: a Task handler on a server that advertised - `"cancel"` is asked to stop; otherwise the server keeps executing. - **Measured** over a real loopback (`SocketServer` with `SocketBackend`) for - the stop, through `~Bridge`. -- `QtWebSocketBackend::cancelPending` — **G2** (unchanged). Work: as above. - **Measured** in the Qt suite against a real `QtWebSocketServer`, through - `~Bridge`. -- `Bridge::setExecuteDeadline` over either backend — the deadline's stop - reaches the server's Task handler through a cancel. **Measured** for both - over a real loopback. - -The G-levels of these verbs are unchanged and are still read, not measured: -the tests above measure the work column only. The measuring tests are in +gives the remote backends' rows. Their *work* column — a Task handler on a +server that advertised `"cancel"` is asked to stop — is measured over a real +loopback, through `~Bridge` and through the execute deadline, in `tests/net/test_socket_backend.cpp` and `tests/qt/test_qt_websocket.cpp` -(`[cancel]`). +(`[cancel]`). Their G-levels are read, not measured. ## Lifetime & ownership