Skip to content

Commit 735b657

Browse files
authored
make Immediate class thread-safe (#5207)
1 parent f25c556 commit 735b657

File tree

4 files changed

+38
-10
lines changed

4 files changed

+38
-10
lines changed

src/workerd/api/global-scope.c++

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -997,11 +997,14 @@ bool Navigator::sendBeacon(jsg::Lock& js, kj::String url, jsg::Optional<Body::In
997997
// ======================================================================================
998998

999999
Immediate::Immediate(IoContext& context, TimeoutId timeoutId)
1000-
: contextRef(context.getWeakRef()),
1000+
: ioContext(context.addObject(context)),
10011001
timeoutId(timeoutId) {}
10021002

10031003
void Immediate::dispose() {
1004-
contextRef->runIfAlive([&](IoContext& context) { context.clearTimeoutImpl(timeoutId); });
1004+
// IoPtr will throw if the IoContext is no longer valid, which is fine - accessing the Immediate
1005+
// from the wrong IoContext should throw an error, just as accessing the Immediate when it's
1006+
// owning IoContext is gone.
1007+
ioContext->clearTimeoutImpl(timeoutId);
10051008
}
10061009

10071010
jsg::Ref<Immediate> ServiceWorkerGlobalScope::setImmediate(jsg::Lock& js,

src/workerd/api/global-scope.h

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -414,12 +414,10 @@ class Immediate final: public jsg::Object {
414414
}
415415

416416
private:
417-
// On the off chance user code holds onto to the Ref<Immediate> longer than
418-
// the IoContext remains alive, let's maintain just a weak reference to the
419-
// IoContext here to avoid problems. This reference is used only for handling
420-
// the dispose operation, so it should be perfectly fine for it to be weak
421-
// and a non-op after the IoContext is gone.
422-
kj::Own<IoContext::WeakRef> contextRef;
417+
// Note: We cannot use IoContext::WeakRef here because it's not thread-safe (it's only intended
418+
// to be held from KJ I/O objects, but this is a JSG object which can be accessed by V8's GC
419+
// on different threads). Instead, we use IoPtr<IoContext> which is safe to hold from JSG objects.
420+
IoPtr<IoContext> ioContext;
423421
TimeoutId timeoutId;
424422
};
425423

src/workerd/api/node/tests/timers-nodejs-test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,3 +496,27 @@ export const timersIntervalPromise = {
496496
}
497497
},
498498
};
499+
500+
export const testImmediateFromOtherContext = {
501+
async test(_, env) {
502+
// Set up the global setImmediate...
503+
await env.SUBREQUEST.fetch('http://example.com');
504+
505+
// Try to access it from a different IoContext.
506+
await env.SUBREQUEST.fetch('http://example.com');
507+
},
508+
};
509+
510+
export default {
511+
fetch() {
512+
if (globalThis.IMMEDIATE_TEST === undefined) {
513+
globalThis.IMMEDIATE_TEST = setImmediate(() => {});
514+
globalThis.IMMEDIATE_TEST[Symbol.dispose]();
515+
} else {
516+
throws(() => globalThis.IMMEDIATE_TEST[Symbol.dispose](), {
517+
message: /perform I\/O on behalf of a different request/,
518+
});
519+
}
520+
return new Response();
521+
},
522+
};

src/workerd/api/node/tests/timers-nodejs-test.wd-test

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ const unitTests :Workerd.Config = (
77
modules = [
88
(name = "worker", esModule = embed "timers-nodejs-test.js")
99
],
10-
compatibilityDate = "2025-01-09",
11-
compatibilityFlags = ["nodejs_compat", "no_nodejs_compat_v2"],
10+
compatibilityDate = "2025-10-04",
11+
compatibilityFlags = ["nodejs_compat"],
12+
bindings = [
13+
( name = "SUBREQUEST", service = "nodejs-timers-test" ),
14+
],
1215
)
1316
),
1417
],

0 commit comments

Comments
 (0)