Repository navigation
Conversation
d82e59e to
463f413
Compare
|
💬 Approved with comments — No defects found. The new behaviour is behind the DEFER_CROSS_CONTEXT_ACTIONS autogate. There is one optional documentation note, and some changed files were not fully checked. Findings: 1 info · 1 suggestion The decision is advisory: this comment never approves, blocks or requests changes on the pull request. 📋 Code Quality: 💬 1 infoNo defects found. The gating is complete, including the new onHibernate change in worker.c++. Recommendation: 💬 Optional Nits
View full review
This review did not fully check some changed files; they are listed below. 📝 Documentation: 💬 1 suggestionOptional: the abort-signal reference doesn't say what happens with the autogate off. Recommendation: 💬 Optional Nits
View full review
✅ All other checks passed🔒 Security: ✅ LGTM
📖 AGENTS.md: ✅ LGTMThe new line in ⚡ Performance: ✅ LGTMNo performance concerns. Reviewed commit: |
|
Since last review: 0 resolved, 0 still open, 0 new. Reviewed commit: 85143edb · github run |
181cae7 to
9257f39
Compare
da36ecd to
a397ab5
Compare
33ee348 to
405637d
Compare
4e05894 to
086b8eb
Compare
086b8eb to
b7c28c8
Compare
2c8b392 to
58ec246
Compare
An actor's IoContext outlives its requests, so IoContext::run() can be called while the actor is idle, or lose its request while it waits for a lock. run() then failed with an untyped internal error. Now run() checks for a request when called and after taking each lock. Without one, it skips the callback and fails with a JavaScript Error carrying NO_CURRENT_INCOMING_REQUEST_DETAIL_ID, so callers can tell this case apart. JavaScript sees its message instead of "internal error". An aborted context fails with its abort exception instead, as run() already does when it has a request. To keep a trail for the cases that are bugs, a run called without a request logs a warning, which reaches Sentry, at most once an hour. A run that loses its request while waiting for a lock doesn't log: the request can end during any wait, and the stack trace would only show the event loop. This is behind a new autogate, DEFER_CROSS_CONTEXT_ACTIONS, named for the deferral of cross-context actions that the next commit builds on this. With it off, run() fails as before. The delete queue signal task still aborts the IoContext on this error; the next commit fixes that.
Other IoContexts can queue actions on an IoContext, e.g. to settle one of its promises or deliver an AbortSignal's abort. Running them needs a current IncomingRequest. An idle actor, such as one holding hibernatable WebSockets, has none, so the run failed and aborted the IoContext, breaking the actor for all its clients. Now actions queued while an actor has no request wait for its next request, which runs them as a waitUntil task, counting toward its limits. A run that loses its request while waiting for a lock also leaves them queued. Any other failure, including an action's own, still aborts the IoContext. Actions still queued when the IoContext ends are dropped, with a NOSENTRY warning. The onHibernate event doesn't run them: they would use up its handler's CPU budget, and the cleanup after the handler would cancel whatever they start. They stay queued until the IoContext ends. The isolate's memory limit bounds the queue, since actions keep what they act on alive on its heap. Two kinds of action escaped it: - A request to arm the RPC subscription of an AbortSignal received over RPC holds only a weak reference. Queue at most one per signal. - An abort copied its reason for each queued delivery and RPC clone, without counting the copies. Now the native deliveries share one copy and the RPC clones another, and both count as external memory. The arm request also now checks its weak reference with tryAddRef(), since a garbage collection may condemn the signal before it runs. handle_cross_request_promise_resolution only guarantees which IoContext a promise settles in, not when, so deferring keeps its contract. The old behavior broke the actor, so we accept the risk of changing it without a compatibility flag. The DEFER_CROSS_CONTEXT_ACTIONS autogate from the previous commit also covers the deferral, the one-per-signal limit and the external memory accounting of the shared copies. With it off, they behave as before. The sharing itself and tryAddRef() apply either way: sharing an immutable copy changes no behavior, and tryAddRef() keeps the arm request from using a condemned signal.
58ec246 to
85143ed
Compare
An actor's IoContext outlives its requests. When another IoContext settles one of its promises, or forwards an AbortSignal abort, the action is queued, and the delete queue signal task runs it with
IoContext::run(). An idle actor, such as one holding hibernatable WebSockets, has no current IncomingRequest, so the run failed and the signal task aborted the IoContext, breaking the actor for all its clients. The same happened if the request ended while the run waited for a lock.This PR changes that:
IoContext::run()checks for a current request when it's called and again after acquiring each lock. Without one, the callback doesn't run, andrun()fails with a JavaScript Error carryingNO_CURRENT_INCOMING_REQUEST_DETAIL_IDinstead of an untyped internal error. As before, it throws if there's no request when it's called, and its promise rejects if the request ends while it waits for a lock. The failure is reported as a user error, with a warning logged at most once an hour.run()while the IoContext has no request. The actions stay queued, andIncomingRequest::delivered()runs them as a waitUntil task of the next request, so that request can't finish before they've run. If a run loses its request while it waits for a lock, the actions stay queued and the IoContext is left alone. Any other failure, including an action's own, still aborts the IoContext.handle_cross_request_promise_resolutiononly guarantees that a promise settles in its owning IoContext, not when, so deferring keeps that contract.