Cancellation token - leaf module.
The cancellation token is a tiny two-atom record that lets a UI
thread (TUI, REPL caller) cooperatively abort an
in-flight turn! AND interrupt the worker future hosting the
blocking provider call. The cooperative side is checked at every
iteration boundary; the future side hard-cancels any HTTP call
that has already started.
Public API:
(cancellation-token) - fresh token
(cancellation-atom token) - cooperative flag atom (pass to turn!)
(cancellation-set-future! token fut) - register the worker future
(cancel! token) - set flag + interrupt registered future
(cancel! token reason) - same, stamping WHO cancelled
(cancel-reason token) - the recorded origin of the cancel
(cancelled? token) - true once cancel! has been called
(cancellation? throwable) - true if exception was caused by cancel!
(preserve-interrupt! t) - re-arm the interrupt flag a catch-all ate
This namespace has zero side effects at load time and depends only on Java interop - channels and the runtime can require it directly without pulling in the rest of the SDK.
Cancellation token - leaf module. The cancellation token is a tiny two-atom record that lets a UI thread (TUI, REPL caller) cooperatively abort an in-flight `turn!` AND interrupt the worker future hosting the blocking provider call. The cooperative side is checked at every iteration boundary; the future side hard-cancels any HTTP call that has already started. Public API: `(cancellation-token)` - fresh token `(cancellation-atom token)` - cooperative flag atom (pass to `turn!`) `(cancellation-set-future! token fut)` - register the worker future `(cancel! token)` - set flag + interrupt registered future `(cancel! token reason)` - same, stamping WHO cancelled `(cancel-reason token)` - the recorded origin of the cancel `(cancelled? token)` - true once `cancel!` has been called `(cancellation? throwable)` - true if exception was caused by `cancel!` `(preserve-interrupt! t)` - re-arm the interrupt flag a catch-all ate This namespace has zero side effects at load time and depends only on Java interop - channels and the runtime can require it directly without pulling in the rest of the SDK.
(cancel! token)(cancel! token reason)Abort the in-flight turn. Flips the cooperative flag and runs every registered
on-cancel! callback exactly once. Each callback is wrapped in its own
try/catch so one bad consumer cannot starve the rest.
The callback list is atomically drained before callbacks run. Repeated stops from a user, watchdog, or shutdown therefore preserve the first reason without re-entering a turn's terminal/unwind path.
reason names the ORIGIN (:client-cancel-turn, :stall-watchdog,
:gateway-shutdown, …). The first one wins and is read back with
cancel-reason. The one-arity call records :unspecified.
Abort the in-flight turn. Flips the cooperative flag and runs every registered `on-cancel!` callback exactly once. Each callback is wrapped in its own `try`/`catch` so one bad consumer cannot starve the rest. The callback list is atomically drained before callbacks run. Repeated stops from a user, watchdog, or shutdown therefore preserve the first reason without re-entering a turn's terminal/unwind path. `reason` names the ORIGIN (`:client-cancel-turn`, `:stall-watchdog`, `:gateway-shutdown`, …). The first one wins and is read back with [[cancel-reason]]. The one-arity call records `:unspecified`.
(cancel-reason token)Why this token was cancelled — the reason of the FIRST cancel! call,
or nil when it was never cancelled (or came from a hand-built token).
Cancellation is the one turn outcome with no stack trace worth reading: the loop only sees an interrupt. Callers log this so a post mortem can tell a user stop from the daemon cancelling its own turn.
Why this token was cancelled — the `reason` of the FIRST `cancel!` call, or nil when it was never cancelled (or came from a hand-built token). Cancellation is the one turn outcome with no stack trace worth reading: the loop only sees an interrupt. Callers log this so a post mortem can tell a user stop from the daemon cancelling its own turn.
(cancellation-atom token)Cooperative flag atom — read with @ at iteration boundaries when
the consumer can return without external help.
Cooperative flag atom — read with `@` at iteration boundaries when the consumer can return without external help.
(cancellation-set-future! token fut)Register a worker Future so cancel! interrupts it. Thin
convenience over on-cancel!: wraps .cancel(true) in a thunk
and discards the returned dispose! (the future's own completion
makes the second cancel a no-op).
Returns the future for convenient threading.
Register a worker `Future` so `cancel!` interrupts it. Thin convenience over `on-cancel!`: wraps `.cancel(true)` in a thunk and discards the returned `dispose!` (the future's own completion makes the second cancel a no-op). Returns the future for convenient threading.
(cancellation-token)Construct a fresh cancellation token.
The token bundles three things every cancellable boundary needs:
::flag — cooperative boolean atom, polled at iteration
boundaries by callers that can return
gracefully.::callbacks — vec of [id thunk] pairs run by cancel! so
any number of in-flight workers (provider
HTTP call, Python eval future, voice recorder)
can register their own hard-cancel hook.::reason — WHO fired the cancel, stamped by cancel! and read
back with cancel-reason. Downstream every cancel
looks identical (a thread interrupt), so without
this stamp a self-inflicted cancel (stall watchdog,
shutdown) is indistinguishable from a user Esc.cancellation-set-future! (legacy single-future API) is kept for
call sites that have not migrated yet; it now routes through the
callback registry too so behaviour stays identical.
Construct a fresh cancellation token.
The token bundles three things every cancellable boundary needs:
- `::flag` — cooperative boolean atom, polled at iteration
boundaries by callers that can return
gracefully.
- `::callbacks` — vec of `[id thunk]` pairs run by `cancel!` so
any number of in-flight workers (provider
HTTP call, Python eval future, voice recorder)
can register their own hard-cancel hook.
- `::reason` — WHO fired the cancel, stamped by `cancel!` and read
back with `cancel-reason`. Downstream every cancel
looks identical (a thread interrupt), so without
this stamp a self-inflicted cancel (stall watchdog,
shutdown) is indistinguishable from a user Esc.
`cancellation-set-future!` (legacy single-future API) is kept for
call sites that have not migrated yet; it now routes through the
callback registry too so behaviour stays identical.(cancellation? e)True if the given throwable was caused by a cancel! call. Channels
should treat this as a normal (cancelled) outcome rather than an
error and avoid showing stack traces.
True if the given throwable was caused by a `cancel!` call. Channels should treat this as a normal (cancelled) outcome rather than an error and avoid showing stack traces.
(cancelled? token)True once cancel! has been called on this token.
True once `cancel!` has been called on this token.
(on-cancel! token thunk)Register a no-arg thunk to fire the moment cancel! is invoked
on token. Returns a dispose! thunk the caller MUST invoke when
the cancellable work finishes normally — otherwise callbacks
accumulate for the token's lifetime.
If cancel! has already fired on this token, thunk runs
synchronously here and dispose! is a no-op. This matches the
contract every consumer wants: registering AFTER cancellation
must still cancel, not silently swallow the request.
Replaces the atom-watch pattern earlier eval boundaries hand-rolled: one shared callback list, no per-consumer add-watch / remove-watch plumbing, no risk of leaving a watch on the flag after the worker finishes.
Register a no-arg `thunk` to fire the moment `cancel!` is invoked on `token`. Returns a `dispose!` thunk the caller MUST invoke when the cancellable work finishes normally — otherwise callbacks accumulate for the token's lifetime. If `cancel!` has already fired on this token, `thunk` runs synchronously here and `dispose!` is a no-op. This matches the contract every consumer wants: registering AFTER cancellation must still cancel, not silently swallow the request. Replaces the atom-watch pattern earlier eval boundaries hand-rolled: one shared callback list, no per-consumer add-watch / remove-watch plumbing, no risk of leaving a watch on the flag after the worker finishes.
(preserve-interrupt! t)Re-arm THIS thread's interrupt flag when t is, or wraps, an
InterruptedException, and answer whether it did.
The JVM CLEARS the flag as it throws, so a best-effort
(catch Throwable _ ...) around anything that BLOCKS -- .waitFor on a
subprocess, Thread/sleep, an unbounded deref -- silently EATS the
cancellation: cancel! interrupted the turn, the handler answered its
fallback value, and the thread polled on to its own deadline as if nothing
had happened. Every catch-all that can see a blocking call runs this FIRST:
the fallback value still stands (the measurement, the git output, the token
really is unavailable), while the cancellation survives to the next
interruptible call, which throws at once.
CancellationException is deliberately NOT re-armed -- some OTHER future was
cancelled; this thread was never interrupted, and pretending otherwise would
abort work nobody asked to stop.
Re-arm THIS thread's interrupt flag when `t` is, or wraps, an `InterruptedException`, and answer whether it did. The JVM CLEARS the flag as it throws, so a best-effort `(catch Throwable _ ...)` around anything that BLOCKS -- `.waitFor` on a subprocess, `Thread/sleep`, an unbounded `deref` -- silently EATS the cancellation: [[cancel!]] interrupted the turn, the handler answered its fallback value, and the thread polled on to its own deadline as if nothing had happened. Every catch-all that can see a blocking call runs this FIRST: the fallback value still stands (the measurement, the git output, the token really is unavailable), while the cancellation survives to the next interruptible call, which throws at once. `CancellationException` is deliberately NOT re-armed -- some OTHER future was cancelled; this thread was never interrupted, and pretending otherwise would abort work nobody asked to stop.
(virtual-threads-available?)True when this JVM exposes Java virtual-thread APIs. Reflection keeps source compatible with older runtimes.
True when this JVM exposes Java virtual-thread APIs. Reflection keeps source compatible with older runtimes.
(worker-future f)(worker-future name f)(worker-future name f {:keys [platform?]})Run f on a cancellable worker Future. Uses a virtual thread when the JVM
supports it, otherwise falls back to a named daemon platform thread.
Pass {:platform? true} for work that can pin a virtual-thread carrier in
native or uninterruptible code. A platform worker costs one thread but cannot
starve the virtual-thread scheduler that runs ordinary lightweight work.
The returned value implements java.util.concurrent.Future plus Clojure
deref/realized? protocols so legacy future call sites can migrate without
losing timeout/cancellation behavior.
Run `f` on a cancellable worker Future. Uses a virtual thread when the JVM
supports it, otherwise falls back to a named daemon platform thread.
Pass `{:platform? true}` for work that can pin a virtual-thread carrier in
native or uninterruptible code. A platform worker costs one thread but cannot
starve the virtual-thread scheduler that runs ordinary lightweight work.
The returned value implements java.util.concurrent.Future plus Clojure
deref/realized? protocols so legacy `future` call sites can migrate without
losing timeout/cancellation behavior.(worker-runtime)Runtime probe for worker execution. :worker-helper is stable metadata for
diagnostics; :virtual-threads? reports whether new worker tasks will use
Java virtual threads.
Runtime probe for worker execution. `:worker-helper` is stable metadata for diagnostics; `:virtual-threads?` reports whether new worker tasks will use Java virtual threads.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |