Skip to content

Replace coroutine_handle with continuation in executor interface#239

Merged
mvandeberg merged 1 commit intocppalliance:developfrom
mvandeberg:pr/continuation
Mar 21, 2026
Merged

Replace coroutine_handle with continuation in executor interface#239
mvandeberg merged 1 commit intocppalliance:developfrom
mvandeberg:pr/continuation

Conversation

@mvandeberg
Copy link
Contributor

@mvandeberg mvandeberg commented Mar 20, 2026

The executor concept's dispatch() and post() now accept continuation& instead of std::coroutine_handle<>. A continuation wraps a coroutine handle with an intrusive next_ pointer, enabling executors to queue work without per-post heap allocation.

The thread pool's post() previously allocated a work node on every call (new work(h)). It now links the caller's continuation directly into an intrusive queue — zero allocation on the hot path.

The continuation lives in the I/O awaitable for caller-handle posting (delay, async_mutex, async_event, stream), and in combinator/trampoline state for parent-dispatch and child-launch patterns (when_all, when_any, timeout, run, run_async). The IoAwaitable concept and io_awaitable_promise_base are unchanged.

Breaking change: all Executor implementations must update dispatch() and post() signatures from coroutine_handle<> to continuation&.

Summary by CodeRabbit

  • New Features

    • Added a first-class continuation type for scheduling coroutine resumption.
  • Refactor

    • Executor and awaitable APIs, examples and docs migrated from raw coroutine-handle usage to continuation-based interfaces.
    • Trampolines, launchers and synchronization primitives updated to store and forward continuations.
  • Performance

    • Threaded executors use caller-owned continuation queues, reducing per-post allocations and improving scheduling efficiency.
  • Documentation

    • Updated examples and concept docs to describe continuation semantics and lifetime requirements.

@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

Warning

Rate limit exceeded

@mvandeberg has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 38 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a247a6b2-0dfd-4bd0-bb99-37233fef1515

📥 Commits

Reviewing files that changed from the base of the PR and between 500e87b and 81fb33d.

