Skip to content

feat!: move quitter out of the library into an example - #481

Merged
sgerbino merged 2 commits into
cppalliance:developfrom
sgerbino:pr/quitter-example
Nov 3, 2026
Merged

sgerbino merged 2 commits into
cppalliance:developfrom
sgerbino:pr/quitter-example

Conversation

@sgerbino

@sgerbino sgerbino commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

This PR removes quitter from the library and keeps it as an example built only on capy's public coroutine protocol. It answers the design question in #431 (quitter's place in the library) with the "remove" option, while keeping the pattern available as example code.

Supersedes #478: with quitter out of the library, the library hooks that #478 adds for it are no longer needed.

Why

quitter needs special support from the library, and nothing else uses that support:

  • the stop exception (stop_requested_exception), which the default error handler catches, so cancellation is reported through exception control flow;
  • in feat!: stop a quitter by abandoning its frames #478, the private launch hooks, a separate stop cause in chain_owner, and stop handling in run and run_async.

The review questioned whether it belongs in the library, and no in-org library (corosio, http, beast2) uses it. Removing it shrinks the library. Keeping it as an example shows that the pattern can be built outside the library.

The example quitter (example/early-quitter)

A stop ends the quitter at the co_await it is suspended in, without resuming its body and without throwing:

  • The quitter that sees the stop walks up its parent quitters to the topmost one, stores canceled as that quitter's result, and resumes whatever awaited it (run_async, run, a task, or when_all). To that awaiter the quitter simply completed. Destroying the top quitter then destroys the suspended frames below it, so RAII runs at every level.
  • Operations resume a small gate frame instead of the body. With a stop pending, the gate takes the operation's result early: a canceled results or error_code ends the quitter, and any other result is delivered, with the stop landing at the next co_await.
  • Because the stop is a value, a quitter that is launched or awaited by a non-quitter must have T = results<...> or std::error_code. The quitter's own set_continuation and await_suspend enforce this with a static_assert, so the launchers need no changes.

Contents:

  • quitter.hpp: the type
  • shutdown.cpp: the former quitter-shutdown example, ported
  • main.cpp: a deep call chain (worker, item, fetch/store, wait)
  • checks.cpp: a self-checking program, registered with add_test so CI runs it, for these cases:
    • a stop at depth;
    • a sync success or cancel with a stop pending;
    • a stop pending at a co_await that never starts the operation;
    • an exception from the early await_resume;
    • a wake racing a stop, both deterministic and on a thread pool;
    • a task between quitters;
    • teardown through a closed executor;
    • when_all.

Locally, the checks pass under clang ASan/UBSan and TSan and under GCC -O2 -Werror. I also broke the header on purpose in three ways to confirm the checks catch each one.

Library changes

  • Removed quitter.hpp, detail/stop_requested_exception.hpp, test/unit/quitter.cpp, example/quitter-shutdown and doc/quitter.md.
  • default_handler now just rethrows a non-null exception_ptr, since there is no cancellation exception left to filter.
  • await_ready_then_suspend loses the completed out-parameter, which only quitter used.
  • Removed the references to quitter in the docs, the glossary, mrdocs.yml and the doc-lint baseline.

This is a breaking change: boost::capy::quitter no longer exists.

Performance

A ready co_await in a tight loop (clang -O3, pinned to one core, no function alignment, so expect about ±15% layout noise):

ns per await
task 0.22
example quitter 1.9
#478 quitter 3.0
develop quitter 14.5

Known gaps compared with #478

  • Cleanup runs after the handler: a stopped quitter is never resumed, so its locals are destroyed when its owner destroys the frame. Under run_async that is after the handler returns, whereas a task's locals are gone before its handler runs. Code that signals completion from the handler must wait for the executor's work (for example thread_pool::join) before touching state the quitter's destructors use. The shutdown demo does this.
  • Stop before start: with no launcher hook, the body runs up to its first co_await, which then stops. feat!: stop a quitter by abandoning its frames #478 stops before the body starts.
  • Reference results are not held. With a stop pending at completion, the stop wins and the result is dropped. Const move-only results are moved rather than held in place.
  • A stopped results<Ts...> needs default-constructible Ts.
  • No MSVC symmetric-transfer workaround in the example yet.
  • Not covered by the checks: run(ex) across executors, when_any, strand and mutex handoff.

CI

On the fork, every Linux and MSVC job passes. Of the macOS jobs, tsan passes. The other three macOS jobs never started on any attempt, because GitHub had no hosted macOS runner capacity, so they still need a run.

@cppalliance-bot

cppalliance-bot commented Nov 3, 2026 •

Copy link
Copy Markdown

An automated preview of the documentation is available at https://481.capy.prtest3.cppalliance.org/index.html

If more commits are pushed to the pull request, the docs will rebuild at the same URL.

2026-11-03 21:31:28 UTC

@cppalliance-bot

cppalliance-bot commented Nov 3, 2026 •

Copy link
Copy Markdown

GCOVR code coverage report https://481.capy.prtest3.cppalliance.org/gcovr/index.html
LCOV code coverage report https://481.capy.prtest3.cppalliance.org/genhtml/index.html
Coverage Diff Report https://481.capy.prtest3.cppalliance.org/diff-report/index.html

Build time: 2026-11-03 21:59:30 UTC

@codecov

codecov Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.46%. Comparing base (1ac98e3) to head (a0dea4e).
⚠️ Report is 2 commits behind head on develop.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop     #481      +/-   ##
===========================================
- Coverage    98.51%   98.46%   -0.05%     
===========================================
  Files          137      135       -2     
  Lines         7484     7250     -234     
===========================================
- Hits          7373     7139     -234     
  Misses         111      111              
Flag Coverage Δ
linux 98.46% <ø> (-0.05%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
include/boost/capy/detail/await_suspend_helper.hpp 100.00% <ø> (ø)
include/boost/capy/detail/run_callbacks.hpp 100.00% <100.00%> (ø)
include/boost/capy/ex/run_async.hpp 100.00% <ø> (ø)
include/boost/capy/task.hpp 100.00% <ø> (ø)

... and 5 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1ac98e3...a0dea4e. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The example quitter ends at the co_await it is suspended in when a
stop is requested, without resuming its body and without throwing.
The quitter that sees the stop stores canceled as the result of the
topmost quitter in its run and resumes whatever awaited it, which
then destroys the suspended frames from the top. Operations resume a
gate frame, which applies the operation-decides rule before the body
runs.

It needs no hooks in the library: a launched or task-awaited quitter
must have T = results<...> or std::error_code, enforced by the
quitter itself. Includes the shutdown and deep call chain demos and
a self-checking program for the stop, race and teardown cases.
quitter now lives in example/early-quitter, which needs nothing from
the library beyond the public coroutine protocol. Removing it also
removes stop_requested_exception, so the default error handler no
longer catches a cancellation exception, and await_ready_then_suspend
loses the out-parameter only quitter used.

The quitter-shutdown example moves to example/early-quitter.
@sgerbino
sgerbino merged commit bdadc6a into cppalliance:develop Nov 3, 2026
40 of 41 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Beast2 Nov 3, 2026
@sgerbino
sgerbino deleted the pr/quitter-example branch November 3, 2026 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants