Skip to content

Allow channel tokens to be created asynchronously#6724

Draft
kentonv wants to merge 5 commits intomainfrom
kenton/async-channel-tokens
Draft

Allow channel tokens to be created asynchronously#6724
kentonv wants to merge 5 commits intomainfrom
kenton/async-channel-tokens

Conversation

@kentonv
Copy link
Copy Markdown
Member

@kentonv kentonv commented May 4, 2026

There are cases where it is difficult to acquire the channel token for a SubrequestChannel or ActorClassChannel synchronously, but until now we have needed to do so in order to serialize Fetchers and DurableObjectClasses.

We can't make serialization itself be async, because this would mess up e-order: A call that needs to wait for something while serializing params might end up being delayed until after some subsequent call which didn't wait, and so would be delivered out-of-order.

To avoid this, we make it possible for a call to be sent with an IOU for the channel tokens. This uses ExternalPusher. The call embeds an external which is a promise capability. Later, the caller invokes the callee's ExternalPusher to push the channel token to it, and resolves the IOU promise to the resulting object. The callee can then unwrap the promise to get their token.


This PR also, in preparation for landing the above, does some cleanup:

  • Remove StreamSink in favor of ExternalPusher, since this new functionality requires ExternalPusher.
  • Remove ServerTopLevelMembrane, since it otherwise prevented ExternalPusher from receiving calls after the RPC call(), and this new functionality needs that.

These two cleanups require that the respective autogates have rolled out, namely rpc-use-external-pusher and jsrpc-session-handle. DO NOT MERGE this PR until those have rolled out! I am leaving this marked as "draft" until that time.

kentonv added 3 commits May 4, 2026 15:33
This mostly reverts commit 0d86b66.

This removes the new `debugContext` string that was being passed around to distinguish params from results. Now that we've debugged the issue, this is more noise than it is worth.

We do keep the `cap.debugInfo()` debug log on failures, since that's not so invasive and is more useful anyway.
DO NOT MERGE until the autogate has been rolled to all of production!
This allows ExternalPusher methods to continue to be invoked after the top-level RPC call().

(DO NOT MERGE until jsrpc-session-handle autogate is rolled out in prod.)
@kentonv kentonv requested a review from harrishancock May 4, 2026 22:19
Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

I'm Bonk, and I've done a quick review of your PR.

This PR enables asynchronous channel token creation for SubrequestChannel/ActorClassChannel (using ExternalPusher + DelayedChannelToken), removes the now-fully-rolled-out StreamSink and ServerTopLevelMembrane code paths, and cleans up the corresponding autogates.

Findings (sorted by severity):

  1. [LOW] Comment for getActorClass() incorrectly says it dispatches to getSubrequestChannelResolved() — should be getActorClassResolved().
  2. [LOW] Typo prmoise in http.c++ and actor.c++ (introduced in this PR).
  3. [LOW] Typos in comments: "Undelying" and "implemnetation" (two locations).

This review was generated by an AI assistant. Findings may contain false positives; please use your judgement.

Comment thread src/workerd/io/io-channels.h Outdated
Comment thread src/workerd/api/http.c++ Outdated
Comment thread src/workerd/api/actor.c++ Outdated
Comment thread src/workerd/io/io-channels.h Outdated
Comment thread src/workerd/io/io-channels.h Outdated
@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 4, 2026

Review posted successfully on PR #6724. Here's a summary of my findings:

Overall assessment: This is a well-structured PR. The architecture for async channel tokens using kj::OneOf<kj::Array<byte>, kj::Promise<kj::Array<byte>>> is clean, and the ExternalPusher/DelayedChannelToken mechanism for avoiding e-order violations is sound. The capnp schema changes maintain wire compatibility (old fields preserved as obsolete*, new ordinals @12/@13 don't collide). The autogate removals for RPC_USE_EXTERNAL_PUSHER and JSRPC_SESSION_HANDLE are consistent with the PR description's prerequisite. Test coverage is good with both synchronous and asynchronous token generation paths tested.

Findings posted (all low severity):

  1. Comment for getActorClass() incorrectly says it dispatches to getSubrequestChannelResolved() — should be getActorClassResolved() (copy-paste from the SubrequestChannel comment)
  2. Typo prmoise instead of promise in http.c++:2235 and actor.c++:314
  3. Typos "Undelying" and "implemnetation" in two comment locations in io-channels.h

All findings include concrete suggestion blocks for one-click fixes.

github run

@kentonv kentonv force-pushed the kenton/async-channel-tokens branch from 3ff9ba6 to 7160d57 Compare May 4, 2026 22:34
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 4, 2026

Codecov Report

❌ Patch coverage is 43.64964% with 386 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.40%. Comparing base (9793e96) to head (4ebf9af).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
src/workerd/io/io-channels.c++ 20.57% 173 Missing and 20 partials ⚠️
src/workerd/server/server.c++ 28.57% 37 Missing and 3 partials ⚠️
src/workerd/api/actor.c++ 30.43% 24 Missing and 8 partials ⚠️
src/workerd/api/container.c++ 0.00% 30 Missing ⚠️
src/workerd/api/http.c++ 36.17% 24 Missing and 6 partials ⚠️
src/workerd/server/channel-token-test.c++ 79.71% 7 Missing and 21 partials ⚠️
src/workerd/io/external-pusher.c++ 25.00% 12 Missing ⚠️
src/workerd/server/channel-token.c++ 77.77% 4 Missing and 4 partials ⚠️
src/workerd/api/worker-rpc.c++ 86.48% 4 Missing and 1 partial ⚠️
src/workerd/tests/test-fixture.h 0.00% 4 Missing ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6724      +/-   ##
==========================================
- Coverage   66.56%   66.40%   -0.17%     
==========================================
  Files         402      402              
  Lines      115885   116078     +193     
  Branches    19410    19437      +27     
==========================================
- Hits        77142    77077      -65     
- Misses      27162    27386     +224     
- Partials    11581    11615      +34     

☔ View full report in Codecov by Sentry.
📢 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.

@kentonv kentonv force-pushed the kenton/async-channel-tokens branch from 7160d57 to 2cddddb Compare May 4, 2026 23:21
kentonv added 2 commits May 4, 2026 19:11
There are cases where it is difficult to acquire the channel token for a SubrequestChannel or ActorClassChannel synchronously, but until now we have needed to do so in order to serialize `Fetcher`s and `DurableObjectClass`es.

We can't make serialization itself be async, because this would mess up e-order: A call that needs to wait for something while serializing params might end up being delayed until after some subsequent call which didn't wait, and so would be delivered out-of-order.

To avoid this, we make it possible for a call to be sent with an IOU for the channel tokens. This uses `ExternalPusher`. The call embeds an external which is a promise capability. Later, the caller invokes the callee's `ExternalPusher` to push the channel token to it, and resolves the IOU promise to the resulting object. The callee can then unwrap the promise to get their token.

(Opus 4.7 wrote the new test cases in channel-token-test but the rest of the code was by hand.)
This makes it so `getSubrequestChannel()` and similar methods of `IoChannelFactory` make sure that the contents of a `props` cap table are fully resolved before forwarding on to the `IoChannelFactory` implementation.

This means that the underlying implementation of `getSubrequestChannelResolved()` et al doens't need to change to start calling `getResolved()` before trying to downcast channel objects to implementation-specific subclasses. This otherwise would have been really annoying to do in the internal codebase.

Relatedly, this adds an `ensureAllResolved()` method to `DynamicWorkerSource`, for resolving channels there.
@kentonv kentonv force-pushed the kenton/async-channel-tokens branch from 2cddddb to 4ebf9af Compare May 5, 2026 00:11
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.

2 participants