⛔ Files ignored due to path filters (19)
  • bench/bench.cpp is excluded by !**/bench/**
  • doc/continuation-rationale.md is excluded by !**/doc/**
  • doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc is excluded by !**/doc/**
  • doc/modules/ROOT/pages/7.examples/7n.custom-executor.adoc is excluded by !**/doc/**
  • doc/modules/ROOT/pages/8.design/8k.Executor.adoc is excluded by !**/doc/**
  • doc/unlisted/execution-executors.adoc is excluded by !**/doc/**
  • doc/unlisted/io-awaitables-executor.adoc is excluded by !**/doc/**
  • include/boost/capy/test/run_blocking.hpp is excluded by !**/test/**
  • include/boost/capy/test/stream.hpp is excluded by !**/test/**
  • src/test/run_blocking.cpp is excluded by !**/test/**
  • test/unit/ex/any_executor.cpp is excluded by !**/test/**
  • test/unit/ex/executor_ref.cpp is excluded by !**/test/**
  • test/unit/ex/frame_cb.cpp is excluded by !**/test/**
  • test/unit/ex/run_async.cpp is excluded by !**/test/**
  • test/unit/ex/strand.cpp is excluded by !**/test/**
  • test/unit/ex/thread_pool.cpp is excluded by !**/test/**
  • test/unit/ex/work_guard.cpp is excluded by !**/test/**
  • test/unit/task.cpp is excluded by !**/test/**
  • test/unit/test_helpers.hpp is excluded by !**/test/**
📒 Files selected for processing (21)
  • example/asio/api/capy_streams.cpp
  • example/asio/api/uni_stream.hpp
  • example/custom-executor/custom_executor.cpp
  • include/boost/capy/concept/executor.hpp
  • include/boost/capy/concept/io_awaitable.hpp
  • include/boost/capy/continuation.hpp
  • include/boost/capy/delay.hpp
  • include/boost/capy/ex/any_executor.hpp
  • include/boost/capy/ex/async_event.hpp
  • include/boost/capy/ex/async_mutex.hpp
  • include/boost/capy/ex/executor_ref.hpp
  • include/boost/capy/ex/io_env.hpp
  • include/boost/capy/ex/run.hpp
  • include/boost/capy/ex/run_async.hpp
  • include/boost/capy/ex/strand.hpp
  • include/boost/capy/ex/thread_pool.hpp
  • include/boost/capy/timeout.hpp
  • include/boost/capy/when_all.hpp
  • include/boost/capy/when_any.hpp
  • src/ex/detail/strand_service.cpp
  • src/ex/thread_pool.cpp
📝 Walkthrough

Walkthrough

Migrates executor scheduling and awaitable storage from raw std::coroutine_handle<> to a new boost::capy::continuation type; updates concepts, type-erasure, executors, combinators, awaitables, runtime services, and examples to accept/produce continuation& and to enqueue caller-owned continuations.

Changes

Cohort / File(s) Summary
New Type
include/boost/capy/continuation.hpp
Adds boost::capy::continuation with std::coroutine_handle<> h and intrusive continuation* next for stable, queueable continuations.
Concepts & Type-Erasure
include/boost/capy/concept/executor.hpp, include/boost/capy/ex/executor_ref.hpp, include/boost/capy/ex/any_executor.hpp
Executor concept, vtable signatures, and public APIs changed from std::coroutine_handle<> to continuation& for dispatch/post; adaptors and docs updated.
Executors & Examples
include/boost/capy/ex/thread_pool.hpp, include/boost/capy/ex/strand.hpp, example/asio/api/capy_streams.cpp, example/asio/api/uni_stream.hpp, example/custom-executor/custom_executor.cpp
Executor implementations and examples now accept continuation&, extract/forward .h, and preserve symmetric-transfer semantics (immediate vs queued).
Awaitables, Timers & Primitives
include/boost/capy/ex/async_event.hpp, include/boost/capy/ex/async_mutex.hpp, include/boost/capy/delay.hpp, include/boost/capy/ex/io_env.hpp, include/boost/capy/timeout.hpp
Awaiters store continuation members; await_suspend assigns .h; timer/stop callbacks and cancellation post continuations instead of raw handles.
Combinators & Run/Async
include/boost/capy/when_all.hpp, include/boost/capy/when_any.hpp, include/boost/capy/ex/run.hpp, include/boost/capy/ex/run_async.hpp
Parent/runner storage migrated to continuation; launcher await_suspend and runner final_suspend write into .h and post continuation objects.
Runtime Impl (strand/thread-pool)
src/ex/strand_service.cpp, src/ex/thread_pool.cpp
Strand invokers expose stable self_ continuation; thread_pool now links caller-owned continuation via next and workers resume stored .h.
Documentation Examples
include/boost/capy/concept/io_awaitable.hpp
Examples updated to construct/assign/post continuation and note stable-address lifetime requirement.

Sequence Diagram(s)

sequenceDiagram
  participant Coroutine as Coroutine (suspended)
  participant Continuation as continuation (object)
  participant Executor as Executor/strand/thread_pool
  participant Worker as Worker Thread

  Coroutine->>Continuation: construct continuation{h}
  Continuation->>Executor: post(continuation&)
  Executor->>Executor: enqueue continuation (intrusive next)
  Executor->>Worker: worker dequeues continuation*
  Worker->>Worker: c->h.resume()
  Worker-->>Executor: done
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

I tuck a handle in my paw so light, 🐇
A tiny next pointer for queueing right.
Executors whisper, workers spring,
Continuations hop — resume the thing.
Hooray, we wake and dance tonight! 🎉

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main architectural change: replacing coroutine_handle with continuation throughout the executor interface, which is the central theme across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cppalliance-bot
Copy link

cppalliance-bot commented Mar 20, 2026

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

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

2026-03-21 03:16:43 UTC

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
include/boost/capy/concept/io_awaitable.hpp (1)

82-99: ⚠️ Potential issue | 🟠 Major

Don't teach copying continuation into async callbacks.

The updated example still says "pass members by value" and forwards cont_ to start_async(). That is unsafe with the intrusive executor API: if the backend captures or forwards a copied continuation, executor.post() can enqueue a node whose storage dies as soon as the callback returns. Keep the continuation in awaiter-owned storage and pass a pointer/reference to that member into the async backend instead.

✏️ Suggested documentation fix
-            cont_ = continuation{h};
-            // Pass members by value; capturing this
-            // risks use-after-free in async callbacks.
+            cont_.h = h;
+            // Copy the stop token / executor, but keep the
+            // continuation in awaiter-owned storage.
             // When the async operation completes, resume
             // via executor.post(cont_) or executor.dispatch(cont_)
             // rather than calling h.resume() directly.
             start_async(
                 env_->stop_token,
                 env_->executor,
-                cont_ );
+                &cont_ );

The completion callback can then resume via ex.post(*cont) / ex.dispatch(*cont).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/boost/capy/concept/io_awaitable.hpp` around lines 82 - 99, The
await_suspend currently copies continuation into cont_ and forwards that copy
into start_async; instead keep the continuation in awaiter-owned storage (cont_
member) and pass a pointer or reference to that member into the async backend
(i.e. call start_async with &cont_ or std::addressof(cont_) / cont_& reference)
so the backend enqueues/uses the awaiter's storage rather than making its own
lifetime-bound copy; update start_async/io backend signatures and any callbacks
to accept and use a continuation* / continuation& and ensure callbacks do not
capture 'this' or the copied continuation by value.
example/asio/api/capy_streams.cpp (2)

112-119: ⚠️ Potential issue | 🔴 Critical

Type mismatch: ex.post(h) passes std::coroutine_handle<> but post now expects continuation&.

The completion handler captures a raw std::coroutine_handle<> (h) and passes it to ex.post(h), but the executor API now requires continuation&. This will fail to compile.

Store a continuation member in the awaitable and post that instead:

🐛 Proposed fix
     template<capy::MutableBufferSequence MB>
     class read_awaitable
     {
         asio_socket* self_;
         MB buffers_;
         capy::io_result<std::size_t> result_;
         std::shared_ptr<cancel_state> cancel_;
+        capy::continuation cont_;

     public:
         // ... constructor ...

         std::coroutine_handle<>
         await_suspend(
             std::coroutine_handle<> h,
             capy::io_env const* env)
         {
             cancel_ = std::make_shared<cancel_state>(env->stop_token);
+            cont_.h = h;

             self_->socket_.async_read_some(
                 capy::to_asio(capy::mutable_buffer_array<8>(buffers_)),
                 net::bind_cancellation_slot(
                     cancel_->signal.slot(),
-                    [this, h, ex = env->executor](
+                    [this, ex = env->executor](
                         boost::system::error_code ec,
                         std::size_t n) mutable
                     {
                         result_.ec = ec;
                         std::get<0>(result_.values) = n;
-                        ex.post(h);
+                        ex.post(cont_);
                     }));

             return std::noop_coroutine();
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/asio/api/capy_streams.cpp` around lines 112 - 119, The completion
handler currently captures a raw std::coroutine_handle<> named h and calls
ex.post(h), but the executor API requires a continuation&, so modify the
awaitable to store a continuation member (e.g., continuation cont_) and
capture/post that instead of h; update the lambda capture list from h to cont_
(and adjust any places where h is set to instead construct or assign cont_ from
the coroutine handle using continuation::from_handle or equivalent), then call
ex.post(cont_) inside the handler so result_.ec and result_.values assignment
remain unchanged while matching the new executor signature.

174-181: ⚠️ Potential issue | 🔴 Critical

Same type mismatch in write_awaitable.

Apply the same fix as read_awaitable — store a continuation member and post it from the completion handler instead of the raw handle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/asio/api/capy_streams.cpp` around lines 174 - 181, The completion
handler for write_awaitable currently captures and posts the raw handle (h)
causing a type mismatch; mirror the read_awaitable fix by storing the
awaitable's continuation in a member (e.g., continuation_) when the awaitable is
created and capture that continuation in the lambda instead of h, assign
result_.ec and std::get<0>(result_.values) = n as before, and call
env->executor.post(continuation_) (or continuation_.post via the stored
continuation) from the completion handler so the correct continuation type is
posted.
🧹 Nitpick comments (2)
include/boost/capy/ex/async_mutex.hpp (1)

28-60: Refresh the implementation note after the h_cont_ rename.

The overview still says the stop callback posts h_ and that the member-ordering constraint depends on h_, but the code now posts cont_. Keeping that rationale stale makes the cancellation/lifetime guarantees harder to audit.

📝 Minimal doc update
-    The stop callback calls ex_.post(h_). The stop_callback is
+    The stop callback calls ex_.post(cont_). The stop_callback is
@@
-    the callback accesses (h_, ex_, claimed_, canceled_). If the
+    the callback accesses (cont_, ex_, claimed_, canceled_). If the
As per coding guidelines, "Files containing non-trivial implementation logic should include a `/* */` block comment after the includes that provides a high-level overview of how the implementation works" and you should warn if the overview appears stale relative to the code.

Also applies to: 167-188

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/boost/capy/ex/async_mutex.hpp` around lines 28 - 60, Update the
top-of-file implementation note in async_mutex to reflect the rename h_ → cont_:
change any wording that says the stop callback posts h_ to say it posts cont_,
and update the member-ordering constraint to reference cont_ (and the union
stop_cb_) instead of h_; ensure examples and explanations that mention h_,
stop_callback, stop_cb_, lock_awaiter, claimed_, canceled_, await_suspend,
await_resume, unlock(), and async_mutex::waiters_ are consistent with the
current code paths (stop callback posts cont_, await_suspend constructs
stop_cb_, await_resume/destructor destroy it, unlock() skips claimed_ waiters
and pops from waiters_), and add a brief note reminding maintainers to update
this overview when renaming members to avoid stale lifetime/cancellation
rationale.
include/boost/capy/concept/executor.hpp (1)

