Skip to content

feat(core): let an embedder abandon work a request forgot - #43

Merged
butterflyfish merged 2 commits into
masterfrom
feat/task-scope
Sep 11, 2026
Merged

butterflyfish merged 2 commits into
masterfrom
feat/task-scope

Conversation

@butterflyfish

Copy link
Copy Markdown
Contributor

Why

A context that serves several logical requests in sequence keeps one JS context, so a promise a request neither awaits nor hands to a background-work API stays queued on it. It resumes during a later request and observes that request's state — a different tenant's environment, a different caller's identity. That is not the state it was written against.

There is no way to stop it today. shutdown_tasks is all-or-nothing and ends the context.

What

Three methods on JSContext:

  • begin_task_scope() opens a scope and makes it current. Every host promise created from then on belongs to it.
  • enter_task_scope(Option<TaskScope>) switches, returning the previous one, for save-and-restore around a nested call.
  • cancel_task_scope(scope) abandons the scope's unfinished promises.

Abandoning means the task is aborted, so the future is dropped and whatever it held — a connection, a buffer, an in-flight request — is released, and the promise is left unsettled so none of its continuations run.

Leaving them unsettled rather than rejecting them is deliberate: a rejection handler is itself a continuation, and it would run in the wrong request.

Aborting, not just flagging

The first cut set a cancellation flag that each task checked before its next poll. That is not enough, and test_task_scope_releases_what_abandoned_work_holds fails against it: a task parked on an upstream that never answers is never polled again, so it holds its connection until the context itself goes away. The scope now owns its tasks' abort handles and aborts them. The flag stays as the fast path for work still being polled.

Compatibility

Nothing changes for an embedder that opens no scope, which is every one of them today — test_promises_outside_a_scope_are_untouched pins that.

One sharp edge, documented

Entering a cancelled or unknown scope leaves no scope current rather than failing, so promises created afterwards belong to nothing and cannot be abandoned later. test_entering_a_dead_scope_leaves_no_scope_current pins the behaviour because the failure is otherwise silent.

Tests

cargo test --features quickjs --test promise — 18 passed. Four are new.

A context that serves several logical requests in sequence keeps one JS
context, so a promise a request neither awaits nor hands to a background-work
API stays queued on it. It resumes during a later request and observes *that*
request's state -- a different tenant's environment, a different caller's
identity -- which is not the state it was written against.

`begin_task_scope` opens a scope and every host promise created from then on
belongs to it. `cancel_task_scope` abandons the unfinished ones: each has its
task aborted, so its future is dropped and whatever it held is released, and
its promise is left unsettled so no continuation of it ever runs. Leaving them
unsettled rather than rejecting them is the point -- a rejection handler is
itself a continuation, and it would run in the wrong request.

Aborting the task is what makes the release real. The cancellation flag alone
is only read the next time a task is polled, and a task parked on an upstream
that never answers is never polled again; it would hold its connection until
the context itself went away. The flag stays as the fast path for work that is
still being polled.

Nothing changes for an embedder that opens no scope, which is every one of
them today.
@butterflyfish
butterflyfish merged commit 60ba562 into master Sep 11, 2026
13 checks passed
@butterflyfish
butterflyfish deleted the feat/task-scope branch September 11, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant