Skip to content

Architecture for robust cancellation - #60281

Closed
Keno wants to merge 104 commits into
masterfrom
kf/cancel
Closed

Architecture for robust cancellation#60281
Keno wants to merge 104 commits into
masterfrom
kf/cancel

Conversation

@Keno

@Keno Keno commented Nov 30, 2025

Copy link
Copy Markdown
Member

Introduction

This commit is a first sketch for what I would like to do for robust cancellation
(i.e. "Making ^C just work"). At this point it's more of a sketch than a real PR,
but I think I've done enough of the design for a design discussion.

The first thing I should say is that the goals of this PR is very narrowly to
make ^C work well. As part of that, we're taking a bit of a step towards
structured concurrency, but I am not intending this PR to be a full implementation
of that.

Given that some of this has been beaten to death in previous issues, I will also
not do my usual motivation overview, instead jumping straight into the implementation.
As I said, the motivation is just to make ^C work reliably at this point.

Setting the stage

Broadly when we're trying to cancel a task, it'll be in one of two broad categories:

  1. Waiting for some other operation to complete (e.g. an IO operation, another task,
    an external event, etc.). Here, the actual cancellation itself is not so difficult
    (after all the task is not running, but suspended in a somehwat well-defined place).
    However, robust cancellation requires us to potentially propagate the cancellation
    signal down the wait tree, since the operation we actually want to cancel may not
    be the root task, but may instead be some operation being performed by the task
    we're waiting on (and we'd prefer not to leak those operations and have rogue tasks
    going around performing potentially side-effecting operations).

  2. Currently running and doing some computation. The core problem is not really one of
    propagation (after all the long-running computation is probably what we're wanting
    to cancel), but rather how to do the cancellation without state corruption. A lot of
    the crashiness of our existing ^C implementation is just that we would simply inject
    an exception in places that are not expecting to handle it.

For a full solution to the problem, we need to have an answer for both of these points.
I will begin with the second, since the first builds upon it.

Cancellation points

This PR introduces the concept of a cancellation request and a cancellation point.
Each task has a cancellation_request field that can be set externally (e.g. by ^C).
Any task performing computation should regularly check this field and abort its
computation if a cancellation request is pending.

For this purpose, the PR provides the @cancel_check macro. This macro turns a pending
cancellation request into a well-modeled exception. Package authors should insert a
call to the macro into any long-running loops. However, there is of course some overhead
to the check and it is therefor inappropriate for tight inner loops.

We attempt to address this with compiler support. Note that this part is currently
incompletely implemented, so the following describes the design rather than the current
state of the PR. Consider the cancel_check macro:

macro cancel_check()
    quote
        local req = Core.cancellation_point!()
        if req !== nothing
            throw(conform_cancellation_request(req))
        end
    end
end

where cancellation_point! is a new intrinsic that defines a cancellation point. The
compiler is semantically permitted to extend the cancellation point across any following
effect_free calls (note for transitivity reasons, the effect is not exactly the same,
but is morally equivalent). Upon passing a cancellation_point!, the system will
set the current task's reset_ctx to this cancellation point. If a cancellation request
occurs before the reset_ctx is cleared, the task's execution will be reset to the
nearest cancellation point. I proposed this mechanism in #52291.

Additionally, the reset_ctx can in principle be used to establish scoped cancellation
handlers for external C libraries as well although I suspect that there are not many
C libraries that are actually reset safe in the required manner (since allocation is not).

Note that cancellation_point! is also intended to be a yield point in order to faciliate
the ^C mechanism described below. However, this is not currently implemented.

Structured cancellation

Turning our attention now to the first of the two cases mentioned above, we tweak the task's
existing queue reference to become a generic (atomic) "waitee" reference. The queue is
required to be obtainable from with object via the new waitqueue generic function.
To cancel a waiter waiting for a waitable waitee object, we

  1. Set the waiter's cancellation request
  2. Load the waitee and call a new generic function cancel_wait!,
    which shall do whatever synchronization and internal bookkeeping is
    required to remove the task from the wait-queue and then resumes the
    task.
  3. The waiter resumes in the wait code. It may now decide how and whether to
    propagate the cancellation to the object it was just waiting on. Note that
    this may involve re-queing a wait (to wait for the cancellation of waitee
    to complete).

The idea here is that this provides a well-defined context for cancellation-propagation
logic to run. I wanted to avoid having any cancellation propagation logic run in parallel
with actual wait code.

How the cancellation propagates is a bit of a policy question and not one that I fully
intend to address in this PR. My plan is to implement a basic state machine that works
well for ^C (by requesting safe cancellation immediately and then requesting increasingly
unsafe modes of cancellation upon timeout or repeated ^C), but I anticipate that external
libraries will want to create their own cancellation request state machines, which the
system supports. The implementation is incomplete, so I will not describe it here yet.

One may note that there are a significant number of additional fully dynamic dispatches
in this scheme (at least waitqueue and cancel_wait! and possibly in the future).
However, note that these dynamic dispatches are confined to the cancellation path, which
is not throughput-sensitive (but is latency sensitive).

^C handling

The handling of ^C is delegated to a dedicated task that then gets notified from the
signal handler when a SIGINT is received (similar to the existing profile listener)
task. There is a little bit of an additional wrinkle in that we need some logic to
kick out a computational-task to its nearset cancellation point if we do not have
any idle threads. This logic is not yet implemented.

Examples to try

julia> sleep(1000)
^CERROR: CancellationRequest: Safe Cancellation (CANCEL_REQUEST_SAFE)
Stacktrace:
 [1] macro expansion
   @ ./condition.jl:134 [inlined]
 [2] _trywait(t::Timer)
   @ Base ./asyncevent.jl:195
 [3] wait
   @ ./asyncevent.jl:204 [inlined]
 [4] sleep(sec::Int64)
   @ Base ./asyncevent.jl:322
 [5] top-level scope
   @ REPL[1]:1

julia> collatz(n) = (n & 1) == 1 ? (3n + 1) : (n÷2)
collatz (generic function with 1 method)

julia> function find_collatz_counterexample()
          i = 1
          while true
             j = i
             while true
                @Base.cancel_check 
                j = collatz(j)
                j == 1 && break
                j == i && error("$j is a collatz counterexample")
             end
             i += 1
          end
       end
find_collatz_counterexample (generic function with 1 method)

julia> find_collatz_counterexample()
^CERROR: CancellationRequest: Safe Cancellation (CANCEL_REQUEST_SAFE)
Stacktrace:
 [1] macro expansion
   @ ./condition.jl:134 [inlined]
 [2] find_collatz_counterexample()
   @ Main ./REPL[2]:6
 [3] top-level scope
   @ REPL[3]:1

julia> wait(@async sleep(100))
^CERROR: TaskFailedException
Stacktrace:
 [1] wait(t::Task; throw::Bool)
   @ Base ./task.jl:367
 [2] wait(t::Task)
   @ Base ./task.jl:360
 [3] top-level scope
   @ REPL[4]:0
 [4] macro expansion
   @ task.jl:729 [inlined]

    nested task error: CancellationRequest: Safe Cancellation (CANCEL_REQUEST_SAFE)
    Stacktrace:
     [1] macro expansion
       @ ./condition.jl:134 [inlined]
     [2] _trywait(t::Timer)
       @ Base ./asyncevent.jl:195
     [3] wait
       @ ./asyncevent.jl:204 [inlined]
     [4] sleep
       @ ./asyncevent.jl:322 [inlined]
     [5] (::var"#2#3")()
       @ Main ./REPL[4]:1

julia> @sync begin
         @async sleep(100)
         @async find_collatz_counterexample()
     end
^CERROR:     nested task error: CancellationRequest: Safe Cancellation (CANCEL_REQUEST_SAFE)
    Stacktrace:
     [1] macro expansion
       @ ./task.jl:1234 [inlined]
     [2] _trywait(t::Timer)
       @ Base ~/julia-cancel/usr/share/julia/base/asyncevent.jl:195
     [3] wait
       @ ./asyncevent.jl:203 [inlined]
     [4] sleep
       @ ./asyncevent.jl:321 [inlined]
     [5] (::var"#45#46")()
       @ Main ./REPL[26]:3

...and 1 more exception.

Stacktrace:
 [1] sync_cancel!(c::Channel{Any}, t::Task, cr::Any, c_ex::CompositeException)
   @ Base ~/julia-cancel/usr/share/julia/base/task.jl:1454
 [2] sync_end(c::Channel{Any})
   @ Base ~/julia-cancel/usr/share/julia/base/task.jl:608
 [3] macro expansion
   @ ./task.jl:663 [inlined]
 [4] (::var"#43#44")()
   @ Main ./REPL[5]

As noted above, the @Base.cancel_check is not intended to be required in the inner loop.
Rather, the compiler is expected to extend the cancellation point from the start of the loop
to the entire function. However, this is not yet implemented.