143-154: Concept uses continuation by value, but implementations take continuation&.

The requires expression declares continuation c (by value), but the documented signatures and all implementations in this PR use continuation& (by reference). When checking ce.dispatch(c) and ce.post(c), passing a value c to a function expecting continuation& will still bind correctly (lvalue binds to lvalue reference), so the constraint works. However, for documentation consistency and clarity, consider using continuation& c in the requires clause to match the actual API.

✏️ Optional: align requires clause with implementation signatures
 template<class E>
 concept Executor =
     std::is_nothrow_copy_constructible_v<E> &&
     std::is_nothrow_move_constructible_v<E> &&
-    requires(E& e, E const& ce, E const& ce2, continuation c) {
+    requires(E& e, E const& ce, E const& ce2, continuation& c) {
         { ce == ce2 } noexcept -> std::convertible_to<bool>;
         { ce.context() } noexcept;
         requires std::is_lvalue_reference_v<decltype(ce.context())> &&
             std::derived_from<
                 std::remove_reference_t<decltype(ce.context())>,
                 execution_context>;
         { ce.on_work_started() } noexcept;
         { ce.on_work_finished() } noexcept;

         { ce.dispatch(c) } -> std::same_as<std::coroutine_handle<>>;
         { ce.post(c) };
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/boost/capy/concept/executor.hpp` around lines 143 - 154, The
requires-clause in the executor concept declares a local parameter as
"continuation c" but actual APIs and implementations use "continuation&"; change
the parameter declaration to "continuation& c" in the requires expression so the
concept matches the documented/implemented signatures (update the occurrences
that check ce.dispatch(c) and ce.post(c) accordingly), ensuring the checks still
reference ce.dispatch and ce.post and the continuation type symbol remains
"continuation" by reference.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@example/asio/api/uni_stream.hpp`:
- Around line 58-68: The methods dispatch and post in the asio_executor_wrapper
use an unqualified type name continuation that won't resolve; update their
parameter types to use the fully-qualified capy::continuation& (and any other
occurrences in asio_executor_wrapper) so the compiler can find the type (e.g.,
change dispatch(continuation& c) and post(continuation& c) signatures to
dispatch(capy::continuation& c) and post(capy::continuation& c) and adjust any
matching declarations/uses accordingly).

In `@include/boost/capy/continuation.hpp`:
- Around line 20-35: Replace the plain block comment above struct continuation
with a proper public-header docstring describing the type: explain that
continuation is an executor-facing schedulable unit that wraps a
std::coroutine_handle<> (h), that the intrusive pointer continuation* next_ is
owned/managed by executors as linkage (not by users), and state the
ownership/aliasing rules — a continuation must have a stable address while
enqueued and must not be enqueued more than once concurrently; also document
copy/move semantics (non-copyable or delegete default as appropriate) and any
thread-safety guarantees or expectations (e.g., callers/executors must
synchronize access). Include these points succinctly in the docstring so tooling
and users see purpose, ownership, copyability/movability, and thread-safety for
continuation, h, and next_.

---

Outside diff comments:
In `@example/asio/api/capy_streams.cpp`:
- Around line 112-119: The completion handler currently captures a raw
std::coroutine_handle<> named h and calls ex.post(h), but the executor API
requires a continuation&, so modify the awaitable to store a continuation member
(e.g., continuation cont_) and capture/post that instead of h; update the lambda
capture list from h to cont_ (and adjust any places where h is set to instead
construct or assign cont_ from the coroutine handle using
continuation::from_handle or equivalent), then call ex.post(cont_) inside the
handler so result_.ec and result_.values assignment remain unchanged while
matching the new executor signature.
- Around line 174-181: The completion handler for write_awaitable currently
captures and posts the raw handle (h) causing a type mismatch; mirror the
read_awaitable fix by storing the awaitable's continuation in a member (e.g.,
continuation_) when the awaitable is created and capture that continuation in
the lambda instead of h, assign result_.ec and std::get<0>(result_.values) = n
as before, and call env->executor.post(continuation_) (or continuation_.post via
the stored continuation) from the completion handler so the correct continuation
type is posted.

In `@include/boost/capy/concept/io_awaitable.hpp`:
- Around line 82-99: The await_suspend currently copies continuation into cont_
and forwards that copy into start_async; instead keep the continuation in
awaiter-owned storage (cont_ member) and pass a pointer or reference to that
member into the async backend (i.e. call start_async with &cont_ or
std::addressof(cont_) / cont_& reference) so the backend enqueues/uses the
awaiter's storage rather than making its own lifetime-bound copy; update
start_async/io backend signatures and any callbacks to accept and use a
continuation* / continuation& and ensure callbacks do not capture 'this' or the
copied continuation by value.

---

Nitpick comments:
In `@include/boost/capy/concept/executor.hpp`:
- Around line 143-154: The requires-clause in the executor concept declares a
local parameter as "continuation c" but actual APIs and implementations use
"continuation&"; change the parameter declaration to "continuation& c" in the
requires expression so the concept matches the documented/implemented signatures
(update the occurrences that check ce.dispatch(c) and ce.post(c) accordingly),
ensuring the checks still reference ce.dispatch and ce.post and the continuation
type symbol remains "continuation" by reference.

In `@include/boost/capy/ex/async_mutex.hpp`:
- Around line 28-60: Update the top-of-file implementation note in async_mutex
to reflect the rename h_ → cont_: change any wording that says the stop callback
posts h_ to say it posts cont_, and update the member-ordering constraint to
reference cont_ (and the union stop_cb_) instead of h_; ensure examples and
explanations that mention h_, stop_callback, stop_cb_, lock_awaiter, claimed_,
canceled_, await_suspend, await_resume, unlock(), and async_mutex::waiters_ are
consistent with the current code paths (stop callback posts cont_, await_suspend
constructs stop_cb_, await_resume/destructor destroy it, unlock() skips claimed_
waiters and pops from waiters_), and add a brief note reminding maintainers to
update this overview when renaming members to avoid stale lifetime/cancellation
rationale.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bac6df14-e21b-4f00-9dd6-2bae70f1980b

📥 Commits

Reviewing files that changed from the base of the PR and between fcd2854 and b2e97e7.

⛔ Files ignored due to path filters (18)
  • bench/bench.cpp is excluded by !**/bench/**
  • doc/continuation-rationale.md is excluded by !**/doc/**
  • doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc is excluded by !**/doc/**
  • doc/modules/ROOT/pages/7.examples/7n.custom-executor.adoc is excluded by !**/doc/**
  • doc/modules/ROOT/pages/8.design/8k.Executor.adoc is excluded by !**/doc/**
  • doc/unlisted/execution-executors.adoc is excluded by !**/doc/**
  • doc/unlisted/io-awaitables-executor.adoc is excluded by !**/doc/**
  • include/boost/capy/test/run_blocking.hpp is excluded by !**/test/**
  • include/boost/capy/test/stream.hpp is excluded by !**/test/**
  • src/test/run_blocking.cpp is excluded by !**/test/**
  • test/unit/ex/any_executor.cpp is excluded by !**/test/**
  • test/unit/ex/executor_ref.cpp is excluded by !**/test/**
  • test/unit/ex/run_async.cpp is excluded by !**/test/**
  • test/unit/ex/strand.cpp is excluded by !**/test/**
  • test/unit/ex/thread_pool.cpp is excluded by !**/test/**
  • test/unit/ex/work_guard.cpp is excluded by !**/test/**
  • test/unit/task.cpp is excluded by !**/test/**
  • test/unit/test_helpers.hpp is excluded by !**/test/**
📒 Files selected for processing (21)
  • example/asio/api/capy_streams.cpp
  • example/asio/api/uni_stream.hpp
  • example/custom-executor/custom_executor.cpp
  • include/boost/capy/concept/executor.hpp
  • include/boost/capy/concept/io_awaitable.hpp
  • include/boost/capy/continuation.hpp
  • include/boost/capy/delay.hpp
  • include/boost/capy/ex/any_executor.hpp
  • include/boost/capy/ex/async_event.hpp
  • include/boost/capy/ex/async_mutex.hpp
  • include/boost/capy/ex/executor_ref.hpp
  • include/boost/capy/ex/io_env.hpp
  • include/boost/capy/ex/run.hpp
  • include/boost/capy/ex/run_async.hpp
  • include/boost/capy/ex/strand.hpp
  • include/boost/capy/ex/thread_pool.hpp
  • include/boost/capy/timeout.hpp
  • include/boost/capy/when_all.hpp
  • include/boost/capy/when_any.hpp
  • src/ex/detail/strand_service.cpp
  • src/ex/thread_pool.cpp

@cppalliance-bot
Copy link

cppalliance-bot commented Mar 20, 2026

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

Build time: 2026-03-21 03:09:18 UTC

@mvandeberg mvandeberg force-pushed the pr/continuation branch 2 times, most recently from 9dd6429 to 834a2eb Compare March 20, 2026 23:42
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
include/boost/capy/timeout.hpp (1)

38-38: Make timeout.hpp self-contained by adding direct includes.

timeout_state directly uses std::array<continuation, 2> on line 38, but this header gets both symbols transitively from when_all.hpp. Adding direct includes for <array> and <boost/capy/continuation.hpp> will make the public header less brittle to future include reorganizations.

♻️ Suggested include additions
+#include <array>
 `#include` <boost/capy/detail/config.hpp>
 `#include` <boost/capy/concept/io_awaitable.hpp>
 `#include` <boost/capy/delay.hpp>
 `#include` <boost/capy/detail/io_result_combinators.hpp>
+#include <boost/capy/continuation.hpp>
 `#include` <boost/capy/error.hpp>
 `#include` <boost/capy/io_result.hpp>
 `#include` <boost/capy/task.hpp>
 `#include` <boost/capy/when_all.hpp>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/boost/capy/timeout.hpp` at line 38, timeout_state uses
std::array<continuation, 2> (member runner_handles_) but relies on transitive
includes; make the header self-contained by adding direct includes for <array>
and <boost/capy/continuation.hpp> at the top of timeout.hpp so continuation and
std::array are declared where timeout_state and runner_handles_ are defined.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@include/boost/capy/concept/executor.hpp`:
- Around line 48-49: Add a resource-lifetime paragraph to the executor concept
docs clarifying the continuation lifetime: state that ce.dispatch(continuation)
and ce.post(continuation) operate on the supplied continuation object itself and
that the caller must ensure the continuation remains alive until the dispatcher
resumes it; explicitly document that dispatch returns std::coroutine_handle<>
referring to the continuation and that destroying or moving the continuation
before resume is undefined behavior. Include this text near the existing
descriptions of ce.dispatch and ce.post (and in the `@par` Buffer Lifetime /
Resource Lifetime section referenced for the concept), and mirror the same
wording for the other occurrences around the comments for lines referencing
dispatch/post/continuation so implementers see a consistent public contract.

---

Nitpick comments:
In `@include/boost/capy/timeout.hpp`:
- Line 38: timeout_state uses std::array<continuation, 2> (member
runner_handles_) but relies on transitive includes; make the header
self-contained by adding direct includes for <array> and
<boost/capy/continuation.hpp> at the top of timeout.hpp so continuation and
std::array are declared where timeout_state and runner_handles_ are defined.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cc7a63cd-31c2-43f5-9d14-721661b68b94

📥 Commits

Reviewing files that changed from the base of the PR and between b2e97e7 and 834a2eb.

⛔ Files ignored due to path filters (19)
  • bench/bench.cpp is excluded by !**/bench/**
  • doc/continuation-rationale.md is excluded by !**/doc/**
  • doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc is excluded by !**/doc/**
  • doc/modules/ROOT/pages/7.examples/7n.custom-executor.adoc is excluded by !**/doc/**
  • doc/modules/ROOT/pages/8.design/8k.Executor.adoc is excluded by !**/doc/**
  • doc/unlisted/execution-executors.adoc is excluded by !**/doc/**
  • doc/unlisted/io-awaitables-executor.adoc is excluded by !**/doc/**
  • include/boost/capy/test/run_blocking.hpp is excluded by !**/test/**
  • include/boost/capy/test/stream.hpp is excluded by !**/test/**
  • src/test/run_blocking.cpp is excluded by !**/test/**
  • test/unit/ex/any_executor.cpp is excluded by !**/test/**
  • test/unit/ex/executor_ref.cpp is excluded by !**/test/**
  • test/unit/ex/frame_cb.cpp is excluded by !**/test/**
  • test/unit/ex/run_async.cpp is excluded by !**/test/**
  • test/unit/ex/strand.cpp is excluded by !**/test/**
  • test/unit/ex/thread_pool.cpp is excluded by !**/test/**
  • test/unit/ex/work_guard.cpp is excluded by !**/test/**
  • test/unit/task.cpp is excluded by !**/test/**
  • test/unit/test_helpers.hpp is excluded by !**/test/**
📒 Files selected for processing (21)
  • example/asio/api/capy_streams.cpp
  • example/asio/api/uni_stream.hpp
  • example/custom-executor/custom_executor.cpp
  • include/boost/capy/concept/executor.hpp
  • include/boost/capy/concept/io_awaitable.hpp
  • include/boost/capy/continuation.hpp
  • include/boost/capy/delay.hpp
  • include/boost/capy/ex/any_executor.hpp
  • include/boost/capy/ex/async_event.hpp
  • include/boost/capy/ex/async_mutex.hpp
  • include/boost/capy/ex/executor_ref.hpp
  • include/boost/capy/ex/io_env.hpp
  • include/boost/capy/ex/run.hpp
  • include/boost/capy/ex/run_async.hpp
  • include/boost/capy/ex/strand.hpp
  • include/boost/capy/ex/thread_pool.hpp
  • include/boost/capy/timeout.hpp
  • include/boost/capy/when_all.hpp
  • include/boost/capy/when_any.hpp
  • src/ex/detail/strand_service.cpp
  • src/ex/thread_pool.cpp
✅ Files skipped from review due to trivial changes (2)
  • include/boost/capy/ex/async_mutex.hpp
  • include/boost/capy/continuation.hpp
🚧 Files skipped from review as they are similar to previous changes (12)
  • example/asio/api/uni_stream.hpp
  • include/boost/capy/delay.hpp
  • example/custom-executor/custom_executor.cpp
  • include/boost/capy/ex/strand.hpp
  • include/boost/capy/concept/io_awaitable.hpp
  • include/boost/capy/ex/thread_pool.hpp
  • include/boost/capy/ex/any_executor.hpp
  • src/ex/thread_pool.cpp
  • example/asio/api/capy_streams.cpp
  • include/boost/capy/ex/executor_ref.hpp
  • src/ex/detail/strand_service.cpp
  • include/boost/capy/ex/io_env.hpp

@mvandeberg mvandeberg force-pushed the pr/continuation branch 3 times, most recently from 89da966 to 500e87b Compare March 21, 2026 02:26
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
include/boost/capy/when_all.hpp (1)

514-517: Phase 2 iteration count is correct, but consider caching for clarity.

The logic correctly reads remaining_count_ before any runners have completed, so it equals the original count. However, reading from an atomic here when the value is guaranteed stable (no concurrent decrements yet) may be confusing to future readers.

Consider caching the count locally in Phase 1 or using a named constant to make the invariant more explicit.

♻️ Optional: Cache count locally
         // Phase 1: Create all runners without dispatching.
         std::size_t index = 0;
         for(auto&& a : *range_)
         {
             // ... existing code ...
             ++index;
         }

         // Phase 2: Post all runners. Any may complete synchronously.
         // After last post, state_ and this may be destroyed.
         auto* handles = state_->runner_handles_.get();
-        std::size_t count = state_->core_.remaining_count_.load(std::memory_order_relaxed);
+        std::size_t count = index;  // equals original count from Phase 1
         for(std::size_t i = 0; i < count; ++i)
             caller_env->executor.post(handles[i]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/boost/capy/when_all.hpp` around lines 514 - 517, Readability: cache
the phase-2 iteration count instead of re-reading the atomic remaining_count_
when dispatching runners. Capture the stable count into a local (e.g., auto
phase2_count = state_->core_.remaining_count_.load(...); or better, store the
original count into a named field like state_->core_.initial_count_ during Phase
1) and then loop using that local (for (std::size_t i = 0; i < phase2_count;
++i) caller_env->executor.post(handles[i]);) so the code no longer reads the
atomic at dispatch and the invariant is explicit; update uses of
remaining_count_ accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@example/asio/api/capy_streams.cpp`:
- Around line 245-249: The example no longer compiles because executor_ref::post
now requires a continuation&, but the read/write callbacks capture and call the
old executor posting pattern; fix both awaitables (read_awaitable and
write_awaitable) by giving each awaitable a stable member of type continuation
(e.g., continuation cont_;), assign cont_ = c inside await_suspend, and update
the completion lambdas to call ex_.post(cont_) (or call net::post(ex_, [this]{
cont_.resume(); }) if using net::post) instead of posting the raw coroutine
handle; ensure the member is used for both read_awaitable and write_awaitable so
the callbacks use the continuation API.
- Around line 238-242: dispatch() is currently always queuing the continuation
via net::post and returning std::noop_coroutine(), which breaks symmetric
transfer because run_awaitable_ex::await_suspend() and the trampoline
final_suspend() expect dispatch() to return the child's handle for inline
transfer when not deferred; change dispatch(continuation& c) to only call
net::post(ex_, ...) and return std::noop_coroutine() when the continuation must
be deferred, otherwise return c.h (preserving the inline path so run() on
asio_executor can transfer directly to the child/parent coroutine); update
dispatch() logic to detect whether execution can be performed inline and choose
between returning c.h or scheduling with net::post accordingly.

---

Nitpick comments:
In `@include/boost/capy/when_all.hpp`:
- Around line 514-517: Readability: cache the phase-2 iteration count instead of
re-reading the atomic remaining_count_ when dispatching runners. Capture the
stable count into a local (e.g., auto phase2_count =
state_->core_.remaining_count_.load(...); or better, store the original count
into a named field like state_->core_.initial_count_ during Phase 1) and then
loop using that local (for (std::size_t i = 0; i < phase2_count; ++i)
caller_env->executor.post(handles[i]);) so the code no longer reads the atomic
at dispatch and the invariant is explicit; update uses of remaining_count_
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ba4bd244-cb1c-462c-a281-dbe0dfbabf3d

📥 Commits

Reviewing files that changed from the base of the PR and between 834a2eb and 89da966.

⛔ Files ignored due to path filters (19)
  • bench/bench.cpp is excluded by !**/bench/**
  • doc/continuation-rationale.md is excluded by !**/doc/**
  • doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc is excluded by !**/doc/**
  • doc/modules/ROOT/pages/7.examples/7n.custom-executor.adoc is excluded by !**/doc/**
  • doc/modules/ROOT/pages/8.design/8k.Executor.adoc is excluded by !**/doc/**
  • doc/unlisted/execution-executors.adoc is excluded by !**/doc/**
  • doc/unlisted/io-awaitables-executor.adoc is excluded by !**/doc/**
  • include/boost/capy/test/run_blocking.hpp is excluded by !**/test/**
  • include/boost/capy/test/stream.hpp is excluded by !**/test/**
  • src/test/run_blocking.cpp is excluded by !**/test/**
  • test/unit/ex/any_executor.cpp is excluded by !**/test/**
  • test/unit/ex/executor_ref.cpp is excluded by !**/test/**
  • test/unit/ex/frame_cb.cpp is excluded by !**/test/**
  • test/unit/ex/run_async.cpp is excluded by !**/test/**
  • test/unit/ex/strand.cpp is excluded by !**/test/**
  • test/unit/ex/thread_pool.cpp is excluded by !**/test/**
  • test/unit/ex/work_guard.cpp is excluded by !**/test/**
  • test/unit/task.cpp is excluded by !**/test/**
  • test/unit/test_helpers.hpp is excluded by !**/test/**
📒 Files selected for processing (21)
  • example/asio/api/capy_streams.cpp
  • example/asio/api/uni_stream.hpp
  • example/custom-executor/custom_executor.cpp
  • include/boost/capy/concept/executor.hpp
  • include/boost/capy/concept/io_awaitable.hpp
  • include/boost/capy/continuation.hpp
  • include/boost/capy/delay.hpp
  • include/boost/capy/ex/any_executor.hpp
  • include/boost/capy/ex/async_event.hpp
  • include/boost/capy/ex/async_mutex.hpp
  • include/boost/capy/ex/executor_ref.hpp
  • include/boost/capy/ex/io_env.hpp
  • include/boost/capy/ex/run.hpp
  • include/boost/capy/ex/run_async.hpp
  • include/boost/capy/ex/strand.hpp
  • include/boost/capy/ex/thread_pool.hpp
  • include/boost/capy/timeout.hpp
  • include/boost/capy/when_all.hpp
  • include/boost/capy/when_any.hpp
  • src/ex/detail/strand_service.cpp
  • src/ex/thread_pool.cpp
✅ Files skipped from review due to trivial changes (3)
  • include/boost/capy/continuation.hpp
  • example/asio/api/uni_stream.hpp
  • include/boost/capy/timeout.hpp
🚧 Files skipped from review as they are similar to previous changes (8)
  • include/boost/capy/ex/async_event.hpp
  • include/boost/capy/ex/run_async.hpp
  • include/boost/capy/ex/thread_pool.hpp
  • include/boost/capy/ex/strand.hpp
  • include/boost/capy/ex/any_executor.hpp
  • example/custom-executor/custom_executor.cpp
  • include/boost/capy/ex/async_mutex.hpp
  • src/ex/detail/strand_service.cpp

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
include/boost/capy/ex/run_async.hpp (1)

97-98: Minor: task_h_ and task_cont_.h store the same handle.

Both task_h_ (line 97/205) and task_cont_.h (set at line 409) hold the same coroutine handle. task_h_ remains necessary because frame_guard in make_trampoline (line 293) references it. This dual storage is intentional but creates a subtle coupling—if one were updated without the other, behavior would be inconsistent.

Consider adding a brief comment near task_h_ clarifying its continued purpose (for frame_guard) vs. task_cont_ (for executor dispatch).

Also applies to: 205-206

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/boost/capy/ex/run_async.hpp` around lines 97 - 98, Add a short
clarifying comment near the members task_h_ and task_cont_ explaining why both
store the same coroutine handle: state that task_h_ is retained specifically for
frame_guard usage inside make_trampoline while task_cont_.h is used for
executor/continuation dispatch, so both must be kept and updated together;
reference the member names task_h_, task_cont_, the frame_guard and the
make_trampoline function in the comment so future maintainers understand the
coupling and do not remove or update one without the other.
include/boost/capy/ex/async_event.hpp (1)

175-178: Consider resetting moved-from continuation state defensively.

cont_(o.cont_) copies h and next_ as-is. Clearing o.cont_ during move makes future refactors safer if move timing ever changes.

Suggested defensive tweak
         wait_awaiter(wait_awaiter&& o) noexcept
             : e_(o.e_)
-            , cont_(o.cont_)
+            , cont_(std::exchange(o.cont_, {}))
             , ex_(o.ex_)
             , claimed_(o.claimed_.load(
                 std::memory_order_relaxed))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/boost/capy/ex/async_event.hpp` around lines 175 - 178, The move
constructor wait_awaiter(wait_awaiter&& o) noexcept currently copies cont_ from
o via cont_(o.cont_) but leaves the moved-from object's continuation intact;
update the move ctor to defensively clear the source continuation (e.g., reset
o.cont_ or set its handle/next_ to null) after transferring so the moved-from
wait_awaiter is left in a safe empty state; locate the wait_awaiter move
constructor and modify the handling of cont_ and o.cont_ accordingly to transfer
ownership and clear the source.
src/ex/detail/strand_service.cpp (1)

243-248: Consider extracting the invoker-posting logic to reduce duplication.

The if blocks in dispatch (lines 243-248) and post (lines 257-262) are identical. A small helper could consolidate this:

♻️ Optional: Extract helper to reduce duplication
+    static void
+    post_invoker(strand_impl& impl, executor_ref ex)
+    {
+        auto invoker = strand_service_impl::make_invoker(impl);
+        auto& self = invoker.h_.promise().self_;
+        self.h = invoker.h_;
+        ex.post(self);
+    }
+
     friend class strand_service;
 };

Then in dispatch and post:

     if(strand_service_impl::enqueue(impl, h))
-    {
-        auto invoker = strand_service_impl::make_invoker(impl);
-        auto& self = invoker.h_.promise().self_;
-        self.h = invoker.h_;
-        ex.post(self);
-    }
+        strand_service_impl::post_invoker(impl, ex);

Also applies to: 257-262

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ex/detail/strand_service.cpp` around lines 243 - 248, Extract the
duplicated invoker-posting logic into a small helper function (e.g.,
make_and_post_invoker or post_invoker) so both dispatch and post call it; the
helper should call strand_service_impl::make_invoker(impl), retrieve
invoker.h_.promise().self_, set self.h = invoker.h_ and then call ex.post(self).
Replace the identical blocks in dispatch and post with a call to this new helper
to remove duplication while preserving the invoker, self, and ex.post semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@include/boost/capy/ex/async_event.hpp`:
- Around line 175-178: The move constructor wait_awaiter(wait_awaiter&& o)
noexcept currently copies cont_ from o via cont_(o.cont_) but leaves the
moved-from object's continuation intact; update the move ctor to defensively
clear the source continuation (e.g., reset o.cont_ or set its handle/next_ to
null) after transferring so the moved-from wait_awaiter is left in a safe empty
state; locate the wait_awaiter move constructor and modify the handling of cont_
and o.cont_ accordingly to transfer ownership and clear the source.

In `@include/boost/capy/ex/run_async.hpp`:
- Around line 97-98: Add a short clarifying comment near the members task_h_ and
task_cont_ explaining why both store the same coroutine handle: state that
task_h_ is retained specifically for frame_guard usage inside make_trampoline
while task_cont_.h is used for executor/continuation dispatch, so both must be
kept and updated together; reference the member names task_h_, task_cont_, the
frame_guard and the make_trampoline function in the comment so future
maintainers understand the coupling and do not remove or update one without the
other.

In `@src/ex/detail/strand_service.cpp`:
- Around line 243-248: Extract the duplicated invoker-posting logic into a small
helper function (e.g., make_and_post_invoker or post_invoker) so both dispatch
and post call it; the helper should call
strand_service_impl::make_invoker(impl), retrieve invoker.h_.promise().self_,
set self.h = invoker.h_ and then call ex.post(self). Replace the identical
blocks in dispatch and post with a call to this new helper to remove duplication
while preserving the invoker, self, and ex.post semantics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 13a97ddb-3170-43cb-ae7b-00fb60d9c13d

📥 Commits

Reviewing files that changed from the base of the PR and between 89da966 and 500e87b.

⛔ Files ignored due to path filters (19)
  • bench/bench.cpp is excluded by !**/bench/**
  • doc/continuation-rationale.md is excluded by !**/doc/**
  • doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc is excluded by !**/doc/**
  • doc/modules/ROOT/pages/7.examples/7n.custom-executor.adoc is excluded by !**/doc/**
  • doc/modules/ROOT/pages/8.design/8k.Executor.adoc is excluded by !**/doc/**
  • doc/unlisted/execution-executors.adoc is excluded by !**/doc/**
  • doc/unlisted/io-awaitables-executor.adoc is excluded by !**/doc/**
  • include/boost/capy/test/run_blocking.hpp is excluded by !**/test/**
  • include/boost/capy/test/stream.hpp is excluded by !**/test/**
  • src/test/run_blocking.cpp is excluded by !**/test/**
  • test/unit/ex/any_executor.cpp is excluded by !**/test/**
  • test/unit/ex/executor_ref.cpp is excluded by !**/test/**
  • test/unit/ex/frame_cb.cpp is excluded by !**/test/**
  • test/unit/ex/run_async.cpp is excluded by !**/test/**
  • test/unit/ex/strand.cpp is excluded by !**/test/**
  • test/unit/ex/thread_pool.cpp is excluded by !**/test/**
  • test/unit/ex/work_guard.cpp is excluded by !**/test/**
  • test/unit/task.cpp is excluded by !**/test/**
  • test/unit/test_helpers.hpp is excluded by !**/test/**
📒 Files selected for processing (21)
  • example/asio/api/capy_streams.cpp
  • example/asio/api/uni_stream.hpp
  • example/custom-executor/custom_executor.cpp
  • include/boost/capy/concept/executor.hpp
  • include/boost/capy/concept/io_awaitable.hpp
  • include/boost/capy/continuation.hpp
  • include/boost/capy/delay.hpp
  • include/boost/capy/ex/any_executor.hpp
  • include/boost/capy/ex/async_event.hpp
  • include/boost/capy/ex/async_mutex.hpp
  • include/boost/capy/ex/executor_ref.hpp
  • include/boost/capy/ex/io_env.hpp
  • include/boost/capy/ex/run.hpp
  • include/boost/capy/ex/run_async.hpp
  • include/boost/capy/ex/strand.hpp
  • include/boost/capy/ex/thread_pool.hpp
  • include/boost/capy/timeout.hpp
  • include/boost/capy/when_all.hpp
  • include/boost/capy/when_any.hpp
  • src/ex/detail/strand_service.cpp
  • src/ex/thread_pool.cpp
✅ Files skipped from review due to trivial changes (2)
  • example/asio/api/uni_stream.hpp
  • include/boost/capy/continuation.hpp
🚧 Files skipped from review as they are similar to previous changes (12)
  • include/boost/capy/concept/io_awaitable.hpp
  • include/boost/capy/ex/run.hpp
  • include/boost/capy/ex/async_mutex.hpp
  • include/boost/capy/timeout.hpp
  • src/ex/thread_pool.cpp
  • include/boost/capy/ex/any_executor.hpp
  • example/custom-executor/custom_executor.cpp
  • include/boost/capy/ex/io_env.hpp
  • include/boost/capy/ex/thread_pool.hpp
  • include/boost/capy/ex/executor_ref.hpp
  • include/boost/capy/ex/strand.hpp
  • include/boost/capy/concept/executor.hpp

@codecov
Copy link

codecov bot commented Mar 21, 2026

Codecov Report

❌ Patch coverage is 98.36066% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.42%. Comparing base (150ee20) to head (81fb33d).
⚠️ Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
include/boost/capy/test/stream.hpp 84.61% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop     #239      +/-   ##
===========================================
+ Coverage    92.39%   92.42%   +0.03%     
===========================================
  Files          168      168              
  Lines         9327     9339      +12     
===========================================
+ Hits          8618     8632      +14     
+ Misses         709      707       -2     
Flag Coverage Δ
linux 92.41% <ø> (+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/delay.hpp 100.00% <100.00%> (ø)
include/boost/capy/ex/any_executor.hpp 75.47% <100.00%> (ø)
include/boost/capy/ex/async_event.hpp 100.00% <100.00%> (ø)
include/boost/capy/ex/async_mutex.hpp 98.90% <100.00%> (ø)
include/boost/capy/ex/executor_ref.hpp 85.00% <100.00%> (ø)
include/boost/capy/ex/io_env.hpp 100.00% <100.00%> (ø)
include/boost/capy/ex/run.hpp 99.46% <100.00%> (+<0.01%) ⬆️
include/boost/capy/ex/run_async.hpp 87.50% <100.00%> (+0.10%) ⬆️
include/boost/capy/ex/strand.hpp 100.00% <100.00%> (ø)
include/boost/capy/ex/thread_pool.hpp 100.00% <100.00%> (ø)
... and 8 more

... and 18 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

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

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
include/boost/capy/ex/run_async.hpp (1)

97-102: Prefer one source of truth for the task handle.

task_h_ and task_cont_.h always refer to the same coroutine, so this duplicates state across both promise specializations and the launch path. If a later edit updates only one of them, cleanup and scheduling diverge. Using task_cont_.h for the trampoline cleanup path would let you drop the extra handle entirely.

Refactor sketch
-        std::coroutine_handle<> task_h_;
         continuation task_cont_;
...
-    } guard{p.task_h_};
+    } guard{p.task_cont_.h};
...
-        p.task_h_ = task_h;
         p.task_cont_.h = task_h;

Apply the same field removal in both promise_type specializations.

Also applies to: 208-213, 415-416

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/boost/capy/ex/run_async.hpp` around lines 97 - 102, The code
duplicates the coroutine handle via task_h_ and task_cont_.h; remove the extra
raw handle and make task_cont_.h the single source of truth by deleting task_h_
from both promise_type specializations and updating make_trampoline, frame_guard
cleanup, and the launch path to use task_cont_.h everywhere for
cancellation/cleanup and scheduling; ensure any places that read or assign
task_h_ now reference task_cont_.h (and update comments) so both promise
specializations are consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ex/thread_pool.cpp`:
- Around line 95-103: impl::~impl() currently drains abandoned continuations too
late (after thread_pool::shutdown()/destroy()), which allows suspended-frame
destructors (e.g., delay_awaitable::~delay_awaitable() calling
timer_service::cancel() via its cached ts_) to run after services are torn down;
move the queue-drain logic that calls pop() and destroys coroutine handles out
of impl::~impl() and invoke it explicitly from thread_pool::~thread_pool()
before calling shutdown() and destroy(), ensuring you still use the same pop()
loop and h.destroy() behavior but executed prior to shutdown()/destroy() so
suspended frames are torn down while services (timer_service, etc.) are still
valid.

---

Nitpick comments:
In `@include/boost/capy/ex/run_async.hpp`:
- Around line 97-102: The code duplicates the coroutine handle via task_h_ and
task_cont_.h; remove the extra raw handle and make task_cont_.h the single
source of truth by deleting task_h_ from both promise_type specializations and
updating make_trampoline, frame_guard cleanup, and the launch path to use
task_cont_.h everywhere for cancellation/cleanup and scheduling; ensure any
places that read or assign task_h_ now reference task_cont_.h (and update
comments) so both promise specializations are consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 58b8a08d-da94-4f48-8198-0038866d5b23

📥 Commits

Reviewing files that changed from the base of the PR and between 500e87b and 4a8bf7e.

⛔ Files ignored due to path filters (19)
  • bench/bench.cpp is excluded by !**/bench/**
  • doc/continuation-rationale.md is excluded by !**/doc/**
  • doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc is excluded by !**/doc/**
  • doc/modules/ROOT/pages/7.examples/7n.custom-executor.adoc is excluded by !**/doc/**
  • doc/modules/ROOT/pages/8.design/8k.Executor.adoc is excluded by !**/doc/**
  • doc/unlisted/execution-executors.adoc is excluded by !**/doc/**
  • doc/unlisted/io-awaitables-executor.adoc is excluded by !**/doc/**
  • include/boost/capy/test/run_blocking.hpp is excluded by !**/test/**
  • include/boost/capy/test/stream.hpp is excluded by !**/test/**
  • src/test/run_blocking.cpp is excluded by !**/test/**
  • test/unit/ex/any_executor.cpp is excluded by !**/test/**
  • test/unit/ex/executor_ref.cpp is excluded by !**/test/**
  • test/unit/ex/frame_cb.cpp is excluded by !**/test/**
  • test/unit/ex/run_async.cpp is excluded by !**/test/**
  • test/unit/ex/strand.cpp is excluded by !**/test/**
  • test/unit/ex/thread_pool.cpp is excluded by !**/test/**
  • test/unit/ex/work_guard.cpp is excluded by !**/test/**
  • test/unit/task.cpp is excluded by !**/test/**
  • test/unit/test_helpers.hpp is excluded by !**/test/**
📒 Files selected for processing (21)
  • example/asio/api/capy_streams.cpp
  • example/asio/api/uni_stream.hpp
  • example/custom-executor/custom_executor.cpp
  • include/boost/capy/concept/executor.hpp
  • include/boost/capy/concept/io_awaitable.hpp
  • include/boost/capy/continuation.hpp
  • include/boost/capy/delay.hpp
  • include/boost/capy/ex/any_executor.hpp
  • include/boost/capy/ex/async_event.hpp
  • include/boost/capy/ex/async_mutex.hpp
  • include/boost/capy/ex/executor_ref.hpp
  • include/boost/capy/ex/io_env.hpp
  • include/boost/capy/ex/run.hpp
  • include/boost/capy/ex/run_async.hpp
  • include/boost/capy/ex/strand.hpp
  • include/boost/capy/ex/thread_pool.hpp
  • include/boost/capy/timeout.hpp
  • include/boost/capy/when_all.hpp
  • include/boost/capy/when_any.hpp
  • src/ex/detail/strand_service.cpp
  • src/ex/thread_pool.cpp
✅ Files skipped from review due to trivial changes (3)
  • include/boost/capy/continuation.hpp
  • include/boost/capy/ex/strand.hpp
  • include/boost/capy/concept/io_awaitable.hpp
🚧 Files skipped from review as they are similar to previous changes (6)
  • include/boost/capy/ex/async_event.hpp
  • example/asio/api/capy_streams.cpp
  • include/boost/capy/ex/any_executor.hpp
  • include/boost/capy/ex/io_env.hpp
  • include/boost/capy/ex/thread_pool.hpp
  • include/boost/capy/ex/async_mutex.hpp

The executor concept's dispatch() and post() now accept
continuation& instead of std::coroutine_handle<>. A continuation
wraps a coroutine handle with an intrusive next_ pointer, enabling
executors to queue work without per-post heap allocation.

The thread pool's post() previously allocated a work node on every
call (new work(h)). It now links the caller's continuation directly
into an intrusive queue — zero allocation on the hot path.

The continuation lives in the I/O awaitable for caller-handle
posting (delay, async_mutex, async_event, stream), and in
combinator/trampoline state for parent-dispatch and child-launch
patterns (when_all, when_any, timeout, run, run_async). The
IoAwaitable concept and io_awaitable_promise_base are unchanged.

Breaking change: all Executor implementations must update dispatch()
and post() signatures from coroutine_handle<> to continuation&.
@mvandeberg
Copy link
Contributor Author

@coderabbitai resume

@coderabbitai
Copy link

coderabbitai bot commented Mar 21, 2026

✅ Actions performed

Reviews resumed.

@mvandeberg mvandeberg merged commit 1047e76 into cppalliance:develop Mar 21, 2026
37 of 38 checks passed
@mvandeberg mvandeberg deleted the pr/continuation branch March 21, 2026 03:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants