Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/spec/concurrency_and_lifetimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,11 @@ returns a `core::async::Task` on `LocalBackend`, which carries a
| The client-side execute deadline | `ClientTimeoutError` | That call |

A stopped handler suspended in a stop-aware await resumes with
`OperationCancelled` on its model's strand. A synchronous handler has nothing
`OperationCancelled` on its model's strand. `LocalBackend::cancelPending`
settles every pending call before it requests any stop, because the stopped
handler settles its own call too, from another thread: were the stop
requested first, that `OperationCancelled` could reach the caller ahead of the
error the verb names. A synchronous handler has nothing
to stop and runs to completion; its result is discarded by the settled state. A
call on a remote backend is settled locally only — the wire carries no
cancellation. `tests/test_coroutine_model.cpp` measures the scope's
Expand Down
15 changes: 10 additions & 5 deletions include/morph/core/backend.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1356,11 +1356,11 @@ class LocalBackend : public detail::IBackend {
// With the swap: a run admitted before this point is in `snapshot`
// and is failed below, and one admitted after it is not.
_cancels->record(exc);
for (auto const& weak : _taskRuns) {
if (auto const run = weak.lock()) {
static_cast<void>(run->stopSource->request_stop());
}
}
// Settle before stopping. A stopped Task handler resumes on its strand
// and settles its own sink with `OperationCancelled`; requested first,
// that settle can land before this one, and the caller would be
// answered with the stop instead of @p exc. Settled first, the
// handler's later settle is the ignored one.
for (auto& weak : snapshot) {
if (auto sink = weak.lock()) {
// May race a reply settling the same sink; `ISettleSink`'s
Expand All @@ -1370,6 +1370,11 @@ class LocalBackend : public detail::IBackend {
sink->settleException(exc);
}
}
for (auto const& weak : _taskRuns) {
if (auto const run = weak.lock()) {
static_cast<void>(run->stopSource->request_stop());
}
}
}

/// @brief Number of entries currently held in the pending list.
Expand Down
30 changes: 30 additions & 0 deletions tests/test_coroutine_model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,36 @@ TEST_CASE("cancelPending on a LocalBackend that stays alive stops its running Ta
REQUIRE(probe().order == std::vector<std::string>{"sleep-start", "sleep-cancelled"});
}

// The stopped handler settles its own call with `OperationCancelled`, from its
// strand. With the strand inline, that settle happens inside `request_stop()`
// itself, so this pins the order `cancelPending` must keep: the caller is
// answered with the error it was given, never with the stop it caused.
TEST_CASE("cancelPending answers a running Task handler's caller with its error and not with the stop",
"[coroutine][model][cancel]") {
coro_test::SchedulerScope const timers;
morph::testing::InlineExecutor pool;
morph::exec::MainThreadExecutor exec;
auto backend = std::make_unique<morph::backend::LocalBackend>(pool);
auto* local = backend.get();
morph::bridge::Bridge bridge{std::move(backend), exec};
morph::bridge::BridgeHandler<CoroModel> handler{bridge, &exec};
armHold(&exec);

std::exception_ptr answered;
handler.execute(CoroSleep{.ms = 60'000}).then([](int) {}).onError([&](const std::exception_ptr& error) {
answered = error;
});
REQUIRE(pumpUntil(exec, [&] {
std::scoped_lock const lock{probe().mtx};
return !probe().order.empty();
}));

local->cancelPending(std::make_exception_ptr(morph::backend::DisconnectedError{}));
REQUIRE(pumpUntil(exec, [&] { return answered != nullptr && probe().holdFinished.load(); }));
CHECK(holds<morph::backend::DisconnectedError>(answered));
CHECK_FALSE(holds<core::async::OperationCancelled>(answered));
}

TEST_CASE("an action queued behind a suspended Task handler does not run once a backend switch failed it",
"[coroutine][model][lifetime]") {
coro_test::SchedulerScope const timers;
Expand Down
Loading