Skip to content

Conversation

@danlapid
Copy link
Collaborator

Introduces a new privileged debug port interface that enables dynamic access to worker entrypoints at runtime.

Currently, service bindings between workerd processes must be configured at startup in the config file. Changing binding targets requires a full process restart. This is problematic for local development tools like Miniflare, which need to dynamically re-target RPC connections at runtime.

Today, Miniflare works around this by running a Node.js TCP server that sits between workerd processes and manually re-routes RPC traffic. This adds significant complexity and overhead.

This commit implements the WorkerdDebugPort interface which exposes all service entrypoints in a process through a privileged RPC interface. External tools can now:

  • Get direct access to any entrypoint with custom props at runtime
  • Start events and invoke methods via JS RPC without HTTP overhead
  • Dynamically switch between entrypoints without process restarts

This enables Miniflare to eliminate its TCP proxy layer and handle dynamic RPC routing natively through workerd.

interface WorkerdDebugPort {
  getEntrypoint(service, entrypoint, props) -> (worker)
  getActor(service, entrypoint, actorId) -> (actor);
}

🤖 Generated with Claude Code

@danlapid danlapid requested a review from penalosa November 21, 2025 17:46
@danlapid danlapid force-pushed the dlapid/debug_port branch 2 times, most recently from ffbdd7c to df9b629 Compare November 21, 2025 17:59
@codspeed-hq
Copy link

codspeed-hq bot commented Nov 21, 2025

CodSpeed Performance Report

Merging #5568 will not alter performance

Comparing dlapid/debug_port (452a4c4) with main (f80cae3)

Summary

✅ 57 untouched
⏩ 30 skipped1

Footnotes

  1. 30 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@github-actions
Copy link

github-actions bot commented Nov 21, 2025

The generated output of @cloudflare/workers-types matches the snapshot in types/generated-snapshot 🎉

@danlapid danlapid marked this pull request as ready for review November 21, 2025 19:24
@danlapid danlapid requested review from a team as code owners November 21, 2025 19:24
@danlapid danlapid requested a review from kentonv November 21, 2025 19:24
@danlapid danlapid force-pushed the dlapid/debug_port branch 2 times, most recently from 277dee0 to 9389f13 Compare November 21, 2025 23:44
Introduces a new privileged debug port interface that enables dynamic
access to worker entrypoints at runtime.

Currently, service bindings between workerd processes must be configured
at startup in the config file. Changing binding targets requires a full
process restart. This is problematic for local development tools like
Miniflare, which need to dynamically re-target RPC connections at runtime.

Today, Miniflare works around this by running a Node.js TCP server that
sits between workerd processes and manually re-routes RPC traffic. This
adds significant complexity and overhead.

This commit implements the WorkerdDebugPort interface which exposes all
service entrypoints in a process through a privileged RPC interface.
External tools can now:

- Get direct access to any entrypoint with custom props at runtime
- Start events and invoke methods via JS RPC without HTTP overhead
- Dynamically switch between entrypoints without process restarts

This enables Miniflare to eliminate its TCP proxy layer and handle
dynamic RPC routing natively through workerd.

```
interface WorkerdDebugPort {
  getEntrypoint(service, entrypoint, props) -> (worker)
  getActor(service, entrypoint, actorId) -> (actor);
}
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
return s.accept(conn);
}

public:
Copy link
Member

Choose a reason for hiding this comment

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

It appears the intent here is to make WorkerdBootstrapImpl public, but this line actually makes the whole rest of the class public, including things that aren't needed.

It seems to me that WrokerdBootstrapImpl should actually be moved out of HttpListener. Previously it was an implementation detail used only by HttpListener, but now some other code unrelated to HttpListener wants to reuse the same implementation. So it needs to live elsewhere. And EventDispatcherImpl should probably become a private nested class inside WorkerdBootstrapImpl...

tasks(*this) {}

kj::Promise<void> run() {
for (;;) {
Copy link
Member

Choose a reason for hiding this comment

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

This for loop and the confusingly-named acceptLoop() method (confusing because it doesn't encapsulate the loop) could be replaced with just using capnp::TwoPartyServer::listen(), which implements the accept loop for you.

if (workerService != nullptr) {
// This is a WorkerService, use getEntrypoint which supports both entrypoints and props
kj::Maybe<kj::StringPtr> maybeEntrypoint;
if (entrypointName.size() > 0) {
Copy link
Member

Choose a reason for hiding this comment

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

Use params.hasEntrypoint() rather than checking for a zero-length string. (Here and below.)

auto service = serviceEntry->service();

// Convert props from Frankenvalue
Frankenvalue props = Frankenvalue::fromCapnp(propsReader);
Copy link
Member

Choose a reason for hiding this comment

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

Probably should check params.hasProps() and skip calling fromCapnp() on default props. (A default-initialized Frankenvalue is all you want.)

actorIdData.size() == SHA256_DIGEST_LENGTH, "Invalid actor ID size", actorIdData.size());

// Create an ActorId from the provided bytes
kj::Own<ActorIdFactory::ActorId> actorId =
Copy link
Member

Choose a reason for hiding this comment

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

This only supports Durable Objects. If the actor namespace is emphemeral (aka colo-local) then the ID is a plain string, not an ActorId.

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