Skip to content

[breaking] implement new cancellation mechanism - #596

Open
Guest0x0 wants to merge 3 commits into
mainfrom
new-cancellation-mechanism
Open

[breaking] implement new cancellation mechanism#596
Guest0x0 wants to merge 3 commits into
mainfrom
new-cancellation-mechanism

Conversation

@Guest0x0

Copy link
Copy Markdown
Collaborator

This PR utilizes a new compiler-builtin mechanism for implementing cancellation. In moonbitlang/async, cancellation consist of two parts:

  • a persistent state attached to each task. This states is what actually implements cancellation, and makes cancellation sticky (i.e. when resuming from a cancelled operation, subsequent unprotected async operation will still get cancelled immediately), so that cancellation can never be accidentally swallowed
  • a signal for notifying the cancelled task about the cancellation, so that the cancelled task can run its defer/errdefer block etc. for cleanup. This signal is previously implemented as a private suberror

The choice of representing suberror has its merits: we get defer/errdefer support and propagation of cancellation signal for free. However, representing cancellation as a normal suberror also allows ordinary catch, in particular catch-all handlers (i.e. catch with a irrefutable pattern) to capture the cancellation signal, which can cause unwanted consequence. For example, if users want to wrap error raised by a function with additional context information with a catch-all handler, they may accidentally capture the cancellation signal and wrap it into something else. Wrapping cancellation into something else would turn a successful cancellation into fatal error, failing the program undesirably.

To solve this problem, we have recently introduced some compiler-builtin mechanism for the cancellation signal. The compiler now provides a builtin cancellation pseudo-error and primitives for raising and capturing the pseudo-error. The special cancellation pseudo-error propagates like an error and can trigger defer & errdefer block. However, catch, including catch-all handlers, can never capture the cancellation signal. This PR utilizes this new mechanism to implement cancellation in moonbitlang/async, replacing the previous suberror approach.

As another consequence of compiler-builtin support for cancellation signal, we can now also track cancellation and error separately at type level. The latest version of MoonBit now provides a nocancel effect mark, representing "cannot raise cancellation signal". noraise no longer covers cancellation signal. For convenience, async still implies raise + cancellation by default. Explicit noraise/nocancel annotation must be added for more precise type signature. With noraise and nocancel separated, many API that are cancellable but never raise an error themselves, such as sleep, can now be assigned a more precise noraise type. In addition, we can also require nocancel in critical cleanup code paths, such as defer/errdefer/TaskGroup::add_defer.

From the compiler's perspective, nocancel merely means "does not raise cancellation signal". But this is not equivalent to "not cancellable", because it is possible to write code that merely swallow the cancellation signal. Fortunately, cancellation related primitives are all hidden in moonbitlang/async. And among the public API of moonbitlang/async, the only function that can erase cancellation from type signature is @async.protect_from_cancel, which also perform true cancellation protection. So for code using only public API of moonbitlang/async, we can guarantee that "nocancel is equivalent to not cancellable". This is also why @async.handle_cancellation is not nocancel: it merely capture the cancellation signal, but does not really protect the code from cancellation.

Changes in this PR

  • [breaking] catch can no longer capture cancellation
  • [breaking] TaskGroup::add_defer now requires the defer block to be nocancel
  • [breaking] the resume_on_cancel parameter of @async.protect_from_cancel is now removed
  • [breaking] @async.is_cancellation_error now always return false and is deprecated
  • @async.with_cancellation_handler is deprecated, in favor of a new API @async.handle_cancellation
  • the following API are now annotated with nocancel: @async.protect_from_cancel, @fs.remove, @fs.rmdir
  • the following API are now annotated with noraise: @async.sleep, @async.CondVar::wait, @async.{Mutex,Semaphore}::acquire, @async.TaskGroup::return_immediately

- `catch` can no longer capture cancellation
- deprecate `@async.with_cancellation_handler` in favor of `@async.handle_cancellation`
- introduce `nocancel` mark on type signature
- group scoped defer block must be `nocancel` now
@Guest0x0
Guest0x0 force-pushed the new-cancellation-mechanism branch from 27dfe0c to f475a42 Compare September 10, 2026 03:08
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 1160

Coverage decreased (-0.05%) to 77.687%

Details

  • Coverage decreased (-0.05%) from the base build.
  • Patch coverage: 15 uncovered changes across 7 files (125 of 140 lines covered, 89.29%).
  • 12 coverage regressions across 10 files.

Uncovered Changes

File Changed Covered %
src/fs/watch.mbt 9 2 22.22%
src/internal/coroutine/coroutine.mbt 27 25 92.59%
src/shell/glob.mbt 8 6 75.0%
src/internal/event_loop/event_loop.mbt 12 11 91.67%
src/internal/event_loop/fs.mbt 1 0 0.0%
src/internal/event_loop/network_unix.mbt 3 2 66.67%
src/internal/event_loop/signal.mbt 1 0 0.0%
Total (20 files) 140 125 89.29%

Coverage Regressions

12 previously-covered lines in 10 files lost coverage.

File Lines Losing Coverage Coverage
src/internal/event_loop/fs.mbt 2 77.78%
src/task_group.mbt 2 92.31%
examples/websocket_echo_server/main.mbt 1 85.0%
src/async.mbt 1 83.61%
src/fs/watch_inotify.mbt 1 76.6%
src/fs/watch_windows.mbt 1 62.71%
src/internal/event_loop/event_loop.mbt 1 75.71%
src/internal/event_loop/io_windows.mbt 1 83.93%
src/internal/event_loop/network.mbt 1 80.0%
src/io/pipe.mbt 1 97.67%

Coverage Stats

Coverage Status
Relevant Lines: 5396
Covered Lines: 4192
Line Coverage: 77.69%
Coverage Strength: 11370.35 hits per line

💛 - Coveralls

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