-
Notifications
You must be signed in to change notification settings - Fork 713
Clean up EventTarget and AbortSignal implementation #7053
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
3f5647f
Refactor RefcountedCanceler to ReleasingCanceler
jasnell 107f0fe
Expose mechanisms for cross-request signaling
jasnell 8652427
Refactor AbortSignal/AbortController
jasnell eb05eea
Update AbortSignal uses in container and http
jasnell 658323d
Update tests for refactored AbortSignal
jasnell a35f6b8
Improve abort signal handling in EventTarget/AbortSignal
jasnell 527fab1
Remove the obsolete native listener from EventTargety
jasnell d8c9f92
Move AbortSignal's pending reason into an Arc
jasnell 0dc8a6c
Tests, todos and doc updates for the AbortSignal/EventTarget updates
jasnell 895a5e1
Default Event's trusted flag to NO
jasnell 04e05af
Update all legacy on*** event handlers to use proper positions
jasnell d3f320b
Apply multiple smaller EventTarget cleanups
jasnell c1a4530
Move internal event dispatches to report policy
jasnell 79e36b4
Make standard Event subclasses moar standard
jasnell facf8fc
Update remaining stale Event-related comments/docs
jasnell ccd6daf
Gate the dispatchEvent error handling fix
jasnell c00b9f1
Address AI review comments
jasnell b1a224a
Address review comments
jasnell File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| # AbortSignal internals: cancellation hooks and the cross-request model | ||
|
|
||
| This document describes how `api::AbortSignal` (`src/workerd/api/basics.h`) delivers aborts | ||
| to native (C++) consumers, and which primitive to use when hooking work to a signal. It is | ||
| aimed at runtime code; the JS-visible behavior follows the WHATWG DOM spec. | ||
|
|
||
| ## State model | ||
|
|
||
| An `AbortSignal` is a plain JS-heap object. Its abort state — `maybeAbortException` (a | ||
| `kj::Exception` derived from the abort reason) and `reason` (the JS value) — requires no | ||
| `IoContext` to create or read. Consequently: | ||
|
|
||
| - `new AbortController()`, `AbortSignal.abort()`, and objects that allocate signals (e.g. | ||
| `WritableStream`, `request.signal`) work at global scope, during module evaluation. | ||
| - A single signal may be created in one request, observed in another, and aborted from a | ||
| third (or from outside any request). JS-visible effects of an abort (state, events) happen | ||
| synchronously in whichever context triggers it. | ||
|
|
||
| What *does* involve I/O — cancelling KJ promises, notifying RPC peers — is handled through | ||
| per-registration cells bound to the registering request, described below. | ||
|
|
||
| ## Choosing a primitive | ||
|
|
||
| | You have... | Use | | ||
| | -------------------------------------------------------------- | ---------------------------------------- | | ||
| | A `kj::Promise` to cancel on abort | `signal->wrap(js, promise)` | | ||
| | A KJ-side object needing promise wrapping + a cancel callback | `signal->newCanceler(js)` + `AbortableImpl` | | ||
| | A native callback to run on abort, in your request's context | `signal->addAbortAction(js, fn)` | | ||
| | A JS-heap reaction (no IoContext involvement) | `signal->addAbortAlgorithm(js, fn)` | | ||
|
|
||
| All four are no-ops (or immediate, see below) for `NEVER_ABORTS` signals, and all native | ||
| variants require an active `IoContext` at registration time. | ||
|
|
||
| ### `wrap(js, promise)` | ||
|
|
||
| Returns a promise that rejects with a `kj::Exception` derived from the abort reason when the | ||
| signal aborts. If the signal is already aborted, the returned promise rejects immediately the | ||
| same way — indistinguishable from an abort arriving right after wrapping. Callers that want | ||
| to surface the JS reason with value identity (e.g. `fetch`) should pre-check `getAborted()` | ||
| and use `getReason()` themselves. | ||
|
|
||
| ### `newCanceler(js)` | ||
|
|
||
| Returns `Cancellation{canceler, registration}`: | ||
|
|
||
| - `canceler` is a solely-owned `ReleasingCanceler` (`src/workerd/util/canceler.h`): wrap I/O | ||
| promises through it, register `ReleasingCanceler::Listener`s for cancel callbacks. Its | ||
| destructor *releases* (never cancels) still-wrapped promises; a `Listener` registered | ||
| after cancellation fires immediately. | ||
| - `registration` keeps the canceler hooked to the signal. **Destroy the registration before | ||
| the canceler** (declare it after the canceler member): the signal reaches the canceler by | ||
| reference, valid only while the registration exists. | ||
|
|
||
| ### `addAbortAction(js, fn)` | ||
|
|
||
| Registers `fn(js, exception)` to run at most once when the signal aborts, always under the | ||
| isolate lock, always in the IoContext current at registration time: | ||
|
|
||
| - Abort triggered in that context: `fn` runs synchronously during the abort. | ||
| - Abort triggered elsewhere (another request, or no request): delivery is deferred into the | ||
| owning context via `IoCrossContextExecutor::tryExecute` and runs on its next turn. The | ||
| deferred task re-takes the registration slot on arrival, so a consumer that went away in | ||
| the meantime turns the delivery into a guaranteed no-op. | ||
| - Owning context already destroyed: the delivery is silently dropped — everything the action | ||
| wanted to touch died with the context. | ||
|
|
||
| Dropping the returned handle (safe from any thread) guarantees `fn` never runs again, so | ||
| `fn` may capture plain references whose lifetime the holder ties to the handle. This is also | ||
| the single point that arms the RPC abort subscription for deserialized signals — every | ||
| native registration path funnels through it. | ||
|
|
||
| `ExecProcess`'s kill-on-abort is the canonical direct consumer. | ||
|
|
||
| ### `addAbortAlgorithm(js, fn)` | ||
|
|
||
| The DOM spec's "add an algorithm to signal's abort algorithms", for reactions whose state | ||
| lives entirely on the JS heap. Runs under the isolate lock in *whichever* context triggers | ||
| the abort, before the `abort` event is dispatched; never runs for synthetic | ||
| `dispatchEvent('abort')` calls. The handle holds only a `jsg::WeakRef` to the signal and must | ||
| be dropped under the isolate lock. Capture JS-heap objects weakly (`JSG_THIS_WEAK`) unless | ||
| the signal genuinely should keep them alive: algorithms are owned and GC-visited by the | ||
| signal, so a strong capture makes a long-lived signal retain the captured object. | ||
|
|
||
| `addEventListener`'s `{signal}` option is the canonical consumer. | ||
|
|
||
| ## Abort sequence | ||
|
|
||
| `triggerAbort` maps 1:1 onto the spec's "signal abort": | ||
|
|
||
| 1. Record the abort reason (and exception) on this signal. | ||
| 2. Record it on every not-yet-aborted dependent signal (`AbortSignal.any()` results), before | ||
| any events fire anywhere. | ||
| 3. Run this signal's abort steps: abort algorithms (FIFO, then emptied) → native | ||
| registrations (routed per owner as above) → RPC clones (reason serialized once) → fire | ||
| the `abort` event (listener exceptions are reported, not propagated; `abort()` cannot | ||
| throw) → drop all listeners. | ||
| 4. Run each collected dependent's abort steps, unlinking it from any remaining sources. | ||
|
|
||
| ## Lifetime and reclamation | ||
|
|
||
| Native and RPC registrations are `kj::Arc`'d cells: an immutable | ||
| `IoCrossContextExecutor` plus a mutex-guarded slot holding the context-bound payload. The | ||
| consumer-side RAII handle clears the slot from any thread; because handles are attached to | ||
| request-owned objects (the wrapped promise, the `AbortableImpl`, ...), IoContext teardown | ||
| reclaims payloads automatically without touching the signal. Empty or defunct-context cells | ||
| are swept on the next registration, bounding a long-lived signal's footprint by its live | ||
| registrations (asserted by `crossRequestRegistrationChurn` in | ||
| `src/workerd/api/tests/abortsignal-test.js` via `getNativeRegistrationCountForTest()`). | ||
|
|
||
| Cells hold no JS-heap references and need no GC visitation; abort algorithms do, and are | ||
| visited by the signal. | ||
|
|
||
| ## Signals received over RPC | ||
|
|
||
| A deserialized signal's pending abort reason arrives through a mutex-guarded, atomically | ||
| refcounted box (`ExternalPusherImpl::PendingAbortReasonBox`) written by the receiving | ||
| request's RPC machinery and readable from any context — so `getAborted()`/`getReason()` | ||
| answer correctly even for signals that crossed request boundaries before the abort was | ||
| processed. Actually *reacting* to the abort (running `triggerAbort`) requires the | ||
| subscription armed via `subscribeToRpcAbort()`, which happens automatically when an `abort` | ||
| listener, `onabort` handler, or any native registration is added. Only the receiving | ||
| request's context can arm it — the underlying promise belongs to that request — so arming | ||
| requested from any other context is routed there through the signal's | ||
| `IoCrossContextExecutor` and takes effect on the receiving request's next turn. If the | ||
| receiving request is already gone, the routing is dropped: no delivery is possible anymore. | ||
| Polling stays accurate regardless (if the peer could still abort, that request's teardown | ||
| wrote an implicit connection-lost abort into the box), and registrations made after that | ||
| point observe the abort through the already-aborted pre-checks instead. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.