cancellation: Hook up libuv to cancellation - #62557
Conversation
62d1d0c to
2077a08
Compare
| // path (they stay registered across parks and are collected by walks) - | ||
| // interior links are rewritten only under `walk_lock`. See the | ||
| // registration protocol in base/cancellation.jl. | ||
| _Atomic(jl_value_t*) waiters_head; // Union{Nothing, Base.WaitEntry2} |
There was a problem hiding this comment.
Not required to be WaitEntry2 - any WaitEntry will do.
| // Serializes cancellation/prune walks against each other (never taken | ||
| // by registration or wakeup); attachment and state stay lock-free (see | ||
| // the concurrency notes above). | ||
| _Atomic(uint8_t) walk_lock; |
There was a problem hiding this comment.
Needs a lock yes, but should be a sleeping lock
| // Registrations pushed since the last walk (approximate); a pusher that | ||
| // trips the threshold runs a pruning walk to unlink registrations of | ||
| // completed tasks. | ||
| _Atomic(uint32_t) push_count; |
There was a problem hiding this comment.
Reasonable, but we can just keep track of how many dead ones there actually are.
| // cancellable park under a source pays a registration. Owned by this | ||
| // task; kept separate from `cached_wait_entry` so that an entry linked | ||
| // on a source is only ever armed for waits governed by that source. | ||
| jl_value_t *cached_cancel_entry; |
There was a problem hiding this comment.
Just always use the WaitEntry2, we don't need two separate caches
| write(io::IO, s::Union{String,SubString{String}}) = | ||
| GC.@preserve s (unsafe_write(io, pointer(s), reinterpret(UInt, sizeof(s))) % Int)::Int | ||
| write(io::IO, s::Union{String,SubString{String}}; cancel::CancelTokenArg=DEFAULT_CANCEL) = | ||
| _with_cancel_arg(() -> GC.@preserve(s, (unsafe_write(io, pointer(s), reinterpret(UInt, sizeof(s))) % Int)::Int), cancel) |
There was a problem hiding this comment.
I don't like this _with_cancel_arg pattern. The token should be resolved once at the highest entry point and then just passed through as the kwarg so the inner code doesn't have to look at the ScopedValue again.
| # scanning for its own identity (`_find_slot`), so an entry may be | ||
| # registered on several waitables at once (wait-any). At most one slot per | ||
| # owner per entry. | ||
| _nslots(w::WaitEntry1) = 1 |
There was a problem hiding this comment.
Let's have a WaitSlotRef{T<:Union{WE1,WE2,WEN}} with getproperty/setproperty for owner/next/aux and treat the WaitEntry as AbstractVector{WaitSlotRef{WE}}. I think that will make the code read better.
| uv_error("shutdown", err) | ||
| end | ||
| ct = current_task() | ||
| if abandoning_external_waits() |
There was a problem hiding this comment.
All of these need to be tied to the cancellation src
| iolock_end() | ||
| checkcancel(src) | ||
| end | ||
| if abandoning_external_waits() |
There was a problem hiding this comment.
If we're already canceled, it doesn't make sense to issue the write
| catch err | ||
| # (catch restored the sigatomic level from the try entry; the | ||
| # teardown helper unwinds it) | ||
| _uv_write_cancelled_teardown!(s, w, uvw, err, src, ct, owner) |
There was a problem hiding this comment.
I don't like doing this via try/catch. Cancellation is not necessarily an unexpected situation. Also, for write in particular, the caller needs to be able to handle partial writes, so we can't throw here.
1aa0b03 to
330b0d2
Compare
|
This is getting there, I think. |
|
I think it's basically ready to go, but we probably need to pkgeval it, since I'm assuming packages are reaching into the wait internals in all sorts of horrible ways. |
|
@pkgeval |
|
@Keno: run Full report: https://pkgeval-reports.julialang.org/?run=gh-5173963448 |
Give every CancellationTokenSource the storage for a registry of
parked-waiter registrations (Base.WaitEntry cancellation slots), through
which the cancellation walk will find and wake tasks blocked under the
source: a lock-free intrusive LIFO (`waiters_head`), a sleeping
`walk_lock` (a lazily-installed ReentrantLock - sources that are never
walked stay small) that serializes only walks (delivery, pruning,
owner-side unregistration) against each other - no park or wakeup path
ever takes it - and a `dead_count` of collectable registrations feeding
the prune heuristic. Like attachment and state, registration is
lock-free.
The variable-sized "many" wait-entry kind (`Core.WaitEntryN`) joins the
runtime alongside it: `nslots` uniform {owner, next, aux} wait slots
following the fixed fields, with the same variable-sized-object treatment
as the source (small-typeof tag, special-cased stock-GC marking and MMTk
scanning, reset-on-write image serialization, egal-by-identity) plus
slot accessors for the Julia side (slot owners are atomic, accessed
relaxed: identity scans read them from threads that do not hold the
slot's protecting lock). The 1- and 2-slot kinds stay ordinary Julia
structs; this kind serves wait-any over arbitrarily many waitables.
The new fixed fields ride along through the full variable-sized-layout
discipline: the Julia-visible field table (jltypes.c), system-image
serialization (which resets the transient runtime state on write), the
stock GC's special-cased marking (the head slot is strong), and the MMTk
object scanner.
The generic runtime knows the new kind where variable-sized objects need
special-casing: `deepcopy`/`Serialization` reconstruct through the
allocator (a generic fixed-size copy would corrupt the heap),
`Core.sizeof`/`summarysize` account the slot tail, the serialization
queue skips the transient fields, and the allocator rejects slot counts
whose allocation size could overflow.
The task side gains a second per-task entry cache
(`Task.cached_cancel_entry`) alongside the existing plain-park cache:
plain (shielded) and cancellable parks arm distinct entries, because an
entry registered on a source's waiter list must never be armed for a
wait that is not cancellable under it - the walk's expected-entry claim
CAS is its only sound eligibility gate (see the delivery commit).
This is purely the storage layer; the registration protocol and the
delivery walk that consume it land separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wire the cancellation walk up to blocked tasks, and make every wait
registration uniform over the number of things it waits for. An entry is
a set of {owner, next, aux} slots - `owner` doubling as the "am I
registered, and on what" membership witness - and comes in kinds by slot
count: the 1-slot `WaitEntry1` serves plain parks, cancellable parks use
the 2-slot `WaitEntry2` (whose second slot registers the wait on the
governing source's waiter list, with the minimum delivery severity in
its aux word), and `Core.WaitEntryN` covers wait-any over arbitrarily
many waitables. The Julia kinds are defined mutually recursively in a
`typegroup`; `WaitEntry` becomes the union of all kinds, and every list
algorithm works through the slot accessors: lists link whole entries,
and a traversal locates its slot in each entry by scanning for its own
identity, so one entry can be registered on several waitables at once.
To give traversals that identity, each wait queue's identity is now
canonically the condition itself (`waitqueue`); the `waitee` relaying
that used to thread through `wait`/`_wait2` is gone.
`cancel!` claims and wakes registered waiters by scheduling the
CancellationRequest as an exception, following the same wake-claim
protocol as `notify` and interrupters. The walk's deferred wakes are
claim-scoped (`deliver_claimed_wake!`): the claim CAS was the wake
ticket, so the delivery never re-claims the way an interrupter's
unconditional swap does, and it is dropped when the task re-registered
meanwhile - a still-eligible re-park is refused by its own registration
recheck, and anything else must not observe the request. For the same
reason the interrupted-wait cleanup drops a claimed-and-enqueued wake
only *after* its relock: the lock round-trip serializes against a
notifier's in-flight claim-and-schedule, making the drop deterministic
(the uv request teardown does the same under the iolock).
The registration is sticky, and every hot path is lock-free, extending
the wait protocol's lazy-corpse philosophy to the source list:
- The first cancellable park under a source publishes the entry with a
CAS push and closes the arm-vs-cancel race by re-reading the state
afterwards - the same seq_cst publish-then-recheck dance the source
already uses for lock-free child attachment. Registration on an
already-cancelled source is refused: the waiter throws instead of
parking.
- A normal wakeup does no registry work at all, and a repeat park
under the same source re-arms the already-registered entry with no
shared-memory operation. `min_severity` admits teardown waits that
re-park until a severity escalation; the re-park is a plain re-arm.
- Only walks rewrite links, serialized by the source's sleeping
`walk_lock` (which no park or wakeup takes; the acquire is shielded,
since a walk may deliver the very cancellation governing the walking
task): the cancellation walk collects the registrations of completed
tasks as it delivers, and dead registrations - counted where they
die, at entry retirement and task teardown - trigger the same
collection as a prune on sources that never get cancelled. The one
eager unregistration left is owner-side rebinding of the task's
cached entry when its governing token changes.
- Plain (shielded) and cancellable parks arm distinct cached entries
(a WaitEntry1 never registered on any source, and the WaitEntry2
carrying the sticky source slot). This is what keeps expected-entry
claims sound: an entry linked on a source is only ever armed for
waits cancellable under it, so the walk's claim CAS structurally
cannot land on a shielded park. Eligibility data outside the claim
word - the aux severity floors - is read racily by the walk and can
be judged against an adjacent arm of the same entry; that misfire is
tolerable for a teardown wait's floor (a spurious below-floor wake
into a wait that already handles interruption conservatively), never
for a shield, which is why shields get their own entry identity
rather than a staged floor.
The wait layer resolves the governing token from the new `cancel`
keyword argument of `wait(::GenericCondition)` (defaulting to the scoped
`CANCEL_TOKEN`, with `nothing` making a wait non-cancellable), and the
core blocking primitives pass it through: task waits, `lock`,
`sleep`/Timer/AsyncCondition waits. Cleanup paths that must not be
interrupted shield themselves - the contended-lock reacquire after a
delivered cancellation, handle close waits, and the Timer/AsyncCondition
callback tasks that own their handle's lifetime.
Cancellation of *running* tasks (asynchronous delivery to compiled
cancellation points) is not part of this change; `cancel!` currently
wakes parked waiters only, and an ABANDON_ALL request also delivers by
interruption until the freeze/escalation machinery lands.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make blocked stream writes and closewrite (shutdown) waits cancellable.
The waiter parks through its WaitEntry (the uv request's data field now
points at the entry rather than the task), registered on the governing
source like any other cancellable wait; the completion callbacks claim
the wake through the standard protocol, so a cancellation and a
completion race safely.
A cancelled write asks libuv to cancel the request (`uv_cancel`, new in
the bundled libuv along with `uv_write_nwritten`). For SAFE severities
the waiter then re-parks (woken only by a severity escalation) until the
completion callback reports the partial write count, so the caller's
buffer is provably no longer in use by the OS when the wait unwinds. At
the abandoning severities - and whenever the waiter cannot wait for the
callback - the request is instead *detached*: the completion callback
owns freeing it, and the written buffer's Julia owner is kept rooted in
a registry keyed by the request until then. Shutdown requests cannot be
cancelled; an interrupted closewrite wait always detaches. Under an
already-ABANDON_EXTERNAL-cancelled scope, writes and shutdowns are
issued fire-and-forget and stream close waits do not park at all.
The owner-carrying entry points (`write(::LibuvStream, ::Vector{UInt8})`
and the internal `_uv_write_owned`/`_unsafe_write_owned` chain) pass the
buffer positionally with `@nospecialize` so the chain stays statically
resolvable for trimmed builds; raw-pointer `unsafe_write` keeps its
pointer-validity contract and detaching it retains the pre-existing
hazard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Give the documented blocking operations an explicit `cancel` keyword argument: the generic and libuv-backed IO read/write surface, `Channel` operations, `Base.Event`, `Semaphore`, `lock(f, ::ReentrantLock)`, process running and waiting, `waitany`/`waitall`, `Experimental.wait_with_timeout`, and the blocking entry points of Sockets (connect/accept/recv/recvfrom/send and the getaddrinfo family) and FileWatching. The keyword defaults to a sentinel meaning "use the scoped `CANCEL_TOKEN`", so the fast paths pay nothing; a token is resolved once, at the operation's entry point - an explicit one is checked there, before the operation has any side effect - and passed through explicitly: positionally on internal chains (which keeps them statically resolvable for trimmed builds), or as the `cancel` keyword of inner operations that take one. Where a composite operation crosses generic, user-extensible methods without a `cancel` keyword, the explicit token gates between the steps (`@cancel_check tok`) and the inner parks are governed by the ambient scope; the scoped default therefore behaves identically everywhere, while an explicit token governs exactly the waits it is threaded into. `cancel = nothing` makes an operation explicitly non-cancellable, shadowing an outer token. The DNS lookups (getaddrinfo/getnameinfo) and UDP sends convert from raw task-pointer request ownership to the WaitEntry protocol, with cancelled lookups handed back to libuv via `uv_cancel` and uncancellable in-flight requests detached to their completion callback (keeping a UDP send's message rooted until then). Process spawns check for cancellation before the child is created. `_atexit` now disarms a stale wait registration and runs the exit hooks in a shielded scope, since a pending cancellation of the exiting task's scope is moot and would only disrupt teardown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the waiter-task-per-task machinery of `waitany`/`waitall` (one spawned task plus an unbounded Channel per call) with a single `Core.WaitEntryN` carrying one slot per waited task plus a slot for the governing cancellation source. Each completion notify claims the entry through the standard wake-claim protocol; the entry stays registered on the still-pending tasks across re-parks, so `waitall` re-arms it instead of re-registering after every completion, and cancellation of the current scope interrupts the wait through its source slot like any other cancellable park. Duplicate tasks share their slot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ords Add the delivery-side cancellation tests: the waiter-registry protocol (sticky registration and re-arm, shield staging on the shared cached entry, token-migration rebinding, pruning of completed tasks' and retired single-use entries, registration racing cancellation, and multi-slot wait-any entries), cancellation of parked waits (sleep/Channel/task/lock/condition and the stdlib waits), level-triggered redelivery and shielding, `min_severity` teardown re-parks, blocked stream write and closewrite cancellation across the severities (cancelled writes return their partial byte count, with delivery at the next cancellation point), and the `cancel` keyword argument surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every parked wait now runs one driver (base/park.jl): check the wait conditions, acquire the entry, arm, enqueue into all the wait queues, recheck, suspend. The driver is generic over a flat iterable of *waitables* - an open protocol (`wait_enqueue!`/`wait_recheck`/ `wait_fired`/`wait_dequeue!`, plus release/reacquire brackets around the suspend) whose dispatch is carried by each call site's concrete container type. The cancellation source is an ordinary waitable: `register_cancellation!`'s halves become `SourceWait`'s enqueue (the sticky lock-free push, or the seq_cst re-arm fence) and recheck (the seq_cst state read closing the arm-vs-cancel race), and the refusal is its throwing fired outcome under the driver's generic fired path (recheck -> self-claim -> withdraw -> deliver). Entry acquisition is the cache contract: the two canonical shapes (sole waitee; waitee + source) reuse the task's cached entries, and any other waitable set gets a fresh, single-use entry - which is exactly what makes specific-wait wakers sound, since entry identity scopes their expected-entry claim CAS to the wait it was created for. Converted onto the driver: condition waits (`wait(c; cancel)`), the lock slow path (`wait_no_relock`, the driver's no-relock policy), the standard libuv request waits (`UvReqWait`, whose dequeue is the request ownership handoff), and `waitany`/`waitall` - per-task `DoneWait`s whose enqueue declines when the task is already done and whose recheck is membership-qualified (done AND the slot witness still set), so the `repark!` loop can own the arm while the caller's bookkeeping runs unarmed, and leaving a wait is a plain `withdraw!` with no disarm. `Experimental.wait_with_timeout` becomes a park over a `TimeoutWait` deadline whose enqueue (running after the arm, where the old code had to place its spawn by hand) starts the claimer; the fresh-entry contract replaces its bespoke single-use-entry reasoning, and `_wait2`'s `entry` escape hatch is gone. The interrupted-wait cleanup - disarm, reacquire, unlink, then the deterministic pending-wake drop - now exists exactly once, in the driver. The blocked-write path (`_uv_write_wait`) intentionally stays a manual composition of the protocol: its cancelled outcome resolves the in-flight request per severity rather than throwing a refusal, which is a policy the generic fired path deliberately does not model. The driver is the parker process of the TLA+ model of the claim protocol (tla/WaitClaim.tla in the design records): its control flow, the registration Dekker, shield integrity under the split entry caches, and the multi-wait loop's rechecks are model-checked. This commit was written with the assistance of generative AI (Claude). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A cancelled `write` previously returned its partial byte count, with the CancellationRequest left to the caller's next cancellation point - the interrupted-POSIX-write shape. Most `write` callers are not prepared to observe short counts, so that shape is now opt-in: `write` (and `unsafe_write`) throw the CancellationRequest once the in-flight request has been resolved for the delivered severity (SAFE awaits the completion callback, so the buffer is provably out of OS hands; abandoning severities detach the request with the buffer kept rooted); bytes already accepted stay written. The new exported generic `writepartial` carries the old contract - it returns the number of bytes the stream accepted and leaves the (level-triggered) delivery to the next cancellation point - and falls back to `write` for IO types whose writes cannot block on cancellable resources. `flush` (and the buffered-write flush splice) still never discards: a cancelled flush write is issued with partial semantics internally, the unwritten tail is spliced back into the send buffer for a later flush to retry, and the cancellation is then thrown. This commit was written with the assistance of generative AI (Claude). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The `cancel` keyword docstrings reference the cancellation types and constants, but the API had no manual section, so Documenter's cross-reference check failed on every rendered `[`CancellationToken`](@ref)` and `[`CancellationRequest`](@ref)`. Add a Cancellation section to the tasks page documenting the token-source API, and list `writepartial` beside `write`. Fixes the docs step of https://buildkite.com/julialang/julia-pr/builds/1118 This commit was written with the assistance of generative AI (Claude). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rotocol The write specialist composed the source registration through the retired `Bool`-returning helper. Both of its uses know their severity floor explicitly (0x00 for the first park; the escalated teardown floor for the re-park), so they spell the protocol directly - wait_enqueue!, wait_recheck, and the self-claim - and the helper's merged shape is gone along with its name. This commit was written with the assistance of generative AI (Claude). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The driver's fired path carried a throwing/returning kind trait (`wait_fired_throws`) so a fired source could throw its refusal from inside `park!`. That distinction does not pull its weight: the driver now uniformly self-claims, dequeues, and returns `nothing`, and the caller re-derives what happened from its own conditions - for a source-governed park, the same `checkcancel` its entry check already makes, so the refusal throw lives at the call site, symmetric with the entry check. `wait_fired`, the trait, the REFUSED why-code and the refusal-delivery helper are gone. Who dequeues on fired is the caller-lifecycle split rather than a kind property: the one-shot/owning form dequeues every registration under the still-held phase-4 protection (a cached entry left linked would corrupt its reuse), while the multi-wait loop dequeues the fired slot only and owns the rest; its caller gates each iteration on `iscancelled` and withdraws before delivering. One sharpening rides along: a nothing-valued notify consumed under an already-cancelled source now delivers at the wait itself - a wait is a cancellation point, and delivery is level-triggered either way. This commit was written with the assistance of generative AI (Claude). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`park!(ws, w, first)` is now a Bool - phases 3-5 only: arm, enqueue, recheck, with `true` meaning parked (a wake is in flight; suspend through `wait_safe_interrupt` to consume it) and `false` meaning a waitable fired and the self-claim won. The driver takes and releases no locks: lock choreography is plain caller code, so the release/reacquire protocol hooks, the relock policy, and the fired-dequeue policy flag are gone, and the wake-payload ambiguity is structural rather than sentinel-patched - a payload (any value, `nothing` included) only ever comes out of `wait_safe_interrupt`. `wait_safe_interrupt` is the only legal way to suspend on an armed park: it consumes one wake, and on an exceptional resume runs the whole interrupted cleanup - disarm, cache-blank, per-kind *self-protecting* dequeues whose lock round-trips serialize in-flight claimers, then the deterministic pending-wake drop, then cache restore/retire - before rethrowing. Site catches owe nothing to the protocol; they only restore their own callers' lock contracts, reacquiring shielded and only what that contract demands (`wait(c)` must exit lock-held for its callers' `finally unlock`; `wait_no_relock` and the uv sites reacquire nothing, so a cancellation unwind does not sleep on locks it does not need). Registrations always move through withdraw variants rather than raw list operations: the lazy settle is `withdraw!(ws, w, WAKE_VALUE)`, the fired branch `withdraw!(ws, w, WAKE_FIRED)` (both under the caller's held locks; `wait_dequeue!`'s cleanup/withdraw whys self-protect, shielded), and entry release rides every exit - fresh entries retire idempotently, structurally closing the leak the pruning test caught. The saved lock state is `lockstate` - "token" belongs to cancellation. This commit was written with the assistance of generative AI (Claude). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On the exceptional path out of the internal wait layer, relocking the condition's lock only for the caller's `finally unlock` to release it again was a pointless acquire/release pair - and worse, a cancellation unwind sleeping on a contended lock it is about to drop. The internal `wait(c, tok)` now throws having consumed the waiting frame's own lock level: entry-check and refusal throws `unlock` one level, and the suspend path's catch restores the *enclosing* frames' hold through the new `relockall_but_one` (an internal generic in the `unlockall`/`relockall` family that keeps the saved state opaque; a no-op for depth-1 holds, so the common unwind performs no lock operation at all, while nested holders keep their invariant levels). The saved state is called `lockstate` throughout - "token" belongs to the cancellation tokens. Internal wait sites adopt the `locked && unlock` finally idiom (the waiting frame skips its unlock after a wait-throw; enclosing frames' unconditional finallys remain correct), with sites that have real under-lock unwind work reacquiring explicitly and shielded: the Channel puts' eager-increment rollback, the LibuvStream teardowns' queue/ throttle reads, the UDP receive-stop mirror, and the polling-file- watcher timer cleanup - which also had its reacquired iolock leak past the rethrow, starving the event loop process-wide once a poll was cancelled. Converted sites: Channel operations, `Base.Event`, `Semaphore`, task `_wait`, `Process` exit waits, Timer/AsyncCondition `_trywait` and close waits, the LibuvStream/BufferStream stream waits, `wait_close`, and the Sockets `accept`/`wait_connected`/`recvfrom` and FileWatching watcher waits. The exported `wait(c::GenericCondition; cancel)` keeps its public contract - exceptional exits hold the lock - through a shim that restores one shielded level in its catch; it asserts the caller contract itself first, so a violation (not locked, or locked by another task) propagates with the lock state untouched instead of the restore acquiring a lock the caller never held. `wait_no_relock` needs no restore anywhere, per its name. Also fixes two CI breakages from the previous push: the `unlock` docstring had been orphaned onto a helper inserted between it and its function (the docs cross-reference check failed tree-wide), and `jl_new_wait_entry` was missing its `JL_CANSAFEPOINT` annotation (analyzegc). This commit was written with the assistance of generative AI (Claude). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ource The two `_wait2` variants survived every park conversion because they are not waits: they subscribe a freshly constructed, never-started task to a condition's (or a task-completion's) next notify, whose claim-and-schedule is the subscribed task's *first* schedule - a start trigger; the current task never suspends. The name now says so, and the fresh-task requirement is enforced as never started, scheduled, or armed: a task that `@async` has merely queued still arms successfully, but its own first park then collides with the foreign registration - the arm failure unwinds that wait's lock choreography from the outside and leaks the waited object's lock (a leaked cond SpinLock wedges the process at the next touch, e.g. a sleep timer's close). The internal condition wait now also consumes its frame's lock level on any throw out of the park phases, so even a future contract violation errors cleanly instead of wedging. The contract is documented at the definition: subscriptions are not parks; waits of the current task go through `park!`. Subscriptions are now governed by the *waiter's* birth cancellation source - the CANCEL_TOKEN of the scope captured at its construction: tasks inherit a cancellation source from birth, and if that source is cancelled while the task is still subscribed (or already at subscribe time), the task dies with the CancellationRequest instead of starting. Mechanically this reuses the park machinery for another task: the subscription arms the waiter's sticky cancel entry, registers it on the source with the same publish-then-recheck dance as a park's phases 4-5 (claiming back the waiter's arm on refusal), and the cancellation walk's claim-scoped delivery kills a never-started task by raising the request at start. Every current in-tree subscriber is cleanup-class and is therefore constructed shielded, preserving its behavior: the Timer/AsyncCondition callback tasks already were; the channel `bind` close hook (a bound channel must close - releasing its blocked users - even when the scope that bound it is cancelled), `errormonitor`'s reporter (a cancelled scope still gets its failure report), and the REPL teardown task gain explicit `CANCEL_TOKEN => nothing` construction. The die-with-birth-source behavior is the default contract for future subscribers, parallel to parks being cancellable by default with `cancel = nothing` as the opt-out. A sweep for machinery orphaned by the park-driver rounds found none: the registration, refusal, fired-path, cleanup and bracket helpers removed across those rounds have no remaining references. This commit was written with the assistance of generative AI (Claude). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Since the write layer routes owner-carrying writes (String/Array) directly into `_unsafe_write_owned`, the sysimage workload session no longer compiles the raw-pointer `unsafe_write` positional entries on its own, and their statements silently dropped out of the image while interactive startup still reaches them through generic IO plumbing. REPL/test/precompilation.jl then fails on every platform with exactly these two statements traced at startup. Pin them as hardcoded precompile statements, per that test's guidance. Fixes the Test-group failure in https://buildkite.com/julialang/julia-pr/builds/1136 This commit was written with the assistance of generative AI (Claude). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ests A write large enough to be split into multiple OS-sized chunk requests could not really be cancelled: our libuv cancellation dequeues the targeted request out of order, so cancelling the awaited last chunk said nothing about the earlier ones still queued against the same buffer, and a SAFE cancellation therefore drained the entire remaining write before it was honored - on a slow sink, arbitrarily long after the request. Split writes now count down their pending completion callbacks on the wait entry's witness-slot aux (all readers and writers of which run under the iolock): every chunk request points at the wait entry, intermediate completions decrement - recording the first real error, which must win the final wake over the close-induced UV_ECANCELED cascade - and only the callback that reaches zero claims and wakes the waiter. Cancellation marks the sweep on the same aux word and `uv_cancel`s every in-flight request tail-first: the chunks sit contiguously in the stream's queue, so cancelling from the tail dequeues behind the still-active head and the wire always keeps a clean prefix. The waiter owns and frees every chunk request, which makes the reported count exact - completed chunks plus the cancelled head's partial - rather than the previous "last chunk or nothing"; abandoning severities detach whatever is still in flight with the buffer kept rooted until each detached callback has run. The single-request path is unchanged (a pending count of zero). This commit was written with the assistance of generative AI (Claude). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A comments-only pass over the additions of this PR: deduplicated against the definitions, trimmed to the final design (no references to intermediate states or out-of-tree artifacts), and two stale cross-references corrected. This commit was written with the assistance of generative AI (Claude). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The OS pipe buffer's quota is advisory and grows, so the whole write can complete before the cancellation lands; the sweep then settles the completed requests in full and the reported count equals the request. Fixes the Windows Test failure in https://buildkite.com/julialang/julia-pr/builds/1167 This commit was written with the assistance of generative AI (Claude). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extracted from #60281. Extends the wait protocol from #62430 with support for multiple simultaneous wait queues and uses this to wait for both cancellation and libuv completion at the same time.