Skip to content

Add waitUntil, fail-open and abort to the Rust io-context crate - #8088

Merged
bartlomieju merged 13 commits into
mainfrom
biwanczuk/rust-io-context-request-ops
Nov 3, 2026
Merged

bartlomieju merged 13 commits into
mainfrom
biwanczuk/rust-io-context-request-ops

Conversation

@bartlomieju

@bartlomieju bartlomieju commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

These are the IoContext operations behind ctx.waitUntil(),
ctx.passThroughOnException() and ctx.abort(), so that a Rust
ExecutionContext can behave exactly like the C++ one.

  • IoContext::add_wait_until() makes the request wait for a promise
    after the response, through IoContext::awaitJs() and addWaitUntil().
    A rejection is logged as an uncaught exception, as in C++.
  • IoContext::set_fail_open() calls IoContext::setFailOpen().
  • IoContext::abort() aborts the request with the reason, encoded by
    jsg::Lock::exceptionToKj(), or with the default exception, then
    terminates execution and returns the error for the method to return.
    ctx.abort() always terminates right after aborting, so abort() does
    too, and no caller can forget to; it is #[must_use]. The default
    exception is now created by IoContext::abortWithoutReasonException()
    for both implementations, so the logged file and line match.

Once execution is terminating, add_wait_until() and abort() do nothing
and return a pending error. JavaScript can't call them then, but Rust
can after catching a termination.

Two additions to Rust JSG support these:

  • jsg::Lock::terminate_execution_now() is the counterpart of C++
    terminateExecutionNow(). It requests termination and runs V8 code so
    that it is delivered at once, then returns a pending error instead of
    unwinding with JsExceptionThrown. A #[jsg_method] that returns it
    throws nothing, and no more JavaScript runs, including catch and
    finally blocks. If execution is already terminating, it doesn't enter
    V8 again; a pending ordinary exception is replaced by the
    termination. It panics if V8 doesn't deliver the termination, as C++
    asserts, since an ordinary error would let a catch or finally block
    run.
  • jsg::v8::VoidPromise is a FromJS type that converts an argument as
    C++ JSG converts jsg::Promise (and so kj::Promise, which
    ctx.waitUntil() takes): a promise is taken as is, a thenable is
    followed only if unwrap_custom_thenables is on, and anything else
    becomes a promise resolved with undefined. FromJS for Local
    follows every thenable whatever the flag, so it doesn't match.

Rust JSG's shims must not use C++ JSG, but io_context_add_wait_until()
and io_context_abort() call IoContext::awaitJs() and
jsg::Lock::exceptionToKj() as a reviewed exception. Reimplementing them
over V8 would drift from C++ in ways tests are unlikely to catch:
rejection logging, the onAbort join, the tunneled-exception encoding,
and the retryable/overloaded details. This is sound because the shims
run only after currentWithId() has checked that the IoContext is
current. They then run synchronously under the isolate lock the
caller's jsg::Lock stands for, with handles from the caller's
HandleScope. The "C++ JSG" section of io-context/ffi.c++ spells this
out. Every use is counted in cpp-jsg-allowlist.txt, and the allowlist
test now also counts getCurrentLock(), so a new shim can't reach C++
JSG without raising a count. When C++ JSG throws JsExceptionThrown, the
shim returns false and Rust passes the pending exception on with
Error::pending().