I hope this will fix (more of a checklist for me at this point) #4037 #6283 #25790 #29369 #36379 #42072 #43451 #43451 #45055 #47839 #50045 #56462 #56545 #58105 #58849 #58689
Closes #49541
Part of #33248 #52291
Refs, but yet to be decided #58259 #35026 #39699

TODO

  • Audit all uses of wait()
  • Look into libuv write cancellation
  • Implement libuv write cancellation on Windows
  • Submit libuv write cancellation PR upstream (stream: Implement cancellation support for uv_write_t libuv/libuv#4966)
  • Look into BLAS cancellation punted
  • Propose BLAS cancellation upstream
  • (Maybe in a future PR) More compiler optimizations for cancellation_point!
  • Merge assymetric fences
  • (Maybe in a future PR) Optimize reset_ctx establishment speed
  • (Separate PR) Pointer-int-union optimizations
  • Implement the "unfriendly" cancellation types
  • Early interruption of inference
  • Outlined cancellation handlers

@jakobnissen

This comment has been minimized.

@jpsamaroo

Copy link
Copy Markdown
Member

Just want to say that I really like this approach! ❤️

I especially like the design of making Base.@cancel_check a point that the task can longjmp back to from effect-free code, as it (presumably) doesn't require waiting until said code finishes to hit a cancellation exit (as shown with the Collatz example). I am a bit confused how this will be implemented - will the ^C handling logic detect when a task is executing code within an effect-free region? Is the idea that reset_ctx (which is stored on the task) is cleared by the compiler once the effect-free region is left (which is also presumably a point where the task cooperatively checks for a cancellation request), and so the ^C handling logic knows when it can just forcibly suspend and reset the task via longjmp to reset_ctx?

Regarding async logic implemented in Base, what is the intended policy for whether to cancel just the waiter, or waiter+waitee? Should we expect that all async resources from Base will cancel on/within any async call, for predictability? That is to say, if we have a producer-consumer setup on a Channel, can I expect that both sides will receive an exception once/while they interact with the Channel? Similarly wondering about this for Threads.Condition, Base.Event, etc. If the answer is "yes", will there be a way to opt-out of this (in the case that the resource needs to continue operating normally to allow surrounding library logic to cancel itself)?

Regarding more than just ^C, can we expect that SIGTERM (and maybe also SIGSTOP and other fun signals) will one day invoke this logic as well? I can imagine that when trying to terminate a complex application, having the first course of action be to cancel ongoing work is conducive to a safe and expedient shutdown. We would of course want to then do the finalizer and atexit dance, which should hopefully now be able to do their jobs without concern for resources being locked or otherwise unavailable for cleanup.

Regarding timeouts and other structured cancellation, will it be possible to target a cancellation request at a particular task? I can imagine that this will avoid having to implement APIs like wait(obj; timeout), as we can just wrap the wait(obj) call with some logic that will send a cancellation request to just that task if the timeout expires before wait returns. This would also make it easy for libraries like Dagger to request arbitrary user code (running within a Dagger-launched task) to cancel when Dagger decides it's desirable (possibly without direct user input).

Aside: I do think it's worth thinking more on whether we want users to have a way to target the cancellation at a library/task/arbitrary machinery, but as mentioned, this is mostly an orthogonal concern.

@Keno

Keno commented Nov 30, 2025

Copy link
Copy Markdown
Member Author

I am a bit confused how this will be implemented - will the ^C handling logic detect when a task is executing code within an effect-free region?

Kind of. The canceler checks reset_ctx. If non-null, it sends a signal to the thread, which then longjmps to reset_ctx if still non-null and if cancellation_request is set on the currently running task.

Is the idea that reset_ctx (which is stored on the task) is cleared by the compiler once the effect-free region is left (which is also presumably a point where the task cooperatively checks for a cancellation request)

Yes

Regarding async logic implemented in Base, what is the intended policy for whether to cancel just the waiter, or waiter+waitee?

Policy decision by the async library, so I'm not really expressing a preference. For now, wait cancels the waitee and there's
wait_nocancel to opt out. In the future there could be fancier APIs for cancellation scope.

Should we expect that all async resources from Base will cancel on/within any async call, for predictability?

I was at this point not expecting cancellation to propagate through channels and conditions - rather I was expecting that it would cancel the wait on those objects and then the thrown exception might potentially cancel the expected producer in its cleanup scope - however, that's a bit of an orthogonal API design question that I don't have a strong opinion on.

Regarding more than just ^C, can we expect that SIGTERM (and maybe also SIGSTOP and other fun signals)

Maybe - I could imagine SIGTERM trying to cancel all tasks in the system simultaneously with this mechanism - I don't know if the tree-based cancellation makes sense there, but it could be useful for graceful shutdown.

Regarding timeouts and other structured cancellation, will it be possible to target a cancellation request at a particular task?

PR provides a cancel! API.

@Keno

Keno commented Nov 30, 2025

Copy link
Copy Markdown
Member Author

I guess I should have said that I want the cancellation point to be a preemption point rather than a yield point. We don't currently have that concept, so a bit of an open question whether those are different, but I wanted to be precise.

@Keno

Keno commented Dec 2, 2025

Copy link
Copy Markdown
Member Author

Capturing some slack discussion with @vtjnash. This reflects my best understanding, but @vtjnash was trying to make a larger point that I don't quite understand.

  1. Are plain wait, yield, yieldto, etc. cancellation points?

Probably not. The correctness of these functions depends on being paired with a unique schedule that resumes it and has correctness guarantees that need to be enforced at a higher level. There does not seem to be any good to way to make these automatic cancellation points.

@vtjnash provided the example ct = current_task(); t = @task(yieldto(ct); nothing); yieldto(t); wait(t). This task t cannot be canceled, because it's not at a cancellation point.

  1. Are locks cancellation points?

I think we need to have both versions, with the user selecting the appropriate one.

  1. How is the cancellation guaranteed without seq_cst on the waitee field (which would be expensive).

I think some variant of the following works:

Cancelling thread:

while (true)
jl_atomic_store_relaxed(&t->cancellation_request, req);
SYS_membarrier
if cancel_wait!(jl_atomic_load_acquire(&t->waitee), t)
     break
end
end

Waiting thread:

jl_atomic_store_release(&t->waitee, wait);
barrier();
if (cancelled(jl_atomic_load_relaxed(&t->cancellation_request))) throw();
wait();
  1. What happens to threads that haven't started yet.

I think this is a cancellation point and the thread dies. This is different from @async wait() because you can't put try/catch around it.

  1. Does having the waitee field prevent GC of events that will never fire?

I think it can be weakref.

  1. Libuv does not support write cancellation

Probably should be fixed, ignore for now.

  1. The term "safe cancellation" is inappropriate, because it can fail and hang, which doesn't feel very safe.

Not attached to the term. The naming was due to the possibility of introducing more unsafe cancellation variants (to be used on timeout or repeated ^C) that, while being more likely to succeed, could leave the system in an inconsistent state. Useful for looking around for debugging, but not semantically sound.

@Keno

Keno commented Dec 4, 2025

Copy link
Copy Markdown
Member Author

Now with compiler and reset_ctx support, courtesy of claude (note the absence of explicit cancellation points inside the inner loop):

julia> collatz(n) = (n & 1) == 1 ? (3n + 1) : (n÷2)
collatz (generic function with 1 method)

julia> function find_collatz_counterexample_inner()
           i = 1
           while true
               j = i
               while true
                   j = collatz(j)
                   j == 1 && break
                   j == i && return j
               end
               i += 1
           end
       end
find_collatz_counterexample_inner (generic function with 1 method)

julia> function find_collatz_counterexample2()
           @Base.cancel_check
           return find_collatz_counterexample_inner()
       end
find_collatz_counterexample2 (generic function with 1 method)

julia> find_collatz_counterexample2()
^CERROR: CancellationRequest: Safe Cancellation (CANCEL_REQUEST_SAFE)
Stacktrace:
 [1] handle_cancellation!(_req::Any)
   @ Base ./task.jl:1423
 [2] macro expansion
   @ ./condition.jl:133 [inlined]
 [3] find_collatz_counterexample2()
   @ Main ./REPL[2]:2
 [4] top-level scope
   @ REPL[3]:1

@vtjnash vtjnash added needs nanosoldier run This PR should have benchmarks run on it needs pkgeval Tests for all registered packages should be run with this change labels Dec 4, 2025
Keno added a commit that referenced this pull request Dec 4, 2025
Asymmetric atomic fences are a performance optimization of regular
atomic fences (the seq_cst version of which we expose as
`Base.Threads.atomic_fence`). The problem with these regular fences
is that they require a CPU fence instruction, which can be very
expensive and is thus unsuitable for code in the hot path.
Asymmetric fences on the other hand split an ordinary fence into
two: A `light` side where the fence is extremely cheap (only a
compiler reordering barrier) and a `heavy` side where the fence
is very expensive.

Basically the way it works is that the heavy side does a system call
that issues an inter-processor-interrupt (IPI) which then issues
the appropriate barrier instruction on the other CPU (i.e. both
CPUs will have issues a barrier instruction, one of them
just does it asynchronously due to interrupt).

The `light` and `heavy` naming here is taken from C++ PR1202R5 [1],
which is the proposal for the same feature in the C++ standard
library (to appear in the next iteration of the C++ concurrency
spec).

On the julia side, these functions are exposed as
`Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`.
The light side lowers to `fence singlethread` in llvm IR (the
Core.Intrinsic atomic_fence is adjusted appropriately to faciliate
this). The heavy side has OS-specifc implementations, where:

1. Linux/FreeBSD try to use the `membarrier` syscall or a
   fallback to `mprotect` for systems that don't have it.
2. Windows uses the `FlushProcessWriteBuffers` syscall.
3. macOS uses an implementation from the dotnet runtime
   (dotnet/runtime#44670), which
   the dotnet folks have checked with Apple does the right
   thing by happenstance (i.e. an IPI/memory barrier is needed
   to execute the syscall), but looks a little nonsensical by itself.
   However, since it's what Apple recommended to dotnet, I don't
   see much risk here, though I wouldn't be surprised if Apple added
   a proper syscall for this in the future (since freebsd has it now).

I intend to use this in #60281, but it's an independently useful
feature.

[1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf
Keno added a commit that referenced this pull request Dec 4, 2025
Asymmetric atomic fences are a performance optimization of regular
atomic fences (the seq_cst version of which we expose as
`Base.Threads.atomic_fence`). The problem with these regular fences
is that they require a CPU fence instruction, which can be very
expensive and is thus unsuitable for code in the hot path.
Asymmetric fences on the other hand split an ordinary fence into
two: A `light` side where the fence is extremely cheap (only a
compiler reordering barrier) and a `heavy` side where the fence
is very expensive.

Basically the way it works is that the heavy side does a system call
that issues an inter-processor-interrupt (IPI) which then issues
the appropriate barrier instruction on the other CPU (i.e. both
CPUs will have issues a barrier instruction, one of them
just does it asynchronously due to interrupt).

The `light` and `heavy` naming here is taken from C++ PR1202R5 [1],
which is the proposal for the same feature in the C++ standard
library (to appear in the next iteration of the C++ concurrency
spec).

On the julia side, these functions are exposed as
`Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`.
The light side lowers to `fence singlethread` in llvm IR (the
Core.Intrinsic atomic_fence is adjusted appropriately to faciliate
this). The heavy side has OS-specifc implementations, where:

1. Linux/FreeBSD try to use the `membarrier` syscall or a
   fallback to `mprotect` for systems that don't have it.
2. Windows uses the `FlushProcessWriteBuffers` syscall.
3. macOS uses an implementation from the dotnet runtime
   (dotnet/runtime#44670), which
   the dotnet folks have checked with Apple does the right
   thing by happenstance (i.e. an IPI/memory barrier is needed
   to execute the syscall), but looks a little nonsensical by itself.
   However, since it's what Apple recommended to dotnet, I don't
   see much risk here, though I wouldn't be surprised if Apple added
   a proper syscall for this in the future (since freebsd has it now).

Note that unlike the C++ spec, I have specified that
`atomic_fence_heavy` does synchronize with `atomic_fence`. This
matches the underlying system call. I suspect C++ chose to omit
this for a hypothetical future architecture that has instruction
support for doing this from userspace that would then not
synchronize with ordinary barriers, but I think I would rather
cross that bridge when we get there.

I intend to use this in #60281, but it's an independently useful
feature.

[1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf
Keno added a commit that referenced this pull request Dec 4, 2025
Asymmetric atomic fences are a performance optimization of regular
atomic fences (the seq_cst version of which we expose as
`Base.Threads.atomic_fence`). The problem with these regular fences
is that they require a CPU fence instruction, which can be very
expensive and is thus unsuitable for code in the hot path.
Asymmetric fences on the other hand split an ordinary fence into
two: A `light` side where the fence is extremely cheap (only a
compiler reordering barrier) and a `heavy` side where the fence
is very expensive.

Basically the way it works is that the heavy side does a system call
that issues an inter-processor-interrupt (IPI) which then issues
the appropriate barrier instruction on the other CPU (i.e. both
CPUs will have issues a barrier instruction, one of them
just does it asynchronously due to interrupt).

The `light` and `heavy` naming here is taken from C++ PR1202R5 [1],
which is the proposal for the same feature in the C++ standard
library (to appear in the next iteration of the C++ concurrency
spec).

On the julia side, these functions are exposed as
`Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`.
The light side lowers to `fence singlethread` in llvm IR (the
Core.Intrinsic atomic_fence is adjusted appropriately to faciliate
this). The heavy side has OS-specifc implementations, where:

1. Linux/FreeBSD try to use the `membarrier` syscall or a
   fallback to `mprotect` for systems that don't have it.
2. Windows uses the `FlushProcessWriteBuffers` syscall.
3. macOS uses an implementation from the dotnet runtime
   (dotnet/runtime#44670), which
   the dotnet folks have checked with Apple does the right
   thing by happenstance (i.e. an IPI/memory barrier is needed
   to execute the syscall), but looks a little nonsensical by itself.
   However, since it's what Apple recommended to dotnet, I don't
   see much risk here, though I wouldn't be surprised if Apple added
   a proper syscall for this in the future (since freebsd has it now).

Note that unlike the C++ spec, I have specified that
`atomic_fence_heavy` does synchronize with `atomic_fence`. This
matches the underlying system call. I suspect C++ chose to omit
this for a hypothetical future architecture that has instruction
support for doing this from userspace that would then not
synchronize with ordinary barriers, but I think I would rather
cross that bridge when we get there.

I intend to use this in #60281, but it's an independently useful
feature.

[1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf
Keno added a commit that referenced this pull request Dec 4, 2025
Asymmetric atomic fences are a performance optimization of regular
atomic fences (the seq_cst version of which we expose as
`Base.Threads.atomic_fence`). The problem with these regular fences
is that they require a CPU fence instruction, which can be very
expensive and is thus unsuitable for code in the hot path.
Asymmetric fences on the other hand split an ordinary fence into
two: A `light` side where the fence is extremely cheap (only a
compiler reordering barrier) and a `heavy` side where the fence
is very expensive.

Basically the way it works is that the heavy side does a system call
that issues an inter-processor-interrupt (IPI) which then issues
the appropriate barrier instruction on the other CPU (i.e. both
CPUs will have issues a barrier instruction, one of them
just does it asynchronously due to interrupt).

The `light` and `heavy` naming here is taken from C++ PR1202R5 [1],
which is the proposal for the same feature in the C++ standard
library (to appear in the next iteration of the C++ concurrency
spec).

On the julia side, these functions are exposed as
`Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`.
The light side lowers to `fence singlethread` in llvm IR (the
Core.Intrinsic atomic_fence is adjusted appropriately to faciliate
this). The heavy side has OS-specifc implementations, where:

1. Linux/FreeBSD try to use the `membarrier` syscall or a
   fallback to `mprotect` for systems that don't have it.
2. Windows uses the `FlushProcessWriteBuffers` syscall.
3. macOS uses an implementation from the dotnet runtime
   (dotnet/runtime#44670), which
   the dotnet folks have checked with Apple does the right
   thing by happenstance (i.e. an IPI/memory barrier is needed
   to execute the syscall), but looks a little nonsensical by itself.
   However, since it's what Apple recommended to dotnet, I don't
   see much risk here, though I wouldn't be surprised if Apple added
   a proper syscall for this in the future (since freebsd has it now).

Note that unlike the C++ spec, I have specified that
`atomic_fence_heavy` does synchronize with `atomic_fence`. This
matches the underlying system call. I suspect C++ chose to omit
this for a hypothetical future architecture that has instruction
support for doing this from userspace that would then not
synchronize with ordinary barriers, but I think I would rather
cross that bridge when we get there.

I intend to use this in #60281, but it's an independently useful
feature.

[1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf
Keno added a commit that referenced this pull request Dec 4, 2025
Asymmetric atomic fences are a performance optimization of regular
atomic fences (the seq_cst version of which we expose as
`Base.Threads.atomic_fence`). The problem with these regular fences
is that they require a CPU fence instruction, which can be very
expensive and is thus unsuitable for code in the hot path.
Asymmetric fences on the other hand split an ordinary fence into
two: A `light` side where the fence is extremely cheap (only a
compiler reordering barrier) and a `heavy` side where the fence
is very expensive.

Basically the way it works is that the heavy side does a system call
that issues an inter-processor-interrupt (IPI) which then issues
the appropriate barrier instruction on the other CPU (i.e. both
CPUs will have issues a barrier instruction, one of them
just does it asynchronously due to interrupt).

The `light` and `heavy` naming here is taken from C++ PR1202R5 [1],
which is the proposal for the same feature in the C++ standard
library (to appear in the next iteration of the C++ concurrency
spec).

On the julia side, these functions are exposed as
`Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`.
The light side lowers to `fence singlethread` in llvm IR (the
Core.Intrinsic atomic_fence is adjusted appropriately to faciliate
this). The heavy side has OS-specifc implementations, where:

1. Linux/FreeBSD try to use the `membarrier` syscall or a
   fallback to `mprotect` for systems that don't have it.
2. Windows uses the `FlushProcessWriteBuffers` syscall.
3. macOS uses an implementation from the dotnet runtime
   (dotnet/runtime#44670), which
   the dotnet folks have checked with Apple does the right
   thing by happenstance (i.e. an IPI/memory barrier is needed
   to execute the syscall), but looks a little nonsensical by itself.
   However, since it's what Apple recommended to dotnet, I don't
   see much risk here, though I wouldn't be surprised if Apple added
   a proper syscall for this in the future (since freebsd has it now).

Note that unlike the C++ spec, I have specified that
`atomic_fence_heavy` does synchronize with `atomic_fence`. This
matches the underlying system call. I suspect C++ chose to omit
this for a hypothetical future architecture that has instruction
support for doing this from userspace that would then not
synchronize with ordinary barriers, but I think I would rather
cross that bridge when we get there.

I intend to use this in #60281, but it's an independently useful
feature.

[1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf
Keno added a commit that referenced this pull request Dec 4, 2025
Asymmetric atomic fences are a performance optimization of regular
atomic fences (the seq_cst version of which we expose as
`Base.Threads.atomic_fence`). The problem with these regular fences
is that they require a CPU fence instruction, which can be very
expensive and is thus unsuitable for code in the hot path.
Asymmetric fences on the other hand split an ordinary fence into
two: A `light` side where the fence is extremely cheap (only a
compiler reordering barrier) and a `heavy` side where the fence
is very expensive.

Basically the way it works is that the heavy side does a system call
that issues an inter-processor-interrupt (IPI) which then issues
the appropriate barrier instruction on the other CPU (i.e. both
CPUs will have issues a barrier instruction, one of them
just does it asynchronously due to interrupt).

The `light` and `heavy` naming here is taken from C++ PR1202R5 [1],
which is the proposal for the same feature in the C++ standard
library (to appear in the next iteration of the C++ concurrency
spec).

On the julia side, these functions are exposed as
`Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`.
The light side lowers to `fence singlethread` in llvm IR (the
Core.Intrinsic atomic_fence is adjusted appropriately to faciliate
this). The heavy side has OS-specifc implementations, where:

1. Linux/FreeBSD try to use the `membarrier` syscall or a
   fallback to `mprotect` for systems that don't have it.
2. Windows uses the `FlushProcessWriteBuffers` syscall.
3. macOS uses an implementation from the dotnet runtime
   (dotnet/runtime#44670), which
   the dotnet folks have checked with Apple does the right
   thing by happenstance (i.e. an IPI/memory barrier is needed
   to execute the syscall), but looks a little nonsensical by itself.
   However, since it's what Apple recommended to dotnet, I don't
   see much risk here, though I wouldn't be surprised if Apple added
   a proper syscall for this in the future (since freebsd has it now).

Note that unlike the C++ spec, I have specified that
`atomic_fence_heavy` does synchronize with `atomic_fence`. This
matches the underlying system call. I suspect C++ chose to omit
this for a hypothetical future architecture that has instruction
support for doing this from userspace that would then not
synchronize with ordinary barriers, but I think I would rather
cross that bridge when we get there.

I intend to use this in #60281, but it's an independently useful
feature.

[1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf
Keno added a commit that referenced this pull request Dec 4, 2025
Asymmetric atomic fences are a performance optimization of regular
atomic fences (the seq_cst version of which we expose as
`Base.Threads.atomic_fence`). The problem with these regular fences
is that they require a CPU fence instruction, which can be very
expensive and is thus unsuitable for code in the hot path.
Asymmetric fences on the other hand split an ordinary fence into
two: A `light` side where the fence is extremely cheap (only a
compiler reordering barrier) and a `heavy` side where the fence
is very expensive.

Basically the way it works is that the heavy side does a system call
that issues an inter-processor-interrupt (IPI) which then issues
the appropriate barrier instruction on the other CPU (i.e. both
CPUs will have issues a barrier instruction, one of them
just does it asynchronously due to interrupt).

The `light` and `heavy` naming here is taken from C++ PR1202R5 [1],
which is the proposal for the same feature in the C++ standard
library (to appear in the next iteration of the C++ concurrency
spec).

On the julia side, these functions are exposed as
`Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`.
The light side lowers to `fence singlethread` in llvm IR (the
Core.Intrinsic atomic_fence is adjusted appropriately to faciliate
this). The heavy side has OS-specifc implementations, where:

1. Linux/FreeBSD try to use the `membarrier` syscall or a
   fallback to `mprotect` for systems that don't have it.
2. Windows uses the `FlushProcessWriteBuffers` syscall.
3. macOS uses an implementation from the dotnet runtime
   (dotnet/runtime#44670), which
   the dotnet folks have checked with Apple does the right
   thing by happenstance (i.e. an IPI/memory barrier is needed
   to execute the syscall), but looks a little nonsensical by itself.
   However, since it's what Apple recommended to dotnet, I don't
   see much risk here, though I wouldn't be surprised if Apple added
   a proper syscall for this in the future (since freebsd has it now).

Note that unlike the C++ spec, I have specified that
`atomic_fence_heavy` does synchronize with `atomic_fence`. This
matches the underlying system call. I suspect C++ chose to omit
this for a hypothetical future architecture that has instruction
support for doing this from userspace that would then not
synchronize with ordinary barriers, but I think I would rather
cross that bridge when we get there.

I intend to use this in #60281, but it's an independently useful
feature.

[1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf
Keno added a commit that referenced this pull request Dec 4, 2025
The `preserve_none` calling convention is a new calling convention in
clang (>= 19) and gcc that preserves a more minimal set of registers
(rsp, rbp on x86_64; lr, fp on aarch64). As a result, if this calling
convention is used with setjmp, those registers do not need to be stored
in the setjmp buffer, allowing us to reduce the size of this buffer and
use fewer instructions to save the buffer. The tradeoff of course is
that these registers may need to be saved anyway, in which case
both the stack usage and the instructions just move to the caller
(which is strictly worse). It is not clear that this is useful for
exceptions (which already have a fair bit of state anyway, so even
in the happy path the savings are not necessarily that big), but
I am thinking about using it for #60281, which has different
characteristics, so this is an easy way to try out whether there
are any unexpected challenges.

Note that preserve_none is a very recent compiler feature, so most
compilers out there do not have it yet. For compatibility, this PR
supports using different jump buffer formats in the runtime and
the generated code.
Comment thread src/codegen.cpp
return FunctionType::get(getInt32Ty(C), {}, false);
},
[](LLVMContext &C) { return AttributeList::get(C,
Attributes(C, {Attribute::ReturnsTwice}),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All stores in the function after this call but before a matching longjmp need to changed to use volatile=true if they can be observed after the longjmp

Keno added a commit that referenced this pull request Dec 5, 2025
Asymmetric atomic fences are a performance optimization of regular
atomic fences (the seq_cst version of which we expose as
`Base.Threads.atomic_fence`). The problem with these regular fences
is that they require a CPU fence instruction, which can be very
expensive and is thus unsuitable for code in the hot path.
Asymmetric fences on the other hand split an ordinary fence into
two: A `light` side where the fence is extremely cheap (only a
compiler reordering barrier) and a `heavy` side where the fence
is very expensive.

Basically the way it works is that the heavy side does a system call
that issues an inter-processor-interrupt (IPI) which then issues
the appropriate barrier instruction on the other CPU (i.e. both
CPUs will have issues a barrier instruction, one of them
just does it asynchronously due to interrupt).

The `light` and `heavy` naming here is taken from C++ PR1202R5 [1],
which is the proposal for the same feature in the C++ standard
library (to appear in the next iteration of the C++ concurrency
spec).

On the julia side, these functions are exposed as
`Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`.
The light side lowers to `fence singlethread` in llvm IR (the
Core.Intrinsic atomic_fence is adjusted appropriately to faciliate
this). The heavy side has OS-specifc implementations, where:

1. Linux/FreeBSD try to use the `membarrier` syscall or a
   fallback to `mprotect` for systems that don't have it.
2. Windows uses the `FlushProcessWriteBuffers` syscall.
3. macOS uses an implementation from the dotnet runtime
   (dotnet/runtime#44670), which
   the dotnet folks have checked with Apple does the right
   thing by happenstance (i.e. an IPI/memory barrier is needed
   to execute the syscall), but looks a little nonsensical by itself.
   However, since it's what Apple recommended to dotnet, I don't
   see much risk here, though I wouldn't be surprised if Apple added
   a proper syscall for this in the future (since freebsd has it now).

Note that unlike the C++ spec, I have specified that
`atomic_fence_heavy` does synchronize with `atomic_fence`. This
matches the underlying system call. I suspect C++ chose to omit
this for a hypothetical future architecture that has instruction
support for doing this from userspace that would then not
synchronize with ordinary barriers, but I think I would rather
cross that bridge when we get there.

I intend to use this in #60281, but it's an independently useful
feature.

[1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf
Keno added a commit to JuliaLang/libuv that referenced this pull request Dec 9, 2025
The ability to request cancellation of a pending or in-progress write
has been requested several times in libuv (at least joyent/libuv#1393 and
libuv#2051), but was not yet implemented. I am currently on a mission to
make Ctrl-C work nicely and reliably in Julia (JuliaLang/julia#60281).
Of course, this would require the ability to cancel in-progress writes
for sane semantics, so I have a renewed interest in this feature, which
this PR attempts to implement.

One primary problem with the API is that the existing callback does not
provide support for passing in the number of written bytes (since it
is expected to always complete). I see two options:

1. Create a new uv_write3 that takes a new cb type that does take this
   argument, or,
2. Create a new public `uv_write_t` field that this information can be
   read from.

This PR takes the first approach, but as it turns out the number of
bytes written needs to be stored anyway, so maybe there is not much
point to this choice.

As mentioned, we do need to store two extra bits of state (the total
number of bytes written and which kind of callback we have). To maintain
ABI compatibility we steal two pointers from the generic req_t reserve
pool. This preserves ABI compatibility, but is a bit awkward, because
it puts these new private fields into a different place than they would
otherwise be. I don't see that we have much choice though, other than
creating a completely new req_t subtype (which does not seem worth it).

The `uv_cancel` function returns `0` on success or `UV_EBUSY` if the
request was submitted using the `uv_write`/`uv_write2` API that does
not take the new callback signature (unless no bytes have been written,
in which case cancellation succeeds).

It also returns `0` if the request is already done. The thought here
was that it would be too racy to return an error code here, but I
think it would be fine to return EALREADY (although I don't know what
the caller would do with that information.

`uv_write3` also takes a flags parameter reserved for future use (just
in case).

The windows side is relatively straightforward in that the only thing
we really need to do is call `CancelIoEx` on the overlapped structure
that's already inside of our req. We do of course need the appropriate
accounting for the number of bytes written.

Discloure: Claude Code was used in the creation of this PR, although
dumb design decisions are probably by me.

I have done some minimal integrated testing of this on Linux.
On windows, I have run the included test, but have not tested
in-situ. I consider this WIP until I've had a chance to run our
full test suite on all platforms, but I wanted to make sure to
open this early for API feedback/concerns.
Keno added a commit to JuliaLang/libuv that referenced this pull request Dec 9, 2025
The ability to request cancellation of a pending or in-progress write
has been requested several times in libuv (at least joyent/libuv#1393 and
libuv#2051), but was not yet implemented. I am currently on a mission to
make Ctrl-C work nicely and reliably in Julia (JuliaLang/julia#60281).
Of course, this would require the ability to cancel in-progress writes
for sane semantics, so I have a renewed interest in this feature, which
this PR attempts to implement.

One primary problem with the API is that the existing callback does not
provide support for passing in the number of written bytes (since it
is expected to always complete). I see two options:

1. Create a new uv_write3 that takes a new cb type that does take this
   argument, or,
2. Create a new public `uv_write_t` field that this information can be
   read from.

This PR takes the first approach, but as it turns out the number of
bytes written needs to be stored anyway, so maybe there is not much
point to this choice.

As mentioned, we do need to store two extra bits of state (the total
number of bytes written and which kind of callback we have). To maintain
ABI compatibility we steal two pointers from the generic req_t reserve
pool. This preserves ABI compatibility, but is a bit awkward, because
it puts these new private fields into a different place than they would
otherwise be. I don't see that we have much choice though, other than
creating a completely new req_t subtype (which does not seem worth it).

The `uv_cancel` function returns `0` on success or `UV_EBUSY` if the
request was submitted using the `uv_write`/`uv_write2` API that does
not take the new callback signature (unless no bytes have been written,
in which case cancellation succeeds).

It also returns `0` if the request is already done. The thought here
was that it would be too racy to return an error code here, but I
think it would be fine to return EALREADY (although I don't know what
the caller would do with that information.

`uv_write3` also takes a flags parameter reserved for future use (just
in case).

The windows side is relatively straightforward in that the only thing
we really need to do is call `CancelIoEx` on the overlapped structure
that's already inside of our req. We do of course need the appropriate
accounting for the number of bytes written.

Discloure: Claude Code was used in the creation of this PR, although
dumb design decisions are probably by me.

I have done some minimal integrated testing of this on Linux.
On windows, I have run the included test, but have not tested
in-situ. I consider this WIP until I've had a chance to run our
full test suite on all platforms, but I wanted to make sure to
open this early for API feedback/concerns.
Keno added a commit to JuliaLang/libuv that referenced this pull request Dec 9, 2025
The ability to request cancellation of a pending or in-progress write
has been requested several times in libuv (at least joyent/libuv#1393 and
libuv#2051), but was not yet implemented. I am currently on a mission to
make Ctrl-C work nicely and reliably in Julia (JuliaLang/julia#60281).
Of course, this would require the ability to cancel in-progress writes
for sane semantics, so I have a renewed interest in this feature, which
this PR attempts to implement.

One primary problem with the API is that the existing callback does not
provide support for passing in the number of written bytes (since it
is expected to always complete). I see two options:

1. Create a new uv_write3 that takes a new cb type that does take this
   argument, or,
2. Create a new public `uv_write_t` field that this information can be
   read from.

This PR takes the first approach, but as it turns out the number of
bytes written needs to be stored anyway, so maybe there is not much
point to this choice.

As mentioned, we do need to store two extra bits of state (the total
number of bytes written and which kind of callback we have). To maintain
ABI compatibility we steal two pointers from the generic req_t reserve
pool. This preserves ABI compatibility, but is a bit awkward, because
it puts these new private fields into a different place than they would
otherwise be. I don't see that we have much choice though, other than
creating a completely new req_t subtype (which does not seem worth it).

The `uv_cancel` function returns `0` on success or `UV_EBUSY` if the
request was submitted using the `uv_write`/`uv_write2` API that does
not take the new callback signature (unless no bytes have been written,
in which case cancellation succeeds).

It also returns `0` if the request is already done. The thought here
was that it would be too racy to return an error code here, but I
think it would be fine to return EALREADY (although I don't know what
the caller would do with that information.

`uv_write3` also takes a flags parameter reserved for future use (just
in case).

The windows side is relatively straightforward in that the only thing
we really need to do is call `CancelIoEx` on the overlapped structure
that's already inside of our req. We do of course need the appropriate
accounting for the number of bytes written.

Discloure: Claude Code was used in the creation of this PR, although
dumb design decisions are probably by me.

I have done some minimal integrated testing of this on Linux.
On windows, I have run the included test, but have not tested
in-situ. I consider this WIP until I've had a chance to run our
full test suite on all platforms, but I wanted to make sure to
open this early for API feedback/concerns.
Keno added a commit to JuliaLang/libuv that referenced this pull request Dec 9, 2025
The ability to request cancellation of a pending or in-progress write
has been requested several times in libuv (at least joyent/libuv#1393 and
libuv#2051), but was not yet implemented. I am currently on a mission to
make Ctrl-C work nicely and reliably in Julia (JuliaLang/julia#60281).
Of course, this would require the ability to cancel in-progress writes
for sane semantics, so I have a renewed interest in this feature, which
this PR attempts to implement.

One primary problem with the API is that the existing callback does not
provide support for passing in the number of written bytes (since it
is expected to always complete). I see two options:

1. Create a new uv_write3 that takes a new cb type that does take this
   argument, or,
2. Create a new public `uv_write_t` field that this information can be
   read from.

This PR takes the first approach, but as it turns out the number of
bytes written needs to be stored anyway, so maybe there is not much
point to this choice.

As mentioned, we do need to store two extra bits of state (the total
number of bytes written and which kind of callback we have). To maintain
ABI compatibility we steal two pointers from the generic req_t reserve
pool. This preserves ABI compatibility, but is a bit awkward, because
it puts these new private fields into a different place than they would
otherwise be. I don't see that we have much choice though, other than
creating a completely new req_t subtype (which does not seem worth it).

The `uv_cancel` function returns `0` on success or `UV_EBUSY` if the
request was submitted using the `uv_write`/`uv_write2` API that does
not take the new callback signature (unless no bytes have been written,
in which case cancellation succeeds).

It also returns `0` if the request is already done. The thought here
was that it would be too racy to return an error code here, but I
think it would be fine to return EALREADY (although I don't know what
the caller would do with that information.

`uv_write3` also takes a flags parameter reserved for future use (just
in case).

The windows side is relatively straightforward in that the only thing
we really need to do is call `CancelIoEx` on the overlapped structure
that's already inside of our req. We do of course need the appropriate
accounting for the number of bytes written.

Discloure: Claude Code was used in the creation of this PR, although
dumb design decisions are probably by me.

I have done some minimal integrated testing of this on Linux.
On windows, I have run the included test, but have not tested
in-situ. I consider this WIP until I've had a chance to run our
full test suite on all platforms, but I wanted to make sure to
open this early for API feedback/concerns.
Keno added a commit to JuliaLang/libuv that referenced this pull request Dec 9, 2025
The ability to request cancellation of a pending or in-progress write
has been requested several times in libuv (at least joyent/libuv#1393 and
libuv#2051), but was not yet implemented. I am currently on a mission to
make Ctrl-C work nicely and reliably in Julia (JuliaLang/julia#60281).
Of course, this would require the ability to cancel in-progress writes
for sane semantics, so I have a renewed interest in this feature, which
this PR attempts to implement.

One primary problem with the API is that the existing callback does not
provide support for passing in the number of written bytes (since it
is expected to always complete). I see two options:

1. Create a new uv_write3 that takes a new cb type that does take this
   argument, or,
2. Create a new public `uv_write_t` field that this information can be
   read from.

This PR takes the first approach, but as it turns out the number of
bytes written needs to be stored anyway, so maybe there is not much
point to this choice.

As mentioned, we do need to store two extra bits of state (the total
number of bytes written and which kind of callback we have). To maintain
ABI compatibility we steal two pointers from the generic req_t reserve
pool. This preserves ABI compatibility, but is a bit awkward, because
it puts these new private fields into a different place than they would
otherwise be. I don't see that we have much choice though, other than
creating a completely new req_t subtype (which does not seem worth it).

The `uv_cancel` function returns `0` on success or `UV_EBUSY` if the
request was submitted using the `uv_write`/`uv_write2` API that does
not take the new callback signature (unless no bytes have been written,
in which case cancellation succeeds).

It also returns `0` if the request is already done. The thought here
was that it would be too racy to return an error code here, but I
think it would be fine to return EALREADY (although I don't know what
the caller would do with that information.

`uv_write3` also takes a flags parameter reserved for future use (just
in case).

The windows side is relatively straightforward in that the only thing
we really need to do is call `CancelIoEx` on the overlapped structure
that's already inside of our req. We do of course need the appropriate
accounting for the number of bytes written.

Discloure: Claude Code was used in the creation of this PR, although
dumb design decisions are probably by me.

I have done some minimal integrated testing of this on Linux.
On windows, I have run the included test, but have not tested
in-situ. I consider this WIP until I've had a chance to run our
full test suite on all platforms, but I wanted to make sure to
open this early for API feedback/concerns.
Keno added a commit that referenced this pull request Dec 15, 2025
Asymmetric atomic fences are a performance optimization of regular
atomic fences (the seq_cst version of which we expose as
`Base.Threads.atomic_fence`). The problem with these regular fences is
that they require a CPU fence instruction, which can be very expensive
and is thus unsuitable for code in the hot path. Asymmetric fences on
the other hand split an ordinary fence into two: A `light` side where
the fence is extremely cheap (only a compiler reordering barrier) and a
`heavy` side where the fence is very expensive.

Basically the way it works is that the heavy side does a system call
that issues an inter-processor-interrupt (IPI) which then issues the
appropriate barrier instruction on the other CPU (i.e. both CPUs will
have issues a barrier instruction, one of them just does it
asynchronously due to interrupt).

The `light` and `heavy` naming here is taken from C++ PR1202R5 [1],
which is the proposal for the same feature in the C++ standard library
(to appear in the next iteration of the C++ concurrency spec).

On the julia side, these functions are exposed as
`Threads.atomic_fence_light` and `Threads.atomic_fence_heavy`. The light
side lowers to `fence singlethread` in llvm IR (the Core.Intrinsic
atomic_fence is adjusted appropriately to faciliate this). The heavy
side has OS-specifc implementations, where:

1. Linux/FreeBSD try to use the `membarrier` syscall or a fallback to
`mprotect` for systems that don't have it.
2. Windows uses the `FlushProcessWriteBuffers` syscall.
3. macOS uses an implementation from the dotnet runtime
(dotnet/runtime#44670), which the dotnet folks
have checked with Apple does the right thing by happenstance (i.e. an
IPI/memory barrier is needed to execute the syscall), but looks a little
nonsensical by itself. However, since it's what Apple recommended to
dotnet, I don't see much risk here, though I wouldn't be surprised if
Apple added a proper syscall for this in the future (since freebsd has
it now).

Note that unlike the C++ spec, I have specified that
`atomic_fence_heavy` does synchronize with `atomic_fence`. This matches
the underlying system call. I suspect C++ chose to omit this for a
hypothetical future architecture that has instruction support for doing
this from userspace that would then not synchronize with ordinary
barriers, but I think I would rather cross that bridge when we get
there.

I intend to use this in #60281, but it's an independently useful
feature.

[1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1202r5.pdf

---------

Co-authored-by: Cody Tapscott <84105208+topolarity@users.noreply.github.com>
Co-authored-by: Keno Fischer <Keno@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Keno and others added 16 commits July 16, 2026 04:35
The vector-write override that carries the buffer's owner for detached
write rooting is a LibuvStream method, but BufferStream - a LibuvStream
subtype with in-memory internals and its own unsafe_write - was caught
by it and failed on the missing send-buffer field. Writes to a
BufferStream complete synchronously, so no uv request can outlive the
caller and the detached-owner bookkeeping does not apply: give it a
vector-write method that forwards to its own unsafe_write. Exposed by
the process-stdio forwarder writing chunks into a BufferStream stdio
target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
The published GMP_jll now carries gmp-mpz_realloc.patch (via
JuliaPackaging/Yggdrasil#14191), closing the double-free window that
task cancellation could open through the mpz reallocation paths, so
the temporary source-build force is no longer needed. Validated the
JLL artifact against the cancellation double-free reproducer and the
gmp testset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
The C side of ^C delivery marks the episode source directly, but a
task running under a scoped child source (e.g. inside @sync) polls its
own source at cancellation points - carrying the episode's
cancellation down to it required the julia-side sigint listener's tree
walk. In a single-threaded process (julia's default) whose only thread
runs a compute-bound task, the listener never gets scheduled, so the
first ^C never landed and only the escalation ladder could get
through.

The per-thread cancellation-delivery paths - which every ^C already
triggers on every thread - now carry the pending episode into the
interrupted task's bound source themselves: if the source is governed
by the episode source (parent-chain membership), the episode's state
byte is CAS-maxed into it, a single async-signal-safe write, and the
task's next cancellation point observes it with no julia-side
scheduling at all. The listener's eventual walk redoes the remaining
bookkeeping level-triggered. Three gates had to open for this to work:
the dispatch-pending flag is set before the per-thread sends (the
handlers race it otherwise), and the senders no longer require a
published interruptible-region context when a ^C dispatch is pending
and the task carries a binding - a purely polling victim between
cancellation points never has one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
…nowledged

When the C-side fast path delivers a ^C entirely on its own - the
episode source marked from the signal thread and the cancellation
acknowledged by a polling task - no tree walk has run, so tasks parked
under the episode source (a sleeping @sync sibling, for example) have
not been woken. The listener's acknowledged-fast-path then skipped the
walk too, and the parked task waited forever. A wakeup that carries a
real press now redelivers even when the active severity is already
acknowledged: the walk is level-triggered and idempotent, waking
whoever is still parked; only spurious wakeups without a pending press
short-circuit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
CI test workers run with JULIA_NUM_THREADS=1, where three testsets
could never finish: their spin-loop victims only terminate through a
cross-thread action (an abandon commit or a cancel! issued by the
canceller task) that a single thread can never perform, wedging the
whole test file. Move them into cancellation_exec.jl, the -t2
subprocess whose charter is exactly the thread-dependent cancellation
tests, and give the abandon-refusal victim a defensive shutdown so a
platform that never commits fails cleanly instead of spinning.

The remaining single-threaded behaviors are legitimate and now
asserted as such: the escalation ladder's listener rungs cannot run
while the victim monopolizes the only thread, so a repeat press
re-offers the first rung and the third press reaches the C-side direct
abandonment - the pty ladder test now branches its expectations on the
child's thread count (with an order-tolerant expectation for the
endgame's interleaved messages), and the catch-all swallow test
accepts the direct path's announcement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
CI exposed three timing holes in the direct (C-side) abandonment rung of
the ^C escalation, all reproducible locally by pinning the process to a
single CPU:

- The "Abandoned the current task" announcement was printed after
  performing the abandonment. The moment the victim thread switches to
  the rescue task, session cleanup can conclude - in a script it exits
  the process - and on a busy machine that exit reliably won the race
  against the message, leaving no trace of why the process died.
  Announce before abandoning instead.

- A press that found the victim transiently inside the allocator or the
  GC (gc_state != 0 or runtime locks held) consumed the rescue-timer
  expiry and fell through to a plain re-request, silently discarding
  the escalation rung. Wait up to 100ms for the victim to return to an
  abandonable state before deciding.

- With more than one worker thread, an unclaimed dispatch notification
  only means the sigint listener has not run *yet*, not that it cannot
  run - the direct rung could fire on a loaded machine and rip away
  whatever thread 0 happened to be running, bypassing the graded
  escalation; and since an unbound current task passed the "governed"
  check, that could even be runtime infrastructure such as the listener
  itself, wedging the session. Gate the delivered-episode arm on the
  session having exactly one worker thread, and require the abandoned
  task to be bound under the ^C episode source.

Fixes the cancellation test failures on the aarch64, mmtk, and rr jobs
of https://buildkite.com/julialang/julia-pr/builds/351.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
The owner-carrying write path called `uv_write(s, p, n; owner)` with a
`@nospecialize`d owner, producing a keyword call whose NamedTuple type
is abstract - an unresolvable dynamic call that fails `--trim`
verification (seen as the JuliaC test failures on all platforms of
https://buildkite.com/julialang/julia-pr/builds/351). Route the keyword
method and the internal caller through a positional core instead.

Also add `unsafe_write(::TTY, ::Ptr{UInt8}, ::UInt)` to the hardcoded
sysimage precompiles: it fell out of the image when it gained the
`cancel` keyword, and REPL startup now compiled it fresh, tripping the
REPL precompilation test's zero-fresh-precompiles budget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
Address the remaining test-side failures from
https://buildkite.com/julialang/julia-pr/builds/351:

- core.jl: add Task's new atomic fields (wait_state, bound_cancel_token,
  preempt_request) to the atomic-field allowlist.
- threads.jl: lock parking now links through the dedicated `lock_queue`
  Task field, not `wait_queue` - check both in `parked_on`, and give the
  park wait a CI-sized timeout.
- cancellation.jl: gate the "^C" testset on unix (uv_kill(SIGINT) on
  Windows terminates the child outright), skip fd polling of a pipe on
  Windows (ENOTSOCK), synchronize run_with_sigint on a readiness marker
  so a slow-starting child is not killed by the first SIGINT before its
  handlers are armed, raise the exec-subprocess watchdog to 600s, and
  match the direct abandonment's announce-before-abandon wording.
- cancellation_exec.jl: size the BLAS cancellation baseline adaptively
  instead of three fixed 12000^2 gemms, which alone could outlast the
  parent's watchdog on an oversubscribed machine.
- misc.jl: a repeat SIGINT can land while the first one's error report
  is being displayed, cancelling the report itself - accept the
  fallback note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
The previous trim fix routed the owner-carrying write path through a
positional core, but the call into `uv_write_noncancel` still compiled
as a generic (trim-unresolvable) call: a default value on a
`@nospecialize` parameter drops the `@nospecialize` from the generated
full-arity method, so a caller whose `owner` is deliberately untyped
cannot devirtualize the call. Write the optional-argument forwarders
out by hand so the full-arity methods keep their bare `@nospecialize`,
and split the token branch at the call site so every path stays
concretely resolvable.

Verified locally against the JuliaC TrimmabilityProject: the trim
verify is clean and the trimmed executable builds and runs. Fixes the
JuliaC test failures on all platforms of
https://buildkite.com/julialang/julia-pr/builds/389.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
SuspendThread is asynchronous: the victim keeps executing until a
GetThreadContext forces the suspension to complete. Both Windows delivery
senders validated - and the cancellation sender consumed - the published
region context between SuspendThread and GetThreadContext, i.e. while the
victim could still run. With concurrent senders (cancel!, preemption, the
sigint dispatch all send best-effort signals), a second sender could pass
validation on a stale reset context after the first delivery had already
made the victim throw and unwind, then SetThreadContext the thread into a
dead frame's garbage - observed as the cancellation_exec.jl -t2 child
dying with EXCEPTION_ACCESS_VIOLATION at a garbage ip (0xa5a3) in the
reset_ctx testset on both Windows arches.

Freeze the thread first (SuspendThread + GetThreadContext), then load,
validate, and consume the region context. Overlapping suspensions nest, so
a concurrent sender now either observes the consumed (NULL) context and
skips, or redirects to the same still-frozen, still-valid reset point. The
abandon sender gets the same ordering: jl_abandon_try_commit now really
does validate against frozen state, as its contract always claimed.

CI failure: https://buildkite.com/julialang/julia-pr/builds/392#019f59bc-6692-42d9-8ea5-5d1430478856

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
…very

Two delivery-robustness fixes found chasing the cancellation test failures
on the slower CI platforms:

FreeBSD selects the POSIX-timer rescue mechanism, whose SIGINT is told
apart from a user press by sigwaitinfo's SI_TIMER si_code - but every
sigwaitinfo use was guarded by _POSIX_C_SOURCE >= 199309L, which glibc
defines through _GNU_SOURCE and FreeBSD does not define at all. The
signal listener there fell back to plain sigwait, so a rescue-timer
expiry was indistinguishable from a user ^C: no escalation warning could
ever print, the expiry was never recorded (so the direct-abandonment gate
never opened), and each phantom "press" re-armed the timer. That left the
REPL pty ladder on FreeBSD completely dead after the first press. Gate
these paths on a new HAVE_SIGWAITINFO that includes FreeBSD, where
sigwaitinfo is available regardless of the feature macro.

jl_abandon_task's delivery-retry loop was bounded by iteration count
(200 sends x 10 x uv_sleep(1)), nominally ~2s - but every uv_sleep is a
reschedule, and on an oversubscribed or serialized machine (CI under
load, rr) the loop stretched to minutes per call. The unsafe_abandon!
testsets spam that call in a 20s wall-clock loop, blowing through the
parent's subprocess watchdog on the rr job. Bound the loop by wall clock
instead.

Relatedly, jl_thread_suspend_and_get_state asserted the per-thread signal
request slot held no request, but a parked best-effort cancellation (5)
or abandon (6) whose victim never consumes signals (e.g. SIGUSR2 blocked)
can occupy it indefinitely; both senders tolerate a displaced delivery.
Seen as an abort in assert builds when a termination signal arrived while
an abandon request was pending.

CI failure: https://buildkite.com/julialang/julia-pr/builds/392#019f59bc-6691-42e6-aa26-0fe946612248

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
Asynchronous delivery of foreign-call cancellation handlers exists only
where JL_HAVE_CANCEL_HANDLER_DELIVERY is defined (linux/darwin
x86_64/aarch64, windows x86_64). On i686-linux, freebsd, and win32 the
handler-stopped foreign spins in the cancellation_exec.jl testsets can
never terminate: the spin waits for a handler that is never delivered,
the unstoppable victims pin both worker threads, and the -t2 child wedges
until the parent watchdog SIGKILLs it. Expose the capability as
jl_have_cancel_handler_delivery() and skip the handler-delivery subtests
and the BLAS cancellation testset where it is absent (the pre-call
pending-cancellation check works everywhere and stays).

Also make both REPL pty testsets kill their session if it fails to exit -
success(p) on a wedged session previously hung the whole test file until
the outer driver timeout (2h on the freebsd job).

CI failure: https://buildkite.com/julialang/julia-pr/builds/392#019f59bc-668e-4feb-a5cb-69e7bcf86fc5

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
…weep

The all-methods return_types(isequal, Tuple{Any,Any}) sweep from #49800
asserts over every isequal method in the session - including ones added
by OTHER test files sharing the worker process. test/arrayops.jl defines
isequal(::totally_not_five26034, ::Number) (and its mirror), which infer
Bool in isolation but degrade to Any once the LinearAlgebra tests have
inflated the ==/Number method tables earlier in the same process. The
failure therefore appears whenever the scheduler happens to run arrayops
(after the LinearAlgebra tests) before missing on one worker -
deterministically reproducible with

    JULIA_CPU_THREADS=1 julia test/runtests.jl LinearAlgebra/triangular2 \
        LinearAlgebra/structuredbroadcast triplequote intrinsics iobuffer \
        staged arrayops combinatorics euler client terminfo errorshow \
        goto llvmcall some docs interpreter floatfuncs missing

Restrict the assertion to methods owned by Base and the stdlibs (module
root not Main); in a fresh process this still covers all 27 shipped
methods.

CI failure: https://buildkite.com/julialang/julia-pr/builds/392#019f59bc-668b-4c95-81ed-e80a15899a30

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KLwCBAR64r3cTdfaC5krU
The cancellation-lowering pass skipped every atomic store when collecting
unsafe points, as a proxy for recognizing its own reset_ctx bookkeeping
stores, and never considered cmpxchg/atomicrmw at all. A reset region thus
stayed published across user atomic operations, so a SAFE cancellation
delivered around e.g. a spin-lock acquire (cmpxchg) or release (atomic
store) would longjmp away and leave the lock owned forever.

Mark the pass's own stores with the julia.reset_safe metadata instead, and
treat unmarked atomic stores and read-modify-write operations as unsafe
points that clear the published reset region.

Reported in #60281 (comment):
#60281 (comment)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPMqtAGKwxaitJWknYkRcX
A cancelled `@sync` awaits its children in sync_cancel!, parking each
teardown wait with min_severity one above the acknowledged request so only
an escalation can interrupt it. That interruption unwound straight out of
the `@sync`, so a SAFE request escalated to ABANDON_EXTERNAL released the
block while internal compute-bound children were still running,
contradicting the documented contract that ABANDON_EXTERNAL keeps awaiting
internal tasks.

Adopt the escalated request instead: re-enter the teardown wait under the
new severity's policy, and report the strongest request when the teardown
completes. ABANDON_ALL still ceases waiting (the children are frozen).

Reported in #60281 (comment):
#60281 (comment)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPMqtAGKwxaitJWknYkRcX
An abandon request could commit while the victim sat between the
scheduler's sleep_check_state transition and the actual sleep (e.g. inside
jl_task_get_next's trypoptask/checkempty callbacks). Abandonment bypasses
the JL_CATCH that restores the sleep state and running count on unwind, so
the rescue task re-entered the scheduler with the sleep bookkeeping still
claimed and tripped the not_sleeping assertion in jl_task_get_next.

Add sleep_check_state to the abandon refusal set: like held runtime locks,
a claimed sleep slot is runtime state that the discarded context cannot be
allowed to leak.

Reported in #60281 (comment):
#60281 (comment)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPMqtAGKwxaitJWknYkRcX
Keno added a commit to JuliaPackaging/Yggdrasil that referenced this pull request Jul 19, 2026
Pick up JuliaLang/libuv#46, which carries libuv/libuv#4966: uv_cancel()
support for in-flight write requests plus the uv_write_nwritten() query,
needed by the upcoming Julia cancellation work (JuliaLang/julia#60281).
On Windows the library now uses WaitOnAddress, linked via
libsynchronization (already present in the checked-in configure).

Co-authored-by: Keno Fischer <Keno@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Keno added a commit to KenoAIStaging/julia that referenced this pull request Jul 19, 2026
Bump the libuv pin to the current julia-uv2-1.48.0 head, picking up
JuliaLang/libuv#46, which carries libuv/libuv#4966: in-flight write
requests can now be cancelled through uv_cancel, and the new
uv_write_nwritten accessor reports how much of a (possibly cancelled)
write was actually submitted. This is the libuv-side half of
cancellable stream writes for the task cancellation work (JuliaLang#60281).

On Windows the reworked pipe writer thread waits with
WaitOnAddress/WakeByAddressSingle from libsynchronization; libuv's own
build links it, but everything in this repository that links the static
libuv.a (flisp, libjulia, the test libraries) takes its Windows system
libraries from WIN_SYSTEM_LIBS, so add it there. It is resolved to a
full path via $(CC) -print-file-name (like the pre-existing libatomic
handling) because those raw-ld links do not search the toolchain's own
library directories, where some environments (e.g. msys2 mingw32) keep
the import library.

The source build is temporarily forced until a LibUV_jll built from the
pinned commit is published.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPMqtAGKwxaitJWknYkRcX
Keno added a commit to KenoAIStaging/julia that referenced this pull request Jul 19, 2026
When a parked task is interrupted (`schedule(t, exc, error=true)`), the
waking party must both win the right to reschedule the task against any
concurrent `notify` and remove the task's wait-queue registration, which
requires the waitee's lock - and reacquiring that lock may itself need
to park. Master handles this with an unsynchronized `list_deletefirst!`
on a foreign task's queue, which is only safe under an ownership
discipline that cancellation precisely cannot satisfy.

This replaces the task-intrusive wait queues with per-wait heap
registrations (`WaitEntry`, one cached per task so the common park does
not allocate) and a single-word wake-claim protocol on the new atomic
`Task.waiting_on` field: `notify` claims a popped entry with an
expected-entry CAS (making stale entries of interrupted waits harmless
to skip), interrupters claim with an unconditional swap and leave the
entry for lazy unlinking by the waiter's own cleanup or a later notify,
and wake sources aimed at one specific wait (`wait_with_timeout`'s
timer task) use a fresh single-use entry so their CAS cannot claim a
later, unrelated wait. A task whose interrupted wait left a stale
registration behind can immediately park elsewhere (e.g. on a lock
during its cleanup) with a fresh entry.

Compared to the previous approach on this branch, this removes the
per-task `wait_state` byte (and its ABA workarounds), the second and
third intrusive link sets, and the duplicated list implementations;
sticky workqueue elements now also record the synchronized wrapper as
their queue identity, so asynchronous deletions take the wrapper's lock
instead of mutating the inner list unsynchronized.

The claim-failed path of `schedule(t, exc, error=true)` intentionally
retains master's blind-fire semantics (legal only for owned tasks);
making interruption safe in that window as well is deferred to the
`cancellation_request` mechanism in JuliaLang#60281.

This commit was written with the assistance of generative AI (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keno added a commit to KenoAIStaging/julia that referenced this pull request Jul 19, 2026
When a parked task is interrupted (`schedule(t, exc, error=true)`), the
waking party must both win the right to reschedule the task against any
concurrent `notify` and remove the task's wait-queue registration, which
requires the waitee's lock - and reacquiring that lock may itself need
to park. Master handles this with an unsynchronized `list_deletefirst!`
on a foreign task's queue, which is only safe under an ownership
discipline that cancellation precisely cannot satisfy.

This replaces the task-intrusive wait queues with per-wait heap
registrations (`WaitEntry`, one cached per task so the common park does
not allocate) and a single-word wake-claim protocol on the new atomic
`Task.waiting_on` field: `notify` claims a popped entry with an
expected-entry CAS (making stale entries of interrupted waits harmless
to skip), interrupters claim with an unconditional swap and leave the
entry for lazy unlinking by the waiter's own cleanup or a later notify,
and wake sources aimed at one specific wait (`wait_with_timeout`'s
timer task) use a fresh single-use entry so their CAS cannot claim a
later, unrelated wait. A task whose interrupted wait left a stale
registration behind can immediately park elsewhere (e.g. on a lock
during its cleanup) with a fresh entry.

Compared to the previous approach on this branch, this removes the
per-task `wait_state` byte (and its ABA workarounds), the second and
third intrusive link sets, and the duplicated list implementations;
sticky workqueue elements now also record the synchronized wrapper as
their queue identity, so asynchronous deletions take the wrapper's lock
instead of mutating the inner list unsynchronized.

The claim-failed path of `schedule(t, exc, error=true)` intentionally
retains master's blind-fire semantics (legal only for owned tasks);
making interruption safe in that window as well is deferred to the
`cancellation_request` mechanism in JuliaLang#60281.

This commit was written with the assistance of generative AI (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keno added a commit to KenoAIStaging/julia that referenced this pull request Jul 19, 2026
When a parked task is interrupted (`schedule(t, exc, error=true)`), the
waking party must both win the right to reschedule the task against any
concurrent `notify` and remove the task's wait-queue registration, which
requires the waitee's lock - and reacquiring that lock may itself need
to park. Master handles this with an unsynchronized `list_deletefirst!`
on a foreign task's queue, which is only safe under an ownership
discipline that cancellation precisely cannot satisfy.

This replaces the task-intrusive wait queues with per-wait heap
registrations (`WaitEntry`, one cached per task so the common park does
not allocate) and a single-word wake-claim protocol on the new atomic
`Task.waiting_on` field: `notify` claims a popped entry with an
expected-entry CAS (making stale entries of interrupted waits harmless
to skip), interrupters claim with an unconditional swap and leave the
entry for lazy unlinking by the waiter's own cleanup or a later notify,
and wake sources aimed at one specific wait (`wait_with_timeout`'s
timer task) use a fresh single-use entry so their CAS cannot claim a
later, unrelated wait. A task whose interrupted wait left a stale
registration behind can immediately park elsewhere (e.g. on a lock
during its cleanup) with a fresh entry.

Compared to the previous approach on this branch, this removes the
per-task `wait_state` byte (and its ABA workarounds), the second and
third intrusive link sets, and the duplicated list implementations;
sticky workqueue elements now also record the synchronized wrapper as
their queue identity, so asynchronous deletions take the wrapper's lock
instead of mutating the inner list unsynchronized.

The claim-failed path of `schedule(t, exc, error=true)` intentionally
retains master's blind-fire semantics (legal only for owned tasks);
making interruption safe in that window as well is deferred to the
`cancellation_request` mechanism in JuliaLang#60281.

This commit was written with the assistance of generative AI (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs nanosoldier run This PR should have benchmarks run on it needs pkgeval Tests for all registered packages should be run with this change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants