Skip to content

Commit c550bf5

Browse files
committed
Add stop token propagation tests and document inheritance behavior (#163)
The run/run_async API already supports stop token injection, but this was undertested and underdocumented, making it appear missing. Add tests verifying that run(ex) inherits the caller's stop token, run(ex, st) overrides it, and run_async propagates tokens to deferred tasks. Restructure the launching docs into a unified stop token propagation section and fix the download_manager example to use io_env propagation.
1 parent 9fe88b0 commit c550bf5

File tree

4 files changed

+192
-19
lines changed

4 files changed

+192
-19
lines changed

doc/modules/ROOT/pages/4.coroutines/4b.launching.adoc

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -91,21 +91,6 @@ run_async(ex,
9191

9292
When no handlers are provided, results are discarded and exceptions are rethrown (causing `std::terminate` if uncaught).
9393

94-
=== Stop Token Support
95-
96-
Pass a stop token to enable cooperative cancellation:
97-
98-
[source,cpp]
99-
----
100-
std::stop_source source;
101-
run_async(ex, source.get_token())(cancellable_task());
102-
103-
// Later, to request cancellation:
104-
source.request_stop();
105-
----
106-
107-
The stop token is propagated to the task and all tasks it awaits.
108-
10994
== run: Executor Hopping Within Coroutines
11095

11196
Inside a coroutine, use `run` to execute a child task on a different executor:
@@ -136,6 +121,48 @@ This pattern is useful for:
136121
* Performing I/O on an I/O-specific context
137122
* Ensuring UI updates happen on the UI thread
138123

124+
== Stop Token Propagation
125+
126+
Both `run_async` and `run` propagate stop tokens to the launched task and all tasks it awaits. The task accesses its token via `co_await this_coro::stop_token`.
127+
128+
=== Injecting a Token with run_async
129+
130+
Since `run_async` is called from non-coroutine code, there is no caller token to inherit. Pass a stop token explicitly:
131+
132+
[source,cpp]
133+
----
134+
std::stop_source source;
135+
run_async(ex, source.get_token())(cancellable_task());
136+
137+
// Later, to request cancellation:
138+
source.request_stop();
139+
----
140+
141+
=== Inheritance with run
142+
143+
`run` is called from within a coroutine, so it inherits the caller's stop token by default:
144+
145+
[source,cpp]
146+
----
147+
task<void> parent()
148+
{
149+
// Child automatically receives our stop token
150+
co_await run(pool.get_executor())(child_task());
151+
}
152+
----
153+
154+
To override with a different token, pass it explicitly:
155+
156+
[source,cpp]
157+
----
158+
task<void> parent()
159+
{
160+
std::stop_source local;
161+
// Child gets local's token, not our caller's
162+
co_await run(pool.get_executor(), local.get_token())(child_task());
163+
}
164+
----
165+
139166
== Handler Threading
140167

141168
Handlers passed to `run_async` are invoked on whatever thread the executor schedules:

doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -316,25 +316,36 @@ task<> with_timeout(task<> operation, std::chrono::seconds timeout)
316316

317317
=== User Cancellation
318318

319-
Connect UI cancellation to stop tokens:
319+
Connect UI cancellation to stop tokens. Pass the token through `run_async` so it propagates automatically via the execution environment—the task accesses it with `co_await this_coro::stop_token` instead of receiving it as a function argument:
320320

321321
[source,cpp]
322322
----
323323
class download_manager
324324
{
325+
executor_ref executor_;
325326
std::stop_source stop_source_;
326-
327+
327328
public:
328329
void start_download(std::string url)
329330
{
330-
run_async(executor_)(download(url, stop_source_.get_token()));
331+
// Token propagated via io_env, not as a function argument
332+
run_async(executor_, stop_source_.get_token())(download(url));
331333
}
332-
334+
333335
void cancel()
334336
{
335337
stop_source_.request_stop();
336338
}
337339
};
340+
341+
task<void> download(std::string url)
342+
{
343+
auto token = co_await this_coro::stop_token; // From run_async's io_env
344+
while (!token.stop_requested())
345+
{
346+
co_await fetch_next_chunk(url);
347+
}
348+
}
338349
----
339350

340351
=== Graceful Shutdown

test/unit/ex/run.cpp

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,97 @@ struct run_test
337337
BOOST_TEST(called);
338338
}
339339

340+
//----------------------------------------------------------
341+
// Stop Token Propagation
342+
//----------------------------------------------------------
343+
344+
static task<bool>
345+
check_stop_requested()
346+
{
347+
auto token = co_await this_coro::stop_token;
348+
co_return token.stop_requested();
349+
}
350+
351+
void
352+
testStopTokenInheritance()
353+
{
354+
// Verify run(ex) inherits the caller's stop token
355+
int dispatch_count = 0;
356+
test_executor ex(1, dispatch_count);
357+
std::stop_source source;
358+
source.request_stop();
359+
bool result = false;
360+
361+
auto outer = [&]() -> task<bool> {
362+
// run(ex) with no explicit stop token should inherit
363+
// the caller's token (which is stopped)
364+
co_return co_await capy::run(ex)(check_stop_requested());
365+
};
366+
367+
run_async(ex, source.get_token(),
368+
[&](bool v) { result = v; })(outer());
369+
370+
BOOST_TEST(result);
371+
}
372+
373+
void
374+
testStopTokenOverrideInnerStopped()
375+
{
376+
// Stop the inner (override) token only.
377+
// Inner task should see stopped; outer should not.
378+
int dispatch_count = 0;
379+
test_executor ex(1, dispatch_count);
380+
std::stop_source caller_source;
381+
std::stop_source override_source;
382+
override_source.request_stop();
383+
384+
bool outer_stopped = true;
385+
bool inner_stopped = false;
386+
387+
auto outer = [&]() -> task<void> {
388+
auto token = co_await this_coro::stop_token;
389+
outer_stopped = token.stop_requested();
390+
inner_stopped = co_await capy::run(ex, override_source.get_token())(
391+
check_stop_requested());
392+
};
393+
394+
run_async(ex, caller_source.get_token())(outer());
395+
396+
BOOST_TEST(!outer_stopped);
397+
BOOST_TEST(inner_stopped);
398+
}
399+
400+
void
401+
testStopTokenOverrideOuterStopped()
402+
{
403+
// Stop the outer (caller) token only.
404+
// Outer task should see stopped; inner (override) should not.
405+
int dispatch_count = 0;
406+
test_executor ex(1, dispatch_count);
407+
std::stop_source caller_source;
408+
caller_source.request_stop();
409+
std::stop_source override_source;
410+
411+
bool outer_stopped = false;
412+
bool inner_stopped = true;
413+
414+
auto outer = [&]() -> task<void> {
415+
auto token = co_await this_coro::stop_token;
416+
outer_stopped = token.stop_requested();
417+
inner_stopped = co_await capy::run(ex, override_source.get_token())(
418+
check_stop_requested());
419+
};
420+
421+
run_async(ex, caller_source.get_token())(outer());
422+
423+
BOOST_TEST(outer_stopped);
424+
BOOST_TEST(!inner_stopped);
425+
}
426+
427+
//----------------------------------------------------------
428+
// Allocator Propagation
429+
//----------------------------------------------------------
430+
340431
void
341432
testAllocatorPropagation()
342433
{
@@ -394,6 +485,9 @@ struct run_test
394485
testStopTokenWithAllocator();
395486
testVoidWithStopToken();
396487
testVoidWithMemoryResource();
488+
testStopTokenInheritance();
489+
testStopTokenOverrideInnerStopped();
490+
testStopTokenOverrideOuterStopped();
397491
testAllocatorPropagation();
398492
testAllocatorPropagationThroughRun();
399493
}

test/unit/ex/run_async.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,46 @@ struct run_async_test
358358
BOOST_TEST(result);
359359
}
360360

361+
void
362+
testScopedCancellation()
363+
{
364+
// Three tasks on the same executor: one with a scoped stop token,
365+
// two with the default (empty) token. Cancelling the scoped token
366+
// should only affect that task, not the others.
367+
std::queue<std::coroutine_handle<>> queue;
368+
queue_executor d(queue);
369+
370+
bool default_1_stopped = true;
371+
bool scoped_stopped = false;
372+
bool default_2_stopped = true;
373+
374+
std::stop_source scoped_source;
375+
376+
run_async(d, [&](bool v) { default_1_stopped = v; })(
377+
check_stop_requested());
378+
run_async(d, scoped_source.get_token(),
379+
[&](bool v) { scoped_stopped = v; })(
380+
check_stop_requested());
381+
run_async(d, [&](bool v) { default_2_stopped = v; })(
382+
check_stop_requested());
383+
384+
BOOST_TEST_EQ(queue.size(), 3u);
385+
386+
// Cancel the scoped source before draining
387+
scoped_source.request_stop();
388+
389+
while(!queue.empty())
390+
{
391+
auto h = queue.front();
392+
queue.pop();
393+
h.resume();
394+
}
395+
396+
BOOST_TEST(!default_1_stopped);
397+
BOOST_TEST(scoped_stopped);
398+
BOOST_TEST(!default_2_stopped);
399+
}
400+
361401
//----------------------------------------------------------
362402
// Allocator Propagation
363403
//----------------------------------------------------------
@@ -641,6 +681,7 @@ struct run_async_test
641681
// Stop Token
642682
testStopTokenPropagation();
643683
testCancellationVisible();
684+
testScopedCancellation();
644685

645686
// Allocator Propagation
646687
testAllocatorPropagation();

0 commit comments

Comments
 (0)