Skip to content

Conversation

@klemens-morgenstern
Copy link

@klemens-morgenstern klemens-morgenstern commented Jan 20, 2026

promise_type constructor should capture by ref, otherwise it'll cause a copy.

The tests check some lifetime issues. The second one is failing, as I would expect the handler to be destroyed.

I don't know if what I'm doing here is supposed to be UB though.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed continuation handle lifecycle management to ensure proper resource cleanup and prevent potential double-destruction issues in asynchronous operations.
    • Enhanced internal reference handling with explicit initialization and cleanup mechanisms.
  • Tests

    • Expanded test coverage for handler teardown scenarios and asynchronous execution deletion semantics.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

The pull request updates constructor and operator signatures in Boost.Capy's asynchronous task framework to accept executor and handler parameters by lvalue reference instead of by value, adds teardown and deletion test methods to verify proper handler destruction during async execution, and introduces explicit lifecycle management for continuation handles with a public destructor in io_awaitable_support.

Changes

Cohort / File(s) Summary
Constructor and Operator Signatures
include/boost/capy/ex/run_async.hpp
Updated promise_type constructor parameters and operator new signatures in primary and specialized templates to accept Ex and Handlers by lvalue reference rather than by value. Initialization still uses std::move for member initialization.
Test Infrastructure
test/unit/task.cpp
Added tear_down_t nested type and two new test methods testTearDown() and testDelete() to validate proper handler destruction during teardown scenarios and deletion semantics in async execution. Includes header for any_executor.hpp.
Continuation Lifecycle Management
include/boost/capy/ex/io_awaitable_support.hpp
Introduced mutable cont_ member initialized to nullptr and public destructor to manage continuation handle lifecycle. Reworked complete() dispatch to clear the continuation handle via std::exchange to prevent double-destruction and ensure safe transfer.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A hop, skip, and bound through async code we've found,
References dance where values once were,
Tests track teardowns with careful bounds,
Continuations cleared when their work is done,
The rabbit approves—clean lifecycle fun!

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The pull request title 'Tests' is vague and generic, failing to convey meaningful information about the actual changes made. Use a more descriptive title that captures the main intent, such as 'Capture handler by reference in promise_type to avoid copying' or 'Add lifetime tests and modify handler capture semantics'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

@cppalliance-bot
Copy link

cppalliance-bot commented Jan 20, 2026

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

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

2026-01-29 16:58:10 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: 0

Caution

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

⚠️ Outside diff range comments (1)
test/unit/task.cpp (1)

16-24: Add <utility> for std::exchange.
tear_down_t uses std::exchange (Line 1127); relying on transitive includes is brittle.

🧩 Proposed include
 `#include` <stdexcept>
 `#include` <string>
 `#include` <type_traits>
+#include <utility>
 `#include` <vector>

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

🤖 Fix all issues with AI agents
In `@include/boost/capy/io_awaitable.hpp`:
- Around line 549-552: The destructor io_awaitable_support() currently calls
cont_.destroy() which can destroy a caller-owned continuation if complete() was
never called; change the ownership semantics so the promise only destroys cont_
when it actually owns it: add an ownership flag (e.g., bool cont_owned_) set to
true when ownership is transferred (or set when complete() takes the handle),
guard cont_.destroy() in the destructor with that flag (or check cont_.address()
!= nullptr/noop before destroying), and add an assertion or log when the
destructor finds a non-noop cont_ but cont_owned_ is false to catch unintended
abandonment; update any code paths that transfer the continuation (methods that
assign cont_) to set/clear cont_owned_ appropriately.
🧹 Nitpick comments (2)
include/boost/capy/io_awaitable.hpp (2)

416-423: The null check if(!cont_) is ineffective as a guard against the noop sentinel.

std::noop_coroutine() returns a non-null handle, so operator bool() returns true. This means !cont_ is always false when cont_ holds the noop sentinel.

The current logic still works correctly because returning/exchanging a noop_coroutine is harmless, but if the intent is to detect "no continuation was set," consider tracking this with an explicit boolean flag or use a different sentinel approach.

💡 Potential clarification using explicit flag
+    bool has_continuation_{false};
     coro cont_{std::noop_coroutine()};
     executor_ref caller_ex_;
     
     void set_continuation(coro cont, executor_ref caller_ex) noexcept
     {
         cont_ = cont;
         caller_ex_ = caller_ex;
+        has_continuation_ = true;
     }
     
     coro complete() noexcept
     {
-        if(!cont_)
+        if(!has_continuation_)
             return std::noop_coroutine();
         // ...

20-24: Consider adding a high-level overview section after the includes.

Per coding guidelines, a /* */ comment block after the includes explaining how the implementation works would help developers quickly understand the file's purpose and design. The existing docstrings are thorough, but a brief overview of the concepts relationship (IoAwaitableIoAwaitableTaskIoLaunchableTask) and the role of io_awaitable_support CRTP mixin would be valuable.

📝 Example overview section
`#include` <type_traits>

/*  io_awaitable.hpp - I/O Awaitable Protocol
 *
 *  This header defines the awaitable protocol for coroutines that
 *  participate in scheduler-affinity and cancellation:
 *
 *  - IoAwaitable: awaitable accepts executor + stop_token in await_suspend
 *  - IoAwaitableTask: task whose promise supports context injection
 *  - IoLaunchableTask: task that can be launched from non-coroutine contexts
 *
 *  io_awaitable_support<Derived> is a CRTP mixin providing storage for
 *  executor, stop_token, and continuation, plus await_transform hooks
 *  for co_await get_executor() and co_await get_stop_token().
 */

namespace boost {

Based on coding guidelines: "The top of the file after the includes, put a nice /* */ section which gives a high level overview of how the implementation works."

Comment on lines 549 to 552
~io_awaitable_support()
{
cont_.destroy();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Destructor destroys a continuation handle that this promise may not own.

When complete() was never called (e.g., coroutine abandoned mid-execution), cont_ still holds the caller's continuation handle. Destroying it here destroys a coroutine frame that belongs to the caller, not this promise.

Typical coroutine ownership semantics: the caller owns their coroutine frame; the callee should only resume (or not resume) the continuation, never destroy it.

This aligns with the PR description noting "the second test currently fails" and "uncertainty about whether the observed behavior constitutes undefined behavior."

Recommendations:

  1. If the intent is cleanup on abandonment, document this ownership transfer clearly and ensure callers understand they're transferring ownership of their continuation.
  2. If the intent is defensive cleanup, consider only destroying if the continuation handle is valid AND some explicit "owned" flag is set.
  3. Consider logging or asserting if cont_ is not noop at destruction to catch unintended abandonment.
🛠️ Safer alternative: guard destruction with ownership flag
+    bool owns_continuation_{false};
     coro cont_{std::noop_coroutine()};
 
     // ... in set_continuation or a separate method for ownership transfer ...
+    void take_continuation_ownership(coro cont, executor_ref caller_ex) noexcept
+    {
+        cont_ = cont;
+        caller_ex_ = caller_ex;
+        owns_continuation_ = true;
+    }
 
     ~io_awaitable_support()
     {
-        cont_.destroy();
+        if(owns_continuation_ && cont_)
+            cont_.destroy();
     }
🤖 Prompt for AI Agents
In `@include/boost/capy/io_awaitable.hpp` around lines 549 - 552, The destructor
io_awaitable_support() currently calls cont_.destroy() which can destroy a
caller-owned continuation if complete() was never called; change the ownership
semantics so the promise only destroys cont_ when it actually owns it: add an
ownership flag (e.g., bool cont_owned_) set to true when ownership is
transferred (or set when complete() takes the handle), guard cont_.destroy() in
the destructor with that flag (or check cont_.address() != nullptr/noop before
destroying), and add an assertion or log when the destructor finds a non-noop
cont_ but cont_owned_ is false to catch unintended abandonment; update any code
paths that transfer the continuation (methods that assign cont_) to set/clear
cont_owned_ appropriately.

@cppalliance-bot
Copy link

cppalliance-bot commented Jan 23, 2026

GCOVR code coverage report https://89.capy.prtest3.cppalliance.org/gcovr/index.html
LCOV code coverage report https://89.capy.prtest3.cppalliance.org/genhtml/index.html
Coverage Diff https://89.capy.prtest3.cppalliance.org/gcovr/coverage_diff.txt

Build time: 2026-01-23 03:32:25 UTC

@vinniefalco
Copy link
Member

Can you please squash this down and rebase

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

🤖 Fix all issues with AI agents
In `@include/boost/capy/ex/io_awaitable_support.hpp`:
- Line 141: The new destructor ~io_awaitable_support() currently destroys the
continuation cont_ unconditionally which is unsafe unless this promise always
owns the continuation; update the implementation and public API to make
ownership explicit: add a boolean member (e.g., owns_cont_) or equivalent guard
checked before destroying cont_, ensure all usages set that flag correctly
(including the other destructor-affected code around the cont_ usage at the
second occurrence you noted), and add a public docstring on the
io_awaitable_support class and its destructor describing the ownership contract
and invariants (i.e., whether the promise owns cont_, who may resume/destroy it,
and when the flag should be set). Ensure references to cont_,
~io_awaitable_support, and the new owns_cont_ are used so reviewers can find the
changes.

std::pmr::memory_resource* alloc_ = nullptr;
executor_ref caller_ex_;
coro cont_;
mutable coro cont_{nullptr};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Confirm continuation ownership before destroying it

~io_awaitable_support() now destroys cont_. That is only safe if this promise unambiguously owns the continuation; otherwise it risks double-destroying or skipping resumption of an awaiting coroutine. Please verify ownership across all usages and either document the ownership contract or gate destruction behind an explicit ownership flag. Also add a docstring for this new public destructor describing behavior and any invariants.

As per coding guidelines, docstrings are required for all classes and functions in public headers in non-detail namespaces.

Also applies to: 203-207

🤖 Prompt for AI Agents
In `@include/boost/capy/ex/io_awaitable_support.hpp` at line 141, The new
destructor ~io_awaitable_support() currently destroys the continuation cont_
unconditionally which is unsafe unless this promise always owns the
continuation; update the implementation and public API to make ownership
explicit: add a boolean member (e.g., owns_cont_) or equivalent guard checked
before destroying cont_, ensure all usages set that flag correctly (including
the other destructor-affected code around the cont_ usage at the second
occurrence you noted), and add a public docstring on the io_awaitable_support
class and its destructor describing the ownership contract and invariants (i.e.,
whether the promise owns cont_, who may resume/destroy it, and when the flag
should be set). Ensure references to cont_, ~io_awaitable_support, and the new
owns_cont_ are used so reviewers can find the changes.

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.

3 participants