Hand-written. ../harvested/concurrency-and-async.md is what the documents say; this file is what
the implementation found, and it wins. Each entry names the harvested entry it answers by that
entry's stable key.
Async::Task#cancelis the current cancellation primitive;#stopis a deprecated backward-compatibility alias, and#cancel'scause:keyword is the out-of-band channel#stopnever had. Supersedesconcurrency-and-async/f75816b6("dexpace-async-async later maps Async::Task#stop and #with_timeout onto the pivot's cancellation in both directions per ASYNC-6, and dexpace-async-concurrent_ruby maps Concurrent::Promises::Future") on the method name only — the mapping it describes is right, and so is itsASYNC-6citation. Verified onasync2.45.1 under Ruby 3.4.10:lib/async/node.rbcarries# Backward compatibility alias for {#cancel}. # @deprecated Use {#cancel} instead.immediately abovedef stop(...) = cancel(...), andAsync::Task.instance_method(:stop).ownerisAsync::Node. The current primitive isAsync::Task#cancel(later = false, cause: $!), and itscause:keyword is materially better for this port than#stopwas: aDexpace::Cancellation#reasonpassed ascause:is readable back off the raisedAsync::Cancelas#cause, which is the out-of-band discriminationXCUT-2andTRANSPORT-3require and which#stopgives no channel for. The sentence the harvested entry quotes lives indocs/sdk-design-ruby/03-seam-by-seam-idiomatic-mapping.md:252-254, a frozen chapter, so it is not corrected in place: §3.3 is right about the mapping and stale about the name until §3 is next deliberately amended by a human. What this repository does meanwhile: phase 8c calls#canceleverywhere and passes the cancellation reason ascause:;dexpace-async-async, which is post-v1 (docs/first-release.md§ What v1 ships without › Post-v1 gems, withSEAM-24's cancellation bridge), is written against#cancelwhen it is built. Because#stopstill forwards, nothing breaks today — what is lost by writing#stopis thecause:channel, silently. This entry re-keys on the next harvest of §3, at which point its citation ofconcurrency-and-async/f75816b6needs revisiting. CitesASYNC-6,SEAM-24,TRANSPORT-7. review ·docs/work/mvp/phase8/phase8c/2026-09-11-phase8c-asynchronous-transport-design.md· high · sha:manual-phase8c-async-task-cancel
- Core's shared mutable state is one frozen
Datasnapshot swapped under aThread::Mutex, not aconcurrent-rubycollection. Resolvesconcurrency-and-async/b44d400b,concurrency-and-async/abfb9ed9,concurrency-and-async/960d89ecandconcurrency-and-async/0e11c51d. All four say the same thing — preferConcurrent::Map/Concurrent::Array/Concurrent::AtomicFixnumover a hand-rolledMutexplus a plainHash, and replace bareMutex.newat review — and all four are unavailable here for the same mechanical reason the Sorbet rules are:concurrent-rubyis a third-party gem,dexpace-core.gemspecis asserted to contain zeroadd_dependencylines (SEAM-1,NFR-1), andrake gates:gemspec_auditrejects one. Library-versus-application again: an application pays that dependency once, a zero-dependency core would impose it on every consumer. What the SDK does instead, perdocs/sdk-design-ruby/03-seam-by-seam-idiomatic-mapping.md§3.6 and §3.7: every piece of shared mutable state indexpace-core— the seam registry's factory table and resolved slot,Dexpace::Async::Future's settled outcome,Dexpace::Cancellation's reason,Dexpace::Closeable's closed latch — is held as one frozenDatavalue in one instance variable, replaced wholesale under aThread::Mutexon the write path and read without any lock on the read path. A reader therefore takes a single reference read and sees a consistent, fully-constructed, deep-frozen picture or the previous one, never a torn mixture, which isSEAM-9's three clauses ("reads observe the latest install without blocking, writes are serialized, a concurrent first-access cannot run the discovery scan twice") implemented rather than argued, and is alsoIO-39's lock-free-read property surviving the seam that used to state it. Verified on 3.2.11 and 4.0.6: a prototype of this shape resolved exactly one instance from 32 concurrent threads with one factory invocation and one scan, and an unresolved registry re-scanned on the next access and picked up a later registration. The rules in this chapter that the substitution does not weaken are adopted verbatim and are what make the shape safe:concurrency-and-async/c0fab747(protect only the smallest critical section),concurrency-and-async/ee54cb68andconcurrency-and-async/f261a143(never hold a lock across I/O — so theSEAM-8warning is emitted outside the mutex, and a callback, a#releaseand a drain always are), andconcurrency-and-async/54d8bb89withconcurrency-and-async/2c743901(prefer eliminating shared mutable state, and model anything crossing a concurrency boundary as an immutableDatavalue), which is precisely why the snapshot is aDataand not aHash. Two Ruby facts make the "smallest critical section" rule non-negotiable here rather than stylistic, both re-verified on 3.2.11, 3.4.10 and 4.0.6:Thread::Mutexis non-reentrant (ThreadError: deadlock; recursive locking) and its ownership is per-fiber, not per-thread (ThreadError: deadlock; lock already owned by another fiber belonging to the same thread), so a lock held across any suspension point deadlocks two fibers of one thread. What is lost and is not replaced:Concurrent::AtomicFixnum-style lock-free counters, andconcurrent-ruby's tested primitives in place of code this repository has to test itself — which is why every one of those objects ships with an explicit contention test rather than an argument. The bounded-pool and deterministic-teardown rules in the same chapter (concurrency-and-async/6764e0b5,concurrency-and-async/dc345cae,concurrency-and-async/df658d73,concurrency-and-async/3692970f,concurrency-and-async/047644ea,concurrency-and-async/dd8e6d2d) are not resolved here and are not weakened: they binddexpace-async-thread, an adapter gem whoseNFR-2budget permits one third-party library, and they are that gem's to answer in phase 8. The styleguide-amendment alternative (scoping chapter 9'sconcurrent-rubypreference to projects that may take the dependency) was considered and not taken; this is recorded as an SDK deviation instead (phase 2). review ·docs/work/mvp/phase2/2026-09-06-phase2-seam-foundations-design.md· high · sha:manual-phase2-no-concurrent-ruby
- A cancelled
Asynctask raises anExceptionthat is not aStandardError, so everyrescuewritten the obvious way is blind to it — the close belongs in anensure. No harvested rule is false here, so no key is backticked; what is added is a fact two of them are read against.error-handling/0871259d("Never rescueExceptionin application code; rescueStandardErroror a named subclass instead") stays true — the repair below is anensure, not a widened rescue — andcross-cutting-invariants/68aad33a(the two required-to-be-loud exceptions toclose_quietly's quiet closing) is unaffected. Design §3.3's check-after-resume rule says a producer that discovers cancellation while holding a response "MUST close any response it holds and settle through the failure channel", and §3.7 makesDexpace.close_quietlythe single sanctioned exit for such a close — it "rescuesStandardErrorfrom#close". Verified onasync2.45.1 under Ruby 3.4.10:Async::StopisAsync::Cancel—lib/async/stop.rbismodule Async; Stop = Cancel; end— andlib/async/cancel.rb:8declaresclass Cancel < Exception, soAsync::Stop.equal?(Async::Cancel)istrueandAsync::Cancel.ancestors.take(3)is[Async::Cancel, Exception, Object]— not aStandardError.Async::Task#cancelraises it inside the task (#stopis the deprecated alias; see this file's## Supersededentry), so arescue => eor arescue StandardErrorin an adapter's send path does not run, whiletask.with_timeout'sAsync::TimeoutErroris aStandardErrorand does. An orphan-close written as arescuetherefore runs on a timeout and not on a cancellation — the exact inverse of whatSEAM-30andASYNC-5are for, and it is silent. What every adapter must do: put the close in anensure, where it is reached on both paths, and letclose_quietlydo the rescuing inside it;close_quietly's own rescue ofStandardErrorfrom#closeis correct as written and needs no widening, because what the cancellation escapes is the reaching of the close, not the close itself. Because the two names are one class,rescue Async::Cancelandrescue Async::Stopcatch the same thing; an adapter that writes both has written one. A second sanctioned quiet-close exit is not the answer — that would give the SDK two answers to one question, andclose_quietly's contract is phase 2's. CitesSEAM-30,ASYNC-5,ASYNC-6,TRANSPORT-7,TRANSPORT-9,TRANSPORT-22,CFG-21,XCUT-13. review ·docs/work/mvp/phase8/2026-09-11-phase8-segmentation-design.md· high · sha:manual-phase8-async-cancel-not-standarderror Async::Task#cancelcannot be called from another OS thread, keeps only anExceptionas itscause:, and onasync2.46Kernel#Asyncinside a task is that task's child. Beside this file's## Supersededentry on#cancel, which stands, and besideconcurrency-and-async/f4beb429(ASYNC-22's "safe for concurrent calls from multiple threads"); three facts execution measured onasync2.46.0 under Ruby 3.3.12, 3.4.10 and 4.0.6 that phase 8c's design stated otherwise or not at all. One.Task#cancelon a task whose fiber is not the current one callsFiber.scheduler.raise, andFiber.scheduleris nil on every OS thread but the reactor's, so a cancellation hook that reaches the task directly from the canceller's thread raisesNoMethodError: private method 'raise' called for nilon that thread and leaves the task running (:runningafterwards, measured). A cancellation that may originate on any thread — aDexpace::Cancellation::Source#cancel, which the conformance suite fires from aThread.new— therefore has to be marshalled into the reactor: phase 8c pushes the reason onto aThread::Queuethat a transient watcher task inside the reactor pops (a scheduler-aware wait, wakeable from any thread) and the watcher cancels the exchange on the reactor's own thread. Two.Task#cancel(cause:)keeps the cause only when it is anException; anything else — the design's fact 8 passed a Symbol — is replaced by the runtime's ownAsync::Cancel::Cause("Cancelling task!"), so a reason travels as an exception (Dexpace::CancelledError.new(reason)) if the cancelled task's own$!.causeis to name it for whoever reads the task; phase 8c's adapter reads no cause back — its pivot is settled with the reason before the watcher cancels the task — so the wrap is for the task tree and a debugger, not a channel the SDK relies on. And both of the adapter's hooks may run AFTER the exchange has ended:Cancellation::Source#cancelandAsync::Completer#settleeach steal their hook list under their mutex and run it outside, so a push onto the exchange's queue has to be total over the queue's close (ClosedQueueErrorrescued at the push), or the raise travels back throughHooks.notifyinto the caller'sSource#cancelon the cancelling thread — reproduced deterministically with an ordinary caller hook registered first. Three.Kernel#Asyncinside a running task delegates toTask.current.async, so the spawned task is the current task's child (inner.parent.equal?(task)measured true); the design's fact 10 ("Async { }inside a reactor is not a child of the caller") does not hold on 2.46.0, and the adapter'scaller_task.asyncspelling is the honest one rather than a distinction the runtime still draws. CitesASYNC-6,ASYNC-22,TRANSPORT-7,TRANSPORT-8. review ·docs/work/mvp/phase8/phase8c/2026-09-11-phase8c-asynchronous-transport-checklist.md· high · sha:manual-phase8c-cancel-across-threads