Repository navigation
Give Rust APIs the IoContext of the current request - #7963
bartlomieju wants to merge 14 commits into
Conversation
piscisaureus
left a comment
There was a problem hiding this comment.
The main points: catching shouldn't be public and safe; the lifetime TODO (I think it's sound today, and it can be made checked cheaply, details inline); and write_logfwdr skips the DO output gate.
One question I couldn't settle, outside the diff: the legacy module registry evaluates internal modules synchronously via resolveInternalImport (jsg/modules.h ~707-714) with no SuppressIoContextScope. That's reachable during a request through the process getter (api/global-scope.c++ ~1365) and DO wrapper modules (api/worker-rpc.c++ ~3309), so module evaluation can see a request's IoContext. I haven't checked whether any Rust builtin is evaluated on those paths. If one can be, suppress the IoContext around Rust builtin evaluation, or document it.
Add the io-context crate, beside //src/workerd/io in a package of its own because //src/workerd/io depends on jsg. It has IoContext::current() and try_current(), now(), the request's clock, and write_logfwdr(). A handle holds the IoContext's id (IoContext::getId()), not a pointer. Each call looks up IoContext::tryCurrent() and fails unless the id matches, so a handle can never reach another request's IoContext or a destroyed one, and lib.rs needs no unsafe. Outside a request, current() fails with the error C++ IoContext::current() throws; the Rust copy of its message and kAsyncIoErrorMessage name each other. In a Durable Object, write_logfwdr() waits for the output gate, as AnalyticsEngine::writeDataPoint() does, and it rejects anything but exactly one serialized message. The shims use no C++ JSG and are checked by cpp-jsg-allowlist-test. io-context-test runs the crate in a real worker through TestFixture, calling Rust through a test-only bridge: inside and outside a request, logfwdr delivery and validation, the output gate, and stale ids.
fa976f7 to
68dddfd
Compare
|
On the module registry question from the review: today's Rust builtins ( |
Take the jsg::Error by reference in to_io(), and return errors rather than unwrapping when converting the time to nanoseconds.
Each copy now names the other, and the test that checks they match.
Either module registry can run a Rust builtin's add_builtin closure during a request: the legacy registry the first time a request resolves the module, for example by reading the process global, and the new registry when a request resolves the synthetic module directly. The closure then saw that request's IoContext. The adapters in rust/jsg/jsg.h now construct a CallScope around each call of a Rust module callback, and the Node.js registration passes SuppressIoContextScope. jsg cannot depend on the IoContext, so the scope is a template parameter with no default, which makes every registration choose it. io-context-test resolves a probe module during a request through each registry and checks that its closure saw no IoContext.
…-context # Conflicts: # src/rust/jsg/modules.rs
lib-test.rs was listed in the crate's sources, but lib.rs never declared it as a module, so it was never compiled and io-context_test passed without running any tests.
|
✅ Approved — No issues found. The shim now passes the id and the shared message constant, so the crate has less surface area and no hand-written unsafe code. Every concern from the earlier discussion is still addressed. Not opened by the correctness review: 5 changed files it was meant to read
The decision is advisory: this comment never approves, blocks or requests changes on the pull request. ✅ All checks passed🔒 Security: ✅ LGTM
📖 AGENTS.md: ✅ LGTMThe 📋 Code Quality: ✅ LGTM
⚡ Performance: ✅ LGTMNo performance issues found. 📝 Documentation: ✅ LGTMNo documentation issues found. Reviewed commit: |
|
Since last review: 0 resolved, 0 still open, 0 new. Carried forward from the last review: docs, kj-style (no author changes in their files since then; earlier findings stand) Not re-run: api-compat, tests, compat-flags, design-simplicity, jsg-gc, memory-safety, rust-first (no author changes in their files since the last review) Reviewed commit: 2a91b052 · github run |
guybedford
left a comment
There was a problem hiding this comment.
LGTM. The id-checked handle settles the soundness question cleanly, and the CallScope wrapping closes the builtin-evaluation gap for both registries. A few nits inline, none blocking.
The shim now returns the current IoContext's id, from IoContext::tryGetCurrentId(), instead of a reference that Rust read the id through. That drops the opaque IoContext type and all hand-written unsafe code from the crate. The message of the error IoContext::current() throws outside a request becomes IoContext::NO_REQUEST_MESSAGE, which Rust reads through the shim instead of keeping a copy, so the test that the copies match goes away. Also builds the test bridge against main, which removed jsg::Number.
…nto biwanczuk/rust-jsg-io-context
Rust APIs had no access to the request they run on behalf of.
This adds the
io-contextcrate (src/workerd/io/io-context):IoContext::current(lock) -> jsg::Result<IoContext<'s>>: outside a request, such as in a Worker's global scope, fails with the same error C++IoContext::current()throws ("Disallowed operation called within global scope…"). The message is nowIoContext::NO_REQUEST_MESSAGE, which Rust reads through the shim, so there is one copy.IoContext::try_current(lock) -> Option<IoContext<'s>>.now() -> jsg::Result<SystemTime>:IoContext::now(), the request's (Spectre-coarsened) clock, to the nanosecond.write_logfwdr(channel, message):IoContext::writeLogfwdr()for one serialized Cap'n Proto message. In a Durable Object, the write waits for the output gate, asAnalyticsEngine::writeDataPoint()does. C++ keeps doing thewaitUntiland pending-event bookkeeping. A message that is empty, not a whole number of words, or followed by more bytes is rejected.It lives beside
//src/workerd/io, in a package of its own, because//src/workerd/iodepends onjsg.How a handle refers to its IoContext
An
IoContext<'s>holds the process-unique id of the C++IoContext(IoContext::getId()), not a pointer to it. Every call into C++ looks upIoContext::tryCurrent()and fails unless its id matches. Ids are never reused, so a handle can never reach another request'sIoContext, or one that has been destroyed, whatever code holds it. The shim returns only the current id (IoContext::tryGetCurrentId()), never a reference, so the crate has no hand-writtenunsafe. The'slifetime ties a handle to thejsg::Lockit came from, and the handle is neitherSendnorSync.The shims call no C++ JSG, and
//src/rust/jsg:cpp-jsg-allowlist-testnow checks them too.Builtin modules see no request
A builtin's exports must not depend on whichever request happens to import it first. But either module registry can run a Rust builtin's
add_builtinclosure during a request:processglobal;resolveInternalModule,require,getBuiltinModule). Only a built-in ES module importing it went through the eval callback'sSuppressIoContextScope.Every Rust module callback goes through
callModuleCallbackinrust/jsg/jsg.h, which now constructs aCallScopearound the call. jsg can't depend on theIoContext, soCallScopeis a template parameter ofRustModuleRegistryandRustBuiltinModuleAdapter. It has no default, so every registration has to choose one. The Node.js registrations inapi/node/node.hpassSuppressIoContextScope, soIoContext::current()fails inside anadd_builtinclosure.Not in this PR
await_io,addTask/waitUntil,IoOwn, metrics, timers and the rest ofIoContext: they have no Rust consumer yet.write_logfwdr()copies the message twice: C++ copies the bytes into an aligned message, then into the outgoing one. Passing the segments of a Rustcapnp::message::Builderwould save one copy.