perf: link an external cancellation source lazily, on first observation - #24
Merged
Merged
Conversation
Closes #23. A Job seeded with an external signal subscribed to it eagerly in prepare(), so every Job paid an AbortSignal listener registration and removal whether or not anything could observe its cancellation. The subscription now happens the first time the Job's cancellation becomes observable: a job.signal, signal(), or context.signal read, or a child start. Internal bookkeeping reads the controller's signal directly and never subscribes. Already-aborted sources are honored without a listener by synchronous rechecks before the body, after it, and before a HandoffJob offer. An external abort during an offer suspension reaches the body only if it observed its signal first; otherwise the consumer's resume() ends the suspension and the result still reports the cancellation. Per-Job (Node 24, 400k iterations): external signal unobserved 1.99 -> 0.98 us, HandoffJob offer/resume with external signal 2.41 -> 1.44 us, plain Job unchanged. Server /echo against the packed artifact: Node 31.6k -> 41.5k rps, Bun 78k -> 101.7k rps.
miinhho
force-pushed
the
perf/lazy-signal-link
branch
from
September 12, 2026 08:15
0f63bca to
5d5a1a9
Compare
This was referenced Sep 12, 2026
Merged
miinhho
added a commit
that referenced
this pull request
Sep 12, 2026
Breaking since 0.2.1: Job is a single-result lifetime (#22) — the Published type parameter, the publisher body argument, JobPublisher, and Job.value() are gone; HandoffJob is the one-shot handoff. #24 and #26 change no public behavior. The README is rewritten for a first-time reader: the model in one paragraph, Jobs grouped by ownership / lifecycle / results / failure, cancellation before context, Supervisor and TaskGroup with their policies, HandoffJob last, and an API summary.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Closes #23.
An external cancellation source is linked lazily, on first observation.
prepare()no longer subscribes toseed.signal/ an inherited foreigncontext.signal. It records them as pending and runs a synchronous recheck, so a source that already aborted still cancels the Job before its body runs (execute({ signal: AbortSignal.abort(reason) }, body)keeps rejecting without callingbody).job.signal,signal(),context.signal, or a childstart()(which already readsparent.signal.throwIfAborted()).Job.contextis aJobContextwhosesignalgetter forwards tojob.signal;withContext()derives contexts that forward likewise instead of copying the signal.prepare,perform,recordFailure,cancel,reconcileFailure,complete) readscontroller.signaldirectly and never subscribes.Job.cancel(), never through listeners on the parent's signal.HandoffJob.offer()does not subscribe (option B from #23).signal()first. Otherwise the consumer'sresume()ends the suspension and the Job's result still reports the cancellation.cancel()/close()(direct, cascaded, deadline) release the handoff as before.resume(aborted). It is a documented narrowing of the HandoffJob contract (README, AGENTS.md).Measured
Per-Job microbenchmark (Node 24, 400k iterations,
/tmp/job-bench.mjs),mainbuild vs this branch:signal()readServer
/echo(50 connections, 8 s) against the packed artifact of this branch, server tree otherwise unchanged:mainBun.serveon the same box: 105k)The Bun gain exceeds the listener's own cost because attaching a listener to Bun's native
request.signalmaterializes the signal; the unobserved path never touches it.A first attempt used an object literal with a
get signal()accessor for the context; that allocated a closure per context and doubled plain-Job cost (0.95 → 2.33 µs).JobContextwith a prototype getter keeps plain Jobs at parity.Tests
job-cancellation.test.ts: unobserved source registers no listener yet its abort ends the Job; firstsignal()read subscribes once and a late reader sees a preceding abort; child start subscribes the parent so the cascade reaches it. The reentrant-registration test is restated at the new registration point (abort during the first observation is visible before the observer continues); the shared-source dedupe test now observessignal()so it still exercises one subscription for two references.handoff-job.test.ts: an offer consults an already-aborted source and rejects without subscribing; an unobserved abort during suspension reaches the body only throughresume()while the result reports the cancellation; a body that observed its signal first is released by a later external abort.Verification
pnpm format,pnpm check,pnpm buildpnpm test— 11 files, 131 testspnpm pack; servertypecheck,vitest --project @tiberjs/server(12/12),leak-probe(live 0, graph 0), and the benchmark above run against the packed tarball