request-ops-test runs each scenario in a real worker twice, on the C++
ExecutionContext and on a test-only Rust resource built on the crate.
It checks that JavaScript, the client and the logs observe the same
things: status, events, RequestObserver failures (including the
exception's file and line), fail-open state, subrequests and logged
uncaught exceptions. Scenarios cover:

  • waitUntil() keeping the request alive
  • rejections, and thenables with and without unwrap_custom_thenables
  • throwing has traps and stack getters
  • promises that never settle, and promises settled by another request,
    with and without handle_cross_request_promise_resolution
  • fail-open
  • abort() with various reasons and with a throwing reason getter
  • calls outside a request, stale IoContext ids, and calls while
    terminating

jsg-test covers terminate_execution_now() and VoidPromise.

DurableObjectState can reuse add_wait_until() but not abort():
state.abort() aborts with its own exception and breaks storage first,
so it will need a lower-level entry point.

@codecov-commenter

codecov-commenter commented Oct 30, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.49606% with 94 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.84%. Comparing base (b690abb) to head (ea97569).

Files with missing lines Patch % Lines
src/workerd/io/io-context/request-ops-test.c++ 75.63% 7 Missing and 79 partials ⚠️
src/workerd/io/io-context/test_bridge.rs 90.24% 4 Missing ⚠️
src/rust/jsg/ffi.c++ 75.00% 0 Missing and 2 partials ⚠️
src/workerd/io/io-context/ffi.c++ 93.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8088      +/-   ##
==========================================
+ Coverage   68.77%   68.84%   +0.06%     
==========================================
  Files         732      733       +1     
  Lines      192040   192545     +505     
  Branches    29905    29997      +92     
==========================================
+ Hits       132076   132557     +481     
+ Misses      42416    42357      -59     
- Partials    17548    17631      +83     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@bartlomieju
bartlomieju force-pushed the biwanczuk/rust-io-context-trace-spans branch from 73517e9 to 0a14de1 Compare October 30, 2026 17:05

@guybedford guybedford left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM. Calling awaitJs() and exceptionToKj() rather than reimplementing them seems like the right call, and the parity tests are very thorough. A few comments inline. This needs a rebase onto the current base, which has await_io_and_run() and test-bridge.c++ already.

Comment thread src/workerd/io/io-context/lib.rs Resolved
Comment thread src/rust/jsg/lib.rs Resolved
Comment thread src/workerd/io/io-context/lib.rs Resolved
Comment thread src/workerd/io/io-context/ffi.rs Resolved
C++ jsg::Lock::terminateExecutionNow() requests a termination, has
V8 deliver it by running V8 code, then unwinds with
JsExceptionThrown, so no more JavaScript runs after the call: not
the rest of the calling function, nor a catch or finally block.
Lock::terminate_execution() only requests a termination, which V8
delivers at its next interrupt check, possibly after the caller's
JavaScript has gone on.

Lock::terminate_execution_now() does what the C++ does up to the
unwinding, and returns the error to unwind with instead: a pending
error, which a #[jsg_method] returns, setting no return value and
throwing nothing. It is #[must_use]. Code that goes on regardless
still cannot run JavaScript or replace the termination, since calls
into JavaScript refuse to run, and the throw_* methods throw nothing,
while execution is terminating.

Tests call it from a method inside a JavaScript try/catch, and check
that neither the catch block nor the code after it runs.
C++ JSG converts a jsg::Promise<void> argument, and so a
kj::Promise<void> one such as ctx.waitUntil() takes, unlike other
promise arguments: a promise is taken as is, a thenable is followed
only if the unwrap_custom_thenables compatibility flag is on (checked
for with `in`, then followed through a resolver), and any other
value, a thenable included when the flag is off, becomes a promise
resolved with undefined. FromJS for Local<Promise> follows every
thenable, whatever the flag.

jsg::v8::VoidPromise is a FromJS type that converts as C++ does for
Promise<void>, so a Rust method that takes one converts its argument
when C++ would, before the method runs, and an omitted argument fails
with the same error, naming the type Promise.

Tests cover promises, other values, thenables with and without the
flag, the order in which a Proxy sees `has` and `get`, a has trap and
a then getter that throw, and an omitted argument.
ExecutionContext::abort() created the exception it aborts with when
given no reason itself. IoContext::abortWithoutReasonException() now
creates it, so that another implementation of ctx.abort() aborts with
an identical exception, down to the file and line it records, which
appear in logs.
These are the IoContext operations behind ctx.waitUntil(),
ctx.passThroughOnException() and ctx.abort(), for a Rust
ExecutionContext to match the C++ one exactly.

- IoContext::add_wait_until() waits for a promise as part of the
  request, through IoContext::awaitJs() and addWaitUntil(), as the C++
  ExecutionContext::waitUntil() does once JSG has converted its
  argument; a VoidPromise argument converts it as JSG does.
- IoContext::set_fail_open() calls IoContext::setFailOpen().
- IoContext::abort() aborts the IoContext with the reason, encoded by
  jsg::Lock::exceptionToKj(), or with the default exception, then
  terminates execution with Lock::terminate_execution_now() and
  returns its error for the method to return. ctx.abort() terminates
  right after aborting, so abort() does too, and no caller can forget
  to. state.abort() in a Durable Object aborts with an exception of
  its own, after breaking storage, so it will need another entry
  point.

Matching awaitJs() and exceptionToKj() exactly means calling them, so
the two shims behind add_wait_until() and abort() use C++ JSG, which
Rust JSG's shims otherwise must not. A section of ffi.c++ explains
the invariant they rely on. Every use, including getCurrentLock(),
which the allowlist test now counts, is in the C++ JSG allowlist as
a reviewed exception, so a new shim can't use C++ JSG without raising
a count. When C++ JSG throws JsExceptionThrown, as when a getter of
the abort reason throws, the shim returns false, and Rust passes the
pending exception on with Error::pending().

request-ops-test runs each scenario in a real worker, once on the C++
ExecutionContext and once on a Rust resource that implements the
three methods with the crate, and checks that JavaScript, the client
and the logs observe the same, including the file and line of the
exception the request fails with. Scenarios cover waitUntil() keeping
the request alive after the response, with a control without it; a
rejected waitUntil() promise, or followed thenable, logged as an
uncaught exception; other values and thenables with and without
unwrap_custom_thenables; an omitted argument; a has trap that throws;
fail-open; abort() without a reason, with an Error, a string, null or
a retryable object, and with a reason whose getter throws, after
which no JavaScript runs; and calls outside a request. It also checks
that the shims reject a stale IoContext id, and that add_wait_until()
and abort() do nothing once execution is terminating.
These run on both the C++ ExecutionContext and the Rust one, like the
other request-ops-test scenarios:

- A rejected waitUntil() promise is handled: no unhandledrejection.
- If logging a rejection runs a `stack` getter that throws, the
  getter's exception rejects unhandled, rather than an internal error,
  and the wait fails with "Promise will never complete.".
- A promise that can never settle has the request canceled as hung.
- A promise that another request settles, while the first still runs:
  without handle_cross_request_promise_resolution the reaction fails in
  the settling request, as an unhandled rejection, and the wait fails
  with "Promise will never complete."; with it, the reaction runs in
  the first request, and the wait completes.

The worker records unhandled rejections as events, and scenarios take
their compatibility flags as a Flags struct instead of a bool.
add_wait_until()'s documentation now says where reactions run and how
the wait fails when the promise settles in another request or is
garbage-collected.
abort() skipped only the reason case once execution was terminating.
Without a reason it still aborted the IoContext and called
terminate_execution_now() again, whose JSON::Stringify enters V8 while
terminating, which V8 DCHECKs against; C++ ctx.abort() can never get
there. It now does nothing while terminating either way, as
add_wait_until() does, and request-ops-test checks the no-reason case
too.
ffi::add_wait_until() and ffi::abort() map the shims' false, an
exception left pending, to Error::pending() themselves, so
IoContext::add_wait_until() and abort() no longer branch on a bool.
Also wraps abort()'s doc comment at 100 columns.
An ordinary error would throw a JavaScript exception that a catch or
finally block could run after. In a callback from V8, catch_panic turns
the panic into an internal error and a termination; only code entered
through callback_scope! without a handler would abort.
@bartlomieju
bartlomieju force-pushed the biwanczuk/rust-io-context-request-ops branch from 59fc0eb to 3d65e44 Compare October 31, 2026 16:02
@bartlomieju
bartlomieju changed the base branch from biwanczuk/rust-io-context-trace-spans to main October 31, 2026 16:02
@bartlomieju
bartlomieju marked this pull request as ready for review October 31, 2026 16:07
@bartlomieju
bartlomieju requested review from a team as code owners October 31, 2026 16:07

@clanky-the-clanker clanky-the-clanker Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Superseded by the review of 4f8b6a77.

This review, of 3d65e44d

💬 Approved with comments — The change is solid and its C++ parity tests are thorough. One WARNING: the public terminate_execution_now() shim enters V8 even when a termination is already pending. No current caller reaches that path.

Findings: 1 warning (1 is an inline comment on the diff)

The decision is advisory: this review is a comment and never approves, blocks or requests changes on the pull request.

🔧 Fix with your agent

Paste the following into your coding agent to investigate and fix these issues.

Fix the following code review issues found in pull request #8088.

Before making changes, review each issue and present a brief summary table:
- For each finding, state whether you agree, disagree, or need clarification
- If you disagree (e.g., the fix would require disproportionate changes
  for minimal ROI, or the finding is factually incorrect), explain why
- If you need to clarify something before deciding, ask those questions
- Then share your plan for which issues to tackle and in what order

## WARNING (1)

### [Code Quality] `isolate_terminate_execution_now` runs `JSON::Stringify` in the entered context even when a termination is already pending. This breaks the repository rule against entering V8 while terminating, and any Rust caller other than the guarded `IoContext::abort()` would hit a V8 debug check.
- **File:** src/rust/jsg/ffi.c++:1735

📋 Code Quality: ⚠️ 1 warning

terminate_execution_now() enters V8 again when it is called while execution is already terminating.

Recommendation: 📝 Minor Comments

  • [WARNING] isolate_terminate_execution_now runs JSON::Stringify in the entered context even when a termination is already pending. This breaks the repository rule against entering V8 while terminating, and any Rust caller other than the guarded IoContext::abort() would hit a V8 debug check. (src/rust/jsg/ffi.c++:1735)
View full review

The VoidPromise conversion, the io-context shims, the allowlist counts and the parity tests all match the C++ code they mirror. The inline comments from the earlier approval have been addressed in later commits:

  • abort() now does nothing while terminating (2062df8).
  • The shim wrappers return jsg::Result<()> (fc52c43).
  • The long doc line is wrapped.
  • The panic in terminate_execution_now() now has a documented reason (3d65e44).

The approver also asked for a rebase onto the current base. That is still outstanding.

isolate_terminate_execution_now is not safe while already terminating (src/rust/jsg/ffi.c++:1728-1737)

  • The shim always calls v8::JSON::Stringify(context, ...), even when a termination is already pending. The repository rules forbid calling a V8 API that takes a context in that state: V8 fails a debug check, or in a release build runs on with the termination pending.
  • Lock::terminate_execution_now() is public. As the pull request description notes, Rust can run again after catching a termination. Only IoContext::abort() guards against this, with is_execution_terminating(). Any other caller, such as a future Durable Object state.abort(), would hit the debug check.
  • Fix: return early from the shim when isolate->IsExecutionTerminating() is already true. This also makes abort()'s guard a second layer of defence rather than the only one. A test that calls terminate_execution_now() twice would cover it.

✅ All other checks passed

🔒 Security: ✅ LGTM

No security issues found.

  • The C++ shims check that the IoContext is current through currentWithId(). They catch JsExceptionThrown and leave the exception pending.
  • add_wait_until() and abort() return early while execution is terminating.
  • The two shims' use of C++ JSG is counted in cpp-jsg-allowlist.txt.

📖 AGENTS.md: ✅ LGTM

No AGENTS.md issues.


⚡ Performance: ✅ LGTM

No performance concerns.


📝 Documentation: ✅ LGTM

No documentation issues.


Reviewed commit: 3d65e44d

Comment thread src/rust/jsg/ffi.c++ Resolved
isolate_terminate_execution_now() ran JSON::Stringify, which takes the
entered context, even when execution was already terminating. V8
DCHECKs against entering through PrepareForExecutionScope then, and the
shim rules in ffi.h forbid it. Lock::terminate_execution_now() is public
and Rust can call it after catching a termination; only
IoContext::abort() guarded against that. The shim now returns at once
when the termination is already pending.

With an ordinary exception pending, the shim still enters V8, and says
why: PrepareForExecutionScope clears the exception and the termination
takes its place, as in jsg::Lock::terminateExecutionNow(). Not entering
would leave the termination undelivered, so JavaScript could catch the
exception and run on.

jsg-test now calls terminate_execution_now() twice, and after throwing
an ordinary exception, and checks that the termination reaches the
harness without any catch block or later code running.

@clanky-the-clanker clanky-the-clanker Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Superseded by the review of 4217e1c5.

This review, of 4f8b6a77

✅ Approved — The only earlier finding, entering V8 while already terminating, is fixed and tested. No new issues found.

The decision is advisory: this review is a comment and never approves, blocks or requests changes on the pull request.


✅ All checks passed

🔒 Security: ✅ LGTM

Both shims check the IoContext id with currentWithId() before they touch C++ JSG. They catch JsExceptionThrown and leave the exception pending. Neither shim enters V8 while execution is terminating.


📖 AGENTS.md: ✅ LGTM

No AGENTS.md issues.


📋 Code Quality: ✅ LGTM

isolate_terminate_execution_now now returns early when IsExecutionTerminating() is true (src/rust/jsg/ffi.c++:1731). The ordinary-exception case still enters V8 so that the termination replaces the exception, as C++ terminateExecutionNow() does, and a comment there explains this. The terminate_execution_now() tests cover calling it twice and calling it with an ordinary exception pending. The allowlist counts for the io-context shims match the file.


⚡ Performance: ✅ LGTM

No performance concerns.


📝 Documentation: ✅ LGTM

No documentation issues.


Reviewed commit: 4f8b6a77

@clanky-the-clanker clanky-the-clanker Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Superseded by the review of 43c731c7.

This review, of 4217e1c5

✅ Approved — The only earlier finding, entering V8 while already terminating, is fixed and tested. No new issues found.

The decision is advisory: this review is a comment and never approves, blocks or requests changes on the pull request.


✅ All checks passed

🔒 Security: ✅ LGTM

Both shims check the IoContext id with currentWithId() before they touch C++ JSG. They catch JsExceptionThrown and leave the exception pending. Neither shim enters V8 while execution is terminating.


📖 AGENTS.md: ✅ LGTM

No AGENTS.md issues.


📋 Code Quality: ✅ LGTM

isolate_terminate_execution_now now returns early when IsExecutionTerminating() is true (src/rust/jsg/ffi.c++:1731). The ordinary-exception case still enters V8 so that the termination replaces the exception, as C++ terminateExecutionNow() does, and a comment there explains this. The terminate_execution_now() tests cover calling it twice and calling it with an ordinary exception pending. The allowlist counts for the io-context shims match the file.


⚡ Performance: ✅ LGTM

No performance concerns.


📝 Documentation: ✅ LGTM

No documentation issues.


Reviewed commit: 4217e1c5 — same changes as 4f8b6a77, so this is that commit's review (no new review was run; line numbers are as of 4f8b6a77)

@clanky-the-clanker clanky-the-clanker Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Approved — The only earlier finding, entering V8 while already terminating, is fixed and tested. No new issues found.

The decision is advisory: this review is a comment and never approves, blocks or requests changes on the pull request.


✅ All checks passed

🔒 Security: ✅ LGTM

Both shims check the IoContext id with currentWithId() before they touch C++ JSG. They catch JsExceptionThrown and leave the exception pending. Neither shim enters V8 while execution is terminating.


📖 AGENTS.md: ✅ LGTM

No AGENTS.md issues.


📋 Code Quality: ✅ LGTM

isolate_terminate_execution_now now returns early when IsExecutionTerminating() is true (src/rust/jsg/ffi.c++:1731). The ordinary-exception case still enters V8 so that the termination replaces the exception, as C++ terminateExecutionNow() does, and a comment there explains this. The terminate_execution_now() tests cover calling it twice and calling it with an ordinary exception pending. The allowlist counts for the io-context shims match the file.


⚡ Performance: ✅ LGTM

No performance concerns.


📝 Documentation: ✅ LGTM

No documentation issues.


Reviewed commit: 43c731c7 — same changes as 4f8b6a77, so this is that commit's review (no new review was run; line numbers are as of 4f8b6a77)

# Conflicts:
#	src/workerd/io/io-context/ffi.c++
#	src/workerd/io/io-context/ffi.h
#	src/workerd/io/io-context/ffi.rs
#	src/workerd/io/io-context/lib.rs

@clanky-the-clanker clanky-the-clanker Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Superseded by the review of ea975690.

This review, of f9736d47

✅ Approved — No issues found. The changes since the last approved review are the merge with main, and they are consistent with the rest of the pull request. The points raised in the earlier discussion are addressed.

The decision is advisory: this review is a comment and never approves, blocks or requests changes on the pull request.


✅ All checks passed

🔒 Security: ✅ LGTM

  • IoContext check: both shims check the IoContext id with currentWithId() before they use C++ JSG.
  • Exceptions: they catch JsExceptionThrown and leave the exception pending.
  • Termination: no path enters V8 while execution is terminating.

📖 AGENTS.md: ✅ LGTM

No AGENTS.md issues.


📋 Code Quality: ✅ LGTM

  • Allowlist counts: the counts in cpp-jsg-allowlist.txt match io-context/ffi.c++ with comments stripped, which is how the allowlist test counts them.
  • APIs used exist: the C++ callees (exceptionToKj, awaitJs, getCurrentLock) and the Rust helpers (into_ffi_maybe, Error::pending) exist and match how the shims call them.
  • Build: the BUILD.bazel dependencies are consistent.
  • Termination: both termination guards return before touching V8, and tests cover them.

⚡ Performance: ✅ LGTM

No performance concerns. The new operations do a constant amount of work per call.


📝 Documentation: ✅ LGTM

No documentation issues.


Reviewed commit: f9736d47

main requires a TypeInfo implementation for every Rust JSG method
parameter type, so VoidPromise now describes Promise<void>, as C++
describes jsg::Promise<void>.

@clanky-the-clanker clanky-the-clanker Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💬 Approved with comments — The code changes are sound and well tested. One WARNING: the Rust contributor guidance in src/rust/AGENTS.md still says no other C++ JSG shim exceptions are allowed, which this pull request now contradicts.

Findings: 1 warning

The decision is advisory: this review is a comment and never approves, blocks or requests changes on the pull request.

🔧 Fix with your agent

Paste the following into your coding agent to investigate and fix these issues.

Fix the following code review issues found in pull request #8088.

Before making changes, review each issue and present a brief summary table:
- For each finding, state whether you agree, disagree, or need clarification
- If you disagree (e.g., the fix would require disproportionate changes
  for minimal ROI, or the finding is factually incorrect), explain why
- If you need to clarify something before deciding, ask those questions
- Then share your plan for which issues to tackle and in what order

## WARNING (1)

### [AGENTS.md] src/rust/AGENTS.md:50 still says `io_context_await_io_and_run()` is the only reviewed C++ JSG shim exception and must not be extended. This pull request adds `io_context_add_wait_until()` and `io_context_abort()` as reviewed exceptions, so update the guidance to list them and the conditions that make them sound.
- **File:** src/rust/AGENTS.md:50

📖 AGENTS.md: ⚠️ 1 warning

src/rust/AGENTS.md still says no other C++ JSG shim exceptions are allowed

Recommendation: 📝 Minor Comments

  • [WARNING] src/rust/AGENTS.md:50 still says io_context_await_io_and_run() is the only reviewed C++ JSG shim exception and must not be extended. This pull request adds io_context_add_wait_until() and io_context_abort() as reviewed exceptions, so update the guidance to list them and the conditions that make them sound. (src/rust/AGENTS.md:50)
View full review

src/rust/AGENTS.md:50 says that the only reviewed exception to the no-C++-JSG rule for shims is io_context_await_io_and_run(), and says "don't extend it". This pull request adds two more reviewed exceptions in src/workerd/io/io-context/ffi.c++:

  • io_context_add_wait_until(), which uses awaitJs()
  • io_context_abort(), which uses exceptionToKj()

Their rationale is documented in src/rust/jsg/README.md and they are counted in the allowlist. Please update the AGENTS.md line to list these exceptions and the conditions under which they are sound, so the guidance no longer contradicts the code.


✅ All other checks passed

🔒 Security: ✅ LGTM

  • Both IoContext shims check that the IoContext is current with currentWithId() before they use C++ JSG.
  • They catch JsExceptionThrown and leave the exception pending.
  • No path enters V8 while execution is terminating.
  • The new uses of C++ JSG are counted in cpp-jsg-allowlist.txt.

📋 Code Quality: ✅ LGTM

No issues found.

  • The VoidPromise TypeInfo entry in rtti.rs and its export from v8.rs match how C++ describes jsg::Promise<void>.
  • The jsg::Promise<void> construction in the io_context_add_wait_until shim matches the constructor in jsg/promise.h.
  • Both termination guards return before touching V8, and request_ops_while_terminating tests them.

⚡ Performance: ✅ LGTM

No performance concerns. Each new operation does a fixed amount of work per call.


📝 Documentation: ✅ LGTM

No documentation issues.


Reviewed commit: ea975690

@bartlomieju
bartlomieju merged commit 8cb616a into main Nov 3, 2026
23 of 24 checks passed
@bartlomieju
bartlomieju deleted the biwanczuk/rust-io-context-request-ops branch November 3, 2026 11:42
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.

3 participants