diff --git a/.github/bonk/specialists/rust-first.md b/.github/bonk/specialists/rust-first.md index 5344178db04..513e62cc56b 100644 --- a/.github/bonk/specialists/rust-first.md +++ b/.github/bonk/specialists/rust-first.md @@ -8,7 +8,7 @@ paths: - src/**/ffi.h - src/**/bridge.h - src/**/cxx-bridge.h - - src/workerd/server/cli-main.* + - src/workerd/server/factory/bootstrap.* - src/workerd/server/config-compiler.* - src/workerd/util/setup-async-io.* budget: 4m diff --git a/AGENTS.md b/AGENTS.md index 6b86850bb12..b6395ccc731 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -162,8 +162,7 @@ Be aware that workerd uses tcmalloc for memory allocation in the typical case. W - **`jsg/`** - JavaScript Glue layer for V8 integration - Core JavaScript engine bindings and type wrappers - Promise handling, memory management, module system -- **`server/`** - Main server implementation and configuration - - Main binary entry point and Cap'n Proto config handling +- **`server/`** - The `workerd` binary: the Rust command line (`cli/`) and server (`server/`) over the C++ worker factory (`factory/`), plus the Cap'n Proto config schema; see `src/workerd/server/AGENTS.md` - **`util/`** - Utility libraries (SQLite, UUID, threading, etc.) ### Multi-Language Support @@ -171,7 +170,7 @@ Be aware that workerd uses tcmalloc for memory allocation in the typical case. W - **`src/cloudflare/`** - Cloudflare-specific APIs (TypeScript) - **`src/node/`** - Node.js compatibility layer (TypeScript) - **`src/pyodide/`** - Python runtime support via Pyodide -- **`src/rust/`** - Rust integration components; see `src/rust/AGENTS.md` for the full macro reference and GC tracing guide +- **`src/rust/`** - Rust crates without a C++ home of their own and the in-tree cxx fork; see `src/rust/AGENTS.md` for the full macro reference and GC tracing guide (component crates such as the server live beside their C++) ### Configuration System @@ -190,6 +189,7 @@ Be aware that workerd uses tcmalloc for memory allocation in the typical case. W | Modify compat flags | `src/workerd/io/compatibility-date.capnp` | ~1400 lines; annotations define flag names + enable dates | | Add autogate | `src/workerd/util/autogate.h` | Add key to WORKERD_AUTOGATES macro; kebab-case name auto-derived; see header comment | | Config schema | `src/workerd/server/workerd.capnp` | Cap'n Proto; capability-based security | +| Server / binary | `src/workerd/server/` | Rust `workerd-server` crate (`server/`) drives the C++ `worker-factory`; see `server/AGENTS.md` | | Worker lifecycle | `src/workerd/io/worker.{h,c++}` | Isolate, Script, Worker, Actor classes | | Request lifecycle | `src/workerd/io/io-context.{h,c++}` | IoContext: the per-request god object | | Coroutine cancellation | `docs/reference/detail/async-patterns.md` | `CURRENT_INVOCATION` with `KJ_DEFER`; `KJ_ON_SCOPE_FAILURE` is exception-only | diff --git a/build/wd_rust_crate.bzl b/build/wd_rust_crate.bzl index 12979e87e33..81ab075ff65 100644 --- a/build/wd_rust_crate.bzl +++ b/build/wd_rust_crate.bzl @@ -77,6 +77,7 @@ def wd_rust_crate( cxx_bridge_tags = [], cxx_bridge_local_defines = [], cxx_bridge_features = [], + cxx_bridge_visibility = [], testonly = False, visibility = None): """Define rust crate. @@ -104,6 +105,8 @@ def wd_rust_crate( cxx_bridge_deps: either a flat dependency list applied to every bridge source, or a dict of bridge source => dependency list. cxx_bridge_hdrs: headers the bridges include!(); defaults to every .h file in the package. + cxx_bridge_visibility: visibility of the generated @cxx libraries, for C++ in + another package that includes a bridge's header (it must also link the crate). testonly: True for a crate that only tests depend on (a test harness, the Rust half of a C++ test). Like other test code, it is not held to //build/rust:lints. """ @@ -144,8 +147,9 @@ def wd_rust_crate( hdrs = hdrs, include_prefix = include_prefix, strip_include_prefix = "", - # Not applying visibility here – if you import the cxxbridge header, you will likely - # also need the rust library itself to avoid linker errors. + # Private by default: a C++ library that includes the bridge header also needs the + # crate itself at link time. + visibility = cxx_bridge_visibility, deps = cxx_bridge_deps.get(bridge_src, []) + [ "//src/rust/cxx/kj-rs", "//src/rust/cxx:cxx", diff --git a/clippy.toml b/clippy.toml index e053c720e78..72776d43635 100644 --- a/clippy.toml +++ b/clippy.toml @@ -2,3 +2,5 @@ allow-unwrap-in-tests = true allow-expect-in-tests = true allow-panic-in-tests = true max-fn-params-bools = 2 +# Proper nouns beyond clippy's default list, so doc comments need not backtick them. +doc-valid-idents = ["SQLite", "WebSockets", ".."] diff --git a/deps/rust/Cargo.lock b/deps/rust/Cargo.lock index aec474ebdca..e4aebeb6a53 100644 --- a/deps/rust/Cargo.lock +++ b/deps/rust/Cargo.lock @@ -526,21 +526,26 @@ dependencies = [ "clang-ast", "clap", "codespan-reporting", + "data-encoding", "dns-lookup", "encoding_rs", "flate2", "foldhash", "futures", + "glob", "hashlink", "http", "http-body", + "httpdate", "hyper", "hyper-util", + "ipnet", "libc", "libz-rs-sys", "lol_html_c_api", "nix", "notify", + "percent-encoding", "pico-args", "proc-macro2", "quote", @@ -565,6 +570,7 @@ dependencies = [ "tokio-rustls", "tower-service", "tracing", + "url", "windows-sys 0.61.2", "x509-cert", ] @@ -865,6 +871,12 @@ dependencies = [ "r-efi 6.0.0", ] +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1135,6 +1147,12 @@ dependencies = [ "libc", ] +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + [[package]] name = "is-macro" version = "0.3.8" diff --git a/deps/rust/Cargo.toml b/deps/rust/Cargo.toml index f94fc284f92..52d582ca696 100644 --- a/deps/rust/Cargo.toml +++ b/deps/rust/Cargo.toml @@ -49,6 +49,16 @@ libz-rs-sys = { version = "0.6", default-features = false, features = ["std", "r futures = "0" http = "1" http-body = "1" +# The server's disk service: `Last-Modified` dates, and URL paths, parsed and percent-decoded. +httpdate = "1" +percent-encoding = "2" +url = "2" +# The server's CryptoKey bindings: hex key material. +data-encoding = "2" +# `workerd test`: the service and entrypoint filters. +glob = "0" +# The server's `network` services: the `allow` / `deny` CIDR ranges. +ipnet = "2" hyper = { version = "1", default-features = false, features = ["client", "http1", "server"] } # The pooled HTTP/1.1 client (kj-hyper/client.rs) is hyper-util's legacy client over a connector # kj-hyper supplies; tower-service is the trait that connector implements. diff --git a/docs/jsg.md b/docs/jsg.md index c6742a0b5eb..f99444fc05c 100644 --- a/docs/jsg.md +++ b/docs/jsg.md @@ -345,25 +345,23 @@ int main() { ### Real-World Reference: workerd Initialization -For a production example, see how workerd initializes V8 in `src/workerd/server/cli-main.c++`: +For a production example, see how workerd initializes V8 in `src/workerd/server/factory/bootstrap.c++`: ```cpp -// From cli-main.c++ serveImpl() -auto platform = jsg::defaultPlatform(0); -WorkerdPlatform v8Platform(*platform); -jsg::V8System v8System(v8Platform, - KJ_MAP(flag, config.getV8Flags()) -> kj::StringPtr { return flag; }, - platform.get()); +// From bootstrap.c++ (Bootstrap) +platform = jsg::defaultPlatform(0); +v8Platform = kj::heap(*platform); +v8System = kj::heap(*v8Platform, + KJ_MAP(flag, config.getV8Flags()) -> kj::StringPtr { return flag; }, platform.get()); ``` -And how isolates are created in `src/workerd/server/server.c++`: +And how isolates are created in `src/workerd/server/factory/worker-factory.c++`: ```cpp -// From server.c++ when creating a worker -auto isolateGroup = v8::IsolateGroup::GetDefault(); -auto api = kj::heap(globalContext->v8System, def.featureFlags, extensions, - limitEnforcer->getCreateParams(), isolateGroup, kj::mv(jsgobserver), - *memoryCacheProvider, pythonConfig); +// From worker-factory.c++ when compiling a worker +auto api = kj::heap(factory.v8System, featureFlags, extensions, + limitEnforcer->getCreateParams(), v8::IsolateGroup::GetDefault(), kj::mv(jsgobserver), + *factory.memoryCacheProvider, options.pythonConfig, kj::mv(listeners)); ``` --- diff --git a/docs/reference/detail/new-module-registry.md b/docs/reference/detail/new-module-registry.md index bb2b84d6d31..e0a9d99c804 100644 --- a/docs/reference/detail/new-module-registry.md +++ b/docs/reference/detail/new-module-registry.md @@ -714,7 +714,7 @@ if (isolate.isUsingNewModuleRegistry()) { The isolate-level bit is set at isolate construction from `isNewModuleRegistryEnabled(flags)` (`io/features.h`), which returns false for Python workers regardless of the `new_module_registry` flag. All other -flag-check sites (`server.c++` registry creation, `worker.c++` compile paths and +flag-check sites (`worker-factory.c++` registry creation, `worker.c++` compile paths and the nodejs_compat_v2 process/buffer warm-up, and the api-level require paths) route through the same function, so a worker can never be split across the two registries. Note that `Cloudflare.compatibilityFlags.new_module_registry` as diff --git a/justfile b/justfile index 41127e59731..351a97bc7d7 100644 --- a/justfile +++ b/justfile @@ -46,7 +46,7 @@ test-compile-flags: exit 1 fi - just _clangd-check "src/workerd/server/server.c++" + just _clangd-check "src/workerd/server/factory/worker-factory.c++" just _clangd-check "src/workerd/server/workerd-api.c++" CLANGD := "clangd" diff --git a/src/rust/AGENTS.md b/src/rust/AGENTS.md index 0a1ed7a4c35..6dfd14656d7 100644 --- a/src/rust/AGENTS.md +++ b/src/rust/AGENTS.md @@ -4,7 +4,7 @@ A dozen or so Rust crates — mostly libraries, plus the `gen-compile-cache` binary — linked into workerd via CXX FFI. No Cargo workspace — entirely Bazel-driven (`wd_rust_crate.bzl` / `wd_rust_binary.bzl`). Clippy pedantic+nursery enabled; `allow-unwrap-in-tests`; `clippy.toml` and `rustfmt.toml` live at the repository root and apply to every crate. -Rust does not have to live here. A crate that belongs to a component lives beside that component's C++, in the same package, in a subdirectory named for the crate: e.g. workerd's entry point (`workerd-cli`, `src/workerd/server/cli/`, next to `cli-main.c++`). Such crates glob their `srcs` from that subdirectory, list `cxx_bridge_hdrs` explicitly, and use the component's C++ namespace for their bridge. This directory holds the crates that have no C++ home of their own, and the in-tree cxx fork. +Rust does not have to live here. A crate that belongs to a component lives beside that component's C++, in a subdirectory named for the crate: e.g. workerd's entry point (`workerd-cli`, `src/workerd/server/cli/`, declared in `src/workerd/server/BUILD.bazel` with its `srcs` globbed from that subdirectory and its `cxx_bridge_hdrs` listed explicitly) and its server (`workerd-server`, `src/workerd/server/server/`, a package of its own over the C++ worker factory in `src/workerd/server/factory/`; see `src/workerd/server/AGENTS.md`). Such crates use the component's C++ namespace for their bridge (`workerd::server`, `workerd::server::cli`), and their generated bridge header sits at the crate's C++ include path (`workerd/server/cli/bridge.rs.h`, `workerd/server/server/bridge.rs.h`). This directory holds the crates that have no C++ home of their own, and the in-tree cxx fork. > **The "CXX FFI" above is the in-tree `src/rust/cxx` fork of [cxx-rs](https://cxx.rs/)** — not stock cxx-rs. It adds deep KJ interoperability upstream lacks: `async` fns become `kj::Promise`, you can return/hold `kj::Own`, `Result` throws `kj::Exception`, and other KJ types cross the boundary (see CXX BRIDGE below). cxx-rs is well represented in LLM training data, so it is easy to "recall" an API that is wrong here — prefer the prior art in these crates and the in-tree CXX sources (especially its `kj-rs` crate) over upstream cxx-rs docs or memory. @@ -22,7 +22,7 @@ _Snapshot — the set drifts as crates come and go; `bazel query //src/rust/...` | `kj/` | Rust bindings for KJ library (`http`, `io`, `own` submodules); `Result` = `Result` | | `cxx/kj-hyper/` | HTTP/1.1, WebSocket and TLS for the Rust server (hyper, rustls; WebSockets are kj's over the upgraded transport) with kj-typed seams: `server::serve_connection` dispatches to a `Handler` with kj arguments, `client::Client` implements `kj::http::Service`; see `src/rust/cxx/AGENTS.md` | | `worker/` | Rust counterpart of `workerd::WorkerInterface`: the `worker::Interface` trait (`into_kj` hands an implementation to C++ as `KjOwn`; `not_supported` answers a `CustomEvent` as C++'s `event->notSupported()` does), `PromisedInterface` (an `Interface` whose target is still starting, as C++'s `PromisedWorkerInterface`) plus FFI bindings; multi-bridge crate | -| `cxx-integration/` | Tokio runtime init; called from C++ `main()` before anything else | +| `cxx-integration/` | One bridge function, `trigger_panic`: the hook C++ tests use to exercise the panic-to-`kj::Exception` conversion at the bridge | | `cxx-integration-test/` | Non-production crate exercising Rust/C++ integration: callbacks, shared structs, `Result` error mapping | | `transpiler/` | TS type stripping via SWC (`ts_strip()`, `StripOnly` mode) | | `python-parser/` | Python import extraction via `ruff_python_parser`; **namespace: `edgeworker::rust::`** | @@ -48,6 +48,7 @@ _Snapshot — the set drifts as crates come and go; `bazel query //src/rust/...` - The crates derived from upstream cxx (`src/rust/cxx/{src,syntax,gen,macro,tests}`) are outside these defaults; the `kj-rs*` crates beside them are inside. - **Tests**: a module's unit tests live beside it in `-test.rs` (`dns.rs` → `dns-test.rs`, `lib.rs` → `lib-test.rs`), never in an inline `mod tests { ... }`, so a diff shows production changes and test changes as separate files. See UNIT TEST FILES below. JSG tests use `jsg_test::Harness::run_in_context()`. Always run the full `src/rust/...` test suite (`bazel test //src/rust/...`) rather than targeting a single crate — changes in shared crates like `jsg` or `jsg-macros` can break downstream consumers - **FFI pointers**: functions receiving raw pointers must be `unsafe fn` (see `jsg/README.md`) +- **`KjOwn`**: dropping one runs `kj::Own::~Own()` through a C++ function generated per type, so `T: kj_rs::OwnTarget` is implemented by the bridge that declares `T` (`type T;`) and holds it in a `KjOwn`. A bridge that only aliases `T` gets none; add `impl KjOwn {}` to the declaring bridge. Details in `cxx/kj-rs/README.md` and `cxx/AGENTS.md` - **Parameter ordering**: `&Lock` / `&mut Lock` must always be the first parameter in any function that takes a lock (matching the C++ convention where `jsg::Lock&` is always first). This applies to free functions, trait methods, and associated functions (excluding `&self`/`&mut self` receivers which come before `lock`). - **Method naming**: do not use `get_` prefixes on methods — e.g. `buf.backing_store()` not `buf.get_backing_store()`. Static constructors belong on the marker struct (`impl ArrayBuffer { fn new(...) }`) not on `impl Local<'_, ArrayBuffer>`. - **FFI naming**: instance methods on an existing handle use a `local__` prefix (e.g. `local_array_buffer_byte_length`). Static constructors that create a new value do **not** use the `local_` prefix — name them `_` (e.g. `array_buffer_new_with_mode`, `array_buffer_maybe_new`, `backing_store_new_resizable`). diff --git a/src/rust/cxx/AGENTS.md b/src/rust/cxx/AGENTS.md index 0193341cf1a..76a14a58fac 100644 --- a/src/rust/cxx/AGENTS.md +++ b/src/rust/cxx/AGENTS.md @@ -43,11 +43,13 @@ Bazel module, Cargo workspace, toolchain configuration, or external `workerd-cxx `kj::WebSocket`, `kj::AsyncIoStream`) so requests reach a C++ `WorkerInterface` and C++ can make outbound requests; see "kj-hyper" below - `kj-rs-io/` — tokio-backed `kj::AsyncIoStream` / `kj::Network` / `kj::LowLevelAsyncIoProvider` - (the I/O providers for the tokio loop, `kj_rs_io::setupTokioAsyncIo()`), `loopback:` addresses - (in-process connections for `workerd test`), the `--watch` file watcher (Rust over `notify`), - and signals. C++ there is interface adaptation only; the one policy + (the I/O providers for the tokio loop, `kj_rs_io::setupTokioAsyncIo()`), the same addresses and + sockets for a Rust caller (`TokioAddress::parse_str`, then `listen` and `TokioListener::accept`, + `connect_first` or `bind_udp`; `wrap_listener` for an inherited socket; they hand back tokio's + own sockets), and the `--watch` file watcher (Rust over `notify`). + C++ there is interface adaptation only; the one policy object that stays C++ is `PeerFilter`, a wrapper over KJ's own `kj::_::NetworkFilter`, which - Rust consults through a bridged `should_allow` + the C++ adapters apply in their connect and accept loops - `tests/` and `kj-rs/tests/` — Rust and C++ bridge integration tests - `tools/bazel/` — Bazel bridge-generation macro used by this component's tests diff --git a/src/rust/cxx/kj-rs-io/async-io.c++ b/src/rust/cxx/kj-rs-io/async-io.c++ index 01c3f7f9ac2..24d19997013 100644 --- a/src/rust/cxx/kj-rs-io/async-io.c++ +++ b/src/rust/cxx/kj-rs-io/async-io.c++ @@ -100,8 +100,6 @@ RawSockaddr encodeSockaddr(const SocketAddress &addr) { } } #endif - case AddressKind::Loopback: - KJ_FAIL_REQUIRE("a loopback: address has no struct sockaddr"); default: KJ_FAIL_REQUIRE("unsupported socket address kind", static_cast(addr.kind)); } @@ -186,10 +184,8 @@ void copyOut(const RawSockaddr &raw, struct sockaddr *addr, kj::uint *length) { *length = raw.length; } -// restrictPeers(): KJ's decision for a typed address. A loopback address is not a network peer -// (loopback.rs) and has no sockaddr for the filter to judge; it is always allowed. +// restrictPeers(): KJ's decision for a typed address. bool allowed(const PeerFilter &filter, const SocketAddress &addr) { - if (addr.kind == AddressKind::Loopback) return true; auto raw = encodeSockaddr(addr); return filter.allows(raw.get(), raw.length); } @@ -227,11 +223,9 @@ kj::Own peerIdentity(const PeerStream &peer, kj::Arc connectAny(::rust::Box address, - ::rust::Vec targets, - kj::Arc filter) { +// coroutine owning copies of everything it needs, so the kj::NetworkAddress that started it may +// be destroyed while it is pending. +kj::Promise connectAny(::rust::Vec targets, kj::Arc filter) { kj::Maybe lastError; for (auto &target: targets) { if (!allowed(*filter, target)) { @@ -239,7 +233,7 @@ kj::Promise connectAny(::rust::Box address, continue; } auto outcome = - co_await connect_target(*address, SocketAddress(target)) + co_await connect_target(SocketAddress(target)) .then( [](::rust::Box stream) -> kj::OneOf<::rust::Box, kj::Exception> { @@ -371,14 +365,14 @@ void TokioConnectionReceiver::getsockname(struct sockaddr *addr, kj::uint *lengt // TokioNetworkAddress kj::Promise> TokioNetworkAddress::connect() { - return connectAny(address_clone(*inner), address_targets(*inner), filter.addRef()) + return connectAny(address_targets(*inner), filter.addRef()) .then([](PeerStream connected) -> kj::Own { return kj::heap(kj::mv(connected.stream)); }); } kj::Promise TokioNetworkAddress::connectAuthenticated() { - return connectAny(address_clone(*inner), address_targets(*inner), filter.addRef()) + return connectAny(address_targets(*inner), filter.addRef()) .then([identityFilter = filter.addRef()]( PeerStream connected) mutable -> kj::AuthenticatedStream { kj::AuthenticatedStream result; @@ -489,7 +483,7 @@ kj::Promise> TokioNetwork::parseAddress( return started( network_parse_address(::rust::Slice( reinterpret_cast(addr.begin()), addr.size()), - static_cast(portHint), *loopback) + static_cast(portHint)) .then([filter = filter.addRef()]( ::rust::Box address) mutable -> kj::Own { return kj::heap(kj::mv(address), kj::mv(filter)); @@ -559,11 +553,11 @@ kj::Promise> TokioLowLevelAsyncIoProvider::wrapConnec kj::Own TokioLowLevelAsyncIoProvider::wrapListenSocketFd( Fd fd, NetworkFilter &filter, kj::uint flags) { - // KJ's interface lends the filter by reference for the receiver's lifetime. workerd's only - // call (inherited listen sockets, server/cli-main.c++) uses the two-argument overload, whose - // filter is KJ's static allow-all; that one is recognised by identity and given an owned - // allow-all filter. Anything else would need a borrowed reference to outlive its owner by - // contract alone, which this backend does not do. + // KJ's interface lends the filter by reference for the receiver's lifetime. workerd's server + // wraps its inherited listen sockets in Rust (server/listen), so the callers left are KJ's own + // two-argument overload, whose filter is KJ's static allow-all; that one is recognised by + // identity and given an owned allow-all filter. Anything else would need a borrowed reference + // to outlive its owner by contract alone, which this backend does not do. if (&filter != &NetworkFilter::getAllAllowed()) { closeTransferredFd(fd, flags); KJ_UNIMPLEMENTED("wrapListenSocketFd with a caller-owned NetworkFilter is not implemented by " @@ -577,8 +571,8 @@ kj::Own TokioLowLevelAsyncIoProvider::wrapListenSocketFd // ======================================================================================= // TokioAsyncIoProvider -// Both pipes are socket pairs: kj's in-memory pipes make a small write wait for a reader, and -// workerd's loopback transport expects real sockets from the provider (see async-io.h). +// Both pipes are socket pairs: kj's in-memory pipes make a small write wait for a reader (see +// async-io.h). kj::OneWayPipe TokioAsyncIoProvider::newOneWayPipe() { auto pair = new_socket_pair(); return kj::OneWayPipe{kj::heap(kj::mv(pair.first)), @@ -612,10 +606,6 @@ TokioAsyncIoContext setupTokioAsyncIo() { return TokioAsyncIoContext(); } -kj::Promise onSignal(int signum) { - return started(wait_for_signal(signum)); -} - // ======================================================================================= // FileWatcher diff --git a/src/rust/cxx/kj-rs-io/async-io.h b/src/rust/cxx/kj-rs-io/async-io.h index f18c79a51b3..cd56e958095 100644 --- a/src/rust/cxx/kj-rs-io/async-io.h +++ b/src/rust/cxx/kj-rs-io/async-io.h @@ -164,25 +164,16 @@ class TokioDatagramPort final: public kj::DatagramPort { // The tokio-backed kj::Network. The address grammar is KJ's SocketAddress::parse for everything // workerd's configs use (net.rs, "Address grammar"). restrictPeers() returns a network sharing -// this one's filter chain and loopback namespace, so derived networks, addresses and receivers +// this one's filter chain, so derived networks, addresses and receivers // remain valid regardless of the order the networks are destroyed in. class TokioNetwork final: public kj::Network { public: // Allow-everything root network (matches KJ's root networks). - TokioNetwork(): filter(kj::arc()), loopback(new_loopback_registry()) {} + TokioNetwork(): filter(kj::arc()) {} TokioNetwork(TokioNetwork &parent, kj::ArrayPtr allow, kj::ArrayPtr deny) - : filter(kj::arc(allow, deny, parent.filter.addRef())), - loopback(loopback_registry_clone(*parent.loopback)) {} - - // Makes parseAddress() accept "loopback:" addresses -- connections serviced within this - // process (loopback.rs) -- on this network and every network derived from it by - // restrictPeers(). For `workerd test`, which uses them to exercise the network stack end to - // end without an external socket; production configs use direct service bindings instead. - void enableLoopback() { - loopback_registry_enable(*loopback); - } + : filter(kj::arc(allow, deny, parent.filter.addRef())) {} kj::Promise> parseAddress( kj::StringPtr addr, kj::uint portHint) override; @@ -192,7 +183,6 @@ class TokioNetwork final: public kj::Network { private: kj::Arc filter; - ::rust::Box loopback; }; // The tokio-backed kj::LowLevelAsyncIoProvider. Each wrap*Fd hands the raw handle -- a Unix fd @@ -294,15 +284,6 @@ struct TokioAsyncIoContext { // the kj::setupAsyncIo() equivalent for the tokio loop. One per thread. TokioAsyncIoContext setupTokioAsyncIo(); -// Resolves when the process receives signal `signum`: the tokio-loop replacement for -// kj::UnixEventPort::onSignal() (workerd's SIGTERM graceful drain). Must be awaited on the -// thread owning the TokioEventPort. The handler is installed before this returns (the promise -// is started eagerly, per the operation-start policy above); unlike UnixEventPort, KJ does not -// block/capture the signal beforehand, so a signal delivered before the *call* takes its default -// disposition (see signal.rs). On Windows, SIGTERM/SIGINT are mapped to the ctrl_break/ctrl_c -// console control events; the promise rejects for other signums. -kj::Promise onSignal(int signum); - // Watches files for changes: kj-rs-io's watcher (watcher.rs, Rust over the `notify` crate -- // inotify on Linux, FSEvents on macOS, ReadDirectoryChangesW on Windows) behind a C++ interface. // It watches each file's directory and judges changes by re-stamping the files, so replaced, diff --git a/src/rust/cxx/kj-rs-io/error.rs b/src/rust/cxx/kj-rs-io/error.rs index 212df036790..eb20e0990c6 100644 --- a/src/rust/cxx/kj-rs-io/error.rs +++ b/src/rust/cxx/kj-rs-io/error.rs @@ -228,6 +228,13 @@ impl From for KjError { } } +/// The failure itself, for a Rust caller that speaks `std::io`. +impl From for std::io::Error { + fn from(error: KjIoError) -> Self { + error.inner + } +} + impl IntoKjException for KjIoError { fn into_kj_exception(self, file: &str, line: u32) -> KjException { KjError::from(self).into_kj_exception(file, line) diff --git a/src/rust/cxx/kj-rs-io/ffi.rs b/src/rust/cxx/kj-rs-io/ffi.rs index 339ffdc7b10..2f781bac466 100644 --- a/src/rust/cxx/kj-rs-io/ffi.rs +++ b/src/rust/cxx/kj-rs-io/ffi.rs @@ -45,7 +45,6 @@ pub use bridge::kj_pieces_count; use crate::error::KjIoError; use crate::error::Result; use crate::error::op; -use crate::loopback::LoopbackRegistry; use crate::net::TokioAddress; use crate::net::TokioDatagram; use crate::net::TokioListener; @@ -65,7 +64,6 @@ use crate::net::listener_port; use crate::net::network_address_from; use crate::net::parse_address; use crate::net::socket_pair; -use crate::signal::wait_for_signal; use crate::stream::TokioStream; use crate::stream::stream_abort_read; use crate::stream::stream_local_addr; @@ -108,9 +106,6 @@ mod bridge { UnixAbstract, /// No fields: `accept(2)` on a unix socket reports this for an unbound peer. UnixUnnamed, - /// `name`: a `loopback:` name (loopback.rs). Not a socket address: it has no `struct - /// sockaddr` form and is not subject to `restrictPeers()`. - Loopback, } /// One socket address, typed. This is the only form an address takes on the bridge: Rust @@ -163,7 +158,6 @@ mod bridge { type TokioListener; type TokioAddress; type TokioDatagram; - type LoopbackRegistry; // --- kj::AsyncIoStream (stream.rs). `buf` is the caller's, uninitialized storage // allowed, valid until the promise settles (KJ's contract): hence `unsafe`. @@ -189,23 +183,11 @@ mod bridge { // --- kj::Network / kj::NetworkAddress (net.rs). Peer filtering is the C++ adapter's: // `address_targets` lists what connect() would try, in order, for it to filter and - // connect one at a time; `listener_accept` reports the peer for it to judge. Every - // kj::Network holds a LoopbackRegistry (loopback.rs), the namespace `loopback:` - // addresses resolve in once enabled; restrictPeers() children share their parent's. - fn new_loopback_registry() -> Box; - fn loopback_registry_clone(registry: &LoopbackRegistry) -> Box; - fn loopback_registry_enable(registry: &LoopbackRegistry); - async fn network_parse_address( - addr: &[u8], - port_hint: u16, - loopback: &LoopbackRegistry, - ) -> Result>; + // connect one at a time; `listener_accept` reports the peer for it to judge. + async fn network_parse_address(addr: &[u8], port_hint: u16) -> Result>; fn network_address_from(addr: &SocketAddress) -> Result>; fn address_targets(addr: &TokioAddress) -> Result>; - async fn connect_target( - addr: &TokioAddress, - target: SocketAddress, - ) -> Result>; + async fn connect_target(target: SocketAddress) -> Result>; fn address_listen(addr: &TokioAddress) -> Result>; fn address_bind_datagram(addr: &TokioAddress) -> Result>; fn address_clone(addr: &TokioAddress) -> Box; @@ -231,7 +213,6 @@ mod bridge { unsafe fn wrap_listen_fd(handle: i64, flags: u32) -> Result>; fn ignore_sigpipe_once() -> Result<()>; - async fn wait_for_signal(signum: i32) -> Result<()>; // --- The --watch file watcher (watcher.rs). `path` is `kj::Path::toNativeString` // output: arbitrary bytes on unix, UTF-8 on Windows. @@ -295,31 +276,9 @@ pub unsafe fn stream_try_read( pub fn network_parse_address( addr: &[u8], port_hint: u16, - loopback: &LoopbackRegistry, ) -> impl Future>> + use<> { let addr = addr.to_vec(); - let loopback = loopback.clone_handle(); - async move { parse_address(&addr, port_hint, &loopback).await } -} - -#[expect( - clippy::unnecessary_box_returns, - reason = "cxx takes an opaque Rust type boxed" -)] -pub fn new_loopback_registry() -> Box { - Box::new(LoopbackRegistry::new()) -} - -#[expect( - clippy::unnecessary_box_returns, - reason = "cxx takes an opaque Rust type boxed" -)] -pub fn loopback_registry_clone(registry: &LoopbackRegistry) -> Box { - Box::new(registry.clone_handle()) -} - -pub fn loopback_registry_enable(registry: &LoopbackRegistry) { - registry.enable(); + async move { parse_address(&addr, port_hint).await } } // ====================================================================================== diff --git a/src/rust/cxx/kj-rs-io/lib.rs b/src/rust/cxx/kj-rs-io/lib.rs index 6bea5ee5e84..aebe422f3f2 100644 --- a/src/rust/cxx/kj-rs-io/lib.rs +++ b/src/rust/cxx/kj-rs-io/lib.rs @@ -32,9 +32,9 @@ //! This is ordinary tokio code. tokio registers a resource with the runtime *entered* on the //! calling thread, and kj-rs-tokio makes a `TokioEventPort` thread a tokio runtime thread for its //! whole life (port.rs, `EnteredRuntime`), so `TcpStream::connect`, `UnixListener::bind_addr`, -//! `AsyncFd::with_interest`, `signal()`, `spawn_blocking` and friends register with the loop's +//! `AsyncFd::with_interest`, `spawn_blocking` and friends register with the loop's //! driver as they are. Two checks are added, both cheap (two thread-local reads): -//! [`ensure_loop_thread`] before every *registration* (connect, listen, wrap, resolve, signals, +//! [`ensure_loop_thread`] before every *registration* (connect, listen, wrap, resolve, //! the hangup watch), so a call on a thread without a port, or under another runtime entered //! over the port's, fails with a `kj::Exception` instead of tokio's "no reactor running" panic //! (a process abort at the bridge); and [`ensure_owner_loop`] at the point an operation is about @@ -58,10 +58,8 @@ //! workerd uses them: `restrictPeers` (KJ's own policy), `connectAuthenticated` / //! `acceptAuthenticated` identities, unix peer credentials, `whenWriteDisconnected` (at one //! extra descriptor per stream; stream.rs), `getSockaddr`, `wrapListenSocketFd` with KJ's fd -//! flags, `onSignal`, `getaddrinfo` with KJ's hints and service names, `unix:` and +//! flags, `getaddrinfo` with KJ's hints and service names, `unix:` and //! `unix-abstract:` addresses, `SO_REUSEADDR`, `TCP_NODELAY`, the accept retry set, SIGPIPE. -//! Added for workerd, with no KJ counterpart: `loopback:` addresses (loopback.rs), which -//! `workerd test` uses to exercise the network stack end to end inside one process. //! //! # Objects //! @@ -70,8 +68,8 @@ //! ├── kj_rs_tokio::TokioAsyncIoContext the loop's tokio runtime (kj-rs-tokio) //! ├── TokioLowLevelAsyncIoProvider wrap*Fd(): an owned fd/SOCKET as i64 -> Rust owns it //! └── TokioAsyncIoProvider -//! └── TokioNetwork kj::Arc + Box, -//! │ both shared down the restrictPeers chain: +//! └── TokioNetwork kj::Arc, shared down the +//! │ restrictPeers chain: //! ├── TokioNetworkAddress Box + filter share //! │ ├── TokioConnectionReceiver Box + filter share //! │ └── TokioDatagramPort Box + filter share @@ -80,8 +78,7 @@ //! TokioStream (stream.rs) Arc: the socket, plus a lazily dup'd hangup watch //! TokioListener (net.rs) Arc<..>: one listening socket per resolved address //! TokioDatagram (net.rs) Arc<..>: one datagram socket -//! TokioAddress (net.rs) the parsed address: SocketAddr list, a unix name, or a loopback -//! queue (loopback.rs; `loopback:` addresses, workerd test only) +//! TokioAddress (net.rs) the parsed address: SocketAddr list or a unix name //! TokioFileWatcher (watcher.rs) Arc<..>: notify watcher + metadata stamps; workerd's --watch //! (server/cli/watch.rs) uses it from Rust, FileWatcher (async-io.h) //! wraps it for C++ @@ -114,15 +111,16 @@ compile_error!("kj-rs-io supports Unix and Windows targets only"); pub use error::exception_type; pub use net::TokioAddress; +pub use net::TokioListener; +pub use net::wrap_listener; +pub use stream::Socket; pub use stream::TokioStream; pub use stream::when_write_disconnected; pub use watcher::TokioFileWatcher; mod error; mod ffi; -mod loopback; mod net; -mod signal; mod stream; mod watcher; @@ -178,7 +176,6 @@ const _: () = { send_sync::(); send_sync::(); send_sync::(); - send_sync::(); send_sync::(); }; diff --git a/src/rust/cxx/kj-rs-io/loopback.rs b/src/rust/cxx/kj-rs-io/loopback.rs deleted file mode 100644 index b43de5a893b..00000000000 --- a/src/rust/cxx/kj-rs-io/loopback.rs +++ /dev/null @@ -1,152 +0,0 @@ -//! `loopback:` addresses: connections serviced within the process, for `workerd test`. -//! -//! A loopback address names a queue. `connect()` makes a socket pair ([`socket_pair`]), queues -//! one end and returns the other; a receiver's `accept()` takes from the queue. The sockets are -//! real, so a test exercises the whole stack above the transport. Loopback is off by default: -//! `parseAddress` reads `loopback:` as a hostname until the network's registry is enabled, which -//! workerd does for `workerd test` only -- in production, direct service bindings do the same -//! job with less machinery. -//! -//! `restrictPeers()` does not apply: it governs network peers, and a loopback connection has -//! none. The adapter (async-io.c++) lets loopback addresses through the filter. -//! -//! A name belongs to the loop that first used it: a queued connection is a socket registered -//! with the connector's runtime, so it must be accepted on that same runtime. Connecting or -//! accepting from another loop thread fails (`ensure_owner_loop`) before anything is queued, -//! rather than handing out a stream the receiver cannot use. - -use std::collections::HashMap; -use std::collections::VecDeque; -use std::sync::Arc; -use std::sync::Mutex; -use std::sync::MutexGuard; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering; - -use tokio::sync::Notify; - -use crate::current_loop_runtime_id; -use crate::ensure_owner_loop; -use crate::error::KjIoError; -use crate::error::Result; -use crate::net::socket_pair; -use crate::stream::TokioStream; - -/// One `kj::Network`'s loopback namespace, shared with every network `restrictPeers()` derives -/// from it (like the peer filter chain). A handle to `Arc`-shared state. -pub struct LoopbackRegistry { - shared: Arc, -} - -struct RegistryShared { - enabled: AtomicBool, - queues: Mutex, Arc>>, -} - -/// The connections made to one loopback name and not yet accepted. -pub struct LoopbackQueue { - name: Vec, - /// The loop the name belongs to (module docs). - owner: tokio::runtime::Id, - pending: Mutex>>, - /// Signalled once per push. A receiver registers its interest (`Notified::enable`) before - /// it checks the queue, so a push between the check and the wait wakes it whatever the - /// interleaving with other receivers. - ready: Notify, -} - -fn lock<'a, T>(mutex: &'a Mutex, op: &'static str) -> Result> { - mutex - .lock() - .map_err(|_| KjIoError::other(op, "loopback state poisoned")) -} - -impl LoopbackRegistry { - pub fn new() -> Self { - Self { - shared: Arc::new(RegistryShared { - enabled: AtomicBool::new(false), - queues: Mutex::new(HashMap::new()), - }), - } - } - - /// Another handle to the same namespace. - pub fn clone_handle(&self) -> Self { - Self { - shared: Arc::clone(&self.shared), - } - } - - /// Makes `parseAddress` accept `loopback:` addresses from now on. Cannot be reversed. - pub fn enable(&self) { - self.shared.enabled.store(true, Ordering::Release); - } - - /// The queue a `loopback:` address names, or `None` when `text` is not one. Until the - /// registry is enabled nothing is, and `loopback:` reads as a hostname. The queue is created - /// on first use and owned by the calling loop from then on. - pub fn parse(&self, text: &[u8]) -> Option>> { - if !self.shared.enabled.load(Ordering::Acquire) { - return None; - } - let name = text.strip_prefix(b"loopback:")?; - Some(self.queue(name)) - } - - fn queue(&self, name: &[u8]) -> Result> { - let owner = current_loop_runtime_id()?; - let mut queues = lock(&self.shared.queues, "parseAddress")?; - Ok(Arc::clone(queues.entry(name.to_vec()).or_insert_with( - || Arc::new(LoopbackQueue::new(name.to_vec(), owner)), - ))) - } -} - -impl Default for LoopbackRegistry { - fn default() -> Self { - Self::new() - } -} - -impl LoopbackQueue { - fn new(name: Vec, owner: tokio::runtime::Id) -> Self { - Self { - name, - owner, - pending: Mutex::new(VecDeque::new()), - ready: Notify::new(), - } - } - - /// The name after `loopback:`. - pub fn name(&self) -> &[u8] { - &self.name - } - - /// `connect()`: one end of a new socket pair is queued for the next `accept()`; the other is - /// the caller's connection. - pub fn connect(&self) -> Result> { - ensure_owner_loop(self.owner)?; - let (ours, theirs) = socket_pair()?; - lock(&self.pending, "connect()")?.push_back(theirs); - self.ready.notify_one(); - Ok(ours) - } - - /// `accept()`: the next queued connection, waiting for one if the queue is empty. Several - /// receivers may wait on one queue; each connection goes to exactly one of them. - pub async fn accept(&self) -> Result> { - ensure_owner_loop(self.owner)?; - loop { - let mut notified = std::pin::pin!(self.ready.notified()); - notified.as_mut().enable(); - // The guard is dropped before waiting: a `connect()` must be able to push meanwhile. - let next = lock(&self.pending, "accept()")?.pop_front(); - if let Some(stream) = next { - return Ok(stream); - } - notified.await; - } - } -} diff --git a/src/rust/cxx/kj-rs-io/net-test.rs b/src/rust/cxx/kj-rs-io/net-test.rs index 2a724d0b9e4..6eea3946213 100644 --- a/src/rust/cxx/kj-rs-io/net-test.rs +++ b/src/rust/cxx/kj-rs-io/net-test.rs @@ -14,15 +14,7 @@ assert_impl_all!(TokioAddress: Send, Sync); assert_impl_all!(TokioDatagram: Send, Sync); fn parse_once(text: &[u8], port_hint: u16) -> Option> { - parse_once_in(text, port_hint, &LoopbackRegistry::new()) -} - -fn parse_once_in( - text: &[u8], - port_hint: u16, - loopback: &LoopbackRegistry, -) -> Option> { - let mut fut = std::pin::pin!(TokioAddress::parse(text, port_hint, loopback)); + let mut fut = std::pin::pin!(TokioAddress::parse(text, port_hint)); let mut cx = Context::from_waker(Waker::noop()); match fut.as_mut().poll(&mut cx) { Poll::Ready(result) => Some(result), @@ -59,58 +51,14 @@ fn err_of(result: Result) -> KjError { fn ip_addrs(addr: &TokioAddress) -> (&[SocketAddr], bool) { match &addr.spec { Spec::Ip { addrs, wildcard } => (addrs, *wildcard), - _ => panic!("expected an IP address"), + #[cfg(unix)] + Spec::Unix(_) => panic!("expected an IP address"), } } /// How many sockets a listener binds (KJ's aggregate receiver: one per resolved address). fn socket_count(listener: &TokioListener) -> usize { - match &listener.shared.backend { - ListenerBackend::Sockets { inners, .. } => inners.len(), - ListenerBackend::Loopback(_) => panic!("expected a socket listener"), - } -} - -#[test] -fn loopback_addresses_need_the_registry_enabled() { - let registry = LoopbackRegistry::new(); - // Disabled: "loopback:svc" is a hostname lookup, which this port-less thread refuses. - let Some(Err(err)) = parse_once_in(b"loopback:svc", 0, ®istry) else { - panic!("expected the hostname lookup to be refused at once") - }; - assert!( - KjError::from(err) - .description() - .contains("no TokioEventPort") - ); - - registry.enable(); - // Binding and listening register with the loop; parsing a loopback address does not. - let _port = kj_rs_tokio::TokioPort::new(); - let addr = parse_once_in(b"loopback:svc", 0, ®istry) - .unwrap() - .unwrap(); - assert_eq!(addr.to_display_bytes(), b"loopback:svc"); - let targets = addr.targets().unwrap(); - assert_eq!(targets, [SocketAddress::loopback(b"svc")]); - // Same name, same queue; the registry's children (clone_handle) see it too. - let again = parse_once_in(b"loopback:svc", 0, ®istry.clone_handle()) - .unwrap() - .unwrap(); - match (&addr.spec, &again.spec) { - (Spec::Loopback(a), Spec::Loopback(b)) => { - assert!(Arc::ptr_eq(a, b)); - } - _ => panic!("expected loopback addresses"), - } - let Err(err) = addr.bind_datagram() else { - panic!("expected bind_datagram() to fail") - }; - assert!( - KjError::from(err) - .description() - .contains("loopback addresses do not support datagrams") - ); + listener.shared.backend.inners.len() } #[test] diff --git a/src/rust/cxx/kj-rs-io/net.rs b/src/rust/cxx/kj-rs-io/net.rs index 030d3f574dd..951c95246ac 100644 --- a/src/rust/cxx/kj-rs-io/net.rs +++ b/src/rust/cxx/kj-rs-io/net.rs @@ -18,8 +18,6 @@ //! - Unix domain: `"unix:/path/to/socket"` (the path is arbitrary bytes) and, on Linux, //! `"unix-abstract:name"` for the abstract namespace -- both documented for workerd's //! `Socket.address` / `ExternalServer.address`. -//! - `"loopback:name"`, once the network's [`LoopbackRegistry`] is enabled (workerd test -//! only): connections serviced within the process (loopback.rs). //! //! # Typed addresses //! @@ -42,6 +40,13 @@ //! at parse time and in `getSockaddr`; that only changes the moment the same error surfaces //! and is not reproduced.) //! +//! # Rust callers +//! +//! workerd's Rust server uses the same addresses without the C++ adapter and without a filter: +//! [`TokioAddress::parse_str`], then `listen` and [`TokioListener::accept`], `connect_first` or +//! `bind_udp`; [`wrap_listener`] for an inherited socket. It gets tokio's own sockets back +//! ([`Socket`], `UdpSocket`) and serves them itself. +//! //! # Runtime and ownership //! //! Every bridged operation here starts with [`ensure_loop_thread`] (lib.rs, "The tokio @@ -77,8 +82,6 @@ use crate::error::op; use crate::ffi::AddressKind; use crate::ffi::PeerStream; use crate::ffi::SocketAddress; -use crate::loopback::LoopbackQueue; -use crate::loopback::LoopbackRegistry; use crate::stream::Socket; use crate::stream::TokioStream; @@ -103,13 +106,6 @@ impl SocketAddress { name: Vec::new(), } } - - fn loopback(name: &[u8]) -> Self { - Self { - name: name.to_vec(), - ..Self::blank(AddressKind::Loopback) - } - } } impl From for SocketAddress { @@ -287,8 +283,6 @@ enum Spec { }, #[cfg(unix)] Unix(UnixName), - /// `"loopback:name"`: the queue the network's registry gave for the name at parse time. - Loopback(Arc), } // ====================================================================================== @@ -448,12 +442,7 @@ impl TokioAddress { } /// `SocketAddress::parse` (kj/async-io-unix.c++), see the module docs. - async fn parse(text: &[u8], port_hint: u16, loopback: &LoopbackRegistry) -> Result { - if let Some(queue) = loopback.parse(text) { - return Ok(Self { - spec: Spec::Loopback(queue?), - }); - } + async fn parse(text: &[u8], port_hint: u16) -> Result { if let Some(unix) = Self::parse_unix(text) { return unix; } @@ -490,6 +479,24 @@ impl TokioAddress { Self::lookup_host(addr_part, None, port).await } + /// [`Self::parse`] for a Rust caller. + pub async fn parse_str(text: &str, port_hint: u16) -> Result { + Self::parse(text.as_bytes(), port_hint).await + } + + /// `connect()` for a Rust caller, which filters nothing: the first of [`Self::targets`] that + /// accepts, else the last failure. + pub async fn connect_first(&self) -> Result { + let mut last = KjIoError::other("connect()", "no addresses to connect to"); + for target in self.targets()? { + match connect_socket(target).await { + Ok(socket) => return Ok(socket), + Err(e) => last = e, + } + } + Err(last) + } + /// `SocketAddress::lookupHost`: `getaddrinfo` with KJ's hints on the loop runtime's /// blocking pool (KJ uses a detached thread per lookup). With a `service` the results carry /// getaddrinfo's ports; without one, `port` is patched in. Host `"*"` becomes a wildcard @@ -539,26 +546,10 @@ impl TokioAddress { Spec::Ip { addrs, .. } => Ok(addrs.iter().copied().map(SocketAddress::from).collect()), #[cfg(unix)] Spec::Unix(name) => Ok(vec![name.to_socket_address()]), - Spec::Loopback(queue) => Ok(vec![SocketAddress::loopback(queue.name())]), - } - } - - /// `connect()` to one of [`Self::targets`]. The address itself supplies what a - /// `SocketAddress` cannot name: the queue behind a loopback target. - fn connect( - &self, - target: SocketAddress, - ) -> impl Future>> + use<> { - let spec = self.spec.clone(); - async move { - match spec { - Spec::Loopback(queue) => queue.connect(), - _ => connect_to(target).await, - } } } - fn listen(&self) -> Result> { + pub fn listen(&self) -> Result> { ensure_loop_thread()?; let backend = match &self.spec { Spec::Ip { addrs, wildcard } => { @@ -576,12 +567,22 @@ impl TokioAddress { Spec::Unix(name) => ListenerBackend::sockets(vec![ListenerInner::Unix( UnixListener::bind_addr(&name.to_tokio()?).map_err(op("bind()"))?, )]), - Spec::Loopback(queue) => ListenerBackend::Loopback(Arc::clone(queue)), }; Ok(Box::new(TokioListener::new(backend)?)) } fn bind_datagram(&self) -> Result> { + Ok(Box::new(TokioDatagram { + shared: Arc::new(DatagramShared { + socket: self.bind_udp()?, + owner: crate::current_loop_runtime_id()?, + }), + })) + } + + /// The datagram socket itself, bound KJ style (`SO_REUSEADDR`; a wildcard is one dual-stack + /// socket), for a Rust caller to read and write. + pub fn bind_udp(&self) -> Result { ensure_loop_thread()?; let (addr, wildcard) = match &self.spec { Spec::Ip { addrs, wildcard } => ( @@ -597,12 +598,6 @@ impl TokioAddress { "Unix datagram sockets are not supported", )); } - Spec::Loopback(_) => { - return Err(KjIoError::other( - "bind()", - "loopback addresses do not support datagrams", - )); - } }; let domain = if addr.is_ipv4() { socket2::Domain::IPV4 @@ -622,13 +617,7 @@ impl TokioAddress { } socket.bind(&addr.into()).map_err(op("bind()"))?; socket.set_nonblocking(true).map_err(op("fcntl()"))?; - let socket = UdpSocket::from_std(socket.into()).map_err(op("bindDatagramPort()"))?; - Ok(Box::new(TokioDatagram { - shared: Arc::new(DatagramShared { - socket, - owner: crate::current_loop_runtime_id()?, - }), - })) + UdpSocket::from_std(socket.into()).map_err(op("bindDatagramPort()")) } /// `kj::NetworkAddress::toString`, byte for byte like KJ's: `"ip:port"`, `"[v6]:port"`, @@ -645,7 +634,6 @@ impl TokioAddress { } #[cfg(unix)] Spec::Unix(name) => name.display_bytes(), - Spec::Loopback(queue) => [b"loopback:".as_slice(), queue.name()].concat(), } } } @@ -656,7 +644,7 @@ impl TokioAddress { /// tokio's connect (`connect(2)`, wait for writability, read `SO_ERROR`), then KJ's /// unconditional `TCP_NODELAY` on outbound TCP sockets (a hard failure in KJ's /// `SocketAddress::socket()`, and here). -async fn connect_to(target: SocketAddress) -> Result> { +async fn connect_socket(target: SocketAddress) -> Result { ensure_loop_thread()?; let socket = match target.kind { AddressKind::Ipv4 | AddressKind::Ipv6 => { @@ -684,7 +672,7 @@ async fn connect_to(target: SocketAddress) -> Result> { )); } }; - Ok(Box::new(TokioStream::new(socket)?)) + Ok(socket) } // ====================================================================================== @@ -758,10 +746,11 @@ fn bind_tcp(addr: SocketAddr, wildcard: bool) -> Result { Ok(ListenerInner::Tcp(listener)) } -/// A `kj::ConnectionReceiver` backend: one or more listening sockets (several when the address -/// resolved to several -- KJ's aggregate receiver), accepted from in round-robin order of -/// readiness; or a loopback queue. A handle to `Arc`-shared state; each `accept()` future owns a -/// share. +/// A `kj::ConnectionReceiver` backend. +/// +/// One or more listening sockets (several when the address resolved to several -- KJ's +/// aggregate receiver), accepted from in round-robin order of readiness. A +/// handle to `Arc`-shared state; each `accept()` future owns a share. pub struct TokioListener { shared: Arc, } @@ -772,20 +761,17 @@ struct ListenerShared { owner: tokio::runtime::Id, } -enum ListenerBackend { - Sockets { - /// Never empty. - inners: Vec, - /// Round-robin start index for the next `accept()` poll, so a busy first socket cannot - /// starve the others. - next: AtomicUsize, - }, - Loopback(Arc), +struct ListenerBackend { + /// Never empty. + inners: Vec, + /// Round-robin start index for the next `accept()` poll, so a busy first socket cannot + /// starve the others. + next: AtomicUsize, } impl ListenerBackend { fn sockets(inners: Vec) -> Self { - Self::Sockets { + Self { inners, next: AtomicUsize::new(0), } @@ -922,19 +908,20 @@ impl ListenerBackend { } impl ListenerShared { - /// One accepted connection: KJ's transient errors are retried here; whether the peer is - /// allowed is the adapter's decision (module docs, "Where filtering happens"). + /// One accepted connection: whether the peer is allowed is the adapter's decision (module + /// docs, "Where filtering happens"). async fn accept(&self) -> Result { + let (socket, peer) = self.accept_socket().await?; + Ok(PeerStream { + stream: Box::new(TokioStream::new(socket)?), + peer, + }) + } + + /// One accepted socket and its peer; KJ's transient errors are retried here. + async fn accept_socket(&self) -> Result<(Socket, SocketAddress)> { crate::ensure_owner_loop(self.owner)?; - let (inners, next) = match &self.backend { - ListenerBackend::Sockets { inners, next } => (inners, next), - ListenerBackend::Loopback(queue) => { - return Ok(PeerStream { - stream: queue.accept().await?, - peer: SocketAddress::loopback(queue.name()), - }); - } - }; + let ListenerBackend { inners, next } = &self.backend; loop { let (socket, peer) = match std::future::poll_fn(|cx| ListenerBackend::poll_accept_any(inners, next, cx)) @@ -950,10 +937,7 @@ impl ListenerShared { { return Err(op("setsockopt(TCP_NODELAY)")(e)); } - return Ok(PeerStream { - stream: Box::new(TokioStream::new(socket)?), - peer, - }); + return Ok((socket, peer)); } } } @@ -968,35 +952,29 @@ impl TokioListener { }) } - /// `kj::ConnectionReceiver::getPort`: the first socket's, like KJ's aggregate receiver; 0 for - /// a loopback receiver, as for KJ's non-IP receivers. - fn port(&self) -> Result { - match &self.shared.backend { - ListenerBackend::Sockets { inners, .. } => inners[0].port(), - ListenerBackend::Loopback(_) => Ok(0), - } + /// The next connection and, of a TCP one, its peer: for a Rust caller, which serves the + /// tokio socket itself. + pub async fn accept(&self) -> Result<(Socket, Option)> { + let (socket, peer) = self.shared.accept_socket().await?; + Ok((socket, ip_socket_addr(&peer).ok())) + } + + /// `kj::ConnectionReceiver::getPort`: the first socket's, like KJ's aggregate receiver. + pub fn port(&self) -> Result { + self.shared.backend.inners[0].port() } /// `kj::ConnectionReceiver::getsockname`: the first socket's. fn local_addr(&self) -> Result { - match &self.shared.backend { - ListenerBackend::Sockets { inners, .. } => inners[0].local_addr(), - ListenerBackend::Loopback(queue) => Ok(SocketAddress::loopback(queue.name())), - } + self.shared.backend.inners[0].local_addr() } } // ====================================================================================== // Bridge entry points (see ffi.rs) -pub async fn parse_address( - addr: &[u8], - port_hint: u16, - loopback: &LoopbackRegistry, -) -> Result> { - Ok(Box::new( - TokioAddress::parse(addr, port_hint, loopback).await?, - )) +pub async fn parse_address(addr: &[u8], port_hint: u16) -> Result> { + Ok(Box::new(TokioAddress::parse(addr, port_hint).await?)) } /// `kj::Network::getSockaddr`: the C++ adapter decoded the caller's `struct sockaddr` into a @@ -1026,11 +1004,8 @@ pub fn address_targets(addr: &TokioAddress) -> Result> { addr.targets() } -pub fn connect_target( - addr: &TokioAddress, - target: SocketAddress, -) -> impl Future>> + use<> { - addr.connect(target) +pub async fn connect_target(target: SocketAddress) -> Result> { + Ok(Box::new(TokioStream::new(connect_socket(target).await?)?)) } pub fn address_listen(addr: &TokioAddress) -> Result> { diff --git a/src/rust/cxx/kj-rs-io/signal.rs b/src/rust/cxx/kj-rs-io/signal.rs deleted file mode 100644 index f903044e2ad..00000000000 --- a/src/rust/cxx/kj-rs-io/signal.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! tokio-backed signal watching: POSIX signals on Unix, the corresponding console control -//! events on Windows. -//! -//! This backs `kj_rs_io::onSignal()` (async-io.h), the tokio-loop replacement for -//! `kj::UnixEventPort::onSignal()` -- workerd uses it for SIGTERM graceful drain. -//! -//! Semantics differences vs `UnixEventPort::onSignal()` (acceptable for the drain use case): -//! -//! - No `siginfo_t` is reported; the promise just resolves. -//! - The handler is registered when the returned future is first polled (tokio registers with -//! the process-global signal registry at `signal()` time). The C++ adapter starts the promise -//! eagerly, so from a caller's point of view the handler is installed by the time `onSignal()` -//! returns; KJ does not block the signal beforehand the way `UnixEventPort::captureSignal()` -//! does, so a signal delivered before the *call* takes its default disposition. -//! - tokio's signal registration is process-wide and persists for the life of the process -//! (dropping the future stops *watching*, but does not restore `SIG_DFL`). -//! -//! Tokio's signal registry is process-global. Its broadcast may run on another runtime's -//! thread on Unix (any other tokio runtime in the process with the signal driver enabled), or -//! on the OS-created console-control thread on Windows. The bridged future's cloned waker owns -//! a kj-rs `ArcWaker`, whose cross-thread fulfiller schedules the next poll on the owning KJ -//! loop. Signal streams can therefore be awaited directly here. The test "onSignal is delivered -//! even when another runtime's thread consumes the signal" in tests/async-io-test.c++ exercises -//! delivery with another runtime parked on a different thread. -//! -//! On Windows the signums workerd actually passes are mapped to console control events: -//! SIGINT -> `ctrl_c` (the interactive interrupt, exactly SIGINT's role) and SIGTERM -> -//! `ctrl_break`. The latter is a conscious choice among the candidates: `CTRL_BREAK_EVENT` is -//! the event a parent process can send programmatically (`GenerateConsoleCtrlEvent`) to ask a -//! console child to wind down, which is what `kill -TERM` is on Unix; `CTRL_CLOSE_EVENT` gives -//! the handler ~5 s before the process is killed regardless, too short for a drain; and -//! `CTRL_SHUTDOWN_EVENT` is delivered to services and at system shutdown, not to a console -//! process someone wants to stop. Anything else errors. - -use crate::ensure_loop_thread; -use crate::error::KjIoError; -use crate::error::Result; -use crate::error::op; - -/// Resolves when the process receives signal `signum` (on Windows: the console control event -/// conventionally mapped to it). Errors immediately for unmapped signums. -pub async fn wait_for_signal(signum: i32) -> Result<()> { - // Creating the signal stream registers with the entered runtime's signal driver (lib.rs, - // "The tokio runtime"). - ensure_loop_thread()?; - #[cfg(unix)] - { - let kind = tokio::signal::unix::SignalKind::from_raw(signum); - let mut sig = tokio::signal::unix::signal(kind).map_err(op("signal"))?; - sig.recv() - .await - .ok_or_else(|| KjIoError::other("signal", "signal stream closed unexpectedly"))?; - Ok(()) - } - #[cfg(windows)] - { - // `` values as the C++ callers pass them (MSVC defines SIGINT=2, SIGTERM=15). - // workerd's only caller passes SIGTERM (graceful drain; server/cli-main.c++); - // SIGINT is mapped for completeness. See the module docs for why SIGTERM is - // CTRL_BREAK_EVENT. - const SIGINT: i32 = 2; - const SIGTERM: i32 = 15; - // tokio's `SetConsoleCtrlHandler` handler broadcasts from an OS-spawned console-ctrl - // thread; the thread-safe waker bridge absorbs that (see the module doc). - let received = match signum { - SIGTERM => { - let mut sig = tokio::signal::windows::ctrl_break().map_err(op("signal"))?; - sig.recv().await - } - SIGINT => { - let mut sig = tokio::signal::windows::ctrl_c().map_err(op("signal"))?; - sig.recv().await - } - _ => { - return Err(KjIoError::other( - "signal", - "kj-rs-io only watches SIGTERM/SIGINT on Windows", - )); - } - }; - received.ok_or_else(|| KjIoError::other("signal", "signal stream closed unexpectedly"))?; - Ok(()) - } -} diff --git a/src/rust/cxx/kj-rs-io/stream.rs b/src/rust/cxx/kj-rs-io/stream.rs index a6c8b4d9caa..af49bda1385 100644 --- a/src/rust/cxx/kj-rs-io/stream.rs +++ b/src/rust/cxx/kj-rs-io/stream.rs @@ -93,8 +93,11 @@ struct Inner { read_abort: tokio::sync::Notify, } -/// A registered tokio socket of either family. Constructed only by net.rs, from sockets it -/// registered on the loop thread (lib.rs, "The tokio runtime"). +/// A registered tokio socket of either family. +/// +/// One net.rs registered on the loop thread (lib.rs, "The tokio runtime"): wrapped in a +/// [`TokioStream`] for C++, or handed to a Rust caller as it is (`TokioListener::accept`, +/// `TokioAddress::connect_first`). pub enum Socket { Tcp(TcpStream), #[cfg(unix)] diff --git a/src/rust/cxx/kj-rs-io/tests/async-io-test.c++ b/src/rust/cxx/kj-rs-io/tests/async-io-test.c++ index 53b96fc1a15..362d73b10ab 100644 --- a/src/rust/cxx/kj-rs-io/tests/async-io-test.c++ +++ b/src/rust/cxx/kj-rs-io/tests/async-io-test.c++ @@ -750,68 +750,6 @@ KJ_TEST("restrictPeers filters accepted peers (disallowed peers are dropped, " kj::byte buffer[1]; KJ_EXPECT(client->tryRead(buffer, 1, 1).wait(ws) == 0); } - -KJ_TEST("onSignal is delivered even when another runtime's thread consumes the signal") { - // Tokio's process-global signal registry broadcasts from whichever runtime consumes the - // signal's wake byte. A second parked runtime (any other tokio runtime in the process with the - // signal driver enabled) can therefore wake this loop's signal future from another thread. - // ArcWaker must deliver that wake through its cross-thread fulfiller so this loop observes - // the signal. - auto io = setupTokioAsyncIo(); - auto &ws = io.getWaitScope(); - - // A second, idle tokio-ported KJ loop parked on another thread for the duration of the test. - // The shutdown promise must be created ON that thread's loop (kj promises are single-loop - // objects); only its CrossThreadPromiseFulfiller half comes back to this thread. - kj::MutexGuarded>>> shutdown; - kj::Thread otherLoop([&shutdown]() { - auto io2 = setupTokioAsyncIo(); - auto paf = kj::newPromiseAndCrossThreadFulfiller(); - *shutdown.lockExclusive() = kj::mv(paf.fulfiller); - paf.promise.wait(io2.getWaitScope()); - }); - auto fulfillShutdown = [&]() { - KJ_IF_SOME(fulfiller, *shutdown.lockExclusive()) { - fulfiller->fulfill(); - } - }; - KJ_DEFER(fulfillShutdown()); - // Wait until the other loop is up and parked (and the shutdown fulfiller exists) before - // raising any signals, so its runtime genuinely participates in the wake-byte race. - shutdown.when([](auto &maybe) { return maybe != kj::none; }, [](auto &) {}); - - // Several rounds, giving each runtime chances to win the wake-byte race. Bounded so a lost - // wake fails with a diagnosis instead of eating the binary's bazel timeout. - for (int i = 0; i < 5; i++) { - auto promise = kj_rs_io::onSignal(SIGUSR2); - // The handler was installed inside onSignal(); the poll only proves the promise is pending. - KJ_EXPECT(!promise.poll(ws)); - KJ_SYSCALL(kill(getpid(), SIGUSR2)); - promise - .exclusiveJoin(io.getTimer().afterDelay(20 * kj::SECONDS).then([]() { - KJ_FAIL_ASSERT("onSignal wake was lost (cross-thread waker regression)"); - })).wait(ws); - } -} - -KJ_TEST("onSignal resolves when the process receives the signal") { - auto io = setupTokioAsyncIo(); - auto &ws = io.getWaitScope(); - - auto promise = kj_rs_io::onSignal(SIGUSR2); - // The handler was installed inside onSignal() (operation-start policy); a signal raised right - // after the call is caught. The poll only proves the promise is pending. - KJ_EXPECT(!promise.poll(ws)); - - KJ_SYSCALL(kill(getpid(), SIGUSR2)); - promise.wait(ws); - - // A second watcher works too (the process-global registration is reusable). - auto again = kj_rs_io::onSignal(SIGUSR2); - KJ_EXPECT(!again.poll(ws)); - KJ_SYSCALL(kill(getpid(), SIGUSR2)); - again.wait(ws); -} #endif // ======================================================================================= @@ -865,73 +803,6 @@ KJ_TEST("restrictPeers: a child network (and its addresses) outlive the parent n KJ_EXPECT(KJ_ASSERT_NONNULL(blocked).getDescription().contains("restrictPeers")); } -KJ_TEST("loopback: addresses connect within the process once enabled") { - auto io = setupTokioAsyncIo(); - auto &ws = io.getWaitScope(); - auto &network = io.getNetwork(); - - // Off by default: "loopback:svc" is then a host "loopback" with service "svc". - KJ_EXPECT_THROW_MESSAGE("getaddrinfo()", network.parseAddress("loopback:svc").wait(ws)); - - kj::downcast(network).enableLoopback(); - auto addr = network.parseAddress("loopback:svc").wait(ws); - KJ_EXPECT(addr->toString() == "loopback:svc"); - auto receiver = addr->listen(); - KJ_EXPECT(receiver->getPort() == 0); - - // A restrictPeers() child shares the namespace, and the filter does not judge loopback - // connections: this restriction would block any real address. - auto restricted = network.restrictPeers({"1.2.3.4/32"_kj}, {}); - auto clientPromise = restricted->parseAddress("loopback:svc").wait(ws)->connect(); - auto server = receiver->accept().wait(ws); - auto client = clientPromise.wait(ws); - - // Real sockets underneath: bytes flow both ways. - client->write("ping"_kjb).wait(ws); - kj::byte buffer[4]; - KJ_EXPECT(server->tryRead(buffer, 4, 4).wait(ws) == 4); - KJ_EXPECT(kj::ArrayPtr(buffer, 4) == "ping"_kjb); - server->write("pong"_kjb).wait(ws); - KJ_EXPECT(client->tryRead(buffer, 4, 4).wait(ws) == 4); - KJ_EXPECT(kj::ArrayPtr(buffer, 4) == "pong"_kjb); - - // Connections made before anyone accepts are queued, and different names are separate. - auto other = network.parseAddress("loopback:other").wait(ws); - auto queued = addr->connect().wait(ws); - auto otherReceiver = other->listen(); - auto otherAccept = otherReceiver->accept(); - KJ_EXPECT(!otherAccept.poll(ws)); - auto accepted = receiver->accept().wait(ws); - queued->write("!"_kjb).wait(ws); - KJ_EXPECT(accepted->tryRead(buffer, 1, 1).wait(ws) == 1); - KJ_EXPECT(!otherAccept.poll(ws)); -} - -KJ_TEST("loopback: a name belongs to the loop that first parsed it") { - auto io = setupTokioAsyncIo(); - auto &ws = io.getWaitScope(); - kj::downcast(io.getNetwork()).enableLoopback(); - auto addr = io.getNetwork().parseAddress("loopback:owned").wait(ws); - auto receiver = addr->listen(); - - // A clone of the address carried to another loop thread cannot connect: the queued end would - // be a socket of that loop, unusable by the receiver here. - kj::Maybe failure; - { - auto other = addr->clone(); - kj::Thread thread([&]() noexcept { - auto otherIo = setupTokioAsyncIo(); - failure = kj::runCatchingExceptions([&]() { other->connect().wait(otherIo.getWaitScope()); }); - }); - } - KJ_EXPECT(KJ_ASSERT_NONNULL(failure).getDescription().contains("different TokioEventPort"), - KJ_ASSERT_NONNULL(failure).getDescription()); - - // Nothing was queued: an accept here still waits. - auto acceptPromise = receiver->accept(); - KJ_EXPECT(!acceptPromise.poll(ws)); -} - KJ_TEST("dropping a just-started connect() then tearing down the context is clean") { // Start a connect(), kick the machinery with one poll, then drop the promise and destroy the // whole context -- exercising cancellation of the connect's readiness registration and the @@ -1065,57 +936,6 @@ KJ_TEST("getSockaddr builds a connectable IPv6 address from a raw sockaddr_in6") kj::byte buf[2]; KJ_EXPECT(server->tryRead(buf, 2, 2).wait(ws) == 2); } - -KJ_TEST("multiple concurrent onSignal for the same signum all fire") { - // tokio broadcasts a signal to every live stream for that signum, so two concurrent - // onSignal(SIGUSR2) must both resolve on a single delivery. - auto io = setupTokioAsyncIo(); - auto &ws = io.getWaitScope(); - auto a = kj_rs_io::onSignal(SIGUSR2); - auto b = kj_rs_io::onSignal(SIGUSR2); - KJ_EXPECT(!a.poll(ws)); // both handlers installed before we raise - KJ_EXPECT(!b.poll(ws)); - KJ_SYSCALL(kill(getpid(), SIGUSR2)); - a.wait(ws); - b.wait(ws); -} - -KJ_TEST("onSignal isolates different signums") { - auto io = setupTokioAsyncIo(); - auto &ws = io.getWaitScope(); - auto usr1 = kj_rs_io::onSignal(SIGUSR1); - auto usr2 = kj_rs_io::onSignal(SIGUSR2); - KJ_EXPECT(!usr1.poll(ws)); - KJ_EXPECT(!usr2.poll(ws)); - KJ_SYSCALL(kill(getpid(), SIGUSR2)); - usr2.wait(ws); - // Only SIGUSR2 was raised; the SIGUSR1 watcher stays pending. - KJ_EXPECT(!usr1.poll(ws)); -} - -KJ_TEST("dropping a pending onSignal does not break later watches") { - // Cancel a registered-but-unfired signal watch, then confirm a fresh watch still delivers -- - // the dropped tokio signal stream must not disturb the process-global registration. ASAN- - // relevant (the drop cancels the stream). - auto io = setupTokioAsyncIo(); - auto &ws = io.getWaitScope(); - { - auto dropped = kj_rs_io::onSignal(SIGUSR2); - KJ_EXPECT(!dropped.poll(ws)); - } - auto again = kj_rs_io::onSignal(SIGUSR2); - KJ_EXPECT(!again.poll(ws)); - KJ_SYSCALL(kill(getpid(), SIGUSR2)); - again.wait(ws); -} - -KJ_TEST("onSignal for an unwatchable signum errors instead of aborting") { - // SIGKILL/SIGSTOP cannot have handlers; tokio's signal() rejects them, which must surface as a - // catchable kj::Exception (a rejected promise), never a crash. No signal is raised. - auto io = setupTokioAsyncIo(); - auto &ws = io.getWaitScope(); - KJ_EXPECT_THROW_MESSAGE("signal", kj_rs_io::onSignal(SIGKILL).wait(ws)); -} #endif // !_WIN32 // ======================================================================================= @@ -1490,7 +1310,7 @@ KJ_TEST("getSockaddr keeps a zero-initialized sockaddr_un usable and prints it l KJ_TEST("provider pipes are socket pairs: a write completes before anyone reads") { // kj's own provider hands out real pipes/socketpairs, and callers rely on their kernel // buffering: a small write completes without a reader waiting (an in-memory kj pipe would - // leave it pending). workerd's loopback transport also asks the provider for real sockets. + // leave it pending). // Both pipe kinds are socket pairs here (async-io.c++ newOneWayPipe explains). auto io = setupTokioAsyncIo(); auto &ws = io.getWaitScope(); diff --git a/src/workerd/api/restore.c++ b/src/workerd/api/restore.c++ index 33897fa9ecf..f7497d6f1c1 100644 --- a/src/workerd/api/restore.c++ +++ b/src/workerd/api/restore.c++ @@ -181,11 +181,11 @@ class LifetimeExtendedSubrequestChannel final: public IoChannelFactory::Subreque }; // EventDispatcher server that forwards events to a restored service channel. This is a minimal -// duplicate of the (private) `Server::WorkerdBootstrapImpl::EventDispatcherImpl` in workerd's -// server.c++, used to implement the `service :WorkerdBootstrap` result of `restoreService()` when -// the restore event is received over RPC. A restored ServiceStub is only ever used to make HTTP -// requests, make JS-RPC calls, or receive the next hop of a restore chain, so other event types -// are unsupported. +// duplicate of the (private) `WorkerdBootstrapImpl::EventDispatcherImpl` in workerd's +// worker-factory-rpc.c++, used to implement the `service :WorkerdBootstrap` result of +// `restoreService()` when the restore event is received over RPC. A restored ServiceStub is only +// ever used to make HTTP requests, make JS-RPC calls, or receive the next hop of a restore chain, +// so other event types are unsupported. class RestoredServiceEventDispatcher final: public rpc::EventDispatcher::Server { public: RestoredServiceEventDispatcher(capnp::HttpOverCapnpFactory& httpOverCapnpFactory, diff --git a/src/workerd/server/AGENTS.md b/src/workerd/server/AGENTS.md index 27d7758f7b3..d651c5108a8 100644 --- a/src/workerd/server/AGENTS.md +++ b/src/workerd/server/AGENTS.md @@ -2,47 +2,127 @@ ## OVERVIEW -Binary + orchestration layer. `:workerd` is a Rust binary: the `:workerd-cli` crate (`cli/`) parses the command line (clap), produces the encoded config (schema files via `config-compiler.c++`), handles `--watch` and `compile`, and runs each serving subcommand (`serve`, `compile`, `test`, `fuzzilli`, `pyodide-lock`, `make-pyodide-baseline-snapshot`) through a `run_*` function in cli-main.c++. `Server` (server.c++, ~6K lines) is the god object: parses `workerd.capnp` config, constructs all service types as nested inner classes, wires sockets/bindings/actors, runs the event loop. +The `workerd` binary and its server. `:workerd` (`workerd.rs`, this package) is a Rust binary over three crates in this directory: + +- `cli/` -- the `workerd-cli` crate (declared in this package's `BUILD.bazel`, sources globbed from `cli/`): parses the command line (clap), produces the encoded config (schema files are compiled by this package's `config-compiler.c++`), handles `compile`, `--watch` and re-exec, and hands `serve` and `test` (which `fuzzilli` and `make-pyodide-baseline-snapshot` are built on) to the server crate's `entry` module. +- `server/` -- the `workerd-server` crate: the single-tenant server. It interprets the config, builds the service graph, binds the sockets, runs the listeners, actors and dynamic workers, drains on SIGTERM, and runs `workerd test`. `Error` is `cxx::KjError`; `bridge.rs` is the one module that allows `unsafe`. +- `factory/` -- C++: `WorkerFactory` (`worker-factory*`), everything that needs the isolate, driven by the server through the cxx bridge in `server/bridge.rs`; and `bootstrap.{h,c++}`, the C++ half of a command. `with_process_context` sets up the process's logging (the JSON logger, when the config asks for structured logging) on the C++ stack around the Rust command; `bootstrap` sets up perfetto, autogates and V8 on the Rust-owned event loop and builds the `WorkerFactory`, which owns them, from the encoded config and the command-line options for `server/entry.rs`, which owns the loop (`kj_rs_tokio::Runtime`) and blocks on `server_serve` / `server_test`. + +This package keeps the config schema (`workerd.capnp`, `compiled-bindings.capnp`), the config compiler, the `WorkerdApi` and the C++ libraries the factory is built from (alarm scheduler, channel tokens, container client, facet tree index, fallback service, actor IDs, JSON logger, Pyodide, V8 platform), and their C++ unit tests. + +**The Rust/C++ boundary.** The factory keeps everything that touches the isolate: compiling a worker (`WorkerdApi`, compatibility flags, extensions, modules), starting a request on it, constructing actors with their storage, alarms and containers, the capnp RPC servers, channel-token encoding and the inspector. It also keeps the single-tenant policy objects, which live in this directory because the production runtime supplies its own: the unlimited `LimitEnforcer`, the clock-corrected `TimerChannel`, the null `IsolateLimitEnforcer`, the default observers. The server owns everything else: config interpretation, bindings and channel numbering, services, listeners, actor lifecycle and eviction, dynamic workers, drain and the test runner. Every bridge function on the C++ side is a free function taking the object first; one that can throw is declared fallible (a C++ exception becomes a `Result` error), the rest do not throw. A `KjOwn` moves ownership in the direction of the call, a `Box` of a Rust type handed to C++ is a cheap `Rc` or `Weak` handle C++ may hold. + +Crossing the boundary: + +- **Bindings cross as one message** (`server/bindings.rs`, `server/channels.rs`; their module docs have the detail). `compile_bindings` interprets `Worker.bindings` into `Globals` (`compiled-bindings.capnp`), every capability a channel number, and `loopback_globals` does the same for `ctx.exports`; the factory compiles the message into `env` and `ctx.exports` under the isolate lock, and at request time the runtime asks the worker's `ChannelFactory` (Rust) for the channel behind a number. A dynamic worker's `env` arrives as KJ capabilities already, so the factory keeps them as the worker's own tables (`CompiledWorker::Impl`), together with its global outbound and tails, and asks the Rust `ChannelFactory` only for the channel numbers past them: the worker's `ctx.exports`, which the server numbers from the counts `WorkerInfo` reports. +- **Workflows are wired after every service exists.** `workflowsEngine` gets two passes in `server/config.rs`: `collect_workflow_configs` (with the actor configs) checks it and synthesizes the `Durable` config of each valid Workflow's namespace, keyed `miniflare-workflows-` and carrying the `Workflow` (name, class, `bindingService`) the later pass reads; `init_workflows` (after the services) resolves the engine's class and the `bindingService`, builds the actor props (`workflowClass` stub, `workflowClassName`, `workflowName`) with the `frankenvalue_*` shims and creates the namespace on the app worker (`WorkerService::init_workflow_namespace`), its storage in the bindingService worker's directory. The bindings compiler gives each configured Workflow a subrequest channel to its bindingService (`workflow_binding_channels`), the inner fetcher of the Workflow's wrapped `ctx.exports` binding; a Workflow class takes no loopback channel. +- **Background tasks run on the KJ loop** (`server/tasks.rs`, whose module doc says why): spawn with `Factory::spawn` / `spawn_detached`, which put the future in the factory's `kj::TaskSet`; never with `kj_rs_tokio::spawn`. +- **`loopback:` addresses** (`server/listen/loopback.rs`) are in-process connections, refused unless the registry is enabled, which only `workerd test` and the in-process server (`server/in_process.rs`) do. An HTTP or TCP listener serves a connection with its hang-up signal (`listen/mod.rs`, `hangup`) and a dialer hands one to kj-hyper's client: `LoopbackStream::hangup`, or a socket's `kj_rs_io::when_write_disconnected`. +- **`--control-fd`** (`Control`, `server/run.rs`): `listen-inspector` and `listen` events, one JSON object per line with `port` last; a config the `Reporter` refuses gets no `listen` event. The server writes to a duplicate of the descriptor (`cli/socket_fd.rs`). +- **Header names the config chooses are table headers** (`buildHeaderTable`, `factory/worker-factory.c++`): the factory's `kj::HttpHeaderTable` holds every header the config's `HttpOptions` name, so kj writes them as the config spells them and the rewriter (`services/rewriter.rs`, whose module doc has the rest) edits `kj::HttpHeaders` by name. +- **Logging** (`server/log.rs`): log from the server with `tracing::error!` / `warn!` / `info!`, which become KJ log lines through `kj_log`. Config errors and warnings go to the `Reporter` (`server/config.rs`), which `entry.rs` prints to stderr, as it does `Tests failed!` and the run's failure (`*** Uncaught exception ***`): as they are, or, under structured logging, as lines of the JSON logger's format (`json_log_to_stderr`, over `buildJsonLogMessage`). The logger's own lines (`KJ_LOG`, `tracing`, the workers' consoles) go to stdout; what supervises the process (miniflare) reads the reason of a failed start from stderr. An error is printed the way KJ prints an exception through `exception_text` (`kj::str`: `file:line: type: description`; a `KjError` carries no stack trace, and one made in Rust has the bridge's location). Every error is reported and the server then does not serve, unless `--watch` is on. + +Bazel labels: `//src/workerd/server:workerd` (and `:workerd_cross`, which `wd_test` runs), `//src/workerd/server:workerd-cli`, `//src/workerd/server/server:workerd-server` (its bridge library `:bridge.rs@cxx`, whose header is ``), `//src/workerd/server/factory:worker-factory` / `:bootstrap` (header-only `:worker-factory-hdrs` / `:bootstrap-hdrs` for the bridges). The Rust packages hold only `.rs` files; the bridges take their C++ headers from `cxx_bridge_deps`. ## KEY FILES -| File | Role | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `workerd.rs` | Crate root of the `:workerd` binary; calls `workerd_cli::main()` | -| `cli/` | `:workerd-cli` crate: `main()` (`lib.rs`), clap definitions (`args.rs`), config sources and the synthesized Python config (`config.rs`), `--watch` (`watch.rs`), executable discovery, compiled-binary format and re-exec (`process.rs`), `--socket-fd` (`socket_fd.rs`), the cxx bridge (`bridge.rs`) | -| `cli-main.h/c++` | C++ side of the CLI, called through `cli/bridge.rs`: V8 and `Server` setup, option forwarding, SIGTERM drain; enables kj-rs-io's `loopback:` addresses for `workerd test` | -| `config-compiler.h/c++` | Compiles a schema-file config (`capnp::SchemaParser` over `schema-file.h`) into an encoded message for `cli/lib.rs`; reports parse errors as data and registers the files it reads for `--watch` | -| `schema-file.h/c++` | The `capnp::SchemaFile` implementations: config files on disk with workerd's import resolution, and the schemas built into the binary | -| `server.h/c++` | `Server` class: 15+ nested inner classes (`WorkerService`, `NetworkService`, `ExternalHttpService`, `DiskDirectoryService`, etc.). Two-phase init: `startServices()` then `listenOnSockets()` | -| `workerd.capnp` | Config schema: `Config`, `Service`, `Worker`, `Socket`, `Extension`. Capability-based security model | -| `workerd-api.h/c++` | `WorkerdApi`: registers all JS API types with JSG, compiles modules/globals, extracts source from config. `Global` struct has 20+ binding variants as `kj::OneOf` | -| `alarm-scheduler.h/c++` | DO alarm scheduling with SQLite-backed persistence | -| `json-logger.h/c++` | Structured JSON logging for tail workers | -| `channel-token.h/c++` | Opaque token encoding for cross-service channel references | -| `v8-platform-impl.h/c++` | Custom `v8::Platform` bridging V8 tasks to KJ event loop | -| `fallback-service.h/c++` | Module fallback resolution via external service | -| `container-client.h/c++` | Experimental (2025): Docker container lifecycle for DO containers | -| `docker-api.capnp` | Cap'n Proto schema for container management | -| `pyodide.h/c++` | Python worker preloading and snapshot management | +### `src/workerd/server/` (this package) -## TEST INFRASTRUCTURE +| File | Role | +| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `workerd.rs` | Crate root of the `:workerd` binary; calls `workerd_cli::main()` | +| `workerd.capnp` | Config schema: `Config`, `Service`, `Worker`, `Socket`, `Extension`. Capability-based security model | +| `compiled-bindings.capnp` | `Global` / `Globals`: one worker's bindings after the server interpreted them; channel numbering rules in its header comment | +| `config-compiler.h/c++` | Compiles a schema-file config (`capnp::SchemaParser` over `schema-file.h`) into an encoded message for `cli/lib.rs`; reports parse errors as data and registers the files it reads for `--watch` | +| `schema-file.h/c++` | The `capnp::SchemaFile` implementations: config files on disk with workerd's import resolution, and the schemas built into the binary | +| `workerd-api.h/c++` | `WorkerdApi`: registers all JS API types with JSG, compiles modules/globals, extracts source from config. `compileGlobals` sets the bindings of a `Globals` message (`compiled-bindings.capnp`) on `env` / `ctx.exports`, one `switch` case per binding kind | +| `alarm-scheduler.h/c++` | DO alarm scheduling with SQLite-backed persistence | +| `channel-token.h/c++`, `channel-token.capnp` | Opaque token encoding for cross-service channel references; the factory is the `ChannelTokenHandler`, the Rust `Server` its resolver | +| `json-logger.h/c++`, `log-schema.capnp` | Structured JSON logging for tail workers | +| `v8-platform-impl.h/c++` | Custom `v8::Platform` bridging V8 tasks to KJ event loop | +| `fallback-service.h/c++` | Module fallback resolution via external service (on its own thread) | +| `container-client.h/c++`, `docker-api.capnp` | Docker container lifecycle for DO containers | +| `facet-tree-index.h/c++` | The on-disk index of an actor's facets | +| `actor-id-impl.h/c++` | `ActorIdFactory` for the single-tenant server | +| `pyodide.h/c++` | Python worker preloading and snapshot management | +| `workerd-debug-port-client.h/c++` | The client side of `--debug-port` | -`server-test.c++` (~6K lines): integration tests using inline Cap'n Proto config strings. Tests full server lifecycle with real V8 isolates. +### `cli/` (`workerd-cli`) -`tests/server-harness.mjs`: Node.js harness spawning `workerd` child processes for E2E tests. Subdirectories: `compile-tests`, `container-client`, `extensions`, `inspector`, `python`, `structured-logging`, `unsafe-eval`, `unsafe-module`, `weakref`. +| File | Role | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `lib.rs` | Crate root: `main()`, the commands, the server's `Watch` future for `--watch` | +| `args.rs` | clap definitions | +| `config.rs` | Config sources (schema file, compiled binary, embedded) and the synthesized Python config | +| `process.rs` | Executable discovery, the compiled-binary format, `Process` (the files to watch, re-exec) | +| `socket_fd.rs` | `--socket-fd` / `--control-fd` descriptors, made owned; the platform halves are `socket_fd/{unix,windows}.rs` | +| `watch.rs` | `--watch`: the file watcher | +| `bridge.rs` | The cxx bridge (`workerd::server::cli`) to `config-compiler.h` and the build facts in `factory/bootstrap.h`; header `` | + +### `server/` (`workerd-server`) + +| File | Role | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `lib.rs` | Crate root: `Error` / `Result` | +| `bridge.rs` | The cxx bridge (`workerd::server`) to `factory/worker-factory.h` and `factory/bootstrap.h`. `extern "C++"`: the factory (compile, start request, actors and storage, RPC, tokens, inspector, `factory_spawn`), the bootstrap, the process's logging and its exit; `extern "Rust"`: the channel objects, the channel factory, the pending command. Ownership rules in its `//!` | +| `entry.rs` | `serve` / `test`: the process's schedule (`with_process_context`, the `kj_rs_tokio::Runtime`, `bootstrap`, `block_on`, exit), `server_serve` / `server_test`, the `Watch` future the command line supplies, printing config errors and the exit status they make, SIGTERM drain, `--watch` reload, shutdown order (settle tasks, drop the factory and with it V8 and the perfetto session, then the Runtime) | +| `run.rs` | `run()` / `test()`: bind sockets, start the inspector (before any isolate exists), build the graph, start the listeners (not for a config the `Reporter` refuses), drain, control-fd events, the check that every `--directory-path` / `--external-addr` override named a service | +| `config.rs` | `Server`: the service graph from the config in three passes (actor configs, construction, link); `Factory`, the Rust owner of the C++ `WorkerFactory`; `Reporter`; resolves channel tokens and debug-port requests | +| `channels.rs` | The handles C++ holds and the runtime reaches into the server through: `SubrequestChannel` (an `Rc`), `ActorClassChannel` (an `Rc`), `ChannelFactory` (a worker's `LinkedChannels`), `WorkerStub`, and the `Weak` back-pointers (`ServerHandle`, `ActorNamespaceHandle`, `ActorHooks`). Single-threaded | +| `bindings.rs` | `compile_bindings` / `loopback_globals`: `Worker.bindings` and `ctx.exports` to the `Globals` message and the channel tables the link stage fills | +| `worker.rs` | `WorkerService`: compiled through the factory, `PendingLink` designators resolved into `LinkedChannels` once every service exists; entrypoints, actor classes, the I/O channel table requests see | +| `actor.rs` | `ActorNamespace` / `ActorContainer`: actor lifecycle, facets, idle shutdown, eviction, and the sweep that drops long-idle `ActorContainer`s. The sweep keeps a container anything but its map holds (a stub or a request, each a `Client`; an eviction in progress), so nothing else holds one strongly: back-pointers to a container are `Weak`. Docker containers, their registry and the cleanup join at shutdown are the factory's (`-actor.c++`) | +| `loader.rs` | Dynamic workers (`workerLoader`): `WorkerStub`, one shared startup future per loaded worker | +| `services/mod.rs` | External and network services over kj-hyper's pooled client (a client per event, TLS config once per service; a `style = proxy` external server is asked for whole URLs), and service construction | +| `services/network.rs` | `TlsOptions` to rustls and the `allow` / `deny` peer filter | +| `services/disk.rs` | The `disk` service (synchronous file I/O in 64 KiB steps) | +| `services/rewriter.rs` | A socket's or external server's `HttpOptions` applied to traffic: URL style, cf blob header, injected headers, the capnp-over-CONNECT host. Headers stay `kj::HttpHeaders`: the rewriter names the edits (`HeaderEdit`) and the factory applies them by table id | +| `listen/mod.rs` | Sockets bound through kj-rs-io (`TokioAddress`; a `loopback:` name is a queue of the server's registry) before the services start; HTTP/HTTPS through kj-hyper, TCP as `connect()` events, the debug port. One future per listener (accept loop plus its connections); an accept-loop failure is fatal, a connection failure ends that connection. A failed HTTP call is logged (`Uncaught exception: ` and the error as KJ prints it, `log_uncaught`); kj-hyper answers it with a bare 500 | +| `listen/udp.rs` | UDP sockets: each peer's datagrams as one flow (`UdpFlow`), dispatched as a `connect()` event | +| `listen/loopback.rs` | The `loopback:` registry: queues by name behind a mutex (kj-hyper's dialers are `Send + Sync`), `tokio::io::duplex` pipes, `LoopbackStream` with its hang-up signal | +| `tasks.rs` | `Factory::spawn` / `spawn_detached` / `settle_tasks`, `TaskHandle` (drop cancels at the next poll), `SpawnedTask` (the future the C++ task set polls) | +| `in_process.rs` | `InProcessServer`: the server run inside a test process on a factory the test built, for `server-test.c++` (see TEST INFRASTRUCTURE) | +| `log.rs` | `install()`: the `tracing` subscriber that renders events the way `KJ_LOG` does and forwards them to `kj_log` | + +### `factory/` (C++) + +| File | Role | +| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `worker-factory.h`, `worker-factory*.c++` | `WorkerFactory`: everything the workers of one run share. `-impl.h` the private state, `worker-factory.c++` compile / request / bindings, `-actor.c++` actors, storage, alarms, containers, `-rpc.c++` the capnp RPC servers, `-listen.c++` the listeners' shims (the rewriter's header edits, rewriting responses, raw TCP `ConnectResponse`, UDP flows) | +| `bootstrap.h/c++` | The C++ half of a command: build capabilities, `pyodide_lock`, `with_process_context` (the crash handler, verbosity and the JSON logger on the stack), `bootstrap` (perfetto, autogates and V8 on the Rust-owned loop, attached to the factory it returns), `cli_exit`, `kj_log`, `json_log_to_stderr` | + +## TEST INFRASTRUCTURE -Pattern: unit tests (`*-test.c++`) at directory level; integration/E2E tests in `tests/` using the harness. +- **Rust unit tests**: a module's tests are in the `-test.rs` beside it (`config-test.rs` for `config.rs`, `listen/mod-test.rs` for `listen/mod.rs`), compiled only into the crates' test targets (`//src/workerd/server/server:workerd-server_test`, `//src/workerd/server:workerd-cli_test`); see `src/rust/AGENTS.md`, UNIT TEST FILES. The server crate's test binary links `//src/workerd/util:setup-async-io` because the C++ it reaches calls `kj::setupAsyncIo()`. +- **C++ unit tests** (`*-test.c++`, `kj_test`): alarm scheduler, channel tokens, container client, facet tree index, fallback service, JSON logger, actor IDs. +- **`server-test.c++`** (`//src/workerd/server:server-test@`): the server end to end, in the test process. Each `TestServer` builds a `WorkerFactory` on the test's V8, timer, network and a temp directory, and hands it to `InProcessServer` (`server/in_process.rs`, through the server crate's bridge), which runs `run::run` / `run::test` on it and queues the config errors and warnings for the test. No connection but UDP's leaves the process: the config's socket and external addresses become `loopback:` names, the test connects to the server's sockets and accepts the server's outbound connections by name (the `network` services' too), and the factory's own C++ clients (Docker, the debug port) dial the same names through the test's `kj::Network`. The factory's timer is the test's mock `kj::TimerImpl`, which `test.wait(n)` advances; the server's own delays run on it too (`Factory::sleep` / `Factory::now`, `server/config.rs`). An unexpected error log, config error or warning fails the case, as does a factory still referenced once the server is closed. Add a case here for every new config error or warning. +- **`tests/`**: `server-harness.mjs`, a Node.js harness spawning the `workerd` binary for E2E tests; it reads the `listen` events off `--control-fd`. Subdirectories: `compile-tests`, `container-client`, `container-shutdown`, `extensions`, `inspector`, `module-fallback`, `python`, `socket-close`, `structured-logging`, `udp-connect`, `udp-connect-ts-streams`, `udp-datagram-drop`, `unsafe-eval`, `unsafe-module`, `weakref`, `wrapped-binding-serialization`. Every `.wd-test` in the repository also runs through `workerd test` (`wd_test` in `build/wd_test.bzl`, over `:workerd_cross`). +- **kj-hyper contract tests** (`src/rust/cxx/kj-hyper/tests`): HTTP and WebSocket semantics against kj's interfaces. +- `:rust-io-link-check` inspects the linked binary's symbols (unix): kj's own `setupAsyncIo()` must not be linked in. ## I/O: THE TOKIO EVENT LOOP -- The process event loop and every socket are tokio-backed (kj-rs-tokio + kj-rs-io); - `//src/workerd/util:setup-async-io` supplies `kj::setupAsyncIo()` over them. -- It is linked per BINARY: `:workerd` and every `kj_test` / `wd_cc_benchmark` binary link - `//src/workerd/util:setup-async-io` (the macros add it); libraries do not depend on it, so - a downstream binary linking workerd libraries keeps its own event loop. One exception for now: - `//src/workerd/tests:test-fixture` still carries the dep so downstream binaries that link it - without linking it themselves keep linking (TODO(cleanup) there; drop it once they depend on - `setup-async-io` or `@capnp-cpp//src/kj:kj-async` themselves). +- One thread, one loop: `server/entry.rs` creates a `kj_rs_tokio::Runtime` (the KJ event loop + on kj-rs-tokio's port, with its tokio `current_thread` runtime) and `block_on`s the command, + which runs as a task on the loop's `LocalSet` while the main thread is blocked in + `promise.wait()`: the KJ loop schedules the thread, and tokio runs whenever it sleeps. The + factory takes its `kj::Timer` from the Runtime's context and its `kj::Network` from a kj-rs-io + provider the bootstrap builds over it, so the C++ side (actor storage, alarms, RPC, containers) + sees KJ I/O interfaces over tokio. The Rust server binds its sockets and dials external + servers through kj-rs-io's Rust API (`TokioAddress`: KJ's address grammar, resolver and socket + options), which hands back tokio's sockets (`server/listen/`, `server/services/`); a `network` + service resolves and connects with tokio itself (`kj_hyper::client::connect_allowed`). HTTP in + both directions is hyper through kj-hyper, SIGTERM is `tokio::signal`, `loopback:` is + `tokio::io::duplex`. +- `kj::setupAsyncIo()` (which `//src/workerd/util:setup-async-io` supplies over kj-rs-tokio and + kj-rs-io) is the same loop for the threads that block on a `kj::WaitScope` from C++: the + inspector and fallback-service threads, and every `kj_test` binary. +- `setup-async-io` is linked per BINARY: `:workerd` (through `factory:bootstrap`) and every + `kj_test` / `wd_cc_benchmark` binary link it (the macros add it); libraries do not depend on + it, so a downstream binary linking workerd libraries keeps its own event loop. One exception + for now: `//src/workerd/tests:test-fixture` still carries the dep so downstream binaries that + link it without linking it themselves keep linking (TODO(cleanup) there; drop it once they + depend on `setup-async-io` or `@capnp-cpp//src/kj:kj-async` themselves). - Never depend on the `@capnp-cpp//src/kj:kj-async` umbrella: it drags in `kj-async-os`, whose definitions collide with the shim's (an ODR violation). Use `:kj-async-core` / `:kj-async-io`. `just check-io-backend-graph` (one `bazel cquery somepath`, run by the lint CI lane) rejects any @@ -51,5 +131,5 @@ Pattern: unit tests (`*-test.c++`) at directory level; integration/E2E tests in `lowLevelProvider`; the holder owns the tokio context and the inert event port, so the context's references are valid for its whole lifetime and torn down once. The inert `kj::UnixEventPort` is never driven (`KJ_UNIMPLEMENTED` if anything tries). I/O objects are used on the loop thread that - created them (kj-rs-io checks and throws otherwise); a `loopback:` name likewise belongs to the - loop that first parsed it. + created them (kj-rs-io checks and throws otherwise); the server's `Rc` state, kj-hyper's objects + and the `loopback:` registry are likewise bound to the loop thread. diff --git a/src/workerd/server/BUILD.bazel b/src/workerd/server/BUILD.bazel index 93f513daafb..f6f460c245d 100644 --- a/src/workerd/server/BUILD.bazel +++ b/src/workerd/server/BUILD.bazel @@ -64,7 +64,8 @@ wd_cc_embed( is_text = True, ) -# The workerd binary. Its entry point is :workerd-cli; :cli-main is the C++ that drives the Server. +# The workerd binary: the command line (:workerd-cli) over the Rust server +# (//src/workerd/server/server) and the C++ worker factory (//src/workerd/server/factory). wd_rust_binary( name = "workerd", srcs = ["workerd.rs"], @@ -76,69 +77,35 @@ wd_rust_binary( ) # The command line (clap), config sources, --watch, and the compiled-binary format. Runs each -# command through :cli-main. +# command through the server crate's `entry` module: the loop, the C++ bootstrap +# (//src/workerd/server/factory:bootstrap), the Rust server. wd_rust_crate( name = "workerd-cli", srcs = glob(["cli/**/*.rs"]), crate_root = "cli/lib.rs", - cxx_bridge_hdrs = [ - "cli-main.h", - "config-compiler.h", - ], + cxx_bridge_deps = ["//src/workerd/server/factory:bootstrap-hdrs"], + cxx_bridge_hdrs = ["config-compiler.h"], cxx_bridge_src = "cli/bridge.rs", - link_deps = [ - ":cli-main", - ":config-compiler", - ], + link_deps = [":config-compiler"], deps = [ ":workerd_capnp_rust", "//src/rust/cxx/kj-rs-io", + "//src/workerd/server/server:workerd-server", "@crates_vendor//:capnp", "@crates_vendor//:clap", + "@crates_vendor//:socket2", "@crates_vendor//:tokio", ] + select({ - "@platforms//os:windows": ["@crates_vendor//:windows-sys"], - "//conditions:default": [ + "@platforms//os:windows": [ "@crates_vendor//:libc", - "@crates_vendor//:socket2", + "@crates_vendor//:windows-sys", ], + "//conditions:default": ["@crates_vendor//:libc"], }), ) -# The C++ side of :workerd-cli that runs the server: sets up V8 and the Server and waits on the -# event loop. -wd_cc_library( - name = "cli-main", - srcs = ["cli-main.c++"], - linkstatic = CC_LIBRARY_LINKSTATIC, - deps = [ - ":cli/bridge.rs@cxx", - ":json-logger", - ":server", - ":v8-platform-impl", - ":workerd_capnp", - "//src/pyodide:pyodide_extra_capnp", - "//src/rust/cxx/kj-rs", - "//src/rust/cxx/kj-rs-io:kj-rs-io-lib", - "//src/workerd/util:autogate", - "//src/workerd/util:perfetto", - # :cli-main exists only for the workerd binary, so it links the binary's kj::setupAsyncIo(). - "//src/workerd/util:setup-async-io", - "@capnp-cpp//src/kj:kj-async-core", - "@capnp-cpp//src/kj:kj-async-io", - ], - # The C++ root of the workerd binary is linked eagerly, as a cc_binary's own sources would be. - # Bazel places it ahead of everything it depends on, and from an eager object the linker - # resolves the rest of the C++ in that order. Left lazy, the Rust side's references would reach - # it only after every library had been registered, and the linker would then find the C++ by a - # depth-first walk of symbol references. That walk decides which copy of a template the binary - # keeps when two libraries carry different ones: V8 is built without exceptions, and its copy - # of libc++'s std::regex parser aborts where workerd's throws. - alwayslink = True, -) - # Compiles a config written as a Cap'n Proto schema file (capnp::SchemaParser over :schema-file) -# into an encoded message, for :workerd-cli. +# into an encoded message, for the command line (:workerd-cli). wd_cc_library( name = "config-compiler", srcs = ["config-compiler.c++"], @@ -289,6 +256,7 @@ wd_cc_library( visibility = ["//visibility:public"], deps = [ ":actor-id-impl", + ":compiled-bindings_capnp", ":fallback-service", ":pyodide", ":workerd-debug-port-client", @@ -320,6 +288,7 @@ wd_cc_library( name = "channel-token", srcs = ["channel-token.c++"], hdrs = ["channel-token.h"], + visibility = [":__subpackages__"], deps = [ ":channel-token_capnp", "//src/workerd/io", @@ -327,50 +296,13 @@ wd_cc_library( ], ) -# Calls kj::setupAsyncIo() (Server::run's own loop) but does not supply it: the BINARY picks the I/O backend by linking -# //src/workerd/util:setup-async-io (the tokio backend; kj_test and wd_cc_benchmark do this for -# every test binary) or @capnp-cpp//src/kj:kj-async (kj's own event loop). A binary that links -# neither fails to link with an undefined kj::setupAsyncIo -- by design, never a silent choice. -wd_cc_library( - name = "server", - srcs = [ - "server.c++", - ], - hdrs = [ - "server.h", - ], - deps = [ - ":actor-id-impl", - ":alarm-scheduler", - ":channel-token", - ":channel-token_capnp", - ":container-client", - ":facet-tree-index", - ":fallback-service", - ":workerd-api", - ":workerd_capnp", - "//src/cloudflare", - "//src/node", - "//src/pyodide:pyodide_static", - "//src/workerd/api:memory-cache", - "//src/workerd/api:pyodide", - "//src/workerd/io", - "//src/workerd/io:bundle-fs", - "//src/workerd/io:worker-entrypoint", - "//src/workerd/jsg", - "//src/workerd/util:autogate", - "//src/workerd/util:perfetto", - "//src/workerd/util:websocket-error-handler", - "@capnp-cpp//src/capnp:capnp-rpc", - "@capnp-cpp//src/kj:kj-async-core", - "@capnp-cpp//src/kj:kj-async-io", - "@capnp-cpp//src/kj/compat:kj-gzip", - "@capnp-cpp//src/kj/compat:kj-tls", - ], -) - wd_capnp_library(src = "docker-api.capnp") +wd_capnp_library( + src = "compiled-bindings.capnp", + deps = [":workerd_capnp"], +) + wd_capnp_library(src = "log-schema.capnp") wd_capnp_library( @@ -414,7 +346,11 @@ wd_cc_library( ], ) -# Calls kj::setupAsyncIo() but does not supply it; see :server. +# Calls kj::setupAsyncIo() (its own thread's loop) but does not supply it: the BINARY picks the I/O +# backend by linking //src/workerd/util:setup-async-io (the tokio backend; kj_test and +# wd_cc_benchmark do this for every test binary) or @capnp-cpp//src/kj:kj-async (kj's own event +# loop). A binary that links neither fails to link with an undefined kj::setupAsyncIo -- by design, +# never a silent choice. wd_cc_library( name = "fallback-service", srcs = [ @@ -440,6 +376,7 @@ wd_cc_library( hdrs = [ "v8-platform-impl.h", ], + visibility = [":__subpackages__"], deps = [ "//src/workerd/io", "//src/workerd/jsg", @@ -466,7 +403,7 @@ wd_capnp_library( # Limit visibility to avoid accidental usage – there should be no need to use this outside of # test-fixture and the workerd binary. visibility = [ - ":__pkg__", + ":__subpackages__", "//src/workerd/api:__pkg__", "//src/workerd/tests:__pkg__", ], @@ -476,7 +413,9 @@ kj_test( size = "large", src = "server-test.c++", deps = [ - ":server", + "//src/rust/cxx/kj-rs-io:kj-rs-io-lib", + "//src/workerd/server/factory:worker-factory", + "//src/workerd/server/server:workerd-server", "//src/workerd/util:test-util", ], ) @@ -484,7 +423,7 @@ kj_test( kj_test( src = "channel-token-test.c++", deps = [ - ":server", + ":channel-token", ], ) diff --git a/src/workerd/server/channel-token.h b/src/workerd/server/channel-token.h index 79e75ca6437..a916e7f2717 100644 --- a/src/workerd/server/channel-token.h +++ b/src/workerd/server/channel-token.h @@ -13,20 +13,20 @@ namespace workerd::server { // Helper class to encode channel tokens for workerd. // -// This is an internal implementation helper for `Server` (in `server.h`), separated out into its -// own module solely for unit testing purposes. Nobody except `Server` should use this interface -// directly. +// This is an internal implementation helper for `WorkerFactory` (in `factory/worker-factory.h`), +// separated out into its own module solely for unit testing purposes. Nobody except +// `WorkerFactory` should use this interface directly. // // Note that all `Frankenvalue`s here are expected to contain cap tables holding live instances // of `SubrequestChannel`, `ActorClassChannel`, and `RpcChannel`. class ChannelTokenHandler { public: - // Callbacks implemented by `Server` (in `server.h`) to resolve entrypoint designators to live + // Callbacks implemented by `WorkerFactory` to resolve entrypoint designators to live // objects. // // (In theory, we could have a decodeChannelToken() method that returns the service name, // entrypoint name, and props as a struct, but this would require extra string copies and would - // also make abstractions a little messier in server.c++.) + // also make abstractions a little messier in the factory.) class Resolver { public: virtual kj::Rc resolveEntrypoint(kj::StringPtr serviceName, @@ -48,7 +48,7 @@ class ChannelTokenHandler { // workerd's implementation of `IoChannelFactory::SelfTokenFactory`. Produces the encoded // "vendor" token to embed in a restored channel token (i.e. the token referring to the // entrypoint whose `[restore]()` method must be called). Concrete implementations live in - // `server.c++` (one for static workers, one for actors). + // `factory/worker-factory.c++` (one for static workers, one for actors). class ServerSelfTokenFactory: public IoChannelFactory::SelfTokenFactory { public: // Get the token referring to "self", to be embedded as the vendor of a restored token. diff --git a/src/workerd/server/cli-main.c++ b/src/workerd/server/cli-main.c++ deleted file mode 100644 index d9687a2180e..00000000000 --- a/src/workerd/server/cli-main.c++ +++ /dev/null @@ -1,415 +0,0 @@ -// Copyright (c) 2017-2026 Cloudflare, Inc. -// Licensed under the Apache 2.0 license found in the LICENSE file or at: -// https://opensource.org/licenses/Apache-2.0 - -#include "cli-main.h" - -#include "server.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include -#include - -#include -#include - -#if _WIN32 -#include - -#include -#include -#endif - -#include - -// since kj installs their global signal handlers -// and exits with 1 Fuzzilli doesn't realize that an application crashed due to the signo. -// Therefore, we install a handler before and just raise the signo -#ifdef WORKERD_FUZZILLI - -void signalHandler(int signo, siginfo_t* info, void* context) noexcept { - // inform reprl - remove debug output for clean testing - struct sigaction sa = {}; - sa.sa_handler = SIG_DFL; - sigemptyset(&sa.sa_mask); - sa.sa_flags = 0; - sigaction(signo, &sa, nullptr); - raise(signo); -} - -void initSignalHandlers() { - struct sigaction action {}; - action.sa_flags = SA_SIGINFO; - action.sa_sigaction = &signalHandler; - - for (auto signo: {SIGBUS, SIGFPE, SIGABRT, SIGILL, SIGTRAP, SIGSEGV}) { - KJ_SYSCALL(sigaction(signo, &action, nullptr)); - } -} -#endif - -using namespace kj_rs; - -namespace workerd::server::cli { -namespace { - -// ======================================================================================= - -// For ASan's leak sanitizer, suppress warnings about leaks with stacks that include "unknown -// modules". This suppression is adopted from the GN build and applies to addresses that LSan can't -// symbolize or even map to a binary – perhaps JIT or snapshot-generated code in V8's case? -// TODO(someday): Suppression is needed to get several python tests to pass under LSan. Investigate -// if this is an actual leak (perhaps a bug in V8 itself since it is suppressed there?) at a later -// time. -#if __has_feature(address_sanitizer) -extern "C" __attribute__((no_sanitize("address"))) __attribute__((visibility("default"))) -__attribute__((used)) const char* -__lsan_default_suppressions() { - return "leak:\n"; -} -#endif - -// ======================================================================================= - -class EntropySourceImpl: public kj::EntropySource { - public: - void generate(kj::ArrayPtr buffer) override { - getEntropy(buffer); - } -}; - -// ======================================================================================= - -constexpr capnp::ReaderOptions CONFIG_READER_OPTIONS = { - .traversalLimitInWords = kj::maxValue - // Configs can legitimately be very large and are not malicious, so use an effectively-infinite - // traversal limit. -}; - -// ======================================================================================= - -class CliMain { - public: - CliMain(StructuredLoggingProcessContext& context, ::rust::Box processParam) - : context(context), - process(kj::mv(processParam)), - server(kj::heap(*fs, - io.provider->getTimer(), - kj::systemPreciseMonotonicClock(), - io.provider->getNetwork(), - entropySource, - Worker::LoggingOptions(Worker::ConsoleMode::STDOUT), - [&](kj::String error) { - if (!process->is_watching()) { - // TODO(someday): Don't just fail on the first error, keep going in order to report - // additional errors. The tricky part is we don't currently have any signal of when - // the server has completely finished loading, and also we probably don't want to - // accept any connections on any of the sockets if the server is partially broken. - context.exitError(error); - } else { - // In --watch mode, we don't want to exit from errors, we want to wait until things - // change. It's OK if we try to serve requests despite brokenness since this is a - // development server. - hadErrors = true; - context.error(error); - } - }, - [&](kj::String warning) { context.warning(warning); })) {} - - // `message` is an encoded config (segment table, then segments); this object owns it for the - // run. - void loadConfig(::rust::Vec message) { - configMessage = kj::mv(message); - auto words = kj::arrayPtr( - reinterpret_cast(configMessage.data()), configMessage.size()); - configReader = kj::heap(words, CONFIG_READER_OPTIONS); - config = configReader->getRoot(); - util::Autogate::initAutogate(config.getAutogates()); - } - - void applyServeOrTestOptions(const ServeOrTestOptions& options) { - for (auto& o: options.directory_overrides) { - server->overrideDirectory(kj::str(o.name), kj::str(o.value)); - } - for (auto& o: options.external_overrides) { - server->overrideExternal(kj::str(o.name), kj::str(o.value)); - } - KJ_IF_SOME(addr, options.inspector_addr) { - server->enableInspector(kj::str(addr)); - } - KJ_IF_SOME(path, options.perfetto_trace_path) { -#ifdef WORKERD_USE_PERFETTO - perfettoTraceDestination = kj::str(path); - perfettoTraceCategories = options.perfetto_trace_categories.map( - [](const ::rust::String& categories) { return kj::str(categories); }); -#else - KJ_UNIMPLEMENTED("perfetto tracing is not supported by this build", kj::str(path)); -#endif - } - if (options.experimental) { - server->allowExperimental(); - } - KJ_IF_SOME(path, options.pyodide_package_disk_cache_dir) { - // The command line checked that the directory exists. - auto dir = KJ_REQUIRE_NONNULL( - openWritableDirectory(kj::str(path)), "package disk cache dir must exist"); - server->setPackageDiskCacheRoot(kj::mv(dir)); - } - KJ_IF_SOME(path, options.pyodide_bundle_disk_cache_dir) { - server->setPyodideDiskCacheRoot(openWritableDirectory(kj::str(path))); - } - if (options.python_save_snapshot) { - server->setPythonCreateSnapshot(); - } - if (options.python_save_baseline_snapshot) { - server->setPythonCreateBaselineSnapshot(); - } - KJ_IF_SOME(path, options.python_load_snapshot) { - server->setPythonLoadSnapshot(kj::str(path)); - } - KJ_IF_SOME(path, options.python_snapshot_dir) { - server->setPythonSnapshotDirectory(openWritableDirectory(kj::str(path))); - } - } - - void applyServeOptions(const ServeOptions& options) { - for (auto& o: options.socket_addr_overrides) { - server->overrideSocket(kj::str(o.name), kj::str(o.value)); - } - for (auto& o: options.socket_fd_overrides) { - server->overrideSocket(kj::str(o.name), - io.lowLevelProvider->wrapListenSocketFd( - static_cast(o.fd), - kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP)); - } - KJ_IF_SOME(fd, options.control_fd) { - server->enableControl(fd); - } - KJ_IF_SOME(addr, options.debug_port) { - server->enableDebugPort(kj::str(addr)); - } - } - - void serve() { - serveImpl([&](jsg::V8System& v8System) { -#if _WIN32 - return server->run(v8System, config); -#else - // Gracefully drain when SIGTERM is received. - return server->run(v8System, config, kj_rs_io::onSignal(SIGTERM)); -#endif - }); - } - - void test(const TestOptions& options) { - if (!options.no_verbose) { - // Always turn on info logging when running tests so that uncaught exceptions are displayed. - // TODO(beta): This can be removed once we improve our error logging story. - kj::_::Debug::setLogLevel(kj::LogSeverity::INFO); - } - if (options.predictable) { - setPredictableModeForTest(); - } - if (options.gc_stress) { - setGcStressModeForTest(); - } - if (options.all_autogates) { - util::Autogate::initAllAutogates(); - } - - KJ_IF_SOME(compatDate, options.compat_date) { - server->setTestCompatibilityDateOverride(kj::str(compatDate)); - } - - // Enable loopback sockets in tests only. - kj::downcast(io.provider->getNetwork()).enableLoopback(); - - auto servicePattern = kj::str("*"); - KJ_IF_SOME(pattern, options.service_pattern) { - servicePattern = kj::str(pattern); - } - auto entrypointPattern = kj::str("*"); - KJ_IF_SOME(pattern, options.entrypoint_pattern) { - entrypointPattern = kj::str(pattern); - } - - serveImpl([&](jsg::V8System& v8System) { - return server->test(v8System, config, servicePattern, entrypointPattern) - .then([this](bool result) -> kj::Promise { - if (!result) { - context.error("Tests failed!"); - } - - if (!process->is_watching()) { - return kj::READY_NOW; - } else { - // Pause forever waiting for watcher. - return kj::NEVER_DONE; - } - }); - }); - } - - private: - StructuredLoggingProcessContext& context; - ::rust::Box process; - - kj::Own fs = kj::newDiskFilesystem(); - kj::AsyncIoContext io = kj::setupAsyncIo(); - EntropySourceImpl entropySource; - - // The reader borrows the message, so the message is declared first. - ::rust::Vec configMessage; - kj::Own configReader; - config::Config::Reader config; - -#ifdef WORKERD_USE_PERFETTO - kj::Maybe perfettoTraceDestination; - kj::Maybe perfettoTraceCategories; -#endif - - kj::Own server; - - // Set by the Server's error callback under --watch, where errors don't exit. - bool hadErrors = false; - - kj::Maybe> openWritableDirectory(kj::StringPtr pathStr) { - return fs->getRoot().tryOpenSubdir(fs->getCurrentPath().eval(pathStr), kj::WriteMode::MODIFY); - } - - template - void serveImpl(Func&& func) noexcept { -#ifdef WORKERD_USE_PERFETTO - kj::Maybe maybePerfettoSession; - KJ_IF_SOME(dest, perfettoTraceDestination) { - maybePerfettoSession = - PerfettoSession(dest, kj::mv(perfettoTraceCategories).orDefault(kj::String())); - } -#endif - TRACE_EVENT("workerd", "serveImpl()"); - - // Configure structured logging in the process context - if (config.hasLogging() ? config.getLogging().getStructuredLogging() - : config.getStructuredLogging()) { - context.enableStructuredLogging(); - } - - auto platform = jsg::defaultPlatform(0); - WorkerdPlatform v8Platform(*platform); - jsg::V8System v8System(v8Platform, - KJ_MAP(flag, config.getV8Flags()) -> kj::StringPtr { return flag; }, platform.get()); - auto promise = func(v8System); - if (process->is_watching()) { - promise = - promise.exclusiveJoin(wait_for_changes(*process).then([this]() { process->reload(); })); - } - promise.wait(io.waitScope); -#ifdef WORKERD_USE_PERFETTO - KJ_IF_SOME(perfettoSession, maybePerfettoSession) { - auto dropMe = kj::mv(perfettoSession); - maybePerfettoSession = kj::none; - } -#endif - - if (getenv("KJ_CLEAN_SHUTDOWN") == nullptr) { - context.exit(); - } - - // Server maintains a reference to the v8 platform. Clean up before destroying the platform. - server = nullptr; - } -}; - -// Runs `body` as the process's main function, with KJ's top-level exception handling and exit-code -// semantics. The command line was already parsed; runMainAndExit() only gets the program name. -template -int32_t run(const CommonOptions& common, ::rust::Box process, Func&& body) { - auto programName = kj::str(common.program_name); - StructuredLoggingProcessContext context(programName); - CliMain main(context, kj::mv(process)); - -#if defined(WORKERD_FUZZILLI) && defined(__linux__) - initSignalHandlers(); -#endif - - char* argv[] = {programName.begin(), nullptr}; - return kj::runMainAndExit(context, [&](kj::StringPtr, kj::ArrayPtr) { - if (common.verbose) { - context.increaseLoggingVerbosity(); - } - body(main); - }, 1, argv); -} - -} // namespace - -::rust::Slice release_version() { - return RELEASE_VERSION.asBytes().as(); -} - -bool perfetto_supported() { -#ifdef WORKERD_USE_PERFETTO - return true; -#else - return false; -#endif -} - -bool fuzzilli_supported() { -#if defined(WORKERD_FUZZILLI) && defined(__linux__) - return true; -#else - return false; -#endif -} - -::rust::String pyodide_lock() { - capnp::MallocMessageBuilder message; - // TODO(EW-8977): Implement option to specify python worker flags. - auto features = message.getRoot(); - features.setPythonWorkers(true); - auto pythonRelease = KJ_REQUIRE_NONNULL(getPythonSnapshotRelease(features)); - auto lock = KJ_REQUIRE_NONNULL(api::pyodide::getPyodideLock(pythonRelease)); - return lock.as(); -} - -int32_t run_serve(const CommonOptions& common, - ::rust::Vec config, - const ServeOrTestOptions& serveOrTest, - const ServeOptions& serve, - ::rust::Box process) { - return run(common, kj::mv(process), [&](CliMain& main) { - main.applyServeOrTestOptions(serveOrTest); - main.applyServeOptions(serve); - main.loadConfig(kj::mv(config)); - main.serve(); - }); -} - -int32_t run_test(const CommonOptions& common, - ::rust::Vec config, - const ServeOrTestOptions& serveOrTest, - const TestOptions& test, - ::rust::Box process) { - return run(common, kj::mv(process), [&](CliMain& main) { - main.applyServeOrTestOptions(serveOrTest); - main.loadConfig(kj::mv(config)); - main.test(test); - }); -} - -} // namespace workerd::server::cli diff --git a/src/workerd/server/cli-main.h b/src/workerd/server/cli-main.h deleted file mode 100644 index 9d696db6a55..00000000000 --- a/src/workerd/server/cli-main.h +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2017-2026 Cloudflare, Inc. -// Licensed under the Apache 2.0 license found in the LICENSE file or at: -// https://opensource.org/licenses/Apache-2.0 - -#pragma once - -// The C++ half of workerd's command line: the functions the Rust entry point (cli/lib.rs) calls -// through the cxx bridge in cli/bridge.rs. Rust parses the command line and produces the encoded config -// (config-compiler.h compiles schema files); these set up V8 and the Server and run it. -// -// Do not include workerd/server/cli/bridge.rs.h here: it includes this header. - -#include - -namespace workerd::server::cli { - -struct CommonOptions; -struct ServeOrTestOptions; -struct ServeOptions; -struct TestOptions; -struct Process; - -::rust::Slice release_version(); -bool perfetto_supported(); -bool fuzzilli_supported(); -::rust::String pyodide_lock(); - -int32_t run_serve(const CommonOptions& common, - ::rust::Vec config, - const ServeOrTestOptions& serveOrTest, - const ServeOptions& serve, - ::rust::Box process); -int32_t run_test(const CommonOptions& common, - ::rust::Vec config, - const ServeOrTestOptions& serveOrTest, - const TestOptions& test, - ::rust::Box process); - -} // namespace workerd::server::cli diff --git a/src/workerd/server/cli/args.rs b/src/workerd/server/cli/args.rs index c3a377a6bcd..47e99db4064 100644 --- a/src/workerd/server/cli/args.rs +++ b/src/workerd/server/cli/args.rs @@ -8,6 +8,7 @@ use clap::Args; use clap::Parser; use clap::Subcommand; +use workerd_server::bridge::ffi as server; use crate::bridge::ffi; @@ -380,39 +381,31 @@ fn parse_control_fd(param: &str) -> Result { .map_err(|_| "Output value must be a file descriptor (non-negative integer).") } -impl From for ffi::Override { - fn from(Override { name, value }: Override) -> Self { - Self { name, value } - } +/// `=` options as a map, last value per name winning. +pub fn override_map(overrides: Vec) -> std::collections::HashMap { + overrides + .into_iter() + .map(|Override { name, value }| (name, value)) + .collect() } -impl From for ffi::ServeOrTestOptions { - fn from(args: ServeOrTestArgs) -> Self { +impl From<&ServeOrTestArgs> for server::ServeOrTestOptions { + fn from(args: &ServeOrTestArgs) -> Self { let (perfetto_trace_path, perfetto_trace_categories) = args .perfetto_trace + .clone() .map(|Override { name, value }| (name, value)) .unzip(); Self { - directory_overrides: args - .directory_overrides - .into_iter() - .map(Into::into) - .collect(), - external_overrides: args - .external_overrides - .into_iter() - .map(Into::into) - .collect(), - inspector_addr: args.inspector_addr.into(), perfetto_trace_path: perfetto_trace_path.into(), perfetto_trace_categories: perfetto_trace_categories.into(), experimental: args.experimental, - pyodide_package_disk_cache_dir: args.pyodide_package_disk_cache_dir.into(), - pyodide_bundle_disk_cache_dir: args.pyodide_bundle_disk_cache_dir.into(), + pyodide_package_disk_cache_dir: args.pyodide_package_disk_cache_dir.clone().into(), + pyodide_bundle_disk_cache_dir: args.pyodide_bundle_disk_cache_dir.clone().into(), python_save_snapshot: args.python_save_snapshot, python_save_baseline_snapshot: args.python_save_baseline_snapshot, - python_load_snapshot: args.python_load_snapshot.into(), - python_snapshot_dir: args.python_snapshot_dir.into(), + python_load_snapshot: args.python_load_snapshot.clone().into(), + python_snapshot_dir: args.python_snapshot_dir.clone().into(), } } } @@ -432,17 +425,14 @@ pub fn socket_overridden_twice(serve: &ServeArgs) -> Option<&str> { }) } -impl ffi::TestOptions { - /// The options for a test run, with the service and entrypoint patterns from `filter`. - pub fn new(args: TestArgs, filter: TestFilter) -> Self { +impl From<&TestArgs> for server::TestOptions { + fn from(args: &TestArgs) -> Self { Self { no_verbose: args.no_verbose, predictable: args.predictable, gc_stress: args.gc_stress, all_autogates: args.all_autogates, - compat_date: args.compat_date.into(), - service_pattern: filter.service_pattern.into(), - entrypoint_pattern: filter.entrypoint_pattern.into(), + compat_date: args.compat_date.clone().into(), } } } diff --git a/src/workerd/server/cli/bridge.rs b/src/workerd/server/cli/bridge.rs index 3f5d632ce89..e1cae70b3c4 100644 --- a/src/workerd/server/cli/bridge.rs +++ b/src/workerd/server/cli/bridge.rs @@ -1,8 +1,8 @@ //! The FFI between the Rust entry point and the C++ side: the config compiler (config-compiler.h) -//! and the driver (cli-main.h). +//! and what the driver (bootstrap.h) knows about the build. The `serve` and `test` commands go +//! through the server crate's bridge instead (`workerd_server::entry`). //! -//! Each command is one call into C++ carrying its parsed options as plain structs. C++ calls back -//! into [`Process`] for `--watch`. +//! C++ calls back into [`Process`] to register the files a config depends on for `--watch`. #![allow( unsafe_code, @@ -10,29 +10,9 @@ )] pub use crate::process::Process; -pub use crate::process::wait_for_changes; #[cxx::bridge(namespace = "workerd::server::cli")] pub mod ffi { - /// A `=` option value. - struct Override { - name: String, - value: String, - } - - /// A `--socket-fd =` value: `fd` is a duplicate of the inherited listening socket - /// for the server to own (kj's `wrapListenSocketFd` with `TAKE_OWNERSHIP`). - struct SocketFd { - name: String, - fd: i64, - } - - struct CommonOptions { - /// Prefixes usage errors, e.g. `workerd serve`. - program_name: String, - verbose: bool, - } - /// A parse error in a schema file. `line` and `column` are 1-based; `end_column` is 0 when /// the error has no extent on the line. struct ConfigParseError { @@ -50,40 +30,8 @@ pub mod ffi { errors: Vec, } - struct ServeOrTestOptions { - directory_overrides: Vec, - external_overrides: Vec, - inspector_addr: KjMaybe, - perfetto_trace_path: KjMaybe, - perfetto_trace_categories: KjMaybe, - experimental: bool, - pyodide_package_disk_cache_dir: KjMaybe, - pyodide_bundle_disk_cache_dir: KjMaybe, - python_save_snapshot: bool, - python_save_baseline_snapshot: bool, - python_load_snapshot: KjMaybe, - python_snapshot_dir: KjMaybe, - } - - struct ServeOptions { - socket_addr_overrides: Vec, - socket_fd_overrides: Vec, - control_fd: KjMaybe, - debug_port: KjMaybe, - } - - struct TestOptions { - no_verbose: bool, - predictable: bool, - gc_stress: bool, - all_autogates: bool, - compat_date: KjMaybe, - service_pattern: KjMaybe, - entrypoint_pattern: KjMaybe, - } - unsafe extern "C++" { - include!("workerd/server/cli-main.h"); + include!("workerd/server/factory/bootstrap.h"); include!("workerd/server/config-compiler.h"); /// Compiles a config written as a Cap'n Proto schema file. Every file it is about to read @@ -104,25 +52,6 @@ pub mod ffi { fn fuzzilli_supported() -> bool; /// The package lock file of the current Pyodide release. fn pyodide_lock() -> Result; - - // Each command runs to completion and exits the process, except under KJ_CLEAN_SHUTDOWN, - // where it returns the exit code. `config` is an encoded message (segment table, then - // segments) in 8-byte words, owned by the driver for the run. An error means the C++ - // driver failed to start. - fn run_serve( - common: &CommonOptions, - config: Vec, - serve_or_test: &ServeOrTestOptions, - serve: &ServeOptions, - process: Box, - ) -> Result; - fn run_test( - common: &CommonOptions, - config: Vec, - serve_or_test: &ServeOrTestOptions, - test: &TestOptions, - process: Box, - ) -> Result; } extern "Rust" { @@ -135,13 +64,5 @@ pub mod ffi { /// without `--watch`. fn watch_file(self: &Process, path: &[u8]) -> Result<()>; - /// Resolves once a watched file has changed and changes have settled. Only called with - /// `--watch`. - async fn wait_for_changes(process: &Process) -> Result<()>; - - /// `--watch`'s reload: replaces the process with a fresh run of the executable and the - /// original arguments, retrying while the executable is missing (mid-rebuild). Does not - /// return; the C++ signature cannot say so. - fn reload(self: &Process); } } diff --git a/src/workerd/server/cli/lib.rs b/src/workerd/server/cli/lib.rs index 9f2e93e5143..2bc5e08294a 100644 --- a/src/workerd/server/cli/lib.rs +++ b/src/workerd/server/cli/lib.rs @@ -2,8 +2,9 @@ //! //! `main()` finds the executable (a config compiled in by `workerd compile` selects a different //! command line), parses the command line, produces the encoded config (C++ compiles schema -//! files), and hands the command to the C++ driver (cli-main.c++), which loads the config into a -//! `Server` and runs it. C++ calls back into [`process::Process`] for `--watch`. +//! files), and hands the command to the server crate (`workerd_server::entry`), whose C++ +//! driver (bootstrap.c++) sets up the process and runs the Rust server. [`process::Process`] +//! is the `--watch` half: the files to watch, and the reload. // Two modules allow `unsafe`: bridge.rs, whose `cxx::bridge` macro expands to FFI declarations, // and socket_fd.rs, where a descriptor number from the command line becomes an owned descriptor. @@ -24,6 +25,10 @@ use std::sync::OnceLock; use clap::CommandFactory; use clap::FromArgMatches; use clap::error::ErrorKind; +use workerd_server::bridge::ffi as server; +use workerd_server::config::Overrides; +use workerd_server::entry; +use workerd_server::run::RunOptions; use crate::bridge::ffi; use crate::config::Config; @@ -61,14 +66,14 @@ pub fn main() -> ExitCode { }; let result = match embedded_config { - Some(embedded_config) => run_compiled(&program_name, executable, &embedded_config), - None => run_command(&program_name, parse(args::Main::command()), executable), + Some(embedded_config) => run_compiled(executable, &embedded_config), + None => run_command(parse(args::Main::command()), executable), }; match result { Ok(code) => u8::try_from(code).map_or(ExitCode::FAILURE, ExitCode::from), Err(error) => { - eprintln!("*** Uncaught exception ***\n{error}"); + eprintln!("*** Uncaught exception ***\n{}", error.description()); ExitCode::FAILURE } } @@ -76,10 +81,9 @@ pub fn main() -> ExitCode { /// Serves the config compiled into this executable. fn run_compiled( - program_name: &str, executable: Option, embedded_config: &[u64], -) -> Result { +) -> workerd_server::Result { type T = args::CompiledMain; let args: T = parse(T::command()); @@ -88,27 +92,21 @@ fn run_compiled( usage_error::(format!("the config compiled into this executable: {error}")) }); let watcher = watcher::(&args.serve_or_test, executable.as_ref()); - let (serve, inherited_sockets) = serve_options::(args.serve); - ffi::run_serve( - &common_options(program_name.to_owned(), &args.global), + let (options, inherited_sockets) = serve_options::(args.serve, &args.serve_or_test); + let process = Process::new(executable, watcher, inherited_sockets); + entry::serve( + args.global.verbose, config.into_words(), - &args.serve_or_test.into(), - &serve, - Box::new(Process::new(executable, watcher, inherited_sockets)), + (&args.serve_or_test).into(), + options, + watching(process), ) } -fn run_command( - program_name: &str, - args: args::Main, - executable: Option, -) -> Result { +fn run_command(args: args::Main, executable: Option) -> workerd_server::Result { type T = args::Main; - let common = common_options( - format!("{program_name} {}", args.command_name()), - &args.global, - ); + let verbose = args.global.verbose; match args.command { args::Command::Serve { @@ -119,15 +117,15 @@ fn run_command( } => { check_serve_or_test::(&serve_or_test); let watcher = watcher::(&serve_or_test, executable.as_ref()); - let (serve, inherited_sockets) = serve_options::(serve); - let process = Box::new(Process::new(executable, watcher, inherited_sockets)); + let (options, inherited_sockets) = serve_options::(serve, &serve_or_test); + let process = Process::new(executable, watcher, inherited_sockets); let config = load_config::(config, const_name.const_name, &process); - ffi::run_serve( - &common, + entry::serve( + verbose, config.into_words(), - &serve_or_test.into(), - &serve, - process, + (&serve_or_test).into(), + options, + watching(process), ) } args::Command::Compile { @@ -150,14 +148,16 @@ fn run_command( } check_serve_or_test::(&serve_or_test); let watcher = watcher::(&serve_or_test, executable.as_ref()); - let process = Box::new(Process::new(executable, watcher, Vec::new())); + let process = Process::new(executable, watcher, Vec::new()); let config = load_config::(config, None, &process); - ffi::run_test( - &common, + entry::test( + verbose, config.into_words(), - &serve_or_test.into(), - &ffi::TestOptions::new(args::TestArgs::default(), args::TestFilter::default()), - process, + (&serve_or_test).into(), + (&args::TestArgs::default()).into(), + run_options(&serve_or_test), + patterns(args::TestFilter::default()), + watching(process), ) } args::Command::Test { @@ -168,14 +168,16 @@ fn run_command( check_serve_or_test::(&serve_or_test); let filter = test.filter.take().unwrap_or_default(); let watcher = watcher::(&serve_or_test, executable.as_ref()); - let process = Box::new(Process::new(executable, watcher, Vec::new())); + let process = Process::new(executable, watcher, Vec::new()); let config = load_config::(config, filter.const_name.clone(), &process); - ffi::run_test( - &common, + entry::test( + verbose, config.into_words(), - &serve_or_test.into(), - &ffi::TestOptions::new(test, filter), - process, + (&serve_or_test).into(), + (&test).into(), + run_options(&serve_or_test), + patterns(filter), + watching(process), ) } args::Command::PyodideLock => { @@ -185,7 +187,7 @@ fn run_command( args::Command::MakePyodideBaselineSnapshot { python_version, output_directory, - } => run_baseline_snapshot(&common, &python_version, output_directory, executable), + } => run_baseline_snapshot(verbose, &python_version, output_directory, executable), } } @@ -223,23 +225,27 @@ fn run_compile( /// Runs a synthesized Python worker's tests with a baseline snapshot saved to `output_directory`. fn run_baseline_snapshot( - common: &ffi::CommonOptions, + verbose: bool, python_version: &str, output_directory: String, executable: Option, -) -> Result { - ffi::run_test( - common, +) -> workerd_server::Result { + let serve_or_test = args::ServeOrTestArgs::default(); + let process = Process::new(executable, None, Vec::new()); + entry::test( + verbose, Config::python_baseline(python_version).into_words(), - &ffi::ServeOrTestOptions { + server::ServeOrTestOptions { experimental: true, python_save_baseline_snapshot: true, pyodide_bundle_disk_cache_dir: Some(".".to_owned()).into(), pyodide_package_disk_cache_dir: Some(output_directory).into(), - ..args::ServeOrTestArgs::default().into() + ..(&serve_or_test).into() }, - &ffi::TestOptions::new(args::TestArgs::default(), args::TestFilter::default()), - Box::new(Process::new(executable, None, Vec::new())), + (&args::TestArgs::default()).into(), + run_options(&serve_or_test), + patterns(args::TestFilter::default()), + watching(process), ) } @@ -280,13 +286,6 @@ fn release_version() -> &'static str { VERSION.get_or_init(|| String::from_utf8_lossy(ffi::release_version()).into_owned()) } -fn common_options(program_name: String, global: &args::GlobalArgs) -> ffi::CommonOptions { - ffi::CommonOptions { - program_name, - verbose: global.verbose, - } -} - /// The config to run: a schema file compiled now (the compiler registers every file it reads with /// the process's watcher before reading it), or an encoded message read from a file or stdin. /// Exits with a usage error on a problem with the file itself. Parse errors in a schema file are @@ -402,19 +401,20 @@ fn check_serve_or_test(serve_or_test: &args::ServeOrTestArgs) } } -/// The `serve` options for the bridge, taking ownership of every `--socket-fd` socket (the server -/// gets a duplicate). A socket given both an address and a descriptor, or a descriptor that is -/// not a listening socket, is a usage error. +/// The `serve` options for the server, taking ownership of every `--socket-fd` socket (the +/// server gets a duplicate). A socket given both an address and a descriptor, or a descriptor that +/// is not a listening socket, is a usage error. fn serve_options( serve: args::ServeArgs, -) -> (ffi::ServeOptions, Vec) { + serve_or_test: &args::ServeOrTestArgs, +) -> (RunOptions, Vec) { if let Some(name) = args::socket_overridden_twice(&serve) { usage_error::(format!( "socket '{name}' is given both --socket-addr and --socket-fd; use one" )); } let mut inherited = Vec::with_capacity(serve.socket_fd_overrides.len()); - let mut socket_fd_overrides = Vec::with_capacity(serve.socket_fd_overrides.len()); + let mut socket_fds = std::collections::HashMap::new(); for args::SocketFd { name, fd } in serve.socket_fd_overrides { let socket = InheritedSocket::take(fd).unwrap_or_else(|error| { let message = match error { @@ -425,35 +425,54 @@ fn serve_options( }; usage_error::(format!("--socket-fd={name}={fd}: {message}")) }); - let fd = socket + let duplicate = socket .duplicate_for_server() .unwrap_or_else(|error| usage_error::(format!("--socket-fd={name}={fd}: {error}"))); - socket_fd_overrides.push(ffi::SocketFd { name, fd }); + socket_fds.insert(name, duplicate); inherited.push(socket); } - let options = ffi::ServeOptions { - socket_addr_overrides: serve - .socket_addr_overrides - .into_iter() - .map(Into::into) - .collect(), - socket_fd_overrides, - control_fd: serve.control_fd.into(), - debug_port: serve.debug_port.into(), + let control = serve.control_fd.map(|fd| { + socket_fd::control_file(fd) + .unwrap_or_else(|error| usage_error::(format!("--control-fd={fd}: {error}"))) + }); + let options = RunOptions { + socket_addresses: args::override_map(serve.socket_addr_overrides), + socket_fds, + control, + debug_port: serve.debug_port, + ..run_options(serve_or_test) }; (options, inherited) } -impl args::Main { - /// The subcommand's name as typed, e.g. "serve". - const fn command_name(&self) -> &'static str { - match self.command { - args::Command::Serve { .. } => "serve", - args::Command::Compile { .. } => "compile", - args::Command::Fuzzilli { .. } => "fuzzilli", - args::Command::Test { .. } => "test", - args::Command::PyodideLock => "pyodide-lock", - args::Command::MakePyodideBaselineSnapshot { .. } => "make-pyodide-baseline-snapshot", - } +/// The options `serve` and `test` share: `--directory-path`, `--external-addr` and +/// `--inspector-addr`. +fn run_options(serve_or_test: &args::ServeOrTestArgs) -> RunOptions { + RunOptions { + overrides: Overrides { + directories: args::override_map(serve_or_test.directory_overrides.clone()), + externals: args::override_map(serve_or_test.external_overrides.clone()), + }, + inspector: serve_or_test.inspector_addr.clone(), + ..RunOptions::default() } } + +/// The `` and `` globs; `*` when not given. +fn patterns(filter: args::TestFilter) -> (String, String) { + let any = || "*".to_owned(); + ( + filter.service_pattern.unwrap_or_else(any), + filter.entrypoint_pattern.unwrap_or_else(any), + ) +} + +/// `process` as the server's `--watch` half, when watching: a config change reloads it. +fn watching(process: Process) -> Option { + process.is_watching().then(|| -> entry::Watch { + Box::pin(async move { + process::wait_for_changes(&process).await?; + process.reload() + }) + }) +} diff --git a/src/workerd/server/cli/socket_fd.rs b/src/workerd/server/cli/socket_fd.rs index d9737a69ce6..24e35d38928 100644 --- a/src/workerd/server/cli/socket_fd.rs +++ b/src/workerd/server/cli/socket_fd.rs @@ -1,9 +1,10 @@ //! `--socket-fd`: listen sockets inherited from the parent process, named by descriptor on the -//! command line. +//! command line; and `--control-fd`, the descriptor the server writes its events to. //! -//! This process keeps the inherited descriptor itself, exactly as it arrived (no close-on-exec, so -//! a `--watch` re-exec passes it on under the same number), and gives the server a duplicate to -//! own. This is the one place a number from the command line becomes an owned descriptor. +//! This process keeps the inherited listen descriptor itself, exactly as it arrived (no +//! close-on-exec, so a `--watch` re-exec passes it on under the same number), and gives the server +//! a duplicate to own. This is the one place a number from the command line becomes an owned +//! descriptor. #![allow( unsafe_code, @@ -41,3 +42,4 @@ impl fmt::Display for Error { mod imp; pub use imp::InheritedSocket; +pub use imp::control_file; diff --git a/src/workerd/server/cli/socket_fd/unix-test.rs b/src/workerd/server/cli/socket_fd/unix-test.rs index 82624857db7..d7467b8eea1 100644 --- a/src/workerd/server/cli/socket_fd/unix-test.rs +++ b/src/workerd/server/cli/socket_fd/unix-test.rs @@ -10,7 +10,10 @@ fn listening_socket_is_taken_and_duplicated() { let fd = listener.into_raw_fd().cast_unsigned(); let socket = InheritedSocket::take(fd).unwrap(); let duplicate = socket.duplicate_for_server().unwrap(); - assert_ne!(i64::from(fd), duplicate); + assert_ne!( + i64::from(fd), + i64::from(std::os::fd::AsRawFd::as_raw_fd(&duplicate)) + ); // Both name the same listening socket. let bound = std::net::TcpStream::connect(("127.0.0.1", port)).unwrap(); drop(bound); diff --git a/src/workerd/server/cli/socket_fd/unix.rs b/src/workerd/server/cli/socket_fd/unix.rs index bfd2382d362..75a257337b2 100644 --- a/src/workerd/server/cli/socket_fd/unix.rs +++ b/src/workerd/server/cli/socket_fd/unix.rs @@ -46,14 +46,23 @@ impl InheritedSocket { Ok(()) } - /// A duplicate for the server to own (kj's `wrapListenSocketFd` with `TAKE_OWNERSHIP`), as - /// the raw descriptor that interface takes. - pub fn duplicate_for_server(&self) -> io::Result { - use std::os::fd::IntoRawFd; - Ok(i64::from(self.0.try_clone()?.into_raw_fd())) + /// A duplicate for the server to own. + pub fn duplicate_for_server(&self) -> io::Result { + Ok(socket2::Socket::from(self.0.try_clone()?)) } } +/// `--control-fd`: a duplicate of the descriptor, as a file the server writes its events to. The +/// inherited descriptor stays open, as `--socket-fd`'s does. +pub fn control_file(fd: u32) -> io::Result { + use std::os::fd::BorrowedFd; + + let raw = i32::try_from(fd).map_err(|_| io::Error::from_raw_os_error(libc::EBADF))?; + // SAFETY: borrowed for the duplication only, which fails with EBADF if `raw` is not open. + let inherited = unsafe { BorrowedFd::borrow_raw(raw) }; + inherited.try_clone_to_owned().map(std::fs::File::from) +} + /// Whether the socket is listening, or `None` where the OS cannot say (macOS has no /// `SO_ACCEPTCONN`; the server finds out at the first `accept()`). #[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))] diff --git a/src/workerd/server/cli/socket_fd/windows.rs b/src/workerd/server/cli/socket_fd/windows.rs index b7de436d9ce..fabd215867f 100644 --- a/src/workerd/server/cli/socket_fd/windows.rs +++ b/src/workerd/server/cli/socket_fd/windows.rs @@ -16,9 +16,38 @@ impl InheritedSocket { Ok(Self(fd)) } - pub fn duplicate_for_server(&self) -> io::Result { - Ok(i64::from(self.0)) + pub fn duplicate_for_server(&self) -> io::Result { + use std::os::windows::io::FromRawSocket; + use std::os::windows::io::IntoRawSocket; + use std::os::windows::io::OwnedSocket; + + // SAFETY: the handle was checked to be a socket; it is released again below, so this + // process keeps it as inherited. + let owned = unsafe { OwnedSocket::from_raw_socket(self.0.into()) }; + let duplicate = owned.try_clone(); + let _ = owned.into_raw_socket(); + Ok(socket2::Socket::from(duplicate?)) + } +} + +/// `--control-fd`: a duplicate of the C runtime descriptor's handle (a parent passes extra +/// descriptors through the C runtime's handle table, as Node.js does), as a file the server writes +/// its events to. The inherited descriptor stays open, as on Unix. +pub fn control_file(fd: u32) -> io::Result { + use std::os::windows::io::BorrowedHandle; + use std::os::windows::io::RawHandle; + + let not_open = || io::Error::new(io::ErrorKind::InvalidInput, "File descriptor is not open."); + let raw = i32::try_from(fd).map_err(|_| not_open())?; + // SAFETY: the C runtime checks the descriptor itself, returning -1 for one that is not open, + // or -2 for a standard stream with no handle. + let handle = unsafe { libc::get_osfhandle(raw) }; + if handle == -1 || handle == -2 { + return Err(not_open()); } + // SAFETY: borrowed for the duplication only; the C runtime keeps owning the handle. + let inherited = unsafe { BorrowedHandle::borrow_raw(handle as RawHandle) }; + inherited.try_clone_to_owned().map(std::fs::File::from) } /// Whether the socket is listening, or `None` where the provider cannot say (the server finds out diff --git a/src/workerd/server/compiled-bindings.capnp b/src/workerd/server/compiled-bindings.capnp new file mode 100644 index 00000000000..4bff90907cd --- /dev/null +++ b/src/workerd/server/compiled-bindings.capnp @@ -0,0 +1,107 @@ +# Copyright (c) 2026 Cloudflare, Inc. +# Licensed under the Apache 2.0 license found in the LICENSE file or at: +# https://opensource.org/licenses/Apache-2.0 + +@0x81303bd8e960bdd1; + +using Cxx = import "/capnp/c++.capnp"; +$Cxx.namespace("workerd::server"); + +using Workerd = import "/workerd/server/workerd.capnp"; + +# The bindings of one worker after the server has interpreted its config: every value is final and +# every capability has its channel number. The server (Rust) produces this message from +# `Worker.bindings` in workerd.capnp and from the worker's exports; the worker factory (C++) +# compiles it into the `env` object and `ctx.exports` under the isolate lock. +# +# Channel numbers index the worker's IoChannelFactory tables. Subrequest channels start at +# IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT (the two below are the global outbound); actor, +# actor-class and worker-loader channels each start at zero. + +struct Global { + name @0 :Text; + + union { + text @1 :Text; + data @2 :Data; + json @3 :Text; + # JSON text, parsed into a value. + + fetcher @4 :UInt32; + # A binding that is only a capability (this and the other `UInt32`s) is its channel number. + loopbackServiceStub @5 :UInt32; + kvNamespace @6 :UInt32; + r2Bucket :group { + channel @7 :UInt32; + bucket @8 :Text; + } + queue @9 :UInt32; + analyticsEngine :group { + channel @10 :UInt32; + dataset @11 :Text; + } + hyperdrive :group { + channel @12 :UInt32; + database @13 :Text; + user @14 :Text; + password @15 :Text; + scheme @16 :Text; + } + + cryptoKey :group { + format @17 :Text; + # "raw", "pkcs8", "spki" or "jwk". + keyData :union { + bytes @18 :Data; + # For raw, pkcs8 and spki: the key material, DER for the latter two. + json @19 :Text; + # For jwk: the key as JSON text. + } + algorithm @20 :Text; + # The algorithm as JSON text: a quoted name, or an object. + extractable @21 :Bool; + usages @22 :List(Workerd.Worker.Binding.CryptoKey.Usage); + } + + ephemeralActorNamespace @23 :UInt32; + loopbackEphemeralActorNamespace :group { + actorChannel @24 :UInt32; + classChannel @25 :UInt32; + } + durableActorNamespace :group { + actorChannel @26 :UInt32; + uniqueKey @27 :Text; + retryPolicy @28 :Workerd.Worker.Binding.DurableObjectNamespaceDesignator.RetryPolicy; + # The binding's `retryPolicy`, already checked against the runtime's limits + # (`api::UserDefinedRetryPolicy`); the runtime's defaults apply when absent. + } + loopbackDurableActorNamespace :group { + actorChannel @29 :UInt32; + uniqueKey @30 :Text; + classChannel @31 :UInt32; + } + actorClass @32 :UInt32; + loopbackActorClass @33 :UInt32; + + wrapped :group { + moduleName @34 :Text; + entrypoint @35 :Text; + innerBindings @36 :List(Global); + } + + unsafeEval @37 :Void; + memoryCache :group { + cacheId @38 :Text; + # Empty when the cache is not shared. + maxKeys @39 :UInt32; + maxValueSize @40 :UInt32; + maxTotalValueSize @41 :UInt64; + } + workerLoader @42 :UInt32; + workerdDebugPort @43 :Void; + } +} + +struct Globals { + globals @0 :List(Global); +} diff --git a/src/workerd/server/container-client.c++ b/src/workerd/server/container-client.c++ index 418aa1eea70..86458d676f3 100644 --- a/src/workerd/server/container-client.c++ +++ b/src/workerd/server/container-client.c++ @@ -1073,7 +1073,7 @@ void configureContainerPrivileges( // Represents a parsed egress mapping. IP/CIDR mappings match destination IPs, // while hostnameGlob mappings match either HTTP hostnames or TLS SNI depending on protocol. // Defined here (not in the header) to avoid pulling kj::OneOf, kj::CidrRange, and -// kj::Vector into server.c++ which includes container-client.h. +// kj::Vector into the factory, which includes container-client.h. struct ContainerClient::EgressMapping { kj::OneOf destination; uint16_t port; // 0 means match all ports diff --git a/src/workerd/server/container-client.h b/src/workerd/server/container-client.h index 22be055ce4c..70479f1c519 100644 --- a/src/workerd/server/container-client.h +++ b/src/workerd/server/container-client.h @@ -239,7 +239,7 @@ class ContainerClient final: public rpc::Container::Server, public kj::Refcounte ChannelTokenHandler& channelTokenHandler; // Opaque implementation struct holding egress mappings. Defined in container-client.c++ to - // avoid pulling heavy types (kj::OneOf, kj::CidrRange, kj::Vector) into server.c++ which + // avoid pulling heavy types (kj::OneOf, kj::CidrRange, kj::Vector) into the factory, which // includes this header. struct EgressState; kj::Own egressState; diff --git a/src/workerd/server/factory/BUILD.bazel b/src/workerd/server/factory/BUILD.bazel new file mode 100644 index 00000000000..5e7e52b96ef --- /dev/null +++ b/src/workerd/server/factory/BUILD.bazel @@ -0,0 +1,115 @@ +load("//:build/linking.bzl", "CC_LIBRARY_LINKSTATIC") +load("//:build/wd_cc_library.bzl", "wd_cc_library") + +package(default_visibility = ["//src/workerd/server:__subpackages__"]) + +# The declarations the server crate's bridge header needs. Header-only: the bridge library depends +# on it, and :worker-factory depends on the bridge library, so the two cannot be one target. +wd_cc_library( + name = "worker-factory-hdrs", + hdrs = ["worker-factory.h"], + deps = [ + "//src/rust/cxx/kj-rs", + "//src/rust/worker:bridge", + "//src/workerd/api:memory-cache", + "//src/workerd/api:pyodide", + "//src/workerd/io", + "//src/workerd/server:channel-token", + "//src/workerd/server:workerd_capnp", + "@capnp-cpp//src/capnp:capnp-rpc", + "@capnp-cpp//src/kj:kj-async-io", + "@capnp-cpp//src/kj/compat:kj-http", + ], +) + +# The C++ side of the Rust server: compiles workers, starts requests, builds actors and their +# storage, serves the capnp RPC protocols, encodes channel tokens, runs the inspector. +wd_cc_library( + name = "worker-factory", + srcs = [ + "worker-factory.c++", + "worker-factory-actor.c++", + "worker-factory-impl.h", + "worker-factory-listen.c++", + "worker-factory-rpc.c++", + ], + linkstatic = CC_LIBRARY_LINKSTATIC, + deps = [ + ":worker-factory-hdrs", + "//src/rust/worker:bridge", + "//src/workerd/api:memory-cache", + "//src/workerd/api:pyodide", + "//src/workerd/io", + "//src/workerd/io:bundle-fs", + "//src/workerd/io:worker-entrypoint", + "//src/workerd/jsg", + "//src/workerd/server:actor-id-impl", + "//src/workerd/server:alarm-scheduler", + "//src/workerd/server:channel-token", + "//src/workerd/server:compiled-bindings_capnp", + "//src/workerd/server:container-client", + "//src/workerd/server:facet-tree-index", + "//src/workerd/server:fallback-service", + "//src/workerd/server:pyodide", + "//src/workerd/server:workerd-api", + "//src/workerd/server:workerd_capnp", + "//src/workerd/server/server:bridge.rs@cxx", + "//src/workerd/util:autogate", + "//src/workerd/util:perfetto", + "//src/workerd/util:sqlite", + "//src/workerd/util:websocket-error-handler", + "@capnp-cpp//src/capnp:capnp-rpc", + "@capnp-cpp//src/capnp/compat:json", + "@capnp-cpp//src/kj:kj-async-core", + "@capnp-cpp//src/kj:kj-async-io", + "@capnp-cpp//src/kj/compat:kj-http", + "@ssl", + ], +) + +# The declarations both bridges (the command line's and the server's) need from :bootstrap. +# Header-only, since the bridge libraries cannot depend on the C++ that includes their headers. +wd_cc_library( + name = "bootstrap-hdrs", + hdrs = ["bootstrap.h"], + deps = [ + "//src/rust/cxx/kj-rs", + "@capnp-cpp//src/kj", + ], +) + +# The C++ half of a command: the process's logging, and the bootstrap that sets up the process and +# V8 on the Rust-owned event loop and builds the worker factory the Rust server runs. +wd_cc_library( + name = "bootstrap", + srcs = ["bootstrap.c++"], + linkstatic = CC_LIBRARY_LINKSTATIC, + deps = [ + ":bootstrap-hdrs", + ":worker-factory", + "//src/pyodide:pyodide_extra_capnp", + "//src/rust/cxx/kj-rs", + "//src/rust/cxx/kj-rs-io:kj-rs-io-lib", + "//src/rust/cxx/kj-rs-tokio:kj-rs-tokio-lib", + "//src/workerd/server:json-logger", + "//src/workerd/server:v8-platform-impl", + "//src/workerd/server:workerd_capnp", + "//src/workerd/server/server:bridge.rs@cxx", + "//src/workerd/util:autogate", + "//src/workerd/util:perfetto", + # :bootstrap exists only for the workerd binary, so it links the binary's kj::setupAsyncIo() + # (the inspector and fallback-service threads' loops). + "//src/workerd/util:setup-async-io", + "//src/workerd/util:thread-scopes", + "@capnp-cpp//src/kj:kj-async-core", + "@capnp-cpp//src/kj:kj-async-io", + ], + # The C++ root of the workerd binary is linked eagerly, as a cc_binary's own sources would be. + # Bazel places it ahead of everything it depends on, and from an eager object the linker + # resolves the rest of the C++ in that order. Left lazy, the Rust side's references would reach + # it only after every library had been registered, and the linker would then find the C++ by a + # depth-first walk of symbol references. That walk decides which copy of a template the binary + # keeps when two libraries carry different ones: V8 is built without exceptions, and its copy + # of libc++'s std::regex parser aborts where workerd's throws. + alwayslink = True, +) diff --git a/src/workerd/server/factory/bootstrap.c++ b/src/workerd/server/factory/bootstrap.c++ new file mode 100644 index 00000000000..c3def361a8a --- /dev/null +++ b/src/workerd/server/factory/bootstrap.c++ @@ -0,0 +1,315 @@ +// Copyright (c) 2017-2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#include "bootstrap.h" + +#include "worker-factory.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#if !_WIN32 +#include +#endif + +#if _WIN32 +#include + +#include +#include +#endif + +#include + +// since kj installs their global signal handlers +// and exits with 1 Fuzzilli doesn't realize that an application crashed due to the signo. +// Therefore, we install a handler before and just raise the signo +#ifdef WORKERD_FUZZILLI + +void signalHandler(int signo, siginfo_t* info, void* context) noexcept { + // inform reprl - remove debug output for clean testing + struct sigaction sa = {}; + sa.sa_handler = SIG_DFL; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(signo, &sa, nullptr); + raise(signo); +} + +void initSignalHandlers() { + struct sigaction action {}; + action.sa_flags = SA_SIGINFO; + action.sa_sigaction = &signalHandler; + + for (auto signo: {SIGBUS, SIGFPE, SIGABRT, SIGILL, SIGTRAP, SIGSEGV}) { + KJ_SYSCALL(sigaction(signo, &action, nullptr)); + } +} +#endif + +using namespace kj_rs; + +namespace workerd::server::cli { +namespace { + +// ======================================================================================= + +// For ASan's leak sanitizer, suppress warnings about leaks with stacks that include "unknown +// modules". This suppression is adopted from the GN build and applies to addresses that LSan can't +// symbolize or even map to a binary – perhaps JIT or snapshot-generated code in V8's case? +// TODO(someday): Suppression is needed to get several python tests to pass under LSan. Investigate +// if this is an actual leak (perhaps a bug in V8 itself since it is suppressed there?) at a later +// time. +#if __has_feature(address_sanitizer) +extern "C" __attribute__((no_sanitize("address"))) __attribute__((visibility("default"))) +__attribute__((used)) const char* +__lsan_default_suppressions() { + return "leak:\n"; +} +#endif + +// ======================================================================================= + +class EntropySourceImpl: public kj::EntropySource { + public: + void generate(kj::ArrayPtr buffer) override { + getEntropy(buffer); + } +}; + +// ======================================================================================= + +bool structuredLogging(const ::rust::Vec& configWords) { + capnp::FlatArrayMessageReader reader(asWords(kj::from(configWords)), CONFIG_READER_OPTIONS); + auto config = reader.getRoot(); + return config.hasLogging() ? config.getLogging().getStructuredLogging() + : config.getStructuredLogging(); +} + +// What the process needs around the Rust server (see the bridge), attached to the factory it +// builds. Members are declared in construction order, which is the reverse of destruction: V8 goes +// before the providers. +class Bootstrap { + public: + Bootstrap(kj_rs_tokio::TokioAsyncIoContext& loop, + const ::rust::Vec& configWords, + const ServeOrTestOptions& serveOrTest, + kj::Maybe test) + : timer(loop.getTimer()), + monotonicClock(kj::systemPreciseMonotonicClock()), + provider(loop.getTimer()) { + KJ_IF_SOME(path, serveOrTest.perfetto_trace_path) { +#ifdef WORKERD_USE_PERFETTO + perfettoSession = PerfettoSession(kj::str(path), + serveOrTest.perfetto_trace_categories + .map([](const ::rust::String& categories) { + return kj::str(categories); + }).orDefault(kj::String())); +#else + KJ_UNIMPLEMENTED("perfetto tracing is not supported by this build", kj::str(path)); +#endif + } + TRACE_EVENT("workerd", "Bootstrap()"); + + // The process is set up for this config and these options: autogates, the test modes, and V8 + // with its flags (V8 reads the test modes as it starts). + capnp::FlatArrayMessageReader reader( + asWords(kj::from(configWords)), CONFIG_READER_OPTIONS); + auto config = reader.getRoot(); + util::Autogate::initAutogate(config.getAutogates()); + KJ_IF_SOME(t, test) { + setUpTestProcess(t); + } + + platform = jsg::defaultPlatform(0); + v8Platform = kj::heap(*platform); + v8System = kj::heap(*v8Platform, + KJ_MAP(flag, config.getV8Flags()) -> kj::StringPtr { return flag; }, platform.get()); + } + + KJ_DISALLOW_COPY_AND_MOVE(Bootstrap); + + // A factory over `configWords`, which it owns for its run. + kj::Own makeFactory(::rust::Vec configWords, + const ServeOrTestOptions& serveOrTest, + kj::Maybe test) { + auto options = kj::heap(); + options->loggingOptions = Worker::LoggingOptions(Worker::ConsoleMode::STDOUT); + applyServeOrTestOptions(*options, serveOrTest); + KJ_IF_SOME(t, test) { + applyTestOptions(*options, t); + } + return kj::heap(*v8System, timer, monotonicClock, provider.getNetwork(), + entropySource, *fs, kj::mv(options), kj::mv(configWords)); + } + + private: + kj::Own fs = kj::newDiskFilesystem(); + EntropySourceImpl entropySource; + kj::Timer& timer; + // The loop's monotonic clock, consistent with `timer`. + const kj::MonotonicClock& monotonicClock; + // The tokio-backed kj::Network over the Runtime's loop, for the factory. + kj_rs_io::TokioAsyncIoProvider provider; + +#ifdef WORKERD_USE_PERFETTO + kj::Maybe perfettoSession; +#endif + + kj::Own platform; + kj::Own v8Platform; + kj::Own v8System; + + kj::Maybe> openWritableDirectory(kj::StringPtr pathStr) { + return fs->getRoot().tryOpenSubdir(fs->getCurrentPath().eval(pathStr), kj::WriteMode::MODIFY); + } + + void applyServeOrTestOptions( + WorkerFactory::Options& options, const ServeOrTestOptions& serveOrTest) { + options.experimental = serveOrTest.experimental; + auto& python = options.pythonConfig; + KJ_IF_SOME(path, serveOrTest.pyodide_package_disk_cache_dir) { + // The command line checked that the directory exists. + python.packageDiskCacheRoot = KJ_REQUIRE_NONNULL( + openWritableDirectory(kj::str(path)), "package disk cache dir must exist"); + } + KJ_IF_SOME(path, serveOrTest.pyodide_bundle_disk_cache_dir) { + python.pyodideDiskCacheRoot = openWritableDirectory(kj::str(path)); + } + python.createSnapshot = serveOrTest.python_save_snapshot; + python.createBaselineSnapshot = serveOrTest.python_save_baseline_snapshot; + KJ_IF_SOME(path, serveOrTest.python_load_snapshot) { + python.loadSnapshotFromDisk = kj::str(path); + } + KJ_IF_SOME(path, serveOrTest.python_snapshot_dir) { + python.snapshotDirectory = openWritableDirectory(kj::str(path)); + } + } + + // The `test` options that apply to the process. + static void setUpTestProcess(const TestOptions& testOptions) { + if (!testOptions.no_verbose) { + // Always turn on info logging when running tests so that uncaught exceptions are displayed. + // TODO(beta): This can be removed once we improve our error logging story. + kj::_::Debug::setLogLevel(kj::LogSeverity::INFO); + } + if (testOptions.predictable) { + setPredictableModeForTest(); + } + if (testOptions.gc_stress) { + setGcStressModeForTest(); + } + if (testOptions.all_autogates) { + util::Autogate::initAllAutogates(); + } + } + + // The `test` options that apply to a factory. + static void applyTestOptions(WorkerFactory::Options& options, const TestOptions& testOptions) { + KJ_IF_SOME(compatDate, testOptions.compat_date) { + options.testCompatibilityDateOverride = kj::str(compatDate); + } + } +}; + +} // namespace + +::rust::Slice release_version() { + return RELEASE_VERSION.asBytes().as(); +} + +bool perfetto_supported() { +#ifdef WORKERD_USE_PERFETTO + return true; +#else + return false; +#endif +} + +bool fuzzilli_supported() { +#if defined(WORKERD_FUZZILLI) && defined(__linux__) + return true; +#else + return false; +#endif +} + +::rust::String pyodide_lock() { + capnp::MallocMessageBuilder message; + // TODO(EW-8977): Implement option to specify python worker flags. + auto features = message.getRoot(); + features.setPythonWorkers(true); + auto pythonRelease = KJ_REQUIRE_NONNULL(getPythonSnapshotRelease(features)); + auto lock = KJ_REQUIRE_NONNULL(api::pyodide::getPyodideLock(pythonRelease)); + return lock.as(); +} + +int32_t with_process_context( + bool verbose, ::rust::Vec config, ::rust::Box command) { + kj::printStackTraceOnCrash(); +#if defined(WORKERD_FUZZILLI) && defined(__linux__) + initSignalHandlers(); +#endif + if (verbose) { + kj::_::Debug::setLogLevel(kj::LogSeverity::INFO); + } + kj::Maybe jsonLogger; + bool structured = structuredLogging(config); + if (structured) { + jsonLogger.emplace(); + } + return run_pending_command(kj::mv(command), kj::mv(config), structured); +} + +kj::Own bootstrap(kj_rs_tokio::TokioAsyncIoContext& loop, + ::rust::Vec config, + const ServeOrTestOptions& options, + kj::Maybe test) { + auto bootstrap = kj::heap(loop, config, options, test); + auto factory = bootstrap->makeFactory(kj::mv(config), options, test); + return factory.attach(kj::mv(bootstrap)); +} + +void cli_exit(int32_t code) { + _exit(code); +} + +void kj_log(uint8_t severity, ::rust::Str file, uint32_t line, ::rust::Str message) { + auto kjSeverity = static_cast(severity); + if (!kj::_::Debug::shouldLog(kjSeverity)) return; + // The file name is kept alive for the call only; KJ formats the line synchronously. + auto fileStr = kj::str(file); + kj::_::Debug::log(fileStr.cStr(), static_cast(line), kjSeverity, "", kj::str(message)); +} + +void json_log_to_stderr(uint8_t severity, ::rust::Str file, uint32_t line, ::rust::Str message) { + auto json = buildJsonLogMessage(static_cast(severity), kj::str(file).cStr(), + static_cast(line), 0, kj::str(message)); + kj::FdOutputStream(STDERR_FILENO).write({json.asBytes(), "\n"_kj.asBytes()}); +} + +} // namespace workerd::server::cli diff --git a/src/workerd/server/factory/bootstrap.h b/src/workerd/server/factory/bootstrap.h new file mode 100644 index 00000000000..b684cea4d0b --- /dev/null +++ b/src/workerd/server/factory/bootstrap.h @@ -0,0 +1,50 @@ +// Copyright (c) 2017-2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#pragma once + +// The C++ half of workerd's command line: the functions the Rust entry point (cli/lib.rs) calls +// through the cxx bridges in cli/bridge.rs (the build's capabilities, the Pyodide lock) and +// server/bridge.rs (the process's logging, the bootstrap, the exit). Rust parses the command +// line, produces the encoded config (config-compiler.h compiles schema files), owns the event loop +// (kj_rs_tokio::Runtime) and runs the server (server/entry.rs); the bootstrap sets up the process +// and V8 around it and builds the worker factory. +// +// Do not include a generated bridge header here: both bridges include this header. + +#include + +#include + +namespace kj_rs_tokio { +struct TokioAsyncIoContext; +} // namespace kj_rs_tokio + +namespace workerd::server { +class WorkerFactory; +struct PendingCommand; +} // namespace workerd::server + +namespace workerd::server::cli { + +struct ServeOrTestOptions; +struct TestOptions; + +::rust::Slice release_version(); +bool perfetto_supported(); +bool fuzzilli_supported(); +::rust::String pyodide_lock(); + +// See the bridge (server/bridge.rs). +int32_t with_process_context( + bool verbose, ::rust::Vec config, ::rust::Box command); +kj::Own bootstrap(kj_rs_tokio::TokioAsyncIoContext& loop, + ::rust::Vec config, + const ServeOrTestOptions& options, + kj::Maybe test); +[[noreturn]] void cli_exit(int32_t code); +void kj_log(uint8_t severity, ::rust::Str file, uint32_t line, ::rust::Str message); +void json_log_to_stderr(uint8_t severity, ::rust::Str file, uint32_t line, ::rust::Str message); + +} // namespace workerd::server::cli diff --git a/src/workerd/server/factory/worker-factory-actor.c++ b/src/workerd/server/factory/worker-factory-actor.c++ new file mode 100644 index 00000000000..f55c0785511 --- /dev/null +++ b/src/workerd/server/factory/worker-factory-actor.c++ @@ -0,0 +1,508 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#include "worker-factory-impl.h" + +#include +#include +#include +#include +#include + +namespace workerd::server { + +// ======================================================================================= +// ActorStorage + +ActorStorage::Impl::Impl(const WorkerFactory& factory, + kj::Maybe> directoryParam, + kj::String uniqueKey, + ::rust::Box actors) + : directory(kj::mv(directoryParam)), + vfsDirectory(directory.map([](kj::Own& d) { return d->clone(); }) + .orDefault([&]() { + return kj::newInMemoryDirectory(kj::systemPreciseCalendarClock()); + })), + vfs(*vfsDirectory), + uniqueKey(kj::mv(uniqueKey)), + alarmScheduler(kj::heap(kj::systemPreciseCalendarClock(), + factory.getImpl().timer, + vfs, + kj::Path({"metadata.sqlite"}), + [uniqueKey = this->uniqueKey.asPtr(), actors = kj::mv(actors)]( + const ActorKey& actor) mutable -> kj::Own { + // The id carries the persisted name so that `ctx.id.name` is set when the alarm runs. + auto id = ActorIdFactoryImpl(uniqueKey).idFromStringNamed( + kj::str(actor.actorId), actor.name.map([](kj::StringPtr n) { return kj::str(n); })); + return actors->actor_for_alarm(kj::heap(kj::mv(id))); + })) {} + +kj::Maybe> ActorStorage::Impl::getFacetTreeIndexIfNotEmpty( + const kj::Directory& dir, kj::StringPtr rootKey) { + return dir.tryOpenFile(kj::Path({kj::str(rootKey, ".facets")}), kj::WriteMode::MODIFY) + .map([](kj::Own&& file) { return kj::heap(kj::mv(file)); }); +} + +kj::Path ActorStorage::Impl::getSqlitePath( + kj::StringPtr rootKey, uint facetId, kj::StringPtr suffix) { + if (facetId == 0) { + return kj::Path({kj::str(rootKey, ".sqlite", suffix)}); + } + return kj::Path({kj::str(rootKey, '.', facetId, ".sqlite", suffix)}); +} + +void ActorStorage::Impl::deleteFacet( + const kj::Directory& dir, kj::StringPtr rootKey, FacetTreeIndex& index, uint facetId) { + index.forEachChild( + facetId, [&](uint childId, kj::StringPtr) { deleteFacet(dir, rootKey, index, childId); }); + // The database may not exist at all if the facet never ran. + dir.tryRemove(getSqlitePath(rootKey, facetId)); + dir.tryRemove(getSqlitePath(rootKey, facetId, "-wal")); + dir.tryRemove(getSqlitePath(rootKey, facetId, "-shm")); +} + +void ActorStorage::Impl::deleteDescendantStorage( + const kj::Directory& dir, kj::StringPtr rootKey, uint parentId) { + KJ_IF_SOME(index, getFacetTreeIndexIfNotEmpty(dir, rootKey)) { + index->forEachChild( + parentId, [&](uint childId, kj::StringPtr) { deleteFacet(dir, rootKey, *index, childId); }); + } else { + // No index, so no facets other than the root. + KJ_ASSERT(parentId == 0); + } +} + +void ActorStorage::Impl::cloneFacet(const kj::Directory& dir, + kj::StringPtr rootKey, + FacetTreeIndex& index, + uint srcId, + uint dstId) { + // Snapshot the children first: allocating the destination's ids mutates the index. + struct Child { + uint id; + kj::String name; + }; + kj::Vector children; + index.forEachChild(srcId, [&](uint childId, kj::StringPtr childName) { + children.add(Child{childId, kj::str(childName)}); + }); + for (auto& child: children) { + cloneFacet(dir, rootKey, index, child.id, index.getId(dstId, child.name)); + } + + // A source without a database has no data, which is what the destination already has. The WAL + // is copied with the database since a crashed process may have left one uncheckpointed; the + // SHM only speeds up the first open. + for (const auto& suffix: {""_kj, "-wal"_kj, "-shm"_kj}) { + auto src = getSqlitePath(rootKey, srcId, suffix); + if (!dir.exists(src)) return; + dir.transfer( + getSqlitePath(rootKey, dstId, suffix), kj::WriteMode::CREATE, src, kj::TransferMode::COPY); + } +} + +ActorStorage::ActorStorage(kj::Own impl): impl(kj::mv(impl)) {} +ActorStorage::~ActorStorage() noexcept(false) = default; +ActorStorage::Impl& ActorStorage::getImpl() const { + return *impl; +} + +kj::Own factory_new_actor_storage(const WorkerFactory& factory, + ::rust::Str path, + ::rust::Str uniqueKey, + ::rust::Box actors) { + kj::Maybe> directory; + if (!path.empty()) { + auto& fs = factory.getImpl().fs; + auto base = fs.getRoot().openSubdir(fs.getCurrentPath().evalNative(kj::str(path)), + kj::WriteMode::CREATE | kj::WriteMode::MODIFY); + directory = base->openSubdir( + kj::Path({kj::str(uniqueKey)}), kj::WriteMode::CREATE | kj::WriteMode::MODIFY); + } + return kj::heap(kj::refcounted( + factory, kj::mv(directory), kj::str(uniqueKey), kj::mv(actors))); +} + +void actor_storage_delete_all(const ActorStorage& storage) { + storage.getImpl().alarmScheduler->deleteAll(); +} + +void actor_storage_delete_facet(const ActorStorage& storage, + ::rust::Str rootKey, + kj::Maybe parentFacetId, + ::rust::Str name) { + auto& impl = storage.getImpl(); + auto& dir = *KJ_UNWRAP_OR(impl.directory, return); + auto key = kj::str(rootKey); + // Without an index there can be no facet storage. + KJ_IF_SOME(index, impl.getFacetTreeIndexIfNotEmpty(dir, key)) { + impl.deleteFacet(dir, key, *index, index->getId(parentFacetId.orDefault(0), kj::str(name))); + } +} + +void actor_storage_clone_facet(const ActorStorage& storage, + ::rust::Str rootKey, + kj::Maybe parentFacetId, + ::rust::Str src, + ::rust::Str dst) { + auto& impl = storage.getImpl(); + auto& dir = *KJ_UNWRAP_OR(impl.directory, return); + auto key = kj::str(rootKey); + // Without an index there is no storage to delete or copy. + KJ_IF_SOME(index, impl.getFacetTreeIndexIfNotEmpty(dir, key)) { + uint parentId = parentFacetId.orDefault(0); + // The destination's storage goes first, as deleting the facet would; then the copy. A source + // without a database has no data, which the emptied destination matches. + uint dstId = index->getId(parentId, kj::str(dst)); + impl.deleteFacet(dir, key, *index, dstId); + impl.cloneFacet(dir, key, *index, index->getId(parentId, kj::str(src)), dstId); + } +} + +kj::Maybe actor_storage_facet_id(const ActorStorage& storage, + ::rust::Str rootKey, + kj::Maybe parentFacetId, + ::rust::Str name) { + auto& dir = *KJ_UNWRAP_OR(storage.getImpl().directory, return kj::none); + // The index is read from disk for this one lookup; the server keeps the number it yields. + auto file = dir.openFile( + kj::Path({kj::str(rootKey, ".facets")}), kj::WriteMode::CREATE | kj::WriteMode::MODIFY); + return FacetTreeIndex(kj::mv(file)).getId(parentFacetId.orDefault(0), kj::str(name)); +} + +// ======================================================================================= +// Actors + +namespace { + +// The actor's hooks into the server: the requests it raises for itself (alarms, hibernated +// WebSocket events, which restart the actor if needed), its facets, and its transitions between +// idle and active. The actor owns a reference as its loopback, so the hooks it borrows as its +// facet manager live as long as it does. +class ActorHooksImpl final: public Worker::Actor::Loopback, + public Worker::Actor::FacetManager, + public RequestTracker::Hooks, + public kj::Refcounted { + public: + explicit ActorHooksImpl(::rust::Box hooks): hooks(kj::mv(hooks)) {} + + kj::Own getWorker(IoChannelFactory::SubrequestMetadata metadata) override { + return hooks->start_request(kj::heap(kj::mv(metadata))); + } + kj::Own addRef() override { + return kj::addRef(*this); + } + + uint getDepth() const override { + return hooks->depth(); + } + kj::Rc getFacet( + kj::StringPtr name, kj::Function()> getStartInfo) override { + return subrequest_channel_into_kj( + hooks->facet(toRust(name), kj::heap(kj::mv(getStartInfo)))); + } + void abortFacet(kj::StringPtr name, kj::Exception reason) override { + hooks->abort_facet(toRust(name), reason); + } + void deleteFacet(kj::StringPtr name) override { + hooks->delete_facet(toRust(name)); + } + void cloneFacet(kj::StringPtr src, kj::StringPtr dst) override { + hooks->clone_facet(toRust(src), toRust(dst)); + } + + void active() override { + hooks->active(); + } + void inactive() override { + hooks->inactive(); + } + + private: + ::rust::Box hooks; +}; + +// Schedules a root actor's alarms with the namespace's scheduler; the prior task is ignored since +// everything runs synchronously here. +class ActorSqliteHooks final: public ActorSqlite::Hooks { + public: + ActorSqliteHooks(kj::Own storage, ActorKey actor) + : storage(kj::mv(storage)), + actor(kj::mv(actor)) {} + + kj::Promise scheduleRun( + kj::Maybe newAlarmTime, kj::Promise priorTask) override { + KJ_IF_SOME(scheduledTime, newAlarmTime) { + storage->alarmScheduler->setAlarm(actor, scheduledTime); + } else { + storage->alarmScheduler->deleteAlarm(actor); + } + return kj::READY_NOW; + } + + private: + kj::Own storage; + ActorKey actor; +}; + +// Facets have their own storage but no alarms: the scheduler delivers only to root actors. +class FacetAlarmHooks final: public ActorSqlite::Hooks { + public: + kj::Promise scheduleRun( + kj::Maybe newAlarmTime, kj::Promise priorTask) override { + // The same message as the production storage factory's. + JSG_FAIL_REQUIRE(Error, "Facets currently cannot set alarms."); + } +}; + +// The actor's storage: SQLite in the namespace's directory, or an in-memory cache over empty +// storage when the namespace has no directory. None for an ephemeral actor. +Worker::Actor::MakeActorCacheFunc makeActorCacheFunc(kj::Own storage, + const ActorStorageSpec& spec, + kj::String rootKey, + kj::Maybe actorName) { + return [storage = kj::mv(storage), durable = spec.durable, facetId = spec.facet_id, + rootKey = kj::mv(rootKey), + actorName = kj::mv(actorName)](const ActorCache::SharedLru& sharedLru, + OutputGate& outputGate, ActorCache::Hooks& hooks, + SqliteObserver& sqliteObserver) mutable -> kj::Maybe> { + if (!durable) return kj::none; + if (storage->directory == kj::none) { + // The cache never flushes (see NullIsolateLimitEnforcer), so this is in-memory storage. + return kj::heap(newEmptyReadOnlyActorStorage(), sharedLru, outputGate, hooks); + } + + kj::Own sqliteHooks; + uint selfId = facetId.orDefault(0); + if (facetId == kj::none) { + sqliteHooks = + kj::heap(kj::addRef(*storage), ActorKey(rootKey.asPtr(), actorName)); + } else { + sqliteHooks = kj::heap(); + } + + auto db = kj::heap(storage->vfs, storage->getSqlitePath(rootKey, selfId), + kj::WriteMode::CREATE | kj::WriteMode::MODIFY); + // The database runs in WAL mode, also after `reset()` (which `deleteAll()` uses, and which + // also deletes the child facets' storage; that is not transactional with the reset, as it is + // in production). + db->run("PRAGMA journal_mode=WAL;"); + db->afterReset([storage = kj::addRef(*storage), rootKey = kj::str(rootKey), selfId]( + SqliteDatabase& db) mutable { + db.run("PRAGMA journal_mode=WAL;"); + storage->deleteDescendantStorage(*KJ_ASSERT_NONNULL(storage->directory), rootKey, selfId); + }); + return kj::heap(kj::mv(db), outputGate, + [](SpanParent) -> kj::Promise { return kj::READY_NOW; }, *sqliteHooks) + .attach(kj::mv(sqliteHooks)); + }; +} + +// The Docker client of the container `id`: the live one, if an earlier incarnation of the actor +// left it running (a `setInactivityTimeout()` timer holds it), else a new one for the class's +// `container` options on its worker's `containerEngine`. +kj::Own getContainerClient(WorkerFactory::Impl& factory, + kj::String id, + config::Worker::ContainerEngine::Reader engine, + config::Worker::DurableObjectNamespace::ContainerOptions::Reader options) { + KJ_REQUIRE(!factory.containerShutdownStarted, + "cannot acquire a container client after graceful shutdown has begun"); + auto& state = *factory.containers.findOrCreate(id, [&]() { + return decltype(factory.containers)::Entry{ + kj::str(id), kj::heap()}; + }); + KJ_IF_SOME(client, state.client) { + return client.addRef(); + } + + KJ_REQUIRE(engine.isLocalDocker(), + "dockerPath must be defined to enable containers on this Durable Object."); + auto docker = engine.getLocalDocker(); + KJ_REQUIRE(docker.hasContainerEgressInterceptorImage(), + "containerEgressInterceptorImage must be configured for containers."); + kj::Maybe imageName; + if (options.getImageName().size() > 0) imageName = kj::str(options.getImageName()); + auto privilegeConf = options.getPrivileges(); + ContainerPrivileges privileges{ + .capabilities = KJ_MAP(c, privilegeConf.getCapabilities()) { return kj::str(c); }, + .devices = + KJ_MAP(device, privilegeConf.getDevices()) { + return ContainerPrivileges::Device{ + .pathOnHost = kj::str(device.getPathOnHost()), + .pathInContainer = kj::str(device.getPathInContainer()), + .cgroupPermissions = kj::str(device.getCgroupPermissions()), + }; + }, + .securityOpt = KJ_MAP(o, privilegeConf.getSecurityOpt()) { return kj::str(o); }, + }; + + // Docker may still be removing the container for the client before this one, which would race + // this client: that removal is cancelled, and this client waits for it to have ended. + auto previousCleanup = state.cleanup.addBranch(); + state.canceler.cancel("a new container client took the container over"_kj); + auto client = kj::refcounted(factory.byteStreamFactory, factory.timer, + factory.network, kj::str(docker.getSocketPath()), kj::mv(id), kj::mv(imageName), + kj::str(docker.getContainerEgressInterceptorImage()), factory.tasks, kj::mv(previousCleanup), + [&state, &tasks = factory.tasks](kj::Promise cleanup) { + // The client's shutdown began: the next incarnation of the actor gets a new client. + state.client = kj::none; + state.cleanup = state.canceler.wrap(kj::mv(cleanup)).catch_([](kj::Exception&&) {}).fork(); + tasks.add(state.cleanup.addBranch()); + }, factory.channelTokenHandler, kj::mv(privileges)); + state.client = *client; + return client; +} + +} // namespace + +ActorHandle::ActorHandle(kj::Own hooks, + kj::Own tracker, + kj::Own actor) + : hooks(kj::mv(hooks)), + tracker(kj::mv(tracker)), + actor(kj::mv(actor)) {} +ActorHandle::~ActorHandle() noexcept(false) { + tracker->shutdown(); +} +Worker::Actor& ActorHandle::getActor() const { + return *actor; +} +kj::Own ActorHandle::addRef() const { + return actor->addRef(); +} + +kj::Own worker_new_actor(const CompiledWorker& worker, + ::rust::Str className, + kj::Maybe> props, + kj::Own id, + const ActorStorage& storage, + const ActorStorageSpec& spec, + ::rust::Box hooks, + kj::Maybe> hibernationManager, + kj::Maybe container) { + auto& impl = worker.getImpl(); + auto actorId = Worker::Actor::cloneId(*id); + + // The name the actor was created with (`idFromName()`), for the alarm scheduler to persist so + // that `ctx.id.name` is restored when the alarm fires after an eviction. + kj::Maybe actorName; + KJ_IF_SOME(doId, actorId.tryGet>()) { + actorName = doId->getName().map([](kj::StringPtr n) { return kj::str(n); }); + } + auto makeActorCache = makeActorCacheFunc( + kj::addRef(storage.getImpl()), spec, kj::str(spec.root_key), kj::mv(actorName)); + + auto makeStorage = [enableSql = spec.enable_sql](jsg::Lock& js, const Worker::Api& api, + ActorCacheInterface& actorCache) -> jsg::Ref { + return js.alloc( + js, IoContext::current().addObject(actorCache), enableSql); + }; + + Frankenvalue propsValue; + KJ_IF_SOME(p, props) propsValue = kj::mv(*p); + + jsg::Dict images; + kj::Maybe containerClient; + KJ_IF_SOME(ref, container) { + auto& factory = impl.factory.getImpl(); + auto workerConf = factory.config.getServices()[ref.service_index].getWorker(); + auto options = workerConf.getDurableObjectNamespaces()[ref.namespace_index].getContainer(); + images.fields = KJ_MAP(image, options.getImages()) { + return jsg::Dict::Field{ + .name = kj::str(image.getName()), .value = kj::str(image.getImage())}; + }; + // Unique per namespace and actor across the machine. + containerClient = rpc::Container::Client(getContainerClient(factory, + kj::str("workerd-", storage.getImpl().uniqueKey, "-", actor_id_key(*id)), + workerConf.getContainerEngine(), options)); + } + + auto actorHooks = kj::refcounted(kj::mv(hooks)); + auto requestTracker = kj::refcounted(*actorHooks); + auto classNameStr = kj::str(className); + // The hibernation event type id is defined outside workerd; WebSocket hibernation needs one. + static constexpr uint16_t hibernationEventTypeId = 8; + auto actor = kj::refcounted(impl.getWorker(), *requestTracker, kj::mv(actorId), + true, kj::mv(makeActorCache), classNameStr.asPtr(), kj::mv(propsValue), kj::mv(makeStorage), + kj::addRef(*actorHooks), impl.timerChannel, kj::refcounted(), + hibernationManager.map([](kj::Rc& m) { return m.toOwn(); }), + hibernationEventTypeId, kj::mv(containerClient), kj::mv(images), *actorHooks); + return kj::heap(kj::mv(actorHooks), kj::mv(requestTracker), kj::mv(actor)); +} + +kj::Own actor_on_broken(const ActorHandle& actor) { + return kj::heap(actor.getActor().onBroken()); +} + +kj::Promise actor_broken(kj::Own broken) { + return kj::mv(*broken); +} + +void actor_abort(const ActorHandle& actor, const AbortReason& reason) { + // Raising the reason across the bridge is the bridge's own KjError -> kj::Exception conversion. + KJ_IF_SOME(exception, kj::runCatchingExceptions([&]() { reason.raise(); })) { + actor.getActor().abort(exception); + } else { + actor.getActor().shutdown(0, kj::none); + } +} + +kj::Promise> actor_lock(const ActorHandle& actor) { + co_return kj::heap( + co_await actor.getActor().getWorker().takeAsyncLockWithoutRequest(nullptr)); +} + +bool actor_shutdown( + const ActorHandle& handle, ::rust::Str reason, kj::Maybe> lock) { + auto& actor = handle.getActor(); + if (actor.isShared()) return false; + KJ_IF_SOME(asyncLock, lock) { + KJ_IF_SOME(manager, actor.getHibernationManager()) { + actor.getWorker().runInLockScope( + *asyncLock, [&](Worker::Lock& lock) { manager.hibernateWebSockets(lock); }); + } + } + actor.shutdown(0, KJ_EXCEPTION(DISCONNECTED, kj::str(reason))); + return true; +} + +void actor_reset_storage(const ActorHandle& actor) { + KJ_IF_SOME(cache, actor.getActor().getPersistent()) { + KJ_IF_SOME(db, cache.getSqliteDatabase()) { + kj::runCatchingExceptions([&]() { db.reset(); }); + } + } +} + +kj::Maybe> actor_hibernation_manager(const ActorHandle& actor) { + return actor.getActor().getHibernationManager().map( + [](HibernationManager& m) { return kj::Rc(m.addRef()); }); +} + +kj::Promise facet_start_resolve(kj::Own start) { + auto info = co_await (*start)(); + co_await info.ensureAllResolved(); + co_return FacetStartInfo{ + .id = kj::heap(kj::mv(info.id)), + .actor_class = KJ_REQUIRE_NONNULL(ActorClassChannelHandle::tryUnwrap(*info.actorClass), + "a facet's class must be one the server made"), + }; +} + +// ======================================================================================= +// Containers + +kj::Promise factory_shutdown_containers(const WorkerFactory& factory) { + auto& impl = factory.getImpl(); + impl.containerShutdownStarted = true; + kj::Vector> cleanups(impl.containers.size()); + for (auto& entry: impl.containers) { + // A client's shutdown replaces its entry's `cleanup`. + KJ_IF_SOME(client, entry.value->client) client.shutdown(); + cleanups.add(entry.value->cleanup.addBranch()); + } + return kj::joinPromises(cleanups.releaseAsArray()); +} + +} // namespace workerd::server diff --git a/src/workerd/server/factory/worker-factory-impl.h b/src/workerd/server/factory/worker-factory-impl.h new file mode 100644 index 00000000000..00b4a28ed01 --- /dev/null +++ b/src/workerd/server/factory/worker-factory-impl.h @@ -0,0 +1,281 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#pragma once + +// The private state behind worker-factory.h, shared by the factory's source files. + +#include "worker-factory.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace workerd::server { + +using kj_rs::Rust; + +// A kj string as Rust text. A kj string need not be UTF-8 (a JS string with a lone surrogate, a +// peer's bytes): `toRust` throws for one that is not, as suits a name or a key; `toRustString` +// replaces what is not with U+FFFD, as suits free text (an exception's description), and does +// not throw. +inline ::rust::Str toRust(kj::StringPtr str) { + return ::rust::Str(str.begin(), str.size()); +} +inline ::rust::String toRustString(kj::StringPtr str) { + return ::rust::String::lossy(str.begin(), str.size()); +} +inline kj::Maybe<::rust::Str> toRust(kj::Maybe str) { + return str.map([](kj::StringPtr s) { return toRust(s); }); +} +inline kj::Maybe toKj(const kj::Maybe<::rust::Str>& str) { + return str.map([](const ::rust::Str& s) { return kj::str(s); }); +} +inline kj::Maybe toKj(const kj::Maybe<::rust::String>& str) { + return str.map([](const ::rust::String& s) { return kj::str(s); }); +} +inline kj::Own toOwn(Frankenvalue value) { + return kj::heap(kj::mv(value)); +} +inline kj::Maybe> toOwn(kj::Maybe value) { + return value.map([](Frankenvalue& v) { return toOwn(kj::mv(v)); }); +} + +// ======================================================================================= + +class InspectorService; + +// The registration point for isolates with the inspector service, which lives on its own thread. +// The inspector service attaches itself when it starts and detaches when it stops. +class InspectorServiceIsolateRegistrar final { + public: + InspectorServiceIsolateRegistrar() = default; + ~InspectorServiceIsolateRegistrar() noexcept(true); + KJ_DISALLOW_COPY_AND_MOVE(InspectorServiceIsolateRegistrar); + + void registerIsolate(kj::StringPtr name, Worker::Isolate& isolate); + + private: + void attach(InspectorService& service); + void detach(); + + kj::MutexGuarded> inspectorService; + friend class InspectorService; +}; + +// Starts the inspector on `address` on its own thread and returns the port it listens on. +uint startInspector(kj::String address, InspectorServiceIsolateRegistrar& registrar); + +// ======================================================================================= + +class WorkerFactory::Impl final: private kj::TaskSet::ErrorHandler { + public: + Impl(WorkerFactory& factory, + jsg::V8System& v8System, + kj::Timer& timer, + const kj::MonotonicClock& monotonicClock, + kj::Network& network, + kj::EntropySource& entropySource, + kj::Filesystem& fs, + kj::Own options, + ::rust::Vec config); + + jsg::V8System& v8System; + kj::Timer& timer; + const kj::MonotonicClock& monotonicClock; + kj::Network& network; + kj::EntropySource& entropySource; + kj::Filesystem& fs; + kj::Own options; + + // The reader borrows the message, so the message is declared first. + ::rust::Vec configMessage; + capnp::FlatArrayMessageReader configReader; + config::Config::Reader config; + + capnp::ByteStreamFactory byteStreamFactory; + kj::HttpHeaderTable::Builder headerTableBuilder; + capnp::HttpOverCapnpFactory httpOverCapnpFactory; + ThreadContext threadContext; + kj::Own headerTable; + + kj::Own memoryCacheProvider; + ChannelTokenHandler channelTokenHandler; + kj::Maybe<::rust::Box> server; + kj::Maybe> inspectorRegistrar; + + // The Docker client of an actor's container. Actors and `setInactivityTimeout()` timers own the + // client; while one lives, the actor's next incarnation takes the same one. + struct Container { + kj::Maybe client; + // Cancels `cleanup` when the next client takes the container over. + kj::Canceler canceler; + // Docker removing the container, once the last client to hold it has shut down. + kj::ForkedPromise cleanup = kj::Promise(kj::READY_NOW).fork(); + }; + // By container id. A callback of each client points at its entry. + kj::HashMap> containers; + // Set by factory_shutdown_containers: no actor may acquire a container client any more. + bool containerShutdownStarted = false; + + // Background work no request owns: container cleanup, trace delivery, and the Rust server's + // tasks (factory_spawn). Declared last, so that it is destroyed first: a task may hold anything + // else here. + kj::TaskSet tasks; + // How many tasks factory_spawn has added; factory_settle_tasks waits for it to stop growing. + uint64_t spawnCount = 0; + + const ServerHandle& getServer() const; + + private: + void taskFailed(kj::Exception&& exception) override; +}; + +// The clock JavaScript sees: the calendar clock for `now()`, timers measured from a fresh +// monotonic reading so that time spent in JavaScript since the last poll does not shorten them. +class WorkerTimerChannel final: public TimerChannel { + public: + WorkerTimerChannel(kj::Timer& timer, const kj::MonotonicClock& monotonicClock) + : timer(timer), + monotonicClock(monotonicClock) {} + + void syncTime() override {} + kj::Date now(kj::Maybe) override { + return kj::systemPreciseCalendarClock().now(); + } + kj::Promise atTime(kj::Date when) override { + return timer.atTime(monotonicClock.now() + (when - now(kj::none))); + } + kj::Promise afterLimitTimeout(kj::Duration t) override { + return timer.afterDelay(t); + } + kj::TimePoint nowForLimitTimeout() override { + return monotonicClock.now(); + } + + private: + kj::Timer& timer; + const kj::MonotonicClock& monotonicClock; +}; + +// Everything requests on a compiled worker share. Refcounted so that the I/O channel factory of a +// request in flight keeps it alive after the server drops the worker. +class CompiledWorker::Impl final: public kj::Refcounted, private kj::TaskSet::ErrorHandler { + public: + Impl(const WorkerFactory& factory, kj::String name); + + const WorkerFactory& factory; + kj::String name; + WorkerTimerChannel timerChannel; + kj::TaskSet waitUntilTasks; + + // The compiled compatibility flags of a config worker; a dynamic worker's live in its source. + kj::Own flagsArena; + // A dynamic worker's source: its `env`, its tails and the content its script points into. + kj::Maybe dynamicSource; + // A dynamic worker's I/O channel tables, numbered as the runtime numbers channels: the + // subrequest table starts with `IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT` slots for the + // global outbound (or a null outbound when the source gave none), then the capabilities the + // `env` held in cap-table order; actor classes and RPC channels start from zero. The Rust + // channel factory serves the numbers past these tables (the worker's own `ctx.exports`). Empty + // for a config worker, whose whole table is the Rust side's. + kj::Vector> subrequestChannels; + kj::Vector> actorClassChannels; + kj::Vector> rpcChannels; + kj::Maybe accessBlobHeader; + bool isDynamic = false; + + kj::Maybe> worker; + // Held from compilation until `worker_set_ctx_exports()` fills the object in; none for a worker + // whose script failed to compile. + kj::Maybe> ctxExportsHandle; + + WorkerInfo info; + + const Worker& getWorker() const { + return *KJ_ASSERT_NONNULL(worker); + } + + private: + void taskFailed(kj::Exception&& exception) override; +}; + +// The storage of a namespace: its directory (none in memory), the VFS, and the alarm scheduler. +class ActorStorage::Impl final: public kj::Refcounted { + public: + Impl(const WorkerFactory& factory, + kj::Maybe> directory, + kj::String uniqueKey, + ::rust::Box actors); + + kj::Maybe> directory; + kj::Own vfsDirectory; // the in-memory directory when there is none + SqliteDatabase::Vfs vfs; + kj::String uniqueKey; + kj::Own alarmScheduler; + + // The facet index of the root actor `rootKey`, read from disk for one operation; none when it + // does not exist (so no facet can have storage). + kj::Maybe> getFacetTreeIndexIfNotEmpty( + const kj::Directory& dir, kj::StringPtr rootKey); + + kj::Path getSqlitePath(kj::StringPtr rootKey, uint facetId, kj::StringPtr suffix = ""_kj); + void deleteFacet( + const kj::Directory& dir, kj::StringPtr rootKey, FacetTreeIndex& index, uint facetId); + void deleteDescendantStorage(const kj::Directory& dir, kj::StringPtr rootKey, uint parentId); + void cloneFacet(const kj::Directory& dir, + kj::StringPtr rootKey, + FacetTreeIndex& index, + uint srcId, + uint dstId); +}; + +class RpcClient::Impl final { + public: + Impl(const WorkerFactory& factory, kj::Own connection) + : factory(factory), + connection(kj::mv(connection)), + rpcSystem(*this->connection) {} + + const WorkerFactory& factory; + kj::Own connection; + capnp::TwoPartyClient rpcSystem; +}; + +// The `WorkerdBootstrap` a peer sees over capnp: every event it starts goes to `service`. +class WorkerdBootstrapImpl final: public rpc::WorkerdBootstrap::Server { + public: + WorkerdBootstrapImpl(kj::Rc service, + capnp::HttpOverCapnpFactory& httpOverCapnpFactory); + kj::Promise startEvent(StartEventContext context) override; + + private: + kj::Rc service; + capnp::HttpOverCapnpFactory& httpOverCapnpFactory; + class EventDispatcherImpl; +}; + +// The debug port: every service's entrypoints and actors, looked up through the server. +class WorkerdDebugPortImpl final: public rpc::WorkerdDebugPort::Server { + public: + WorkerdDebugPortImpl( + ::rust::Box server, capnp::HttpOverCapnpFactory& httpOverCapnpFactory); + kj::Promise getEntrypoint(GetEntrypointContext context) override; + kj::Promise getActor(GetActorContext context) override; + + private: + ::rust::Box server; + capnp::HttpOverCapnpFactory& httpOverCapnpFactory; +}; + +// Trims the name off a durable id longer than production keeps (1024 bytes). +Worker::Actor::Id normalizeActorId(Worker::Actor::Id id); + +} // namespace workerd::server diff --git a/src/workerd/server/factory/worker-factory-listen.c++ b/src/workerd/server/factory/worker-factory-listen.c++ new file mode 100644 index 00000000000..02e082cd381 --- /dev/null +++ b/src/workerd/server/factory/worker-factory-listen.c++ @@ -0,0 +1,144 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +// The listeners' shims: the Rust rewriter's edits applied to kj headers, a response whose +// headers are edited on the way out, the `ConnectResponse` of a raw TCP socket, and a UDP flow as +// a `DatagramChannel` for `connect()` events. + +#include "worker-factory-impl.h" + +#include +#include +#include + +#include + +namespace workerd::server { + +namespace { + +// A `HeaderEdit` with its header looked up in the table. +struct Edit { + kj::HttpHeaderId id; + kj::Maybe value; +}; + +kj::Array resolveEdits( + const kj::HttpHeaderTable& table, ::rust::Slice edits) { + return KJ_MAP(edit, edits) -> Edit { + auto name = kj::str(edit.name); + return Edit{ + .id = KJ_REQUIRE_NONNULL(table.stringToId(name), "header is not in the header table", name), + .value = edit.value.map([](const ::rust::String& value) { return kj::str(value); }), + }; + }; +} + +// A response applying a socket's or external server's `injectResponseHeaders` before forwarding +// to the response it wraps. +class RewritingResponse final: public kj::HttpService::Response { + public: + RewritingResponse(kj::HttpService::Response& inner, kj::Array edits) + : inner(inner), + edits(kj::mv(edits)) {} + + kj::Own send(uint statusCode, + kj::StringPtr statusText, + const kj::HttpHeaders& headers, + kj::Maybe expectedBodySize = kj::none) override { + return inner.send(statusCode, statusText, rewrite(headers), expectedBodySize); + } + + kj::Own acceptWebSocket(const kj::HttpHeaders& headers) override { + return inner.acceptWebSocket(rewrite(headers)); + } + + private: + kj::HttpService::Response& inner; + kj::Array edits; + + kj::HttpHeaders rewrite(const kj::HttpHeaders& headers) { + auto rewritten = headers.cloneShallow(); + for (auto& edit: edits) { + KJ_IF_SOME(value, edit.value) { + rewritten.setPtr(edit.id, value); + } else { + rewritten.unset(edit.id); + } + } + return rewritten; + } +}; + +// The connect() answer of a raw TCP connection: there is no HTTP response to write. +class NullConnectResponse final: public kj::HttpService::ConnectResponse { + public: + void accept(uint statusCode, kj::StringPtr statusText, const kj::HttpHeaders& headers) override {} + kj::Own reject(uint statusCode, + kj::StringPtr statusText, + const kj::HttpHeaders& headers, + kj::Maybe expectedBodySize = kj::none) override { + return newNullOutputStream(); + } +}; + +// A Rust UDP flow as the DatagramChannel of a UdpConnectCustomEvent. +class UdpFlowChannel final: public DatagramChannel { + public: + explicit UdpFlowChannel(::rust::Box flow): flow(kj::mv(flow)) {} + + kj::Promise>> receive() override { + auto datagram = co_await flow->receive(); + if (datagram.ended) co_return kj::none; + co_return kj::heapArray(kj::from(datagram.data)); + } + + kj::Promise send(kj::ArrayPtr datagram) override { + co_await flow->send(datagram.as()); + } + + private: + ::rust::Box flow; +}; + +} // namespace + +kj::Own edit_headers(const kj::HttpHeaderTable& table, + const kj::HttpHeaders& headers, + ::rust::Slice edits, + ::rust::Slice injected) { + auto edited = kj::heap(headers.clone()); + for (auto slice: {edits, injected}) { + for (auto& edit: resolveEdits(table, slice)) { + KJ_IF_SOME(value, edit.value) { + edited->set(edit.id, kj::mv(value)); + } else { + edited->unset(edit.id); + } + } + } + return edited; +} + +kj::Own new_rewriting_response(kj::HttpService::Response& inner, + const kj::HttpHeaderTable& table, + ::rust::Slice edits) { + return kj::heap(inner, resolveEdits(table, edits)); +} + +kj::Own new_null_connect_response() { + return kj::heap(); +} + +kj::Own new_jsgify_websocket_errors() { + return kj::heap(); +} + +kj::Own new_udp_connect_event( + ::rust::Str address, ::rust::Box flow) { + auto channel = kj::heap(kj::mv(flow)); + return kj::heap(kj::str(address), *channel).attach(kj::mv(channel)); +} + +} // namespace workerd::server diff --git a/src/workerd/server/factory/worker-factory-rpc.c++ b/src/workerd/server/factory/worker-factory-rpc.c++ new file mode 100644 index 00000000000..dac982fd617 --- /dev/null +++ b/src/workerd/server/factory/worker-factory-rpc.c++ @@ -0,0 +1,420 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#include "worker-factory-impl.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace workerd::server { + +// ======================================================================================= +// WorkerdBootstrap + +// Dispatches one event to the bootstrap's service. +class WorkerdBootstrapImpl::EventDispatcherImpl final: public rpc::EventDispatcher::Server { + public: + EventDispatcherImpl(capnp::HttpOverCapnpFactory& httpOverCapnpFactory, + kj::Rc service, + kj::Maybe cfBlobJson, + Persistent fromPersistentStub) + : httpOverCapnpFactory(httpOverCapnpFactory), + service(kj::mv(service)), + cfBlobJson(kj::mv(cfBlobJson)), + fromPersistentStub(fromPersistentStub) {} + + kj::Promise getHttpService(GetHttpServiceContext context) override { + IoChannelFactory::SubrequestMetadata metadata; + metadata.cfBlobJson = cfBlobJson.clone(); + metadata.fromPersistentStub = fromPersistentStub; + context.initResults(capnp::MessageSize{4, 1}) + .setHttp(httpOverCapnpFactory.kjToCapnp(getService()->startRequest(kj::mv(metadata)))); + return kj::READY_NOW; + } + + kj::Promise sendTraces(SendTracesContext context) override { + auto traces = + KJ_MAP(trace, context.getParams().getTraces()) { return kj::refcounted(trace); }; + auto event = kj::heap(api::TraceCustomEvent::TYPE, kj::mv(traces)); + auto worker = getWorker(); + auto result = co_await worker->customEvent(kj::mv(event)); + context.getResults().getResult().setOutcome(result.outcome); + } + + kj::Promise prewarm(PrewarmContext context) override { + throwUnsupported(); + } + kj::Promise runScheduled(RunScheduledContext context) override { + throwUnsupported(); + } + kj::Promise runAlarm(RunAlarmContext context) override { + throwUnsupported(); + } + kj::Promise queue(QueueContext context) override { + throwUnsupported(); + } + + kj::Promise jsRpcSession(JsRpcSessionContext context) override { + return api::JsRpcSessionCustomEvent::receiveRpc(context, getWorker()); + } + + kj::Promise udpConnect(UdpConnectContext context) override { + auto worker = getWorker(); + auto& workerRef = *worker; + return api::UdpConnectCustomEvent::receiveRpc(context, workerRef).attach(kj::mv(worker)); + } + + kj::Promise tailStreamSession(TailStreamSessionContext context) override { + auto customEvent = kj::heap(); + auto cap = customEvent->getCap(); + capnp::PipelineBuilder pipelineBuilder; + pipelineBuilder.setTopLevel(cap); + context.setPipeline(pipelineBuilder.build()); + context.getResults().setTopLevel(kj::mv(cap)); + + auto worker = getWorker(); + auto result = co_await worker->customEvent(kj::mv(customEvent)).attach(kj::mv(worker)); + context.getResults().setResult(result.outcome); + } + + private: + capnp::HttpOverCapnpFactory& httpOverCapnpFactory; + kj::Maybe> service; + kj::Maybe cfBlobJson; + Persistent fromPersistentStub; + + kj::Rc getService() { + auto result = + kj::mv(KJ_ASSERT_NONNULL(service, "EventDispatcher can only be used for one request")); + service = kj::none; + return result; + } + + // Events other than HTTP carry no cf blob. + kj::Own getWorker() { + return getService()->startRequest({}); + } + + [[noreturn]] void throwUnsupported() { + JSG_FAIL_REQUIRE(Error, "RPC connections don't yet support this event type."); + } +}; + +WorkerdBootstrapImpl::WorkerdBootstrapImpl(kj::Rc service, + capnp::HttpOverCapnpFactory& httpOverCapnpFactory) + : service(kj::mv(service)), + httpOverCapnpFactory(httpOverCapnpFactory) {} + +kj::Promise WorkerdBootstrapImpl::startEvent(StartEventContext context) { + auto params = context.getParams(); + kj::Maybe cfBlobJson; + if (params.hasCfBlobJson()) cfBlobJson = kj::str(params.getCfBlobJson()); + context.initResults(capnp::MessageSize{4, 1}) + .setDispatcher(kj::heap(httpOverCapnpFactory, service->addRef(), + kj::mv(cfBlobJson), Persistent(params.getFromPersistentStub()))); + return kj::READY_NOW; +} + +kj::Promise factory_accept_bootstrap(const WorkerFactory& factory, + kj::Own stream, + ::rust::Box target) { + capnp::TwoPartyServer server(kj::heap( + subrequest_channel_into_kj(kj::mv(target)), factory.getImpl().httpOverCapnpFactory)); + co_await server.accept(*stream); +} + +// ======================================================================================= +// Debug port + +WorkerdDebugPortImpl::WorkerdDebugPortImpl( + ::rust::Box server, capnp::HttpOverCapnpFactory& httpOverCapnpFactory) + : server(kj::mv(server)), + httpOverCapnpFactory(httpOverCapnpFactory) {} + +kj::Promise WorkerdDebugPortImpl::getEntrypoint(GetEntrypointContext context) { + auto params = context.getParams(); + kj::Maybe> props; + if (params.hasProps()) props = toOwn(Frankenvalue::fromCapnp(params.getProps())); + kj::Maybe entrypoint; + if (params.hasEntrypoint()) entrypoint = params.getEntrypoint(); + auto target = subrequest_channel_into_kj(server->resolve_debug_entrypoint( + toRust(params.getService()), toRust(entrypoint), kj::mv(props))); + context.initResults(capnp::MessageSize{4, 1}) + .setEntrypoint(kj::heap(kj::mv(target), httpOverCapnpFactory)); + return kj::READY_NOW; +} + +kj::Promise WorkerdDebugPortImpl::getActor(GetActorContext context) { + auto params = context.getParams(); + auto target = subrequest_channel_into_kj(server->resolve_debug_actor( + toRust(params.getService()), toRust(params.getEntrypoint()), toRust(params.getActorId()))); + context.initResults(capnp::MessageSize{4, 1}) + .setActor(kj::heap(kj::mv(target), httpOverCapnpFactory)); + return kj::READY_NOW; +} + +kj::Promise factory_accept_debug_port( + const WorkerFactory& factory, kj::Own stream) { + auto& impl = factory.getImpl(); + capnp::TwoPartyServer rpcServer(rpc::WorkerdDebugPort::Client( + kj::heap(impl.getServer().server_clone(), impl.httpOverCapnpFactory))); + co_await rpcServer.accept(*stream); +} + +// ======================================================================================= +// RPC client + +RpcClient::RpcClient(kj::Own impl): impl(kj::mv(impl)) {} +RpcClient::~RpcClient() noexcept(false) = default; +RpcClient::Impl& RpcClient::getImpl() const { + return *impl; +} + +kj::Own new_rpc_client(const WorkerFactory& factory, kj::Own stream) { + return kj::heap(kj::heap(factory, kj::mv(stream))); +} + +kj::Promise rpc_client_custom_event(const RpcClient& client, + kj::Own event, + kj::Maybe<::rust::Str> cfBlobJson) { + auto& impl = client.getImpl(); + auto bootstrap = impl.rpcSystem.bootstrap().castAs(); + auto request = bootstrap.startEventRequest(capnp::MessageSize{4, 0}); + KJ_IF_SOME(cf, cfBlobJson) request.setCfBlobJson(kj::str(cf)); + auto dispatcher = request.send().getDispatcher(); + // Workerd-to-workerd RPC does not support `restore()`, so Frankenvalues need no handler. + auto& factory = impl.factory.getImpl(); + auto result = co_await event->sendRpc(factory.httpOverCapnpFactory, factory.byteStreamFactory, + getUnsupportedFrankenvalueHandler(), kj::mv(dispatcher)); + co_return rust::worker::CustomEventResult{.outcome = rust::worker::toRustOutcome(result.outcome)}; +} + +kj::Promise rpc_client_on_disconnect(const RpcClient& client) { + return client.getImpl().rpcSystem.onDisconnect(); +} + +// ======================================================================================= +// Inspector +// +// The devtools inspector protocol starts with HTTP GETs to /json/version and /json (or +// /json/list), which list the isolates available for inspection, each with a URL and an id the +// client then opens a WebSocket to. The Cloudflare devtools show only the first service in the +// config; Chrome's devtools can inspect every one. + +class InspectorService final: public kj::HttpService, public kj::HttpServerErrorHandler { + public: + InspectorService(kj::Own isolateThreadExecutor, + kj::Timer& timer, + kj::HttpHeaderTable::Builder& headerTableBuilder, + InspectorServiceIsolateRegistrar& registrar) + : isolateThreadExecutor(kj::mv(isolateThreadExecutor)), + timer(timer), + headerTable(headerTableBuilder.getFutureTable()), + server(timer, headerTable, *this, kj::HttpServerSettings{.errorHandler = *this}), + registrar(registrar) { + registrar.attach(*this); + } + + ~InspectorService() noexcept(false) { + KJ_IF_SOME(r, registrar) r.detach(); + } + + void invalidateRegistrar() { + registrar = kj::none; + } + + kj::Promise handleApplicationError( + kj::Exception exception, kj::Maybe response) override { + if (exception.getType() == kj::Exception::Type::DISCONNECTED) { + // Just close the connection. + co_return; + } + KJ_LOG(ERROR, kj::str("Uncaught exception: ", exception)); + KJ_IF_SOME(r, response) { + co_return co_await r.sendError(500, "Internal Server Error", headerTable); + } + } + + kj::Promise request(kj::HttpMethod method, + kj::StringPtr url, + const kj::HttpHeaders& headers, + kj::AsyncInputStream& requestBody, + kj::HttpService::Response& response) override { + kj::HttpHeaders responseHeaders(headerTable); + if (headers.isWebSocket()) { + KJ_IF_SOME(pos, url.findLast('/')) { + auto id = url.slice(pos + 1); + KJ_IF_SOME(isolate, isolates.find(id)) { + // The isolate is held weakly so that it need not know about the inspector; a weak ref + // that no longer upgrades means the isolate is gone and the entry is dropped. + KJ_IF_SOME(ref, isolate->tryAddStrongRef()) { + KJ_LOG(INFO, kj::str("Inspector client attaching [", id, "]")); + auto webSocket = response.acceptWebSocket(responseHeaders); + kj::Duration timerOffset = 0 * kj::MILLISECONDS; + try { + co_return co_await ref->attachInspector( + isolateThreadExecutor->addRef(), timer, timerOffset, *webSocket); + } catch (...) { + auto exception = kj::getCaughtExceptionAsKj(); + if (exception.getType() == kj::Exception::Type::DISCONNECTED) { + KJ_LOG(INFO, "Inspector client detached"_kj); + co_return; + } + kj::throwFatalException(kj::mv(exception)); + } + } else { + isolates.erase(id); + } + } + KJ_LOG(INFO, kj::str("Unknown worker session [", id, "]")); + co_return co_await response.sendError(404, "Unknown worker session", responseHeaders); + } + co_return co_await response.sendError(400, "Invalid request", responseHeaders); + } + + if (method != kj::HttpMethod::GET) { + co_return co_await response.sendError(501, "Unsupported Operation", responseHeaders); + } + + if (url.endsWith("/json/version")) { + responseHeaders.set(kj::HttpHeaderId::CONTENT_TYPE, MimeType::JSON.toString()); + auto content = kj::str("{\"Browser\": \"workerd\", \"Protocol-Version\": \"1.3\" }"); + auto out = response.send(200, "OK", responseHeaders, content.size()); + co_return co_await out->write(content.asBytes()); + } else if (url.endsWith("/json") || url.endsWith("/json/list") || + url.endsWith("/json/list?for_tab")) { + responseHeaders.set(kj::HttpHeaderId::CONTENT_TYPE, MimeType::JSON.toString()); + auto baseWsUrl = KJ_UNWRAP_OR(headers.get(kj::HttpHeaderId::HOST), + { co_return co_await response.sendError(400, "Bad Request", responseHeaders); }); + + kj::Vector entries(isolates.size()); + kj::Vector toRemove; + for (auto& entry: isolates) { + // Upgrading the weak ref tells whether the isolate still exists. + KJ_IF_SOME(ref, entry.value->tryAddStrongRef()) { + (void)ref; + kj::Vector fields(9); + fields.add(kj::str("\"id\":\"", entry.key, "\"")); + fields.add(kj::str("\"title\":\"workerd: worker ", entry.key, "\"")); + fields.add(kj::str("\"type\":\"node\"")); + fields.add(kj::str("\"description\":\"workerd worker\"")); + fields.add(kj::str("\"webSocketDebuggerUrl\":\"ws://", baseWsUrl, "/", entry.key, "\"")); + fields.add(kj::str( + "\"devtoolsFrontendUrl\":\"devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=", + baseWsUrl, "/\"")); + fields.add(kj::str( + "\"devtoolsFrontendUrlCompat\":\"devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=", + baseWsUrl, "/\"")); + fields.add(kj::str("\"faviconUrl\":\"https://workers.cloudflare.com/favicon.ico\"")); + fields.add(kj::str("\"url\":\"https://workers.dev\"")); + entries.add(kj::str('{', kj::strArray(fields, ","), '}')); + } else { + toRemove.add(kj::str(entry.key)); + } + } + for (auto& key: toRemove) { + isolates.erase(key); + } + + auto content = kj::str('[', kj::strArray(entries, ","), ']'); + auto out = response.send(200, "OK", responseHeaders, content.size()); + co_return co_await out->write(content.asBytes()).attach(kj::mv(content), kj::mv(out)); + } + + co_return co_await response.sendError(500, "Not yet implemented", responseHeaders); + } + + // Inspector connections are long-lived WebSockets that must not hold the server open, so they + // live on this HttpServer's own TaskSet and take no part in draining. + kj::Promise listen(kj::Own listener) { + co_return co_await server.listenHttp(*listener); + } + + void registerIsolate(kj::StringPtr name, Worker::Isolate& isolate) { + isolates.insert(kj::str(name), isolate.getWeakRef()); + } + + private: + kj::Own isolateThreadExecutor; + kj::Timer& timer; + kj::HttpHeaderTable& headerTable; + kj::HashMap> isolates; + kj::HttpServer server; + kj::Maybe registrar; +}; + +InspectorServiceIsolateRegistrar::~InspectorServiceIsolateRegistrar() noexcept(true) { + KJ_IF_SOME(service, *inspectorService.lockExclusive()) { + service.invalidateRegistrar(); + } +} + +void InspectorServiceIsolateRegistrar::registerIsolate( + kj::StringPtr name, Worker::Isolate& isolate) { + KJ_IF_SOME(service, *inspectorService.lockExclusive()) { + service.registerIsolate(name, isolate); + } +} + +void InspectorServiceIsolateRegistrar::attach(InspectorService& service) { + *inspectorService.lockExclusive() = service; +} + +void InspectorServiceIsolateRegistrar::detach() { + *inspectorService.lockExclusive() = kj::none; +} + +uint startInspector(kj::String inspectorAddress, InspectorServiceIsolateRegistrar& registrar) { + static constexpr uint UNASSIGNED_PORT = 0; + static constexpr uint DEFAULT_PORT = 9229; + kj::MutexGuarded inspectorPort(UNASSIGNED_PORT); + + // V8 requires CPU profiling to start and stop on the thread that runs JavaScript, so inspector + // messages are dispatched on this (the isolate) thread: its executor goes to the inspector + // service, which `Isolate::attachInspector()` uses to run its dispatch loop here. + auto isolateThreadExecutor = kj::getCurrentThreadExecutor().addRef(); + + kj::Thread thread([inspectorAddress = kj::mv(inspectorAddress), &inspectorPort, ®istrar, + isolateThreadExecutor = kj::mv(isolateThreadExecutor)]() mutable { + kj::AsyncIoContext io = kj::setupAsyncIo(); + kj::HttpHeaderTable::Builder headerTableBuilder; + auto inspectorService = kj::heap( + kj::mv(isolateThreadExecutor), io.provider->getTimer(), headerTableBuilder, registrar); + auto ownHeaderTable = headerTableBuilder.build(); + auto& network = io.provider->getNetwork(); + + // A failure to listen is not reported: the port is never assigned, and the starting thread + // keeps waiting for it. + auto listen = (kj::coCapture( + [&network, &inspectorAddress, &inspectorPort, &inspectorService]() -> kj::Promise { + auto parsed = co_await network.parseAddress(inspectorAddress, DEFAULT_PORT); + auto listener = parsed->listen(); + // Signals the starting thread that the inspector is ready. + *inspectorPort.lockExclusive() = listener->getPort(); + KJ_LOG(INFO, "Inspector is listening"); + co_await inspectorService->listen(kj::mv(listener)); + }))(); + + kj::NEVER_DONE.wait(io.waitScope); + }); + thread.detach(); + + return inspectorPort.when([](const uint& port) { return port != UNASSIGNED_PORT; }, + [](const uint& port) { return port; }); +} + +uint16_t factory_start_inspector(const WorkerFactory& factory, ::rust::Str address) { + auto& impl = factory.getImpl(); + KJ_REQUIRE(impl.inspectorRegistrar == kj::none, "the inspector is already running"); + auto& registrar = *impl.inspectorRegistrar.emplace(kj::heap()); + return startInspector(kj::str(address), registrar); +} + +} // namespace workerd::server diff --git a/src/workerd/server/factory/worker-factory.c++ b/src/workerd/server/factory/worker-factory.c++ new file mode 100644 index 00000000000..c9386fe65a7 --- /dev/null +++ b/src/workerd/server/factory/worker-factory.c++ @@ -0,0 +1,1704 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#include "worker-factory-impl.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace workerd::server { + +// Builds the header table once the headers the config's HttpOptions name (injected, +// forwarded-proto and cf blob headers: external servers' first, then sockets') are in it. kj +// writes a table's headers ahead of the others, in the table's order and spelling, so these reach +// the wire as the config spells them. The names point into the config message. +static kj::Own buildHeaderTable( + kj::HttpHeaderTable::Builder& builder, config::Config::Reader config) { + auto add = [&](config::HttpOptions::Reader options) { + for (auto header: options.getInjectRequestHeaders()) builder.add(header.getName()); + for (auto header: options.getInjectResponseHeaders()) builder.add(header.getName()); + if (options.hasForwardedProtoHeader()) builder.add(options.getForwardedProtoHeader()); + if (options.hasCfBlobHeader()) builder.add(options.getCfBlobHeader()); + }; + for (auto service: config.getServices()) { + if (!service.isExternal()) continue; + auto external = service.getExternal(); + if (external.isHttp()) add(external.getHttp()); + if (external.isHttps()) add(external.getHttps().getOptions()); + } + for (auto socket: config.getSockets()) { + if (socket.isHttp()) add(socket.getHttp()); + if (socket.isHttps()) add(socket.getHttps().getOptions()); + } + return builder.build(); +} + +// ======================================================================================= +// WorkerFactory + +WorkerFactory::Impl::Impl(WorkerFactory& factory, + jsg::V8System& v8System, + kj::Timer& timer, + const kj::MonotonicClock& monotonicClock, + kj::Network& network, + kj::EntropySource& entropySource, + kj::Filesystem& fs, + kj::Own options, + ::rust::Vec config) + : v8System(v8System), + timer(timer), + monotonicClock(monotonicClock), + network(network), + entropySource(entropySource), + fs(fs), + options(kj::mv(options)), + configMessage(kj::mv(config)), + configReader(asWords(kj::from(configMessage)), CONFIG_READER_OPTIONS), + config(configReader.getRoot()), + httpOverCapnpFactory( + byteStreamFactory, headerTableBuilder, capnp::HttpOverCapnpFactory::LEVEL_2), + threadContext( + timer, entropySource, headerTableBuilder, httpOverCapnpFactory, byteStreamFactory), + headerTable(buildHeaderTable(headerTableBuilder, this->config)), + memoryCacheProvider(kj::heap()), + channelTokenHandler(factory), + tasks(*this) { + // The config's logging settings go on top of the caller's. + auto& logging = this->options->loggingOptions; + auto& root = this->config; + if (root.hasLogging()) { + auto conf = root.getLogging(); + logging.structuredLogging = StructuredLogging(conf.getStructuredLogging()); + if (conf.hasStdoutPrefix()) { + logging.stdoutPrefix = kj::ConstString(kj::str(conf.getStdoutPrefix())); + } + if (conf.hasStderrPrefix()) { + logging.stderrPrefix = kj::ConstString(kj::str(conf.getStderrPrefix())); + } + } else { + logging.structuredLogging = StructuredLogging(root.getStructuredLogging()); + } +} + +const ServerHandle& WorkerFactory::Impl::getServer() const { + return *KJ_REQUIRE_NONNULL(server, "the server has not registered with the worker factory"); +} + +void WorkerFactory::Impl::taskFailed(kj::Exception&& exception) { + KJ_LOG(ERROR, exception); +} + +WorkerFactory::WorkerFactory(jsg::V8System& v8System, + kj::Timer& timer, + const kj::MonotonicClock& monotonicClock, + kj::Network& network, + kj::EntropySource& entropySource, + kj::Filesystem& fs, + kj::Own options, + ::rust::Vec config) + : impl(kj::heap(*this, + v8System, + timer, + monotonicClock, + network, + entropySource, + fs, + kj::mv(options), + kj::mv(config))) {} + +WorkerFactory::~WorkerFactory() noexcept(false) = default; + +void factory_spawn(const WorkerFactory& factory, ::rust::Box task) { + auto& impl = factory.getImpl(); + ++impl.spawnCount; + // The task starts on the next turn of the event loop, not inside the spawn: a stub's destructor + // spawns the unlink of its worker precisely because that cannot run where the stub is dropped. + impl.tasks.add(kj::evalLater( + [task = kj::mv(task)]() mutable -> kj::Promise { return task_run(kj::mv(task)); })); +} +void factory_clear_tasks(const WorkerFactory& factory) { + factory.getImpl().tasks.clear(); +} +kj::Promise factory_settle_tasks(const WorkerFactory& factory) { + auto& impl = factory.getImpl(); + // Dropping the server spawns tasks that do deferred work (a dropped stub unlinks its worker on + // the next turn), which may spawn more; every turn's spawns run before the tasks are dropped. + uint64_t seen; + do { + seen = impl.spawnCount; + co_await kj::evalLater([]() {}); + } while (impl.spawnCount != seen); + impl.tasks.clear(); +} +uint64_t factory_timer_now(const WorkerFactory& factory) { + return (factory.getImpl().timer.now() - kj::origin()) / kj::NANOSECONDS; +} +kj::Promise factory_sleep(const WorkerFactory& factory, uint64_t nanos) { + return factory.getImpl().timer.afterDelay(nanos * kj::NANOSECONDS); +} +WorkerFactory::Impl& WorkerFactory::getImpl() const { + return *impl; +} + +kj::Rc WorkerFactory::resolveEntrypoint( + kj::StringPtr serviceName, + kj::Maybe entrypoint, + Frankenvalue props, + Persistent persistent) { + return subrequest_channel_into_kj(impl->getServer().resolve_entrypoint( + toRust(serviceName), toRust(entrypoint), toOwn(kj::mv(props)), persistent.toBool())); +} + +kj::Rc WorkerFactory::resolveActorClass( + kj::StringPtr serviceName, + kj::Maybe entrypoint, + Frankenvalue props, + Persistent persistent) { + return actor_class_channel_into_kj(impl->getServer().resolve_actor_class( + toRust(serviceName), toRust(entrypoint), toOwn(kj::mv(props)), persistent.toBool())); +} + +kj::Rc WorkerFactory::resolveActor(kj::StringPtr namespaceKey, + kj::ArrayPtr id, + kj::Maybe name, + Persistent persistent) { + auto idObj = kj::heap( + normalizeActorId(ActorIdFactoryImpl(namespaceKey).idFromRaw(id, name.clone()))); + return subrequest_channel_into_kj( + impl->getServer().resolve_actor(toRust(namespaceKey), kj::mv(idObj), persistent.toBool())); +} + +::rust::Slice factory_config(const WorkerFactory& factory) { + return kj::from(factory.getImpl().configMessage).asBytes().as(); +} +const kj::HttpHeaderTable& factory_header_table(const WorkerFactory& factory) { + return *factory.getImpl().headerTable; +} + +namespace { +// The encoders take props by mutable reference for the sake of channels they refcount; the +// props are otherwise unchanged. +Frankenvalue& propsRef(kj::Maybe props) { + static Frankenvalue EMPTY_PROPS; + KJ_IF_SOME(p, props) return const_cast(p); + return EMPTY_PROPS; +} +} // namespace + +kj::Own factory_encode_subrequest_token(const WorkerFactory& factory, + ::rust::Str serviceName, + kj::Maybe<::rust::Str> entrypoint, + kj::Maybe props, + bool persistent, + TokenUsage usage) { + auto service = kj::str(serviceName); + auto ep = toKj(entrypoint); + return kj::heap(factory.getImpl().channelTokenHandler.encodeSubrequestChannelToken( + usage, service, ep.map([](kj::String& e) -> kj::StringPtr { return e; }), propsRef(props), + Persistent(persistent))); +} + +kj::Own factory_encode_actor_class_token(const WorkerFactory& factory, + ::rust::Str serviceName, + ::rust::Str className, + kj::Maybe props, + bool persistent, + TokenUsage usage) { + auto service = kj::str(serviceName); + auto cls = kj::str(className); + return kj::heap(factory.getImpl().channelTokenHandler.encodeActorClassChannelToken( + usage, service, cls.asPtr(), propsRef(props), Persistent(persistent))); +} + +kj::Own factory_encode_actor_token(const WorkerFactory& factory, + ::rust::Str uniqueKey, + const ActorIdHandle& id, + bool persistent, + TokenUsage usage) { + auto& abstractId = *KJ_REQUIRE_NONNULL( + id.tryGet>(), "only durable actors have channel tokens"); + auto& idImpl = + KJ_ASSERT_NONNULL(kj::tryDowncast(abstractId)); + return kj::heap(factory.getImpl().channelTokenHandler.encodeActorChannelToken( + usage, kj::str(uniqueKey), idImpl.getRaw(), idImpl.getName(), Persistent(persistent))); +} + +void factory_set_server(const WorkerFactory& factory, ::rust::Box server) { + factory.getImpl().server = kj::mv(server); +} + +bool factory_experimental(const WorkerFactory& factory) { + return factory.getImpl().options->experimental; +} + +Worker::Actor::Id normalizeActorId(Worker::Actor::Id id) { + KJ_IF_SOME(doId, id.tryGet>()) { + KJ_IF_SOME(name, doId->getName()) { + if (name.size() > 1024) { + KJ_ASSERT_NONNULL(kj::tryDowncast(*doId)).clearName(); + } + } + } + return kj::mv(id); +} + +// ======================================================================================= +// Single-tenant policy: no limits, and tracing that feeds tail workers directly. + +namespace { + +class NullIsolateLimitEnforcer final: public IsolateLimitEnforcer { + public: + v8::Isolate::CreateParams getCreateParams() override { + return {}; + } + void customizeIsolate(v8::Isolate* isolate) override {} + ActorCacheSharedLruOptions getActorCacheLruOptions() override { + return {.softLimit = 16 * (1ull << 20), + .hardLimit = 128 * (1ull << 20), + .staleTimeout = 30 * kj::SECONDS, + .dirtyListByteLimit = 8 * (1ull << 20), + .maxKeysPerRpc = 128, + // In-memory-only actors: the cache never flushes to its (empty) backing storage. + .neverFlush = true}; + } + kj::Own enterStartupJs(jsg::Lock&, kj::OneOf&) const override { + return {}; + } + kj::Own enterStartupPython( + jsg::Lock&, kj::OneOf&) const override { + return {}; + } + kj::Own enterDynamicImportJs( + jsg::Lock&, kj::OneOf&) const override { + return {}; + } + kj::Own enterLoggingJs(jsg::Lock&, kj::OneOf&) const override { + return {}; + } + kj::Own enterInspectorJs( + jsg::Lock&, kj::OneOf&) const override { + return {}; + } + void completedRequest(kj::StringPtr id) const override {} + bool exitJs(jsg::Lock& lock) const override { + return false; + } + void reportMetrics(IsolateObserver& isolateMetrics) const override {} + kj::Maybe checkPbkdfIterations(jsg::Lock& lock, size_t iterations) const override { + return kj::none; + } + bool hasExcessivelyExceededHeapLimit() const override { + return false; + } + const TrackedWasmInstanceList& getTrackedWasmInstances() const override { + return trackedWasmInstances; + } + + private: + TrackedWasmInstanceList trackedWasmInstances; +}; + +class NullLimitEnforcer final: public LimitEnforcer, public kj::Refcounted { + public: + kj::Own enterJs(jsg::Lock& lock, IoContext& context) override { + return {}; + } + void topUpActor() override {} + void newSubrequest(bool isInHouse) override {} + void newKvRequest(KvOpType op) override {} + void newAnalyticsEngineRequest() override {} + kj::Promise limitDrain() override { + return kj::NEVER_DONE; + } + kj::Promise limitScheduled() override { + return kj::NEVER_DONE; + } + kj::Duration getAlarmLimit() override { + return 15 * kj::MINUTES; + } + size_t getBufferingLimit() override { + return kj::maxValue; + } + kj::Maybe getLimitsExceeded() override { + return kj::none; + } + kj::Promise onLimitsExceeded() override { + return kj::NEVER_DONE; + } + void setCpuLimitNearlyExceededCallback(kj::Function cb) override {} + void requireLimitsNotExceeded() override {} + void reportMetrics(RequestObserver& requestMetrics) override {} + kj::Duration consumeTimeElapsedForPeriodicLogging() override { + return 0 * kj::SECONDS; + } + size_t getSqliteMemoryUsage() const override { + return 0; + } +}; + +// Records a request's outcome on its tracer, wrapping the WorkerInterface to observe failures. +class RequestObserverWithTracer final: public RequestObserver, public WorkerInterface { + public: + explicit RequestObserverWithTracer(kj::Maybe> tracer) + : tracer(kj::mv(tracer)) {} + + ~RequestObserverWithTracer() noexcept(false) { + KJ_IF_SOME(t, tracer) { + KJ_IF_SOME(ioContext, IoContext::tryCurrent()) { + t->recordTimestamp(ioContext.now()); + } + t->setOutcome(outcome, 0 * kj::MILLISECONDS, 0 * kj::MILLISECONDS); + } + } + + WorkerInterface& wrapWorkerInterface(WorkerInterface& worker) override { + if (tracer != kj::none) { + inner = worker; + return *this; + } + return worker; + } + + void reportFailure( + const kj::Exception& exception, FailureSource source = FailureSource::OTHER) override { + if (outcome == EventOutcome::OK) { + outcome = RequestObserver::outcomeFromException(exception, source); + } + } + + kj::Promise request(kj::HttpMethod method, + kj::StringPtr url, + const kj::HttpHeaders& headers, + kj::AsyncInputStream& requestBody, + kj::HttpService::Response& response) override { + co_return co_await observe( + KJ_ASSERT_NONNULL(inner).request(method, url, headers, requestBody, response)); + } + kj::Promise connect(kj::StringPtr host, + const kj::HttpHeaders& headers, + kj::AsyncIoStream& connection, + ConnectResponse& response, + kj::HttpConnectSettings settings) override { + co_return co_await observe( + KJ_ASSERT_NONNULL(inner).connect(host, headers, connection, response, settings)); + } + kj::Promise prewarm(kj::StringPtr url) override { + co_return co_await observe(KJ_ASSERT_NONNULL(inner).prewarm(url)); + } + kj::Promise runScheduled(kj::Date scheduledTime, kj::StringPtr cron) override { + auto result = co_await observe(KJ_ASSERT_NONNULL(inner).runScheduled(scheduledTime, cron)); + if (outcome == EventOutcome::OK) outcome = result.outcome; + co_return result; + } + kj::Promise runAlarm(kj::Date scheduledTime, uint32_t retryCount) override { + auto result = co_await observe(KJ_ASSERT_NONNULL(inner).runAlarm(scheduledTime, retryCount)); + if (outcome == EventOutcome::OK) outcome = result.outcome; + co_return result; + } + kj::Promise test() override { + co_return co_await observe(KJ_ASSERT_NONNULL(inner).test()); + } + kj::Promise customEvent(kj::Own event) override { + auto result = co_await observe(KJ_ASSERT_NONNULL(inner).customEvent(kj::mv(event))); + if (outcome == EventOutcome::OK) outcome = result.outcome; + co_return result; + } + kj::Promise> abandonAlarm(kj::Date scheduledTime) override { + co_return co_await KJ_ASSERT_NONNULL(inner).abandonAlarm(scheduledTime); + } + + private: + kj::Maybe> tracer; + kj::Maybe inner; + EventOutcome outcome = EventOutcome::OK; + + // Reports the promise's failure, if any, before propagating it. + template + kj::Promise observe(kj::Promise promise) { + try { + co_return co_await promise; + } catch (...) { + auto exception = kj::getCaughtExceptionAsKj(); + reportFailure(exception); + kj::throwFatalException(kj::mv(exception)); + } + } +}; + +class SequentialSpanSubmitter final: public SpanSubmitter { + public: + SequentialSpanSubmitter(kj::Own weakTracer, kj::EntropySource& entropySource) + : weakTracer(kj::mv(weakTracer)), + entropySource(entropySource) {} + KJ_DISALLOW_COPY_AND_MOVE(SequentialSpanSubmitter); + + void submitSpanClose( + tracing::SpanId spanId, kj::Date startTime, kj::Date endTime, Span::TagMap&& tags) override { + weakTracer->runIfAlive([&](BaseTracer& tracer) { + tracing::SpanEndData spanEnd(spanId, endTime, kj::mv(tags)); + if (isPredictableModeForTest()) { + startTime = spanEnd.endTime = kj::UNIX_EPOCH; + } + tracer.addSpanClose(kj::mv(spanEnd), startTime); + }); + } + + void submitSpanUpdate(tracing::SpanId spanId, tracing::SpanUpdate&& update) override { + weakTracer->runIfAlive( + [&](BaseTracer& tracer) { tracer.addSpanUpdate(spanId, kj::mv(update)); }); + } + + void submitSpanException(tracing::SpanId spanId, + kj::Date timestamp, + kj::Maybe code, + kj::String name, + kj::String message, + kj::Maybe stack) override { + weakTracer->runIfAlive([&](BaseTracer& tracer) { + if (isPredictableModeForTest()) { + timestamp = kj::UNIX_EPOCH; + } + tracer.addSpanException( + spanId, timestamp, kj::mv(code), kj::mv(name), kj::mv(message), kj::mv(stack)); + }); + } + + bool submitSpanOpen(tracing::SpanId spanId, + tracing::SpanId parentSpanId, + kj::ConstString operationName, + kj::Date startTime) override { + bool submitted = false; + weakTracer->runIfAlive([&](BaseTracer& tracer) { + if (isPredictableModeForTest()) { + startTime = kj::UNIX_EPOCH; + } + tracer.addSpanOpen(spanId, parentSpanId, kj::mv(operationName), startTime); + submitted = true; + }); + return submitted; + } + + tracing::SpanId makeSpanId() override { + if (isPredictableModeForTest()) { + return tracing::SpanId(nextSpanId++); + } + return tracing::SpanId::fromEntropy(entropySource); + } + + private: + uint64_t nextSpanId = 1; + kj::Own weakTracer; + kj::EntropySource& entropySource; +}; + +} // namespace + +// ======================================================================================= +// The server's channels as KJ channels + +SubrequestChannelHandle::SubrequestChannelHandle(::rust::Box channel) + : channel(kj::mv(channel)) {} + +kj::Own SubrequestChannelHandle::startRequest( + IoChannelFactory::SubrequestMetadata metadata) { + return channel->start_request(kj::heap(kj::mv(metadata))); +} +kj::Promise SubrequestChannelHandle::evictForTest(IoChannelFactory::EvictWebSocketMode mode) { + return channel->evict_for_test(mode == IoChannelFactory::EvictWebSocketMode::HIBERNATE); +} +void SubrequestChannelHandle::requireAllowsTransfer() { + channel->require_allows_transfer(); +} +kj::OneOf, kj::Promise>> SubrequestChannelHandle::getTokenMaybeSync( + IoChannelFactory::ChannelTokenUsage usage) { + return kj::mv(*channel->token(usage)); +} + +ActorClassChannelHandle::ActorClassChannelHandle(::rust::Box channel) + : channel(kj::mv(channel)) {} + +kj::Maybe<::rust::Box> ActorClassChannelHandle::tryUnwrap( + IoChannelFactory::ActorClassChannel& channel) { + KJ_IF_SOME(handle, kj::tryDowncast(channel)) { + return handle.channel->actor_class_channel_clone(); + } + return kj::none; +} + +void ActorClassChannelHandle::requireAllowsTransfer() { + channel->require_allows_transfer(); +} +kj::OneOf, kj::Promise>> ActorClassChannelHandle::getTokenMaybeSync( + IoChannelFactory::ChannelTokenUsage usage) { + return kj::mv(*channel->token(usage)); +} + +kj::Rc subrequest_channel_into_kj(::rust::Box channel) { + return kj::rc(kj::mv(channel)); +} +kj::Rc actor_class_channel_into_kj( + ::rust::Box channel) { + return kj::rc(kj::mv(channel)); +} + +// ======================================================================================= +// Requests + +namespace { + +// A dynamic worker's stub as the runtime's `WorkerStubChannel`. +class WorkerStubChannelHandle final: public WorkerStubChannel { + public: + explicit WorkerStubChannelHandle(::rust::Box stub): stub(kj::mv(stub)) {} + + kj::Rc getEntrypointResolved( + kj::Maybe name, Frankenvalue props, kj::Maybe limits) override { + return subrequest_channel_into_kj(stub->entrypoint(toRust(name), toOwn(kj::mv(props)))); + } + kj::Rc getActorClassResolved( + kj::Maybe name, Frankenvalue props, kj::Maybe limits) override { + return actor_class_channel_into_kj(stub->actor_class(toRust(name), toOwn(kj::mv(props)))); + } + + private: + ::rust::Box stub; + + static kj::Maybe<::rust::Str> toRust(kj::Maybe& name) { + return name.map([](kj::String& n) { return server::toRust(n); }); + } +}; + +class CacheHttpClientImpl final: public kj::HttpClient { + public: + CacheHttpClientImpl(IoChannelFactory::SubrequestChannel& parent, + kj::HttpHeaderId cacheNamespaceHeader, + kj::Maybe cacheName, + kj::Maybe cfBlobJson, + SpanParent parentSpan) + : client(asHttpClient(parent.startRequest({kj::mv(cfBlobJson), kj::mv(parentSpan)}))), + cacheName(kj::mv(cacheName)), + cacheNamespaceHeader(cacheNamespaceHeader) {} + + Request request(kj::HttpMethod method, + kj::StringPtr url, + const kj::HttpHeaders& headers, + kj::Maybe expectedBodySize = kj::none) override { + auto headersCopy = headers.cloneShallow(); + KJ_IF_SOME(name, cacheName) { + headersCopy.setPtr(cacheNamespaceHeader, name); + } + return client->request(method, url, headersCopy, expectedBodySize); + } + + private: + kj::Own client; + kj::Maybe cacheName; + kj::HttpHeaderId cacheNamespaceHeader; +}; + +class CacheClientImpl final: public CacheClient { + public: + CacheClientImpl(kj::Rc cacheService, + kj::HttpHeaderId cacheNamespaceHeader) + : cacheService(kj::mv(cacheService)), + cacheNamespaceHeader(cacheNamespaceHeader) {} + + kj::Own getDefault(CacheClient::SubrequestMetadata metadata) override { + return kj::heap(*cacheService, cacheNamespaceHeader, kj::none, + kj::mv(metadata.cfBlobJson), kj::mv(metadata.parentSpan)); + } + kj::Own getNamespace( + kj::StringPtr cacheName, CacheClient::SubrequestMetadata metadata) override { + return kj::heap(*cacheService, cacheNamespaceHeader, + kj::encodeUriComponent(cacheName), kj::mv(metadata.cfBlobJson), + kj::mv(metadata.parentSpan)); + } + + private: + kj::Rc cacheService; + kj::HttpHeaderId cacheNamespaceHeader; +}; + +// Access info from the JSON blob of the worker's `accessBlobHeader`: +// `{"app_aud": "...", "jwt_claims": {...}}`. +class BlobAccessInfo final: public AccessInfo { + public: + BlobAccessInfo(kj::String audience, + kj::Maybe jwtClaimsJson, + kj::Maybe identityServiceChannel) + : audience(kj::mv(audience)), + jwtClaimsJson(kj::mv(jwtClaimsJson)), + identityServiceChannel(identityServiceChannel) {} + + kj::StringPtr getAudience() override { + return audience; + } + kj::Maybe getIdentityServiceChannel() override { + return identityServiceChannel; + } + // The props of the identity binding: the audience and the claims. + kj::String getPropsJson() { + capnp::JsonCodec codec; + auto escapedAud = codec.encode(capnp::Text::Reader(audience)); + KJ_IF_SOME(claims, jwtClaimsJson) { + return kj::str("{\"aud\":", escapedAud, ",\"jwtClaims\":", claims, "}"); + } + return kj::str("{\"aud\":", escapedAud, "}"); + } + + private: + kj::String audience; + kj::Maybe jwtClaimsJson; + kj::Maybe identityServiceChannel; +}; + +kj::Own parseAccessBlob( + kj::StringPtr json, kj::Maybe identityServiceChannel) { + capnp::JsonCodec jsonCodec; + capnp::MallocMessageBuilder arena; + auto root = arena.initRoot(); + jsonCodec.decodeRaw(json, root); + KJ_REQUIRE(root.isObject(), "accessBlobHeader value must be a JSON object"); + + kj::Maybe appAud; + kj::Maybe jwtClaimsJson; + for (auto field: root.getObject()) { + auto name = field.getName(); + if (name == "app_aud") { + KJ_REQUIRE(field.getValue().isString(), "access blob `app_aud` must be a string"); + appAud = kj::str(field.getValue().getString()); + } else if (name == "jwt_claims") { + KJ_REQUIRE(field.getValue().isObject(), "access blob `jwt_claims` must be a JSON object"); + jwtClaimsJson = jsonCodec.encodeRaw(field.getValue()); + } + } + auto audience = + KJ_REQUIRE_NONNULL(kj::mv(appAud), "accessBlobHeader JSON must contain an `app_aud` field"); + return kj::refcounted( + kj::mv(audience), kj::mv(jwtClaimsJson), identityServiceChannel); +} + +// The `IoChannelFactory` of one request: the worker's Rust channel table, plus what stays in +// C++ (a dynamic worker's env tables, channel tokens, the debug port). +class RustIoChannelFactory final: public IoChannelFactory { + public: + RustIoChannelFactory(kj::Own worker, ::rust::Box channels) + : worker(kj::mv(worker)), + channels(kj::mv(channels)) {} + + ~RustIoChannelFactory() noexcept(false) { + // A request's drain task runs in the worker's own task set and may hold the worker's last + // references (the server dropped the worker meanwhile; `channels` may hold the Rust side's, + // and drops it before this member), so destroying the worker here would destroy the task set + // from inside one of its tasks. The factory's tasks drop this reference on a later turn. + worker->factory.getImpl().tasks.add(kj::evalLater([worker = kj::mv(worker)]() {})); + } + + kj::Own startSubrequest(uint channel, SubrequestMetadata metadata) override { + KJ_IF_SOME(env, envSubrequestChannel(channel)) return env.startRequest(kj::mv(metadata)); + // The access binding gets the request's identity as props, as the production embedder does + // through channel tokens. + kj::Maybe> props; + KJ_IF_SOME(accessChannel, channels->access_binding_channel()) { + if (channel == accessChannel) { + KJ_IF_SOME(info, IoContext::current().getAccessInfo()) { + props = toOwn(Frankenvalue::fromJson(kj::downcast(info).getPropsJson())); + } + } + } + return channels->subrequest_channel(channel, kj::mv(props), false) + ->start_request(kj::heap(kj::mv(metadata))); + } + + capnp::Capability::Client getCapability(uint channel) override { + KJ_FAIL_REQUIRE("no capability channels"); + } + + kj::Own getCache() override { + return kj::heap(subrequest_channel_into_kj(channels->cache_channel()), + worker->factory.getImpl().threadContext.getHeaderIds().cfCacheNamespace); + } + + TimerChannel& getTimer() override { + return worker->timerChannel; + } + + kj::Promise writeLogfwdr( + uint channel, kj::FunctionParam buildMessage) override { + auto& context = IoContext::current(); + auto headers = kj::HttpHeaders(context.getHeaderTable()); + auto client = context.getHttpClient(channel, true, kj::none, "writeLogfwdr"_kjc); + auto urlStr = kj::str("https://fake-host"); + + capnp::MallocMessageBuilder requestMessage; + auto requestBuilder = requestMessage.initRoot(); + buildMessage(requestBuilder); + capnp::JsonCodec json; + auto requestJson = json.encode(requestBuilder.getAs()); + + co_await context.waitForOutputLocks(); + + auto innerReq = client->request(kj::HttpMethod::POST, urlStr, headers, requestJson.size()); + auto request = attachToRequest(kj::mv(innerReq), kj::Rc(kj::mv(client))); + co_await request.body->write(requestJson.asBytes()) + .attach(kj::mv(requestJson), kj::mv(request.body)); + auto response = co_await request.response; + KJ_REQUIRE(response.statusCode >= 200 && response.statusCode < 300, + "writeLogfwdr request returned an error"); + co_await response.body->readAllBytes().attach(kj::mv(response.body)); + } + + kj::Rc getSubrequestChannelResolved(uint channel, + kj::Maybe props, + kj::Maybe versionRequest, + Persistent persistent) override { + KJ_IF_SOME(env, envSubrequestChannel(channel)) { + // Only a `ctx.exports` template takes props; an env channel is a concrete one. + KJ_REQUIRE(props == kj::none, "can't override props for this service"); + return env.addRef(); + } + return subrequest_channel_into_kj( + channels->subrequest_channel(channel, toOwn(kj::mv(props)), persistent.toBool())); + } + + kj::Rc getGlobalActor(uint channel, + const ActorIdFactory::ActorId& id, + kj::Maybe locationHint, + ActorGetMode mode, + bool enableReplicaRouting, + ActorRoutingMode routingMode, + SpanParent parentSpan, + kj::Maybe version, + Persistent persistent) override { + JSG_REQUIRE(mode == ActorGetMode::GET_OR_CREATE, Error, + "workerd only supports GET_OR_CREATE mode for getting actor stubs"); + JSG_REQUIRE(!enableReplicaRouting, Error, "workerd does not support replica routing."); + switch (routingMode) { + case ActorRoutingMode::PRIMARY_ONLY: + case ActorRoutingMode::DEFAULT: + // workerd has only primaries. + break; + } + return subrequest_channel_into_kj(channels->global_actor( + channel, kj::heap(normalizeActorId(id.clone())), persistent.toBool())); + } + + kj::Rc getColoLocalActor( + uint channel, kj::StringPtr id, SpanParent parentSpan) override { + return subrequest_channel_into_kj(channels->colo_local_actor(channel, toRust(id))); + } + + kj::Rc getActorClassResolved( + uint channel, kj::Maybe props, Persistent persistent) override { + if (channel < worker->actorClassChannels.size()) { + KJ_REQUIRE(props == kj::none, "can't override props for this actor class"); + return worker->actorClassChannels[channel]->addRef(); + } + return actor_class_channel_into_kj( + channels->actor_class(channel, toOwn(kj::mv(props)), persistent.toBool())); + } + + kj::Rc getRpcChannel(uint channel) override { + KJ_REQUIRE(channel < worker->rpcChannels.size(), "invalid RPC channel number"); + return worker->rpcChannels[channel]->addRef(); + } + + void abortAllActors(kj::Maybe reason) override { + channels->abort_all_actors(reason); + } + void deleteAllActors(kj::Maybe reason) override { + channels->delete_all_actors(reason); + } + kj::Promise evictAllActorsForTest(EvictWebSocketMode mode) override { + return channels->evict_all_actors_for_test(mode == EvictWebSocketMode::HIBERNATE) + .attach(kj::addRef(*this)); + } + void abortIsolate(kj::StringPtr reason) noexcept override { + channels->abort_isolate(toRustString(reason)); + } + + kj::Own loadIsolate(uint loaderChannel, + kj::Maybe name, + kj::Function()> fetchSource) override { + auto nameStr = name.map([](kj::String& n) { return toRust(n); }); + return kj::refcounted(channels->load_isolate( + loaderChannel, nameStr, kj::heap(kj::mv(fetchSource)))); + } + + kj::Network& getWorkerdDebugPortNetwork() override { + requireDebugPort(); + return worker->factory.getImpl().network; + } + rpc::WorkerdDebugPort::Client getWorkerdDebugPort() override { + requireDebugPort(); + auto& factory = worker->factory.getImpl(); + return kj::heap( + factory.getServer().server_clone(), factory.httpOverCapnpFactory); + } + + kj::Rc subrequestChannelFromToken( + ChannelTokenUsage usage, kj::ArrayPtr token) override { + return tokens().decodeSubrequestChannelToken(usage, token); + } + kj::Rc actorClassFromToken( + ChannelTokenUsage usage, kj::ArrayPtr token) override { + return tokens().decodeActorClassChannelToken(usage, token); + } + kj::Rc rpcChannelFromToken( + ChannelTokenUsage usage, kj::ArrayPtr token) override { + return tokens().decodeRpcChannelToken(usage, token); + } + kj::Rc makeRestoredSubrequestChannelResolved( + kj::Rc selfTokenFactory, + Frankenvalue restoreParams, + kj::Rc inner, + Persistent persistent) override { + return tokens().makeRestoredSubrequestChannel( + kj::mv(selfTokenFactory), kj::mv(restoreParams), kj::mv(inner), persistent); + } + kj::Rc makeRestoredRpcChannelResolved(kj::Rc selfTokenFactory, + Frankenvalue restoreParams, + Persistent persistent) override { + return tokens().makeRestoredRpcChannel( + kj::mv(selfTokenFactory), kj::mv(restoreParams), persistent); + } + + private: + kj::Own worker; + ::rust::Box channels; + + ChannelTokenHandler& tokens() { + return worker->factory.getImpl().channelTokenHandler; + } + // A dynamic worker's env channel behind `channel`; none when the Rust table serves it. + kj::Maybe envSubrequestChannel(uint channel) { + if (channel < worker->subrequestChannels.size()) return *worker->subrequestChannels[channel]; + return kj::none; + } + void requireDebugPort() { + KJ_REQUIRE( + channels->has_debug_port(), "workerdDebugPort binding is not enabled for this worker"); + } +}; + +// The self-token of a static worker's entrypoint, for `ctx.restore()`. +class StaticServiceSelfTokenFactory final: public ChannelTokenHandler::ServerSelfTokenFactory { + public: + StaticServiceSelfTokenFactory(ChannelTokenHandler& tokens, + kj::String serviceName, + kj::Maybe entrypoint, + Persistent persistent, + Frankenvalue props) + : tokens(tokens), + serviceName(kj::mv(serviceName)), + entrypoint(kj::mv(entrypoint)), + persistent(persistent), + props(kj::mv(props)) {} + + kj::OneOf, kj::Promise>> getSelfToken( + IoChannelFactory::ChannelTokenUsage usage) override { + return tokens.encodeSubrequestChannelToken(usage, serviceName, + entrypoint.map([](kj::String& e) -> kj::StringPtr { return e; }), props, persistent); + } + + private: + ChannelTokenHandler& tokens; + kj::String serviceName; + kj::Maybe entrypoint; + Persistent persistent; + Frankenvalue props; +}; + +// Defers creating the entrypoint until the first event, so that an HTTP request's access blob +// header can be parsed into the access info the entrypoint is constructed with. Only `request()` +// carries headers; every other event constructs the entrypoint without access info. +class AccessHeaderExtractor final: public WorkerInterface { + public: + AccessHeaderExtractor(kj::String headerName, + kj::Maybe identityServiceChannel, + kj::Function(kj::Maybe>)> factory) + : headerName(kj::mv(headerName)), + identityServiceChannel(identityServiceChannel), + factory(kj::mv(factory)) {} + + kj::Promise request(kj::HttpMethod method, + kj::StringPtr url, + const kj::HttpHeaders& headers, + kj::AsyncInputStream& requestBody, + kj::HttpService::Response& response) override { + kj::Maybe> accessInfo; + headers.forEach([&](kj::StringPtr name, kj::StringPtr value) { + if (strcaseeq(name, headerName)) accessInfo = parseAccessBlob(value, identityServiceChannel); + }); + KJ_ASSERT(inner == kj::none, "request() called more than once"); + auto& worker = *inner.emplace(factory(kj::mv(accessInfo))); + co_await worker.request(method, url, headers, requestBody, response); + } + kj::Promise connect(kj::StringPtr host, + const kj::HttpHeaders& headers, + kj::AsyncIoStream& connection, + ConnectResponse& response, + kj::HttpConnectSettings settings) override { + co_await getInner().connect(host, headers, connection, response, kj::mv(settings)); + } + kj::Promise prewarm(kj::StringPtr url) override { + co_await getInner().prewarm(url); + } + kj::Promise runScheduled(kj::Date scheduledTime, kj::StringPtr cron) override { + co_return co_await getInner().runScheduled(scheduledTime, cron); + } + kj::Promise runAlarm(kj::Date scheduledTime, uint32_t retryCount) override { + co_return co_await getInner().runAlarm(scheduledTime, retryCount); + } + kj::Promise> abandonAlarm(kj::Date scheduledTime) override { + co_return co_await getInner().abandonAlarm(scheduledTime); + } + kj::Promise test() override { + co_return co_await getInner().test(); + } + kj::Promise customEvent(kj::Own event) override { + co_return co_await getInner().customEvent(kj::mv(event)); + } + + private: + kj::String headerName; + kj::Maybe identityServiceChannel; + kj::Function(kj::Maybe>)> factory; + kj::Maybe> inner; + + WorkerInterface& getInner() { + KJ_IF_SOME(w, inner) return *w; + return *inner.emplace(factory(kj::none)); + } +}; + +kj::Own createEntrypoint(CompiledWorker::Impl& worker, + IoChannelFactory::SubrequestMetadata metadata, + kj::Maybe entrypointName, + Frankenvalue props, + kj::Maybe> actor, + ::rust::Box channels, + ::rust::Box tails, + kj::Maybe> accessInfo) { + auto& threadContext = worker.factory.getImpl().threadContext; + + // The test event is not traced. A dynamic worker's tails are its source's; the server passes + // it none. + kj::Vector> bufferedTailWorkers; + kj::Vector> streamingTailWorkers; + if (entrypointName.orDefault("") != "test"_kj) { + for (size_t i = 0; i < tails->len(); i++) { + (tails->is_streaming(i) ? streamingTailWorkers : bufferedTailWorkers).add(tails->take(i)); + } + KJ_IF_SOME(dynamic, worker.dynamicSource) { + for (auto& tail: dynamic.tails) bufferedTailWorkers.add(tail->startRequest({})); + for (auto& tail: dynamic.streamingTails) streamingTailWorkers.add(tail->startRequest({})); + } + } + + kj::Maybe> workerTracer; + if (!bufferedTailWorkers.empty() || !streamingTailWorkers.empty()) { + auto executionModel = + actor == kj::none ? ExecutionModel::STATELESS : ExecutionModel::DURABLE_OBJECT; + kj::Maybe durableObjectId; + KJ_IF_SOME(a, actor) { + KJ_SWITCH_ONEOF(a->getId()) { + KJ_CASE_ONEOF(id, kj::Own) { + durableObjectId = id->toString(); + } + KJ_CASE_ONEOF(id, kj::String) { + durableObjectId = kj::str(id); + } + } + } + auto tailStreamWriter = tracing::initializeTailStreamWriter( + streamingTailWorkers.releaseAsArray(), worker.waitUntilTasks); + auto trace = kj::refcounted(kj::none, kj::none, kj::none, kj::none, kj::none, nullptr, + entrypointName.clone(), executionModel, kj::mv(durableObjectId)); + kj::Rc tracer = kj::rc( + kj::none, kj::mv(trace), PipelineLogLevel::FULL, kj::none, kj::mv(tailStreamWriter)); + + // The buffered tail workers get the trace once it completes. The observer and the IoContext + // each hold the tracer, so it lives until every event is recorded. + if (!bufferedTailWorkers.empty()) { + worker.waitUntilTasks.add(tracer->onComplete().then( + kj::coCapture([tailWorkers = bufferedTailWorkers.releaseAsArray()]( + kj::Own trace) mutable -> kj::Promise { + for (auto& tailWorker: tailWorkers) { + auto event = kj::heap( + api::TraceCustomEvent::TYPE, kj::arr(kj::addRef(*trace))); + co_await tailWorker->customEvent(kj::mv(event)); + } + }))); + } + tracer->setMakeUserRequestSpanFunc( + [&tracerRef = *tracer, &entropySource = threadContext.getEntropySource()]( + tracing::TraceId traceId, kj::Maybe traceFlags) { + return SpanParent(kj::rc( + kj::refcounted(tracerRef.getWeakRef(), entropySource), + kj::mv(traceId), traceFlags)); + }); + workerTracer = kj::mv(tracer); + } + + kj::Own observer = + kj::refcounted(workerTracer.clone()); + + kj::Maybe triggerContext; + KJ_IF_SOME(ctx, metadata.userSpanParent.toSpanContext()) { + KJ_IF_SOME(spanId, ctx.getSpanId()) { + triggerContext = tracing::InvocationSpanContext( + ctx.getTraceId(), tracing::TraceId::nullId, spanId, ctx.getTraceFlags()); + } + } + + auto ioChannelFactory = kj::rc(kj::addRef(worker), kj::mv(channels)); + return newWorkerEntrypoint(threadContext, kj::atomicAddRef(worker.getWorker()), + entrypointName.clone(), kj::mv(props), kj::mv(actor), kj::refcounted(), {}, + kj::mv(ioChannelFactory), kj::mv(observer), worker.waitUntilTasks, true, kj::mv(workerTracer), + kj::mv(metadata.cfBlobJson), kj::none, kj::mv(triggerContext), IsDynamicDispatch::NO, + kj::mv(accessInfo), kj::mv(metadata.restoredSelfTokenFactory), metadata.fromPersistentStub, + kj::mv(metadata.clientAddress)); +} + +} // namespace + +kj::Own worker_start_request(const CompiledWorker& worker, + kj::Maybe<::rust::Str> entrypoint, + kj::Maybe> props, + kj::Maybe actor, + ::rust::Box channels, + kj::Own metadata, + ::rust::Box tails) { + TRACE_EVENT("workerd", "worker_start_request()"); + auto& impl = worker.getImpl(); + auto entrypointName = toKj(entrypoint); + Frankenvalue propsValue; + KJ_IF_SOME(p, props) propsValue = kj::mv(*p); + auto meta = kj::mv(*metadata); + // A static worker's entrypoint mints its own self-token: a caller-supplied one could read and + // manipulate the parameters of the entrypoint's own `[restore]()` method. An actor's requests + // get theirs from the server instead (the token names the actor, not the class), and nothing + // could reload a dynamic worker from a token. + if (!impl.isDynamic && actor == kj::none) { + meta.restoredSelfTokenFactory = kj::rc( + impl.factory.getImpl().channelTokenHandler, kj::str(impl.name), entrypointName.clone(), + Persistent(impl.info.persistent_self_tokens), propsValue.clone()); + } + auto actorRef = actor.map([](const ActorHandle& a) { return a.addRef(); }); + + KJ_IF_SOME(headerName, impl.accessBlobHeader) { + auto identityChannel = channels->access_binding_channel(); + return kj::heap(kj::str(headerName), identityChannel, + [&impl, meta = kj::mv(meta), entrypointName = kj::mv(entrypointName), + props = kj::mv(propsValue), actor = kj::mv(actorRef), channels = kj::mv(channels), + tails = kj::mv(tails)](kj::Maybe> accessInfo) mutable { + return createEntrypoint(impl, kj::mv(meta), entrypointName, kj::mv(props), kj::mv(actor), + kj::mv(channels), kj::mv(tails), kj::mv(accessInfo)); + }).attach(kj::addRef(impl)); + } + return createEntrypoint(impl, kj::mv(meta), entrypointName, kj::mv(propsValue), kj::mv(actorRef), + kj::mv(channels), kj::mv(tails), kj::none); +} + +// ======================================================================================= +// Compiling a worker + +namespace { + +// Sets the globals of an encoded `Globals` message on `target`. +void compileGlobals(jsg::Lock& lock, + const Worker::Api& api, + kj::ArrayPtr words, + v8::Local target) { + capnp::FlatArrayMessageReader reader(asWords(words), CONFIG_READER_OPTIONS); + WorkerdApi::from(api).compileGlobals(lock, reader.getRoot().getGlobals(), target); +} + +} // namespace + +// ======================================================================================= +// Value shims + +kj::Own worker_interface_attach( + kj::Own inner, ::rust::Box keep) { + return inner.attach(kj::mv(keep)); +} + +kj::Own new_request_metadata( + kj::Maybe<::rust::Str> cfBlobJson, kj::Maybe<::rust::Str> clientAddress) { + return kj::heap(IoChannelFactory::SubrequestMetadata{ + .cfBlobJson = toKj(cfBlobJson), .clientAddress = toKj(clientAddress)}); +} +kj::Maybe<::rust::String> request_metadata_cf_blob_json(const RequestMetadata& metadata) { + return metadata.cfBlobJson.map([](const kj::String& s) { return toRustString(s); }); +} + +void request_metadata_set_from_persistent_stub(RequestMetadata& metadata, bool persistent) { + if (persistent) metadata.fromPersistentStub = Persistent::YES; +} + +namespace { + +// The self-token of a root actor, for `ctx.restore()`: only the namespace and id, so that +// holding it does not keep the actor from being evicted. +class ActorSelfTokenFactory final: public ChannelTokenHandler::ServerSelfTokenFactory { + public: + ActorSelfTokenFactory(ChannelTokenHandler& tokens, + kj::String uniqueKey, + kj::Own id, + Persistent persistent) + : tokens(tokens), + uniqueKey(kj::mv(uniqueKey)), + id(kj::mv(id)), + persistent(persistent) {} + + kj::OneOf, kj::Promise>> getSelfToken( + IoChannelFactory::ChannelTokenUsage usage) override { + auto& idImpl = KJ_ASSERT_NONNULL(kj::tryDowncast(*id)); + return tokens.encodeActorChannelToken( + usage, uniqueKey, idImpl.getRaw(), idImpl.getName(), persistent); + } + + private: + ChannelTokenHandler& tokens; + kj::String uniqueKey; + kj::Own id; + Persistent persistent; +}; + +} // namespace + +void request_metadata_set_actor_self_token(const WorkerFactory& factory, + RequestMetadata& metadata, + ::rust::Str uniqueKey, + const ActorIdHandle& id, + bool persistent) { + // Ephemeral actors are not serializable, so they have no self-token. + auto& doId = KJ_UNWRAP_OR(id.tryGet>(), return); + metadata.restoredSelfTokenFactory = + kj::rc(factory.getImpl().channelTokenHandler, kj::str(uniqueKey), + doId->clone(), Persistent(persistent)); +} + +void exception_throw(const kj::Exception& exception) { + kj::throwFatalException(exception.clone()); +} + +::rust::String exception_text(const AbortReason& error) { + // Raising the error across the bridge is the bridge's own KjError -> kj::Exception conversion. + return kj::str(KJ_ASSERT_NONNULL(kj::runCatchingExceptions([&]() { + error.raise(); + }))).as(); +} + +kj::Own frankenvalue_from_json(::rust::Str json) { + return kj::heap(Frankenvalue::fromJson(kj::str(json))); +} +kj::Own frankenvalue_clone(const Frankenvalue& value) { + return kj::heap(const_cast(value).clone()); +} +bool frankenvalue_is_empty(const Frankenvalue& value) { + return value.empty(); +} +kj::Own frankenvalue_new() { + return kj::heap(); +} +void frankenvalue_set_service_stub( + Frankenvalue& value, ::rust::Str name, ::rust::Box channel) { + value.setProperty(kj::str(name), + Frankenvalue::fromCapability(static_cast(rpc::SerializationTag::SERVICE_STUB), + subrequest_channel_into_kj(kj::mv(channel)).toOwn())); +} + +kj::Own actor_id_clone(const ActorIdHandle& id) { + return kj::heap(Worker::Actor::cloneId(const_cast(id))); +} +::rust::String actor_id_key(const ActorIdHandle& id) { + KJ_SWITCH_ONEOF(id) { + KJ_CASE_ONEOF(obj, kj::Own) { + return toRustString(obj->toString()); + } + KJ_CASE_ONEOF(str, kj::String) { + return toRustString(str); + } + } + KJ_UNREACHABLE; +} +kj::Own actor_id_from_name(::rust::Str name) { + return kj::heap(kj::str(name)); +} +kj::Own actor_id_from_hex(::rust::Str hex) { + auto decoded = kj::decodeHex(kj::str(hex)); + KJ_REQUIRE(decoded.size() == SHA256_DIGEST_LENGTH, + "Invalid Durable Object ID: expected 64 hex characters (32 bytes)", decoded.size()); + return kj::heap(kj::Own( + kj::heap(decoded.begin(), kj::none))); +} + +// ======================================================================================= +// CompiledWorker + +CompiledWorker::Impl::Impl(const WorkerFactory& factory, kj::String name) + : factory(factory), + name(kj::mv(name)), + timerChannel(factory.getImpl().timer, factory.getImpl().monotonicClock), + waitUntilTasks(*this) {} + +void CompiledWorker::Impl::taskFailed(kj::Exception&& exception) { + KJ_LOG(ERROR, exception); +} + +CompiledWorker::CompiledWorker(kj::Own impl): impl(kj::mv(impl)) {} +CompiledWorker::~CompiledWorker() noexcept(false) = default; +CompiledWorker::Impl& CompiledWorker::getImpl() const { + return *impl; +} + +namespace { + +// Collects what validation reveals about a worker: its entrypoints and classes, and its errors. +struct ErrorReporter final: public Worker::ValidationErrorReporter { + kj::Vector errors; + kj::Vector warnings; + kj::Vector entrypoints; + kj::Vector<::rust::String> actorClasses; + kj::Vector<::rust::String> workflowClasses; + + void addError(kj::String error) override { + errors.add(kj::mv(error)); + } + void addWarning(kj::String warning) override { + warnings.add(kj::mv(warning)); + } + void addEntrypoint(kj::Maybe exportName, kj::Array methods) override { + entrypoints.add(EntrypointInfo{ + .name = toRustString(exportName.orDefault(""_kj)), + .is_default = exportName == kj::none, + .handlers = toRust(methods), + }); + } + void addActorClass(kj::StringPtr exportName) override { + actorClasses.add(toRustString(exportName)); + } + void addWorkflowClass(kj::StringPtr exportName, kj::Array methods) override { + // A workflow class is a stateless entrypoint at runtime. + entrypoints.add(EntrypointInfo{ + .name = toRustString(exportName), .is_default = false, .handlers = toRust(methods)}); + workflowClasses.add(toRustString(exportName)); + } + + static ::rust::Vec<::rust::String> toRust(kj::ArrayPtr strings) { + ::rust::Vec<::rust::String> result; + result.reserve(strings.size()); + for (auto& s: strings) result.push_back(toRustString(s)); + return result; + } + static ::rust::Vec<::rust::String> toRust(kj::Vector& strings) { + return toRust(strings.asPtr()); + } + template + static ::rust::Vec toRust(kj::Vector& values) { + ::rust::Vec result; + result.reserve(values.size()); + for (auto& v: values) result.push_back(kj::mv(v)); + return result; + } +}; + +MainModuleIsPython isPythonMainModule(config::Worker::Reader conf) { + if (!conf.isModules() || conf.getModules().size() == 0) return MainModuleIsPython::NO; + return conf.getModules()[0].isPythonModule() ? MainModuleIsPython::YES : MainModuleIsPython::NO; +} + +// Compiles a config worker's compatibility flags into `arena`, as the worker's errors. +CompatibilityFlags::Reader compileFlags(const WorkerFactory::Options& options, + config::Worker::Reader conf, + capnp::MallocMessageBuilder& arena, + ErrorReporter& errorReporter) { + auto flags = arena.initRoot(); + KJ_IF_SOME(overrideDate, options.testCompatibilityDateOverride) { + if (conf.hasCompatibilityDate()) { + errorReporter.addError( + kj::str("Worker specifies compatibilityDate but --compat-date was provided. " + "When using --compat-date, workers must not specify compatibilityDate in the " + "config. Use compatibilityFlags to enable/disable specific flags if needed.")); + } + // FUTURE_FOR_TEST admits any valid date, such as 2999-12-31. + compileCompatibilityFlags(overrideDate, conf.getCompatibilityFlags(), flags, errorReporter, + options.experimental, CompatibilityDateValidation::FUTURE_FOR_TEST, nullptr, + isPythonMainModule(conf)); + } else if (conf.hasCompatibilityDate()) { + compileCompatibilityFlags(conf.getCompatibilityDate(), conf.getCompatibilityFlags(), flags, + errorReporter, options.experimental, CompatibilityDateValidation::CODE_VERSION, nullptr, + isPythonMainModule(conf)); + } else { + errorReporter.addError(kj::str("Worker must specify compatibilityDate.")); + } + return flags.asReader(); +} + +// The global outbound of a dynamic worker whose source gave it none: every request fails. +class NullGlobalOutboundChannel final: public IoChannelFactory::SubrequestChannel { + public: + kj::Own startRequest(IoChannelFactory::SubrequestMetadata metadata) override { + JSG_FAIL_REQUIRE(Error, + "This worker is not permitted to access the internet via global functions like fetch(). " + "It must use capabilities (such as bindings in 'env') to talk to the outside world."); + } + + // The null outbound is hard to reach: nothing normally refers to it, but a `Fetcher` for the + // `next` outbound pulled off an incoming `Request` points at it. Transfer is refused because + // nothing needs it; were it allowed, `startRequest()`'s error would mislead once transferred. + void requireAllowsTransfer() override { + JSG_FAIL_REQUIRE(DOMDataCloneError, "The null global outbound is not transferrable."); + } + kj::OneOf, kj::Promise>> getTokenMaybeSync( + IoChannelFactory::ChannelTokenUsage usage) override { + JSG_FAIL_REQUIRE(DOMDataCloneError, "The null global outbound is not transferrable."); + } +}; + +// Fills a dynamic worker's channel tables from its fetched source: the global outbound slots, +// then the capabilities in `env`, each rewritten into the channel number it was given. +void indexDynamicChannels(CompiledWorker::Impl& impl, DynamicWorkerSource& source) { + kj::Rc globalOutbound = + kj::mv(source.globalOutbound).orDefault([]() -> kj::Rc { + return kj::rc(); + }); + for (uint i = 1; i < IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT; i++) { + impl.subrequestChannels.add(globalOutbound.addRef()); + } + impl.subrequestChannels.add(kj::mv(globalOutbound)); + + source.env.rewriteCaps([&](kj::Own entry) { + KJ_IF_SOME(channel, kj::tryDowncast(*entry)) { + uint number = impl.subrequestChannels.size(); + impl.subrequestChannels.add(channel.addRef()); + return kj::heap(IoChannelCapTableEntry::SUBREQUEST, number); + } else KJ_IF_SOME(channel, kj::tryDowncast(*entry)) { + uint number = impl.actorClassChannels.size(); + impl.actorClassChannels.add(channel.addRef()); + return kj::heap(IoChannelCapTableEntry::ACTOR_CLASS, number); + } else KJ_IF_SOME(channel, kj::tryDowncast(*entry)) { + uint number = impl.rpcChannels.size(); + impl.rpcChannels.add(channel.addRef()); + return kj::heap(IoChannelCapTableEntry::RPC, number); + } else { + JSG_FAIL_REQUIRE(DOMDataCloneError, + "Dynamic 'env' contains one or more objects that are not supported for use in " + "'env', although they would be supported in 'props'."); + } + }); +} + +kj::Promise preloadPython(WorkerFactory::Impl& factory, CompatibilityFlags::Reader flags) { + if (!flags.getPythonWorkers()) co_return; + KJ_IF_SOME(release, getPythonSnapshotRelease(flags)) { + co_await fetchPyodideBundle(factory.options->pythonConfig, getPythonBundleName(release), + release.getIntegrity(), factory.network, factory.timer); + } +} + +bool hasWasmModules(const WorkerSource& source) { + KJ_IF_SOME(modules, source.variant.tryGet()) { + for (auto& module: modules.modules) { + if (module.content.is()) return true; + } + } + return false; +} + +bool supportsStartupSnapshot(CompatibilityFlags::Reader featureFlags, const WorkerSource& source) { + return !featureFlags.getPythonWorkers() && !featureFlags.getNewModuleRegistry() && + !source.variant.is() && !hasWasmModules(source); +} + +} // namespace + +kj::Promise> factory_new_worker(const WorkerFactory& constFactory, + const WorkerSpec& spec, + kj::Maybe configService, + kj::Maybe> dynamicSource) { + auto& factory = constFactory.getImpl(); + auto name = kj::str(spec.name); + TRACE_EVENT("workerd", "factory_new_worker()", "name", name.cStr()); + auto impl = kj::refcounted(constFactory, kj::mv(name)); + impl->isDynamic = dynamicSource != kj::none; + ErrorReporter errorReporter; + auto& options = *factory.options; + bool experimental = options.experimental; + + // The code, its flags and its `env`, from the config or the dynamic source. + CompatibilityFlags::Reader featureFlags; + kj::Maybe source; + kj::Maybe moduleFallback; + capnp::List::Reader extensions; + kj::Function)> compileBindings; + KJ_IF_SOME(index, configService) { + auto conf = factory.config.getServices()[index].getWorker(); + extensions = factory.config.getExtensions(); + impl->flagsArena = kj::heap(); + featureFlags = compileFlags(options, conf, *impl->flagsArena, errorReporter); + source = WorkerdApi::extractSource(impl->name, conf, featureFlags, errorReporter); + if (conf.hasModuleFallback()) moduleFallback = conf.getModuleFallback(); + compileBindings = [globals = kj::heapArray(kj::from(spec.globals))]( + jsg::Lock& lock, const Worker::Api& api, v8::Local target) { + compileGlobals(lock, api, globals, target); + }; + } else { + auto fetched = co_await (*KJ_ASSERT_NONNULL(dynamicSource))(); + co_await fetched.ensureAllResolved(); + auto& dynamic = impl->dynamicSource.emplace(kj::mv(fetched)); + indexDynamicChannels(*impl, dynamic); + featureFlags = dynamic.compatibilityFlags; + source = kj::mv(dynamic.source); + compileBindings = [&env = dynamic.env]( + jsg::Lock& js, const Worker::Api&, v8::Local target) { + env.populateJsObject(js, jsg::JsObject(target)); + }; + } + auto& workerSource = KJ_ASSERT_NONNULL(source); + // The server passes the header only under `--experimental`. + KJ_IF_SOME(header, spec.access_blob_header) impl->accessBlobHeader = kj::str(header); + + co_await preloadPython(factory, featureFlags); + + // The file system roots use their defaults; the config does not expose mount points. + auto workerFs = newWorkerFileSystem(kj::heap(), getBundleDirectory(workerSource)); + + // Python workers never use the new module registry, whatever their flags say. + bool usingNewModuleRegistry = isNewModuleRegistryEnabled(featureFlags); + kj::Maybe> newModuleRegistry; + if (usingNewModuleRegistry) { + KJ_REQUIRE(experimental, + "The new ModuleRegistry implementation is an experimental feature. " + "You must run workerd with `--experimental` to use this feature."); + // Module URLs follow the virtual file system: a module at "/foo/bar/baz.js" in the bundle + // is "file:///foo/bar/baz.js". + const jsg::Url& bundleBase = workerFs->getBundleRoot(); + using ArtifactBundler = api::pyodide::ArtifactBundler; + KJ_IF_SOME(exception, kj::runCatchingExceptions([&]() { + newModuleRegistry = WorkerdApi::newWorkerdModuleRegistry( + workerSource.variant.tryGet(), featureFlags, + options.pythonConfig, bundleBase, extensions, moduleFallback.clone(), + ArtifactBundler::makeDisabledBundler()); + })) { + // A registry that cannot be built from the source is a config error (duplicate module + // names, Python modules without the flag). Report it and substitute an inert script and a + // built-ins-only registry so that this error is the one reported; the worker never runs. + errorReporter.addError(kj::str(exception.getDescription())); + workerSource = WorkerSource(Worker::Script::ScriptSource{""_kj, impl->name, nullptr}); + newModuleRegistry = WorkerdApi::newWorkerdModuleRegistry(kj::none, featureFlags, + options.pythonConfig, bundleBase, capnp::List::Reader{}, kj::none, + ArtifactBundler::makeDisabledBundler()); + } + } + + // The isolate of the Worker, or of the zygote that snapshots its startup. + auto makeIsolate = [&](kj::StringPtr isolateName, Worker::Isolate::InspectorPolicy policy, + kj::Maybe snapshotConfig) { + auto limitEnforcer = kj::refcounted(); + auto listeners = KJ_MAP(listener, spec.inbound_listeners) { + return Worker::Api::InboundListener{ + .protocol = kj::str(listener.protocol), + .address = kj::str(listener.address), + .port = listener.port, + }; + }; + auto api = kj::heap(factory.v8System, featureFlags, extensions, + limitEnforcer->getCreateParams(), jsg::newIsolateGroup(), + kj::atomicRefcounted(), *factory.memoryCacheProvider, + options.pythonConfig, kj::mv(listeners), kj::mv(snapshotConfig)); + Worker::LoggingOptions isolateLoggingOptions = options.loggingOptions; + isolateLoggingOptions.consoleMode = + workerSource.variant.is() && !usingNewModuleRegistry + ? Worker::ConsoleMode::INSPECTOR_ONLY + : options.loggingOptions.consoleMode; + return kj::atomicRefcounted(kj::mv(api), + kj::atomicRefcounted(), isolateName, kj::mv(limitEnforcer), policy, + kj::mv(isolateLoggingOptions)); + }; + + // Behind the STARTUP_SNAPSHOT autogate, a throwaway zygote Worker evaluates the top-level code + // to produce a V8 startup snapshot of it. + kj::Maybe snapshotConfig; + if (util::Autogate::isEnabled(util::AutogateKey::STARTUP_SNAPSHOT) && + supportsStartupSnapshot(featureFlags, workerSource)) { + // The zygote reports into a reporter of its own: its failures must never surface as the + // Worker's. + ErrorReporter zygoteErrors; + auto zygoteIsolate = + makeIsolate(kj::str(impl->name, "-snapshot"), Worker::Isolate::InspectorPolicy::DISALLOW, + jsg::SnapshotConfig( + jsg::MutableSnapshot{.artifact = kj::atomicRefcounted()})); + auto zygoteScript = zygoteIsolate->newScript(impl->name, workerSource, + IsolateObserver::StartType::COLD, SpanParent(nullptr), + newWorkerFileSystem(kj::heap(), getBundleDirectory(workerSource)), false, + zygoteErrors, api::pyodide::ArtifactBundler::makeDisabledBundler()); + // As the Worker's, except that `ctx.exports` is not kept: a v8::Global outliving the + // zygote isolate breaks snapshot creation. + auto zygote = + kj::atomicRefcounted(kj::mv(zygoteScript), kj::atomicRefcounted(), + [&](jsg::Lock& lock, const Worker::Api& api, v8::Local target, + v8::Local) { compileBindings(lock, api, target); }, + IsolateObserver::StartType::COLD, SpanParent(nullptr), + Worker::Lock::TakeSynchronously(kj::none), zygoteErrors); + if (zygoteErrors.errors.empty()) { + kj::Own artifact; + zygoteIsolate->runInLockScope( + Worker::Lock::TakeSynchronously(kj::none), [&](jsg::Lock& lock) { + artifact = jsg::IsolateBase::from(lock.v8Isolate).extractSnapshotArtifact(); + }); + // TODO(soon): start the Worker from the snapshot artifact. + (void)artifact; + } else { + auto errors = kj::strArray(zygoteErrors.errors, "\n"); + KJ_LOG( + INFO, "startup snapshot skipped: the zygote Worker failed to start", impl->name, errors); + } + } + + // With the inspector enabled it is always fully trusted. + auto& registrar = factory.inspectorRegistrar; + auto inspectorPolicy = registrar == kj::none + ? Worker::Isolate::InspectorPolicy::DISALLOW + : Worker::Isolate::InspectorPolicy::ALLOW_FULLY_TRUSTED; + auto isolate = makeIsolate(impl->name, inspectorPolicy, kj::mv(snapshotConfig)); + KJ_IF_SOME(r, registrar) { + r->registerIsolate(impl->name, *isolate); + } + + if (!usingNewModuleRegistry) { + KJ_IF_SOME(fallback, moduleFallback) { + KJ_REQUIRE(experimental, + "The module fallback service is an experimental feature. " + "You must run workerd with `--experimental` to use the module fallback service."); + auto& apiIsolate = isolate->getApi(); + apiIsolate.setModuleFallbackCallback( + [client = kj::heap(kj::str(fallback)), + featureFlags = apiIsolate.getFeatureFlags()](jsg::Lock& js, kj::StringPtr specifier, + kj::Maybe referrer, jsg::CompilationObserver& observer, + jsg::ModuleRegistry::ResolveMethod method, + kj::Maybe rawSpecifier) mutable + -> kj::Maybe> { + kj::HashMap attributes; + KJ_IF_SOME(moduleOrRedirect, + client->tryResolve(fallback::Version::V1, + method == jsg::ModuleRegistry::ResolveMethod::IMPORT + ? fallback::ImportType::IMPORT + : fallback::ImportType::REQUIRE, + specifier, rawSpecifier.orDefault(nullptr), referrer.orDefault(kj::String()), + attributes)) { + KJ_SWITCH_ONEOF(moduleOrRedirect) { + KJ_CASE_ONEOF(redirect, kj::String) { + // A 301 from the fallback service: the specifier of the module to load instead. + return kj::Maybe(kj::mv(redirect)); + } + KJ_CASE_ONEOF(module, kj::Own) { + KJ_IF_SOME(compiled, + WorkerdApi::tryCompileModule(js, *module, observer, featureFlags)) { + return kj::Maybe(kj::mv(compiled)); + } + KJ_LOG(ERROR, "Fallback service does not support this module type", module->which()); + } + } + } + return kj::none; + }); + } + } + + kj::Maybe> ownContent; + KJ_IF_SOME(dynamic, impl->dynamicSource) ownContent = kj::mv(dynamic.ownContent); + auto script = isolate->newScript(impl->name, workerSource, IsolateObserver::StartType::COLD, + SpanParent(nullptr), workerFs.attach(kj::mv(ownContent)), false, errorReporter, + api::pyodide::ArtifactBundler::makeDisabledBundler(), kj::mv(newModuleRegistry)); + + // `ctx.exports` is filled in once the validator has found the entrypoints, which needs the + // Worker constructed first; the handle is held until then. + auto worker = kj::atomicRefcounted(kj::mv(script), kj::atomicRefcounted(), + [&](jsg::Lock& lock, const Worker::Api& api, v8::Local target, + v8::Local ctxExports) { + impl->ctxExportsHandle = lock.v8Ref(ctxExports); + compileBindings(lock, api, target); + }, + IsolateObserver::StartType::COLD, SpanParent(nullptr), + Worker::Lock::TakeSynchronously(kj::none), errorReporter); + + worker->runInLockScope(Worker::Lock::TakeSynchronously(kj::none), + [&](Worker::Lock& lock) { lock.validateHandlers(errorReporter); }); + + impl->info = WorkerInfo{ + .entrypoints = ErrorReporter::toRust(errorReporter.entrypoints), + .actor_classes = ErrorReporter::toRust(errorReporter.actorClasses), + .workflow_classes = ErrorReporter::toRust(errorReporter.workflowClasses), + .errors = ErrorReporter::toRust(errorReporter.errors), + .warnings = ErrorReporter::toRust(errorReporter.warnings), + .persistent_self_tokens = featureFlags.getAllowIrrevocableStubStorage(), + .env_subrequest_channels = impl->isDynamic + ? static_cast( + impl->subrequestChannels.size() - IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT) + : 0, + .env_actor_classes = static_cast(impl->actorClassChannels.size()), + }; + impl->worker = kj::mv(worker); + co_return kj::heap(kj::mv(impl)); +} + +WorkerInfo worker_info(const CompiledWorker& worker) { + return worker.getImpl().info; +} + +void worker_set_ctx_exports(const CompiledWorker& worker, ::rust::Slice globals) { + auto& impl = worker.getImpl(); + impl.getWorker().runInLockScope( + Worker::Lock::TakeSynchronously(kj::none), [&](Worker::Lock& lock) { + KJ_IF_SOME(handle, impl.ctxExportsHandle) { + JSG_WITHIN_CONTEXT_SCOPE(lock, lock.getContext(), [&](jsg::Lock& js) { + compileGlobals(lock, impl.getWorker().getIsolate().getApi(), kj::from(globals), + handle.getHandle(js)); + }); + } + // The handle is dropped now, under the lock. + impl.ctxExportsHandle = kj::none; + }); +} + +void worker_unlink(const CompiledWorker& worker) { + auto& impl = worker.getImpl(); + impl.waitUntilTasks.clear(); + impl.subrequestChannels.clear(); + impl.actorClassChannels.clear(); + impl.rpcChannels.clear(); + KJ_IF_SOME(dynamic, impl.dynamicSource) { + dynamic.tails = nullptr; + dynamic.streamingTails = nullptr; + } +} + +} // namespace workerd::server diff --git a/src/workerd/server/factory/worker-factory.h b/src/workerd/server/factory/worker-factory.h new file mode 100644 index 00000000000..a0ecf33830e --- /dev/null +++ b/src/workerd/server/factory/worker-factory.h @@ -0,0 +1,407 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#pragma once + +// The worker factory: the part of workerd's server that needs the isolate, kept in C++ and driven +// by the Rust server through the cxx bridge in server/bridge.rs. It compiles workers, starts +// requests on them, constructs actors and their storage, speaks the capnp RPC protocols, encodes +// channel tokens and runs the inspector. Policy that is workerd's alone (the unlimited +// LimitEnforcer, the clock-corrected TimerChannel, the null IsolateLimitEnforcer, the default +// observers) lives here too: the production runtime supplies its own. +// +// Every function the bridge declares is a free function in this namespace taking the object as its +// first argument, and is documented on its declaration there. One that can throw is declared +// fallible in the bridge, where a C++ exception becomes a `Result` error on the Rust side; the rest +// do not throw. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace workerd::jsg { +class V8System; +} + +// The bridge names kj's handler as kj-hyper's bridge does (kj-hyper/kj-hyper.h). +namespace workerd::rust::kj_hyper { +using WebSocketErrorHandler = kj::WebSocketErrorHandler; +} + +namespace workerd::server { + +// Rust types of the bridge, defined in server/channels.rs and server/tasks.rs. +struct ServerHandle; +struct ChannelFactory; +struct SubrequestChannel; +struct SpawnedTask; +struct ActorClassChannel; +struct ActorNamespaceHandle; +struct ActorHooks; +struct AbortReason; +struct KeepAlive; +struct WorkerInterfaceList; +struct WorkerStub; +struct UdpFlow; + +// The hibernation manager of an evicted actor, handed to its replacement. +using HibernationManager = Worker::Actor::HibernationManager; +// The isolate lock, as an eviction takes it to hibernate the actor's WebSockets. +using ActorLock = Worker::AsyncLock; +// A `Worker::Actor::Id`: a 32-byte durable id, or an ephemeral name. +using ActorIdHandle = Worker::Actor::Id; +// The source of a dynamic worker, as `IoChannelFactory::loadIsolate()` receives it: the function +// that fetches it. `factory_new_worker()` fetches the source and compiles the worker from it; the +// capabilities in its `env` become the worker's own channel tables (`CompiledWorker::Impl`). +using DynamicSource = kj::Function()>; +// One request's metadata. +using RequestMetadata = IoChannelFactory::SubrequestMetadata; +// What a channel token is for: RPC, or Durable Object storage. +using TokenUsage = IoChannelFactory::ChannelTokenUsage; +// A channel token as the ChannelTokenHandler encodes it: ready bytes, or a promise of them. +using PendingToken = kj::OneOf, kj::Promise>>; +// A facet's start info, as `Worker::Actor::FacetManager::getFacet()` receives it: the function +// that resolves the facet's class and id. +using FacetStart = kj::Function()>; +// An actor's `onBroken()` promise. It holds no reference to the actor: it rejects when the actor +// is destroyed. +using ActorBroken = kj::Promise; + +class CompiledWorker; +class ActorHandle; +class ActorStorage; +class SubrequestChannelHandle; +class ActorClassChannelHandle; +class RpcClient; +struct WorkerSpec; +struct WorkerInfo; +struct ContainerRef; +struct FacetStartInfo; +struct ActorStorageSpec; +struct HeaderEdit; +struct UdpDatagram; + +// How the config message is read: configs can legitimately be very large and are not malicious. +constexpr capnp::ReaderOptions CONFIG_READER_OPTIONS = {.traversalLimitInWords = kj::maxValue}; +inline kj::ArrayPtr asWords(kj::ArrayPtr words) { + return kj::arrayPtr(reinterpret_cast(words.begin()), words.size()); +} + +// ======================================================================================= + +// Everything the workers of one run share. +class WorkerFactory final: private ChannelTokenHandler::Resolver { + public: + struct Options { + bool experimental = false; + // When set, every worker uses this compatibility date and none may specify its own. + kj::Maybe testCompatibilityDateOverride; + Worker::LoggingOptions loggingOptions; + api::pyodide::PythonConfig pythonConfig; + }; + + // `config` is the encoded config message; the factory owns it for the run and reads it with an + // unlimited traversal limit, as the server does. `options` is owned rather than moved because + // `PythonConfig` cannot be moved. + // `monotonicClock` must read consistently with `timer` whenever the timer is advanced. + WorkerFactory(jsg::V8System& v8System, + kj::Timer& timer, + const kj::MonotonicClock& monotonicClock, + kj::Network& network, + kj::EntropySource& entropySource, + kj::Filesystem& fs, + kj::Own options, + ::rust::Vec config); + ~WorkerFactory() noexcept(false); + KJ_DISALLOW_COPY_AND_MOVE(WorkerFactory); + + class Impl; + Impl& getImpl() const; + + private: + // Bridge functions receive every handle as `const&`, so the state behind one is `mutable`. + mutable kj::Own impl; + + kj::Rc resolveEntrypoint(kj::StringPtr serviceName, + kj::Maybe entrypoint, + Frankenvalue props, + Persistent persistent) override; + kj::Rc resolveActorClass(kj::StringPtr serviceName, + kj::Maybe entrypoint, + Frankenvalue props, + Persistent persistent) override; + kj::Rc resolveActor(kj::StringPtr namespaceKey, + kj::ArrayPtr id, + kj::Maybe name, + Persistent persistent) override; +}; + +::rust::Slice factory_config(const WorkerFactory& factory); +const kj::HttpHeaderTable& factory_header_table(const WorkerFactory& factory); +bool factory_experimental(const WorkerFactory& factory); +void factory_set_server(const WorkerFactory& factory, ::rust::Box server); +void factory_spawn(const WorkerFactory& factory, ::rust::Box task); +void factory_clear_tasks(const WorkerFactory& factory); +kj::Promise factory_settle_tasks(const WorkerFactory& factory); +uint64_t factory_timer_now(const WorkerFactory& factory); +kj::Promise factory_sleep(const WorkerFactory& factory, uint64_t nanos); + +kj::Promise> factory_new_worker(const WorkerFactory& factory, + const WorkerSpec& spec, + kj::Maybe configService, + kj::Maybe> dynamicSource); + +// ======================================================================================= + +// A compiled worker: its isolate, script and `Worker`, and what compiling them revealed. +class CompiledWorker final { + public: + class Impl; + explicit CompiledWorker(kj::Own impl); + ~CompiledWorker() noexcept(false); + KJ_DISALLOW_COPY_AND_MOVE(CompiledWorker); + + Impl& getImpl() const; + + private: + mutable kj::Own impl; +}; + +WorkerInfo worker_info(const CompiledWorker& worker); +void worker_set_ctx_exports(const CompiledWorker& worker, ::rust::Slice globals); +void worker_unlink(const CompiledWorker& worker); +kj::Own worker_start_request(const CompiledWorker& worker, + kj::Maybe<::rust::Str> entrypoint, + kj::Maybe> props, + kj::Maybe actor, + ::rust::Box channels, + kj::Own metadata, + ::rust::Box tails); +kj::Own worker_interface_attach( + kj::Own inner, ::rust::Box keep); +kj::Own worker_new_actor(const CompiledWorker& worker, + ::rust::Str className, + kj::Maybe> props, + kj::Own id, + const ActorStorage& storage, + const ActorStorageSpec& spec, + ::rust::Box hooks, + kj::Maybe> hibernationManager, + kj::Maybe container); + +// ======================================================================================= + +// A live actor. Owns the `Worker::Actor`, the hooks it borrows (its facet manager and its request +// tracker's hooks, one object, which the actor also holds as its loopback) and its request +// tracker; requests on it are started through the worker. Every actor reference `addRef()` hands +// out counts as an active request until it is dropped, and the tracker's hooks tell the server +// when the actor goes from idle to active and back. The handle's own reference is not counted, so +// `Worker::Actor::isShared()` is "a request holds the actor". +class ActorHandle final { + public: + ActorHandle(kj::Own hooks, + kj::Own tracker, + kj::Own actor); + // Silences the tracker's hooks: references handed out earlier may outlive the handle. + ~ActorHandle() noexcept(false); + KJ_DISALLOW_COPY_AND_MOVE(ActorHandle); + Worker::Actor& getActor() const; + kj::Own addRef() const; + + private: + kj::Own hooks; + kj::Own tracker; + mutable kj::Own actor; +}; + +kj::Own actor_on_broken(const ActorHandle& actor); +kj::Promise actor_broken(kj::Own broken); +void actor_abort(const ActorHandle& actor, const AbortReason& reason); +kj::Promise> actor_lock(const ActorHandle& actor); +bool actor_shutdown( + const ActorHandle& actor, ::rust::Str reason, kj::Maybe> lock); +void actor_reset_storage(const ActorHandle& actor); +kj::Maybe> actor_hibernation_manager(const ActorHandle& actor); +kj::Promise facet_start_resolve(kj::Own start); + +kj::Own actor_id_clone(const ActorIdHandle& id); +::rust::String actor_id_key(const ActorIdHandle& id); +kj::Own actor_id_from_name(::rust::Str name); +kj::Own actor_id_from_hex(::rust::Str hex); + +// The storage of one Durable Object namespace: its directory (or none, for in-memory storage), +// the SQLite VFS over it, and the alarm scheduler. +class ActorStorage final { + public: + class Impl; + explicit ActorStorage(kj::Own impl); + ~ActorStorage() noexcept(false); + KJ_DISALLOW_COPY_AND_MOVE(ActorStorage); + + Impl& getImpl() const; + + private: + mutable kj::Own impl; +}; + +kj::Own factory_new_actor_storage(const WorkerFactory& factory, + ::rust::Str path, + ::rust::Str uniqueKey, + ::rust::Box actors); +void actor_storage_delete_all(const ActorStorage& storage); +// Facets have numbers only in a directory (SQLite storage). In-memory storage is an ActorCache per +// actor, in which they have none, and nothing to delete or clone. +void actor_storage_delete_facet(const ActorStorage& storage, + ::rust::Str rootKey, + kj::Maybe parentFacetId, + ::rust::Str name); +void actor_storage_clone_facet(const ActorStorage& storage, + ::rust::Str rootKey, + kj::Maybe parentFacetId, + ::rust::Str src, + ::rust::Str dst); +kj::Maybe actor_storage_facet_id(const ActorStorage& storage, + ::rust::Str rootKey, + kj::Maybe parentFacetId, + ::rust::Str name); + +kj::Promise factory_shutdown_containers(const WorkerFactory& factory); + +// ======================================================================================= + +kj::Own new_request_metadata( + kj::Maybe<::rust::Str> cfBlobJson, kj::Maybe<::rust::Str> clientAddress); +kj::Maybe<::rust::String> request_metadata_cf_blob_json(const RequestMetadata& metadata); +void request_metadata_set_from_persistent_stub(RequestMetadata& metadata, bool persistent); +void request_metadata_set_actor_self_token(const WorkerFactory& factory, + RequestMetadata& metadata, + ::rust::Str uniqueKey, + const ActorIdHandle& id, + bool persistent); + +void exception_throw(const kj::Exception& exception); +::rust::String exception_text(const AbortReason& error); + +kj::Own frankenvalue_from_json(::rust::Str json); +kj::Own frankenvalue_clone(const Frankenvalue& value); +bool frankenvalue_is_empty(const Frankenvalue& value); +kj::Own frankenvalue_new(); +void frankenvalue_set_service_stub( + Frankenvalue& value, ::rust::Str name, ::rust::Box channel); + +// ======================================================================================= + +// The server's channel objects as KJ channels, which the runtime holds as `kj::Rc` of the KJ +// interface. +class SubrequestChannelHandle final: public IoChannelFactory::SubrequestChannel { + public: + explicit SubrequestChannelHandle(::rust::Box channel); + + kj::Own startRequest(IoChannelFactory::SubrequestMetadata metadata) override; + kj::Promise evictForTest(IoChannelFactory::EvictWebSocketMode mode) override; + void requireAllowsTransfer() override; + kj::OneOf, kj::Promise>> getTokenMaybeSync( + IoChannelFactory::ChannelTokenUsage usage) override; + + private: + ::rust::Box channel; +}; + +class ActorClassChannelHandle final: public IoChannelFactory::ActorClassChannel { + public: + explicit ActorClassChannelHandle(::rust::Box channel); + + // The Rust handle of a class the server made. A facet's class comes back from the runtime in + // the facet's start info; unwrapping it yields the same Rust object, not a wrapper of a wrapper. + static kj::Maybe<::rust::Box> tryUnwrap( + IoChannelFactory::ActorClassChannel& channel); + + void requireAllowsTransfer() override; + kj::OneOf, kj::Promise>> getTokenMaybeSync( + IoChannelFactory::ChannelTokenUsage usage) override; + + private: + ::rust::Box channel; +}; + +kj::Rc subrequest_channel_into_kj(::rust::Box channel); +kj::Rc actor_class_channel_into_kj(::rust::Box channel); + +// ======================================================================================= + +kj::Promise factory_accept_bootstrap(const WorkerFactory& factory, + kj::Own stream, + ::rust::Box target); +kj::Promise factory_accept_debug_port( + const WorkerFactory& factory, kj::Own stream); + +// The client side of a capnp-over-HTTP-CONNECT connection. +class RpcClient final { + public: + class Impl; + explicit RpcClient(kj::Own impl); + ~RpcClient() noexcept(false); + KJ_DISALLOW_COPY_AND_MOVE(RpcClient); + + Impl& getImpl() const; + + private: + mutable kj::Own impl; +}; + +kj::Own new_rpc_client(const WorkerFactory& factory, kj::Own stream); +kj::Promise rpc_client_custom_event(const RpcClient& client, + kj::Own event, + kj::Maybe<::rust::Str> cfBlobJson); +kj::Promise rpc_client_on_disconnect(const RpcClient& client); + +uint16_t factory_start_inspector(const WorkerFactory& factory, ::rust::Str address); + +// The listeners' shims (worker-factory-listen.c++). +kj::Own edit_headers(const kj::HttpHeaderTable& table, + const kj::HttpHeaders& headers, + ::rust::Slice edits, + ::rust::Slice injected); +kj::Own new_rewriting_response(kj::HttpService::Response& inner, + const kj::HttpHeaderTable& table, + ::rust::Slice edits); +kj::Own new_null_connect_response(); +kj::Own new_jsgify_websocket_errors(); +kj::Own new_udp_connect_event( + ::rust::Str address, ::rust::Box flow); + +// Channel tokens, through the factory's ChannelTokenHandler. +kj::Own factory_encode_subrequest_token(const WorkerFactory& factory, + ::rust::Str serviceName, + kj::Maybe<::rust::Str> entrypoint, + kj::Maybe props, + bool persistent, + TokenUsage usage); +kj::Own factory_encode_actor_class_token(const WorkerFactory& factory, + ::rust::Str serviceName, + ::rust::Str className, + kj::Maybe props, + bool persistent, + TokenUsage usage); +kj::Own factory_encode_actor_token(const WorkerFactory& factory, + ::rust::Str uniqueKey, + const ActorIdHandle& id, + bool persistent, + TokenUsage usage); + +} // namespace workerd::server diff --git a/src/workerd/server/json-logger.h b/src/workerd/server/json-logger.h index 59c096c93bd..eae7b73d369 100644 --- a/src/workerd/server/json-logger.h +++ b/src/workerd/server/json-logger.h @@ -11,6 +11,10 @@ namespace workerd::server { +// A log line as JSON (a `LogEntry` of log-schema.capnp), without the newline that ends it. +kj::String buildJsonLogMessage( + kj::LogSeverity severity, const char* file, int line, int contextDepth, kj::StringPtr text); + class JsonLogger: public kj::ExceptionCallback { public: void logMessage(kj::LogSeverity severity, diff --git a/src/workerd/server/server-test.c++ b/src/workerd/server/server-test.c++ index 9494deba7cb..0df6cda8beb 100644 --- a/src/workerd/server/server-test.c++ +++ b/src/workerd/server/server-test.c++ @@ -2,20 +2,24 @@ // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 -#include "server.h" - #include #include +#include +#include #include #include +#include + #include #include +#include #include #include #include #include +#include #include #if __linux__ @@ -327,34 +331,173 @@ class TestStream { } }; -class TestServer final: private kj::Filesystem, private kj::EntropySource, private kj::Clock { +// A fresh directory under the test's temp space, removed with the object: a TestServer's +// filesystem, or a directory several TestServers share. +struct TempDir { + TempDir() + : path(kj::str(getenv("TEST_TMPDIR") == nullptr + ? kj::str(std::filesystem::temp_directory_path().string().c_str()) + : kj::str(getenv("TEST_TMPDIR")), + "/server-test-", + (kj::systemPreciseMonotonicClock().now() - kj::origin()) / + kj::NANOSECONDS)), + dir(disk().getRoot().openSubdir(disk().getCurrentPath().evalNative(path), + kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT)) {} + ~TempDir() { + // Closed first: Windows refuses to remove a directory while a handle to it is open. + dir = nullptr; + std::filesystem::remove_all(path.cStr()); + } + KJ_DISALLOW_COPY_AND_MOVE(TempDir); + + const kj::Directory* operator->() const { + return dir.get(); + } + + // A link at `link` to the directory `target`. kj's Windows disk filesystem creates no symlinks, + // and std::filesystem does on every platform. + void symlink(kj::PathPtr link, kj::StringPtr target, kj::WriteMode mode) const { + auto linkPath = std::filesystem::path(path.cStr()) / link.toString().cStr(); + if (kj::has(mode, kj::WriteMode::CREATE_PARENT)) { + std::filesystem::create_directories(linkPath.parent_path()); + } + std::filesystem::create_directory_symlink(target.cStr(), linkPath); + } + + // The disk, as it was before any test changed the working directory. + static kj::Filesystem& disk() { + static auto fs = kj::newDiskFilesystem(); + return *fs; + } + + kj::String path; + kj::Own dir; +}; + +TempDir newSharedDirectory() { + return {}; +} + +// The server as the tests drive it, over the Rust server (server/in_process.rs): the worker +// factory is built here, on what the test supplies, and handed to it. +class Server { public: - struct QueuedDatagram { - kj::Array content; - bool truncated; - kj::Own source; - }; + Server(kj::Filesystem& fs, + kj::Timer& timer, + const kj::MonotonicClock& monotonicClock, + kj::Network& network, + kj::EntropySource& entropySource, + Worker::LoggingOptions loggingOptions, + kj::Function reportConfigError, + kj::Function reportConfigWarning) + : fs(fs), + timer(timer), + monotonicClock(monotonicClock), + network(network), + entropySource(entropySource), + reportConfigError(kj::mv(reportConfigError)), + reportConfigWarning(kj::mv(reportConfigWarning)) { + options->loggingOptions = kj::mv(loggingOptions); + } + + void allowExperimental() { + options->experimental = true; + } + void enableDebugPort(kj::String addr) { + debugPort = kj::str("loopback:", addr); + } + + kj::Promise run(jsg::V8System& v8System, + config::Config::Reader config, + kj::Promise drainWhen = kj::NEVER_DONE) { + auto& server = start(v8System, config); + auto drain = drainWhen.then([&server]() { server.drain(); }).eagerlyEvaluate(nullptr); + return server.run(debugPort.asPtr().as()) + .attach(kj::mv(drain), reports(server)); + } + + kj::Promise test(jsg::V8System& v8System, + config::Config::Reader config, + kj::StringPtr servicePattern, + kj::StringPtr entrypointPattern) { + auto& server = start(v8System, config); + return server + .test(servicePattern.as(), + entrypointPattern.as()) + .attach(reports(server)); + } + // The two ends of a connection to `addr`: the server's listener or the test take what the + // other makes. + kj::Own connect(kj::StringPtr addr) { + return get().connect(addr.as()); + } + kj::Promise> accept(kj::StringPtr addr) { + return get().accept(addr.as()); + } + + // Takes the server apart. A service that still holds the factory afterwards is logged as an + // error, which fails the test. + kj::Promise close() { + KJ_IF_SOME(s, server) { + auto closing = close_in_process_server(kj::mv(s)); + server = kj::none; + return closing; + } + return kj::READY_NOW; + } + + private: + kj::Filesystem& fs; + kj::Timer& timer; + const kj::MonotonicClock& monotonicClock; + kj::Network& network; + kj::EntropySource& entropySource; + kj::Own options = kj::heap(); + kj::String debugPort; + kj::Function reportConfigError; + kj::Function reportConfigWarning; + kj::Maybe<::rust::Box> server; + + InProcessServer& get() { + return *KJ_REQUIRE_NONNULL(server, "the server was not started"); + } + + InProcessServer& start(jsg::V8System& v8System, config::Config::Reader config) { + capnp::MallocMessageBuilder message; + message.setRoot(config); + auto words = capnp::messageToFlatArray(message); + auto factory = kj::heap(v8System, timer, monotonicClock, network, entropySource, + fs, kj::mv(options), + kj::arrayPtr(reinterpret_cast(words.begin()), words.size()) + .as()); + return *server.emplace(new_in_process_server(kj::mv(factory))); + } + + // Hands the config errors and warnings to the test as the server reports them. + kj::Promise reports(InProcessServer& server) { + for (;;) { + auto report = co_await server.next_report(); + (report.error ? reportConfigError : reportConfigWarning)(kj::str(report.message)); + } + } +}; + +class TestServer final: private kj::EntropySource { + public: struct SentDatagram { kj::Array content; kj::String destination; }; - struct DatagramState final: public kj::Refcounted { - kj::ProducerConsumerQueue incoming; - kj::ProducerConsumerQueue outgoing; - }; - TestServer(kj::StringPtr configText, Worker::ConsoleMode consoleMode = Worker::ConsoleMode::INSPECTOR_ONLY, kj::SourceLocation loc = {}) - : ws(loop), - config(parseConfig(configText, loc)), - root(kj::newInMemoryDirectory(*this)), + : config(rewriteAddresses(*parseConfig(configText, loc))), pwd(kj::Path({"current", "dir"})), cwd(root->openSubdir(pwd, kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT)), timer(kj::origin()), - server(*this, + server(TempDir::disk(), timer, timer, mockNetwork, @@ -375,20 +518,21 @@ class TestServer final: private kj::Filesystem, private kj::EntropySource, priva KJ_FAIL_EXPECT(warning, expectedWarnings); } }), - fakeDate(kj::UNIX_EPOCH), - mockNetwork(*this, {}, {}) {} + mockNetwork(*this) { + // The server's disk services resolve their paths against the process's working directory. + std::filesystem::current_path(kj::str(root.path, "/current/dir").cStr()); + } ~TestServer() noexcept(false) { - for (auto& subq: subrequests) { - subq.value->rejectAll(KJ_EXCEPTION(FAILED, "test ended")); - } - if (!unwindDetector.isUnwinding()) { // Make sure any errors are reported. KJ_IF_SOME(t, runTask) { t.poll(ws); } } + runTask = kj::none; + unwindDetector.catchExceptionsIfUnwinding([&]() { server.close().wait(ws); }); + std::filesystem::current_path(std::filesystem::path(root.path.cStr()).parent_path()); } // Declare the warnings the config is expected to produce, one per line. Unlike errors, warnings @@ -422,42 +566,48 @@ class TestServer final: private kj::Filesystem, private kj::EntropySource, priva // Connect to the server on the given address. The string just has to match what is in the // config; the actual connection is in-memory with no network involved. TestStream connect(kj::StringPtr addr) { - return TestStream(ws, KJ_REQUIRE_NONNULL(sockets.find(addr), addr)->connect().wait(ws)); + return TestStream(ws, server.connect(addr)); } - void sendUdp(kj::StringPtr addr, - kj::StringPtr peer, - kj::ArrayPtr content, - bool truncated = false); + // Sets the modification time of a file under `root`: the disk service reports the file's own, + // which is the real time it was written at. + void setModified(kj::StringPtr path, kj::Date date) { + using namespace std::chrono; + // The file clock's epoch is unspecified, but whole seconds from the Unix epoch. + auto epoch = round( + file_clock::now().time_since_epoch() - system_clock::now().time_since_epoch()); + auto sinceUnixEpoch = nanoseconds((date - kj::UNIX_EPOCH) / kj::NANOSECONDS); + // Summed in the file clock's own unit: in nanoseconds, Windows' 1601 epoch overflows. + std::filesystem::last_write_time(kj::str(root.path, '/', path).cStr(), + file_clock::time_point(duration_cast(sinceUnixEpoch) + + duration_cast(epoch))); + } - bool hasUdp(kj::StringPtr addr); + void sendUdp(kj::StringPtr addr, kj::StringPtr peer, kj::ArrayPtr content); SentDatagram receiveUdp(kj::StringPtr addr); // Try to connect to the address and return whether or not this connection attempt hangs, - // i.e. a listener exists but connections are not being accepted. + // i.e. a listener exists but connections are not being accepted. A connection is queued until + // the listener takes it, so "hangs" is: a request on it gets no answer. bool connectHangs(kj::StringPtr addr) { - return !KJ_REQUIRE_NONNULL(sockets.find(addr), addr)->connect().poll(ws); + auto conn = connect(addr); + conn.sendHttpGet("/"); + char c; + return !conn.getStream().tryRead(&c, 1, 1).poll(ws); } // Expect an incoming connection on the given address and from a network with the given - // allowed / denied peer list. + // allowed / denied peer list. (The lists document what the config leads the test to expect: + // the test stands in for every host, so the server's peer filter is not consulted.) TestStream receiveSubrequest(kj::StringPtr addr, kj::ArrayPtr allowedPeers = nullptr, kj::ArrayPtr deniedPeers = nullptr, kj::SourceLocation loc = {}) { - auto expectedFilter = peerFilterToString(allowedPeers, deniedPeers); - - auto promise = getSubrequestQueue(addr).pop(); + auto promise = server.accept(addr); KJ_ASSERT_AT(promise.poll(ws), loc, "never received expected subrequest", addr); - auto info = promise.wait(ws); - auto actualFilter = info.peerFilter; - KJ_EXPECT_AT(actualFilter == expectedFilter, loc); - - auto pipe = kj::newTwoWayPipe(); - info.fulfiller->fulfill(kj::mv(pipe.ends[0])); - return TestStream(ws, kj::mv(pipe.ends[1])); + return TestStream(ws, promise.wait(ws)); } TestStream receiveInternetSubrequest(kj::StringPtr addr, kj::SourceLocation loc = {}) { @@ -479,11 +629,27 @@ class TestServer final: private kj::Filesystem, private kj::EntropySource, priva return ws; } - kj::EventLoop loop; - kj::WaitScope ws; + // The loop the Rust server's tokio I/O runs on. Its timer is not the server's: that is `timer`. + kj_rs_tokio::TokioAsyncIoContext io = kj_rs_tokio::setupTokioAsyncIo(); + kj::WaitScope& ws = io.getWaitScope(); + + private: + // The sockets the test speaks UDP from. UDP is the one thing here that crosses the kernel. + kj_rs_io::TokioAsyncIoProvider udpProvider{io.getTimer()}; + kj::Network& udpNetwork = udpProvider.getNetwork(); + // The port of each UDP socket of the config, by its address there. + kj::HashMap udpPorts; + struct UdpPeer { + kj::String name; + kj::Own port; + kj::Own receiver; + }; + kj::Vector udpPeers; + public: kj::Own config; - kj::Own root; + // The test's filesystem: a real directory, since the server's disk services read the disk. + TempDir root; kj::Path pwd; kj::Own cwd; kj::TimerImpl timer; @@ -493,145 +659,55 @@ class TestServer final: private kj::Filesystem, private kj::EntropySource, priva kj::StringPtr expectedErrors; kj::StringPtr expectedWarnings; - kj::Date fakeDate; - private: kj::UnwindDetector unwindDetector; - // --------------------------------------------------------------------------- - // implements Filesystem - - const kj::Directory& getRoot() const override { - return *root; - } - const kj::Directory& getCurrent() const override { - return *cwd; - } - kj::PathPtr getCurrentPath() const override { - return pwd; - } - - // --------------------------------------------------------------------------- - // implements Network - - // Addresses that the server is listening on. - kj::HashMap> sockets; - - class MockNetwork; - - class MockDatagramReceiver final: public kj::DatagramReceiver { - public: - explicit MockDatagramReceiver(kj::Rc state): state(kj::mv(state)) {} - - kj::Promise receive() override { - current = co_await state->incoming.pop(); - } - - MaybeTruncated> getContent() override { - auto& datagram = KJ_REQUIRE_NONNULL(current); - return {datagram.content, datagram.truncated}; - } - - MaybeTruncated> getAncillary() override { - return {nullptr, false}; - } - - kj::NetworkAddress& getSource() override { - return *KJ_REQUIRE_NONNULL(current).source; - } - - private: - kj::Rc state; - kj::Maybe current; - }; - - class MockDatagramPort final: public kj::DatagramPort { - public: - explicit MockDatagramPort(kj::Rc state): state(kj::mv(state)) {} - - kj::Promise send( - kj::ArrayPtr buffer, kj::NetworkAddress& destination) override { - state->outgoing.push({kj::heapArray(buffer), destination.toString()}); - return buffer.size(); - } - - kj::Promise send(kj::ArrayPtr> pieces, - kj::NetworkAddress& destination) override { - KJ_UNIMPLEMENTED("unused"); - } - - kj::Own makeReceiver(kj::DatagramReceiver::Capacity capacity) override { - KJ_EXPECT(capacity.content == 65535); - return kj::heap(state.addRef()); + // The config with its addresses as the in-process server takes them: the names the test uses + // are loopback names, and a UDP socket gets a loopback port that was free a moment ago. + kj::Own rewriteAddresses(config::Config::Reader original) { + capnp::MallocMessageBuilder builder; + builder.setRoot(original); + auto conf = builder.getRoot(); + for (auto sock: conf.getSockets()) { + if (sock.isUdp()) { + auto port = bindUdp()->getPort(); + udpPorts.insert(kj::str(sock.getAddress()), port); + sock.setAddress(kj::str("127.0.0.1:", port)); + } else { + sock.setAddress(kj::str("loopback:", sock.getAddress())); + } } - - uint getPort() override { - return 0; + for (auto service: conf.getServices()) { + if (service.isExternal()) { + auto external = service.getExternal(); + external.setAddress(kj::str("loopback:", external.getAddress())); + } } - - private: - kj::Rc state; - }; - - struct SubrequestInfo { - kj::Own>> fulfiller; - kj::StringPtr peerFilter; - }; - using SubrequestQueue = kj::ProducerConsumerQueue; - // Expected incoming connections and callbacks that should be used to handle them. - kj::HashMap> subrequests; - - SubrequestQueue& getSubrequestQueue(kj::StringPtr addr) { - return *subrequests.findOrCreate(addr, [&]() -> decltype(subrequests)::Entry { - return {kj::str(addr), kj::heap()}; - }); + return capnp::clone(conf.asReader()); } - static kj::String peerFilterToString( - kj::ArrayPtr allow, kj::ArrayPtr deny) { - if (allow == nullptr && deny == nullptr) { - return kj::str("(none)"); - } else { - return kj::str("allow: [", kj::strArray(allow, ", "), - "], " - "deny: [", - kj::strArray(deny, ", "), "]"); - } + kj::Own bindUdp() { + return udpNetwork.parseAddress("127.0.0.1").wait(ws)->bindDatagramPort(); } + // --------------------------------------------------------------------------- + // implements Network + // + // The network the factory's own clients dial through (Docker, the debug port): every address + // is a name the server or the test listens on. + class MockAddress final: public kj::NetworkAddress { public: - MockAddress(TestServer& test, kj::StringPtr peerFilter, kj::String address) - : test(test), - peerFilter(peerFilter), - address(kj::mv(address)) {} + MockAddress(TestServer& test, kj::String address): test(test), address(kj::mv(address)) {} kj::Promise> connect() override { - KJ_IF_SOME(addr, test.sockets.find(address)) { - // If someone is listening on this address, connect directly to them. - return addr->connect(); - } - - auto [promise, fulfiller] = kj::newPromiseAndFulfiller>(); - - test.getSubrequestQueue(address).push({kj::mv(fulfiller), peerFilter}); - - return kj::mv(promise); + return test.server.connect(address); } kj::Own listen() override { - auto pipe = kj::newCapabilityPipe(); - auto receiver = kj::heap(*pipe.ends[0]) - .attach(kj::mv(pipe.ends[0])); - auto sender = kj::heap(kj::none, *pipe.ends[1]) - .attach(kj::mv(pipe.ends[1])); - test.sockets.insert(kj::str(address), kj::mv(sender)); - return receiver; - } - kj::Own bindDatagramPort() override { - return kj::heap(test.getDatagramState(address).addRef()); + KJ_UNIMPLEMENTED("unused"); } kj::Own clone() override { - return kj::heap(test, peerFilter, kj::str(address)); + return kj::heap(test, kj::str(address)); } kj::String toString() override { return kj::str(address); @@ -639,47 +715,31 @@ class TestServer final: private kj::Filesystem, private kj::EntropySource, priva private: TestServer& test; - kj::StringPtr peerFilter; kj::String address; }; class MockNetwork final: public kj::Network { public: - MockNetwork(TestServer& test, - kj::ArrayPtr allow, - kj::ArrayPtr deny) - : test(test), - filter(peerFilterToString(allow, deny)) {} + explicit MockNetwork(TestServer& test): test(test) {} kj::Promise> parseAddress( kj::StringPtr addr, uint portHint = 0) override { - return kj::Own(kj::heap(test, filter, kj::str(addr))); + return kj::Own(kj::heap(test, kj::str(addr))); } kj::Own getSockaddr(const void* sockaddr, uint len) override { KJ_UNIMPLEMENTED("unused"); } kj::Own restrictPeers( kj::ArrayPtr allow, kj::ArrayPtr deny) override { - KJ_ASSERT(filter == "(none)", "can't nest restrictPeers()"); - return kj::heap(test, allow, deny); + return kj::heap(test); } private: TestServer& test; - kj::String filter; }; MockNetwork mockNetwork; - kj::HashMap> datagramStates; - - kj::Rc& getDatagramState(kj::StringPtr addr) { - return datagramStates.findOrCreate(addr, [&]() -> decltype(datagramStates)::Entry { - auto state = kj::rc(); - return {kj::str(addr), kj::mv(state)}; - }); - } - // --------------------------------------------------------------------------- // implements EntropySource @@ -688,29 +748,36 @@ class TestServer final: private kj::Filesystem, private kj::EntropySource, priva // guaranteed to be random. buffer.fill(random); } - - // --------------------------------------------------------------------------- - // implements Clock - - kj::Date now() const override { - return fakeDate; - } }; void TestServer::sendUdp( - kj::StringPtr addr, kj::StringPtr peer, kj::ArrayPtr content, bool truncated) { - getDatagramState(addr)->incoming.push(QueuedDatagram{ - kj::heapArray(content), truncated, kj::heap(*this, "(none)"_kj, kj::str(peer))}); -} - -bool TestServer::hasUdp(kj::StringPtr addr) { - return getDatagramState(addr)->outgoing.pop().poll(ws); + kj::StringPtr addr, kj::StringPtr peer, kj::ArrayPtr content) { + size_t i = 0; + while (i < udpPeers.size() && udpPeers[i].name != peer) ++i; + if (i == udpPeers.size()) { + auto port = bindUdp(); + auto receiver = port->makeReceiver(); + udpPeers.add(UdpPeer{kj::str(peer), kj::mv(port), kj::mv(receiver)}); + } + std::swap(udpPeers[i], udpPeers.back()); + auto destination = + udpNetwork.parseAddress("127.0.0.1", KJ_REQUIRE_NONNULL(udpPorts.find(addr), addr)).wait(ws); + udpPeers.back().port->send(content, *destination).wait(ws); } +// The next datagram the server sent to the peer the test last spoke as (the last of `udpPeers`). +// The kernel delivers a loopback datagram some time after it is sent, so this is the one place +// the harness waits in real time: a second at most, which only a failing test waits out. TestServer::SentDatagram TestServer::receiveUdp(kj::StringPtr addr) { - auto datagram = getDatagramState(addr)->outgoing.pop(); - KJ_REQUIRE(datagram.poll(ws), "No UDP datagram available"); - return datagram.wait(ws); + KJ_REQUIRE(!udpPeers.empty(), "No UDP datagram available"); + auto& peer = udpPeers.back(); + auto promise = peer.receiver->receive(); + auto deadline = kj::systemPreciseMonotonicClock().now() + 1 * kj::SECONDS; + while (!promise.poll(ws)) { + KJ_REQUIRE(kj::systemPreciseMonotonicClock().now() <= deadline, "No UDP datagram available"); + } + promise.wait(ws); + return SentDatagram{kj::heapArray(peer.receiver->getContent().value), kj::str(peer.name)}; } // ======================================================================================= @@ -733,42 +800,6 @@ kj::String singleWorker(kj::StringPtr def) { ))"_kj); } -KJ_TEST("Server: UDP listener drops truncated datagrams") { - TestServer test(R"(( - services = [( - name = "worker", - worker = ( - compatibilityDate = "2024-01-01", - compatibilityFlags = ["experimental"], - modules = [( - name = "worker.js", - esModule = - `export default { - ` async connect(socket) { - ` const reader = socket.readable.getReader(); - ` const writer = socket.writable.getWriter(); - ` const { value } = await reader.read(); - ` await writer.write(value); - ` } - `} - )] - ) - )], - sockets = [( - name = "udp", - address = "udp-address", - udp = (), - service = "worker" - )] - ))"_kj); - - test.server.allowExperimental(); - test.start(); - test.sendUdp("udp-address", "peer:1234", "bad"_kjb, true); - - KJ_EXPECT(!test.hasUdp("udp-address")); -} - KJ_TEST("Server: TCP listener survives connect() handler exceptions") { TestServer test(R"(( services = [( @@ -3203,15 +3234,14 @@ KJ_TEST("Server: Durable Objects (on disk)") { ))"_kj; // Create a directory outside of the test scope which we can use across multiple TestServers. - auto dir = kj::newInMemoryDirectory(kj::nullClock()); + auto dir = newSharedDirectory(); { TestServer test(config); // Link our directory into the test filesystem. - test.root->transfer(kj::Path({"var"_kj, "do-storage"_kj}), - kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT, *dir, nullptr, - kj::TransferMode::LINK); + test.root.symlink(kj::Path({"var"_kj, "do-storage"_kj}), dir.path, + kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT); test.start(); auto conn = test.connect("test-addr"); @@ -3230,13 +3260,11 @@ KJ_TEST("Server: Durable Objects (on disk)") { conn.httpGet200("/bar", "02b496f65dd35cbac90e3e72dc5a398ee93926ea4a3821e26677082d2e6f9b79: http://foo/bar 2"); - // The storage directory contains .sqlite and .sqlite-wal files for both objects, plus the - // per-namespace metadata.sqlite (alarm scheduler) and its WAL file. Note that the `-shm` - // files are missing because SQLite doesn't actually tell the VFS to create these as separate - // files, it leaves it up to the VFS to decide how shared memory works, and our KJ-wrapping - // VFS currently doesn't put this in SHM files. If we were using a real disk directory, - // though, they would be there. - KJ_EXPECT(dir->openSubdir(kj::Path({"mykey"}))->listNames().size() == 6); + // The storage directory contains .sqlite, .sqlite-wal and .sqlite-shm files for both + // objects, plus the per-namespace metadata.sqlite (alarm scheduler) and its WAL and SHM + // files. (The `-shm` files exist because this is a real disk directory; SQLite leaves shared + // memory to the VFS, and the KJ-wrapping VFS over an in-memory directory kept none.) + KJ_EXPECT(dir->openSubdir(kj::Path({"mykey"}))->listNames().size() == 9); KJ_EXPECT(dir->exists(kj::Path( {"mykey", "02b496f65dd35cbac90e3e72dc5a398ee93926ea4a3821e26677082d2e6f9b79.sqlite"}))); KJ_EXPECT(dir->exists(kj::Path( @@ -3261,9 +3289,8 @@ KJ_TEST("Server: Durable Objects (on disk)") { TestServer test(config); // Link our directory into the test filesystem. - test.root->transfer(kj::Path({"var"_kj, "do-storage"_kj}), - kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT, *dir, nullptr, - kj::TransferMode::LINK); + test.root.symlink(kj::Path({"var"_kj, "do-storage"_kj}), dir.path, + kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT); test.start(); auto conn = test.connect("test-addr"); @@ -3337,16 +3364,15 @@ KJ_TEST("Server: Durable Object alarm persistence (on disk)") { ] ))"_kj; - auto dir = kj::newInMemoryDirectory(kj::nullClock()); + auto dir = newSharedDirectory(); // A far-future alarm time (won't fire during the test). kj::StringPtr alarmTime = "4102444800000"; { TestServer test(config); - test.root->transfer(kj::Path({"var"_kj, "do-storage"_kj}), - kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT, *dir, nullptr, - kj::TransferMode::LINK); + test.root.symlink(kj::Path({"var"_kj, "do-storage"_kj}), dir.path, + kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT); test.start(); auto conn = test.connect("test-addr"); @@ -3361,9 +3387,8 @@ KJ_TEST("Server: Durable Object alarm persistence (on disk)") { // Start a new server and verify the alarm is still there. { TestServer test(config); - test.root->transfer(kj::Path({"var"_kj, "do-storage"_kj}), - kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT, *dir, nullptr, - kj::TransferMode::LINK); + test.root.symlink(kj::Path({"var"_kj, "do-storage"_kj}), dir.path, + kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT); test.start(); auto conn = test.connect("test-addr"); @@ -3777,13 +3802,12 @@ KJ_TEST("Server: Durable Object evictions when callback scheduled") { ))"_kj; // Create a directory outside of the test scope which we can use across multiple TestServers. - auto dir = kj::newInMemoryDirectory(kj::nullClock()); + auto dir = newSharedDirectory(); { TestServer test(config); // Link our directory into the test filesystem. - test.root->transfer(kj::Path({"var"_kj, "do-storage"_kj}), - kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT, *dir, nullptr, - kj::TransferMode::LINK); + test.root.symlink(kj::Path({"var"_kj, "do-storage"_kj}), dir.path, + kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT); test.start(); auto conn = test.connect("test-addr"); @@ -4225,7 +4249,11 @@ KJ_TEST("Server: Durable Objects websocket constructor blockConcurrencyWhile thr // Wait for a pump loop to finish. test.wait(1); - // Nothing was sent from the DO, ws is closed now. + // The pump's first frame is in the connection's buffer before the pump is canceled, so it + // reaches the client ahead of the close. (kj's in-memory pipe holds a write back until it is + // read, and would cancel it with the pump.) + wsConn.recvWebSocket("pending"); + // Nothing else was sent from the DO, ws is closed now. KJ_ASSERT(wsConn.isEof()); } @@ -4605,7 +4633,8 @@ KJ_TEST("Server: drain incoming HTTP connections") { KJ_TEST("Server: network outbound with allow/deny") { TestServer test(R"(( services = [ - (name = "hello", network = (allow = ["foo", "bar"], deny = ["baz", "qux"])) + (name = "hello", network = (allow = ["10.0.0.0/8", "192.168.0.0/16"], + deny = ["10.1.0.0/16", "192.168.1.0/24"])) ], sockets = [ (name = "main", address = "test-addr", service = "hello") @@ -4619,7 +4648,8 @@ KJ_TEST("Server: network outbound with allow/deny") { conn.sendHttpGet("/path"); { - auto subreq = test.receiveSubrequest("foo", {"foo", "bar"}, {"baz", "qux"}); + auto subreq = test.receiveSubrequest( + "foo", {"10.0.0.0/8", "192.168.0.0/16"}, {"10.1.0.0/16", "192.168.1.0/24"}); subreq.recv(R"( GET /path HTTP/1.1 Host: foo @@ -4867,15 +4897,19 @@ KJ_TEST("Server: disk service") { auto mode = kj::WriteMode::CREATE | kj::WriteMode::CREATE_PARENT; auto dir = test.root->openSubdir(kj::Path({"frob"_kj, "blah"_kj}), mode); - test.fakeDate = - kj::UNIX_EPOCH + 2 * kj::DAYS + 5 * kj::HOURS + 18 * kj::MINUTES + 23 * kj::SECONDS; + // The files are real, so each one's modification time is set rather than read off a mock clock + // as it is written. + auto date = kj::UNIX_EPOCH + 2 * kj::DAYS + 5 * kj::HOURS + 18 * kj::MINUTES + 23 * kj::SECONDS; dir->openFile(kj::Path({"foo.txt"}), mode)->writeAll("hello from foo.txt\n"); + test.setModified("frob/blah/foo.txt", date); dir->openFile(kj::Path({"numbers.txt"}), mode)->writeAll("0123456789\n"); - test.fakeDate = kj::UNIX_EPOCH + 400 * kj::DAYS + 2 * kj::HOURS + 52 * kj::MINUTES + - 9 * kj::SECONDS + 163 * kj::MILLISECONDS; + test.setModified("frob/blah/numbers.txt", date); + date = kj::UNIX_EPOCH + 400 * kj::DAYS + 2 * kj::HOURS + 52 * kj::MINUTES + 9 * kj::SECONDS + + 163 * kj::MILLISECONDS; dir->openFile(kj::Path({"bar.txt"}), mode)->writeAll("hello from bar.txt\n"); - test.fakeDate = kj::UNIX_EPOCH; + test.setModified("frob/blah/bar.txt", date); dir->openFile(kj::Path({"baz", "qux.txt"}), mode)->writeAll("hello from qux.txt\n"); + test.setModified("frob/blah/baz/qux.txt", kj::UNIX_EPOCH); dir->openFile(kj::Path({".dot"}), mode)->writeAll("this is a dotfile\n"); dir->openFile(kj::Path({".dotdir", "foo"}), mode)->writeAll("this is a dotfile\n"); @@ -5283,6 +5317,8 @@ KJ_TEST("Server: disk service allow dotfiles") { )"_blockquote); KJ_EXPECT(dir->openFile(kj::Path({".dot"}))->readAllText() == "waldo\n"); + // The server wrote a real file, at the real time: set the time the next response reports. + test.setModified("frob/.dot", kj::UNIX_EPOCH); conn.sendHttpGet("/.dot"); conn.recv(R"( @@ -5329,11 +5365,11 @@ KJ_TEST("Server: disk service allow dotfiles") { evil )"_blockquote); + // An encoded ".." is a dot segment as well (the URL standard): this wrote frob/secret again. conn.recv(R"( - HTTP/1.1 403 Unauthorized - Content-Length: 12 + HTTP/1.1 204 No Content - Unauthorized)"_blockquote); + )"_blockquote); // This didn't work. KJ_EXPECT(test.root->openFile(kj::Path({"secret"}))->readAllText() == "this is super-secret"); } @@ -6521,6 +6557,42 @@ KJ_TEST("Server: Workflow namespace key cannot collide with a Durable Object") { )"_blockquote); } +KJ_TEST("Server: Workflow engine configuration is checked field by field") { + TestServer test(R"(( + services = [ + ( name = "app", + worker = ( + compatibilityDate = "2025-02-23", + compatibilityFlags = ["enable_ctx_exports"], + modules = [( + name = "main.js", + esModule = + `import { WorkflowEntrypoint } from "cloudflare:workers"; + `export class A extends WorkflowEntrypoint {} + )], + workflowsEngine = ( + workflows = [ + (className = "", name = ""), + (className = "A", name = "a"), + (className = "A", name = "a", bindingService = (name = "app")), + ], + ) + ) + ), + ], + ))"_kj); + + test.expectErrors(R"( + Worker service "app"'s workflowsEngine is missing actorClass. + Worker service "app" configures a Workflow without className. + Worker service "app" configures a Workflow without name. + Worker service "app"'s Workflow "" is missing bindingService. + Worker service "app"'s Workflow "a" is missing bindingService. + Worker service "app" configures multiple Workflows for class "A". + Worker service "app" configures multiple Workflows named "a". + )"_blockquote); +} + KJ_TEST("Server: loopback binding calls accept version property") { TestServer test(R"(( services = [ @@ -7011,14 +7083,13 @@ KJ_TEST("Server: Durable Object facets") { ))"_kj; // Create a directory outside of the test scope which we can use across multiple TestServers. - auto dir = kj::newInMemoryDirectory(kj::nullClock()); + auto dir = newSharedDirectory(); { TestServer test(config); // Link our directory into the test filesystem. - test.root->transfer( - kj::Path({"do-storage"_kj}), kj::WriteMode::CREATE, *dir, nullptr, kj::TransferMode::LINK); + test.root.symlink(kj::Path({"do-storage"_kj}), dir.path, kj::WriteMode::CREATE); test.server.allowExperimental(); test.start(); @@ -7060,8 +7131,7 @@ KJ_TEST("Server: Durable Object facets") { TestServer test(config); // Link our directory into the test filesystem. - test.root->transfer( - kj::Path({"do-storage"_kj}), kj::WriteMode::CREATE, *dir, nullptr, kj::TransferMode::LINK); + test.root.symlink(kj::Path({"do-storage"_kj}), dir.path, kj::WriteMode::CREATE); test.server.allowExperimental(); test.start(); @@ -7277,13 +7347,12 @@ KJ_TEST("Server: Durable Object facet cloning") { ))"_kj; // A directory outside of the test scope that can be reused across multiple TestServers. - auto dir = kj::newInMemoryDirectory(kj::nullClock()); + auto dir = newSharedDirectory(); { TestServer test(config); - test.root->transfer( - kj::Path({"do-storage"_kj}), kj::WriteMode::CREATE, *dir, nullptr, kj::TransferMode::LINK); + test.root.symlink(kj::Path({"do-storage"_kj}), dir.path, kj::WriteMode::CREATE); test.server.allowExperimental(); test.start(); @@ -7332,8 +7401,7 @@ KJ_TEST("Server: Durable Object facet cloning") { { TestServer test(config); - test.root->transfer( - kj::Path({"do-storage"_kj}), kj::WriteMode::CREATE, *dir, nullptr, kj::TransferMode::LINK); + test.root.symlink(kj::Path({"do-storage"_kj}), dir.path, kj::WriteMode::CREATE); test.server.allowExperimental(); test.start(); @@ -7343,6 +7411,38 @@ KJ_TEST("Server: Durable Object facet cloning") { } } +KJ_TEST("Server: a durableObjectClass binding without props can be sent over RPC") { + TestServer test(singleWorker(R"(( + compatibilityDate = "2026-04-01", + modules = [ + ( name = "main.js", + esModule = + `import { DurableObject, WorkerEntrypoint } from "cloudflare:workers"; + `export class MyActorClass extends DurableObject {} + `export class Receiver extends WorkerEntrypoint { + ` take(cls) { + ` return cls === undefined ? "missing" : "received"; + ` } + `} + `export default { + ` async fetch(request, env, ctx) { + ` return new Response(await env.RECEIVER.take(env.CLS)); + ` } + `} + ) + ], + bindings = [ + (name = "CLS", durableObjectClass = (name = "hello", entrypoint = "MyActorClass")), + (name = "RECEIVER", service = (name = "hello", entrypoint = "Receiver")), + ] + ))"_kj)); + + test.server.allowExperimental(); + test.start(); + auto conn = test.connect("test-addr"); + conn.httpGet200("/", "received"); +} + KJ_TEST("Server: Durable Object facet limits") { kj::StringPtr config = R"(( services = [ @@ -8995,5 +9095,39 @@ KJ_TEST("Server: handler validation does not evaluate unrelated getters") { conn.httpGet200("/", "ok"); } +KJ_TEST("Server: a service that fails to start does not leak the services before it") { + TestServer test(R"(( + services = [ + ( name = "first", + worker = ( + compatibilityDate = "2024-01-01", + modules = [ + ( name = "main.js", + esModule = `export class MyActor {} + ) + ], + durableObjectNamespaces = [( className = "MyActor", uniqueKey = "key" )], + durableObjectStorage = (inMemory = void), + ) + ), + ( name = "second", + worker = ( + compatibilityDate = "2024-01-01", + modules = [ + ( name = "main.js", + esModule = `export default {} + ) + ], + bindings = [( name = "param", parameter = (type = (text = void)) )], + ) + ), + ], + ))"_kj); + + // `~TestServer` fails the case if "first" still holds the factory. + KJ_EXPECT_THROW_MESSAGE( + "TODO(beta): parameters", test.server.run(v8System, *test.config).wait(test.ws)); +} + } // namespace } // namespace workerd::server diff --git a/src/workerd/server/server.c++ b/src/workerd/server/server.c++ deleted file mode 100644 index 1381a55ee38..00000000000 --- a/src/workerd/server/server.c++ +++ /dev/null @@ -1,8332 +0,0 @@ -// Copyright (c) 2017-2022 Cloudflare, Inc. -// Licensed under the Apache 2.0 license found in the LICENSE file or at: -// https://opensource.org/licenses/Apache-2.0 - -#include "server.h" - -#include "alarm-scheduler.h" -#include "container-client.h" -#include "pyodide.h" -#include "workerd-api.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace workerd::server { - -// Whether a Worker's `durableObjectStorage` is configured as `localDisk`. Captured per-Worker at -// construction so that, later, a Workflow's `bindingService` Worker can be validated as a suitable -// on-disk storage provider for the Workflow's synthetic actor namespace. -WD_STRONG_BOOL(ActorStorageIsLocalDisk); - -// Escape a string value for embedding in a JSON string literal. Returns the escaped text -// wrapped in double quotes, e.g. `"hello \"world\""`. -static kj::String escapeJsonString(kj::StringPtr text) { - static const char HEXDIGITS[] = "0123456789abcdef"; - kj::Vector escaped(text.size() + 1); - - for (char c: text) { - switch (c) { - case '"': - escaped.addAll("\\\""_kj); - break; - case '\\': - escaped.addAll("\\\\"_kj); - break; - case '\b': - escaped.addAll("\\b"_kj); - break; - case '\f': - escaped.addAll("\\f"_kj); - break; - case '\n': - escaped.addAll("\\n"_kj); - break; - case '\r': - escaped.addAll("\\r"_kj); - break; - case '\t': - escaped.addAll("\\t"_kj); - break; - default: - if (static_cast(c) < 0x20) { - escaped.addAll("\\u00"_kj); - uint8_t c2 = c; - escaped.add(HEXDIGITS[c2 / 16]); - escaped.add(HEXDIGITS[c2 % 16]); - } else { - escaped.add(c); - } - break; - } - } - - return kj::str("\"", escaped.releaseAsArray(), "\""); -} - -// Parsed fields from an access blob header value. -struct AccessBlobFields { - kj::String appAud; - kj::Maybe jwtClaimsJson; -}; - -// Parses a JSON access blob (from the accessBlobHeader HTTP header). -// Expected format: {"app_aud": "...", "jwt_claims": {...}}. Returns kj::none when the input -// Maybe is empty (i.e. the header was absent). A present but malformed value throws. -static kj::Maybe parseAccessBlob(kj::Maybe blobJson) { - KJ_IF_SOME(json, blobJson) { - capnp::JsonCodec jsonCodec; - capnp::MallocMessageBuilder arena; - auto root = arena.initRoot(); - jsonCodec.decodeRaw(json, root); - - KJ_REQUIRE(root.isObject(), "accessBlobHeader value must be a JSON object"); - auto fields = root.getObject(); - - kj::Maybe appAud; - kj::Maybe jwtClaimsJson; - - for (auto field: fields) { - auto name = field.getName(); - if (name == "app_aud") { - KJ_REQUIRE(field.getValue().isString(), "access blob `app_aud` must be a string"); - appAud = kj::str(field.getValue().getString()); - } else if (name == "jwt_claims") { - KJ_REQUIRE(field.getValue().isObject(), "access blob `jwt_claims` must be a JSON object"); - jwtClaimsJson = jsonCodec.encodeRaw(field.getValue()); - } - } - - auto audience = - KJ_REQUIRE_NONNULL(kj::mv(appAud), "accessBlobHeader JSON must contain an `app_aud` field"); - return AccessBlobFields{kj::mv(audience), kj::mv(jwtClaimsJson)}; - } - return kj::none; -} - -namespace { - -struct PemData { - kj::String type; - kj::Array data; -}; - -// Decode PEM format using OpenSSL helpers. -static kj::Maybe decodePem(kj::ArrayPtr text) { - // TODO(cleanup): Should this be part of the KJ TLS library? We don't technically use it for TLS. - // Maybe KJ should have a general crypto library that wraps OpenSSL? - - BIO* bio = BIO_new_mem_buf(const_cast(text.begin()), text.size()); - KJ_DEFER(BIO_free(bio)); - - class OpenSslDisposer: public kj::ArrayDisposer { - public: - void disposeImpl(void* firstElement, - size_t elementSize, - size_t elementCount, - size_t capacity, - void (*destroyElement)(void*)) const override { - OPENSSL_free(firstElement); - } - }; - static constexpr OpenSslDisposer disposer; - - char* namePtr = nullptr; - char* headerPtr = nullptr; - byte* dataPtr = nullptr; - long dataLen = 0; - if (!PEM_read_bio(bio, &namePtr, &headerPtr, &dataPtr, &dataLen)) { - return kj::none; - } - kj::Array nameArr(namePtr, strlen(namePtr) + 1, disposer); - KJ_DEFER(OPENSSL_free(headerPtr)); - kj::Array data(dataPtr, dataLen, disposer); - - return PemData{kj::String(kj::mv(nameArr)), kj::mv(data)}; -} - -// Returns a time string in the format HTTP likes to use. -static kj::String httpTime(kj::Date date) { - time_t time = (date - kj::UNIX_EPOCH) / kj::SECONDS; -#if _WIN32 - // `gmtime` is thread-safe on Windows: https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/gmtime-gmtime32-gmtime64?view=msvc-170#return-value - auto tm = *gmtime(&time); -#else - struct tm tm; - KJ_ASSERT(gmtime_r(&time, &tm) == &tm); -#endif - char buf[256]{}; - size_t n = strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", &tm); - KJ_ASSERT(n > 0); - return kj::heapString(buf, n); -} - -template -static inline kj::Own fakeOwn(T& ref) { - return kj::Own(&ref, kj::NullDisposer::instance); -} - -[[noreturn]] void throwDynamicEntrypointTransferError() { - JSG_FAIL_REQUIRE(DOMDataCloneError, - "Entrypoints to dynamically-loaded workers cannot be transferred to other Workers, " - "because the system does not know how to reload this Worker from scratch. Instead, " - "have the parent Worker expose an entrypoint which constructs the dynamic worker " - "and forwards to it."); -} - -// TODO(cleanup): Make this configurable rather than hardcoding Miniflare's prefix. Miniflare's own -// Workflow engine namespaces use `miniflare-workflows-` as their unique key, which -// determines actor IDs and the storage subdirectory; matching it lets both reach the same local -// instances, so a configurable key must keep that value for Miniflare. -constexpr kj::StringPtr WORKFLOW_NAMESPACE_KEY_PREFIX = "miniflare-workflows-"_kj; - -} // namespace - -// ======================================================================================= - -Server::Server(kj::Filesystem& fs, - kj::Timer& timer, - const kj::MonotonicClock& monotonicClock, - kj::Network& network, - kj::EntropySource& entropySource, - Worker::LoggingOptions loggingOptions, - kj::Function reportConfigError, - kj::Function reportConfigWarning) - : fs(fs), - timer(timer), - monotonicClock(monotonicClock), - network(network), - entropySource(entropySource), - reportConfigError(kj::mv(reportConfigError)), - reportConfigWarning(kj::mv(reportConfigWarning)), - loggingOptions(loggingOptions), - memoryCacheProvider(kj::heap()), - channelTokenHandler(*this), - tasks(*this) {} - -struct Server::GlobalContext { - jsg::V8System& v8System; - capnp::ByteStreamFactory byteStreamFactory; - capnp::HttpOverCapnpFactory httpOverCapnpFactory; - ThreadContext threadContext; - kj::HttpHeaderTable& headerTable; - - GlobalContext( - Server& server, jsg::V8System& v8System, kj::HttpHeaderTable::Builder& headerTableBuilder) - : v8System(v8System), - httpOverCapnpFactory( - byteStreamFactory, headerTableBuilder, capnp::HttpOverCapnpFactory::LEVEL_2), - threadContext(server.timer, - server.entropySource, - headerTableBuilder, - httpOverCapnpFactory, - byteStreamFactory), - headerTable(headerTableBuilder.getFutureTable()) {} -}; - -class Server::Service: public IoChannelFactory::SubrequestChannel { - public: - // Cross-links this service with other services. Must be called once before `startRequest()`. - virtual void link(Worker::ValidationErrorReporter& errorReporter) {} - - // Second linking phase, run after every service has completed `link()`. This exists so that a - // service can wire up actor namespaces whose storage or backing lives in *another* service -- - // specifically, Workflow namespaces take their storage from their `bindingService` Worker, which - // is only guaranteed to be linked (and thus have its storage available) once the first pass is - // complete. Services with no such namespaces don't need to override this. - virtual void linkActorNamespaces(Worker::ValidationErrorReporter& errorReporter) {} - - // Drops any cross-links created during link(). This called just before all the services are - // destroyed. An `Rc` cannot be destroyed unless the object it points to still exists, so - // we must clear all the `Rc`s before we can actually destroy the `Service`s. - virtual void unlink() {} - - // Begin an incoming request. Returns a `WorkerInterface` object that will be used for one - // request then discarded. - virtual kj::Own startRequest( - IoChannelFactory::SubrequestMetadata metadata) override = 0; - - // Returns true if the service exports the given handler, e.g. `fetch`, `scheduled`, etc. - virtual bool hasHandler(kj::StringPtr handlerName) = 0; - - // Return the service itself, or the underlying service if this instance wraps another service as - // with EntrypointService. - virtual Service* service() { - return this; - } - - // Implemented by EntrypointService for loopback ctx.exports entrypoints, to allow props to be - // specified. `persistent` is whether the (loopback) target worker has - // `allow_irrevocable_stub_storage` enabled, recorded on the resulting channel. - virtual kj::Rc forProps(Frankenvalue props, Persistent persistent) { - KJ_FAIL_REQUIRE("can't override props for this service"); - } - - void requireAllowsTransfer() override { - // We consider all `Service` implementations to be safe to transfer, except for dynamic workers - // which we'll handle explicitly. - } -}; - -class Server::ActorClass: public IoChannelFactory::ActorClassChannel { - public: - // The caller must call this before calling newActor(). If it returns a promise, then the - // caller must await the promise before calling other methods. - // - // In particular, this is needed with dynamically-loaded workers. The isolate may still be - // loading when the caller calls `getDurableObjectClass()` on it. - virtual kj::Maybe> whenReady() { - return kj::none; - } - - // Construct a new instance of the class. The parameters here are passed into `Worker::Actor`'s - // constructor. - virtual kj::Own newActor(kj::Maybe tracker, - Worker::Actor::Id actorId, - Worker::Actor::MakeActorCacheFunc makeActorCache, - Worker::Actor::MakeStorageFunc makeStorage, - kj::Own loopback, - kj::Maybe> manager, - kj::Maybe container, - jsg::Dict containerImages, - kj::Maybe facetManager) = 0; - - // Start a request on the actor. (The actor must have been created using newActor().) - virtual kj::Own startRequest( - IoChannelFactory::SubrequestMetadata metadata, kj::Own actor) = 0; - - virtual kj::Rc forProps(Frankenvalue props, Persistent persistent) { - KJ_FAIL_REQUIRE("can't override props for this actor class"); - } -}; - -Server::~Server() noexcept { - // This destructor is explicitly `noexcept` because if one of the `unlink()`s throws then we'd - // have a hard time avoiding a segfault later... and we're shutting down the server anyway so - // whatever, better to crash. - - // It's important to cancel all tasks before we start tearing down. Actors may have background - // work, which we can cancel by aborting them. - abortAllActors(KJ_EXCEPTION(DISCONNECTED, "Server shutting down.")); - tasks.clear(); - - // Unlink all the services, which should remove all refcount cycles. - unlinkWorkerLoaders(); - for (auto& service: services) { - service.value->unlink(); - } - - // Verify that unlinking actually eliminated cycles. Otherwise we have a memory leak -- and - // potentially use-after-free if we allow the `Server` to be destroyed while services still - // exist. - for (auto& service: services) { - KJ_ASSERT( - !service.value->isShared(), "service still has references after unlinking", service.key); - } -} - -// ======================================================================================= - -class Server::ActorNamespace final { - public: - friend class Server; - - ActorNamespace(kj::Rc actorClass, - const ActorConfig& config, - const kj::Clock& clock, - kj::Timer& timer, - capnp::ByteStreamFactory& byteStreamFactory, - ChannelTokenHandler& channelTokenHandler, - kj::Network& dockerNetwork, - kj::Maybe dockerPath, - kj::Maybe containerEgressInterceptorImage, - kj::TaskSet& waitUntilTasks, - Persistent selfTokensArePersistent) - : actorClass(kj::mv(actorClass)), - config(config), - clock(clock), - timer(timer), - byteStreamFactory(byteStreamFactory), - channelTokenHandler(channelTokenHandler), - dockerNetwork(dockerNetwork), - dockerPath(dockerPath), - containerEgressInterceptorImage(containerEgressInterceptorImage), - waitUntilTasks(waitUntilTasks), - selfTokensArePersistent(selfTokensArePersistent) {} - - void link(kj::Maybe serviceActorStorage) { - KJ_IF_SOME(dir, serviceActorStorage) { - KJ_IF_SOME(d, config.tryGet()) { - this->actorStorage.emplace( - dir.openSubdir(kj::Path({d.uniqueKey}), kj::WriteMode::CREATE | kj::WriteMode::MODIFY)); - } - } - - KJ_IF_SOME(d, config.tryGet()) { - auto idFactory = kj::heap(d.uniqueKey); - AlarmScheduler::GetActorFn getActor = - [this, idFactory = kj::mv(idFactory)]( - const ActorKey& actor) mutable -> kj::Own { - Worker::Actor::Id id = idFactory->idFromStringNamed( - kj::str(actor.actorId), actor.name.map([](kj::StringPtr n) { return kj::str(n); })); - auto actorContainer = this->getActorContainer(kj::mv(id)); - return newPromisedWorkerInterface( - actorContainer->startRequest({}).attach(actorContainer->addRef())); - }; - - KJ_IF_SOME(as, this->actorStorage) { - // Create per-namespace alarm scheduler backed by on-disk storage in the - // namespace directory, alongside the per-actor .sqlite files. - this->ownAlarmScheduler = kj::heap( - clock, timer, as.vfs, kj::Path({"metadata.sqlite"}), kj::mv(getActor)); - } else { - // No on-disk storage -- create an in-memory alarm scheduler. - auto memDir = kj::newInMemoryDirectory(clock); - auto vfs = kj::heap(*memDir); - this->ownAlarmScheduler = kj::heap( - clock, timer, *vfs, kj::Path({"metadata.sqlite"}), kj::mv(getActor)) - .attach(kj::mv(vfs), kj::mv(memDir)); - } - - this->alarmScheduler = *KJ_ASSERT_NONNULL(ownAlarmScheduler); - } - } - - const ActorConfig& getConfig() { - return config; - } - - bool isEvictable() const { - bool result = true; - KJ_SWITCH_ONEOF(config) { - KJ_CASE_ONEOF(c, Durable) { - result = c.isEvictable; - } - KJ_CASE_ONEOF(c, Ephemeral) { - result = c.isEvictable; - } - } - return result; - } - - kj::Rc getActorChannel( - Worker::Actor::Id id, Persistent persistent = Persistent::NO) { - KJ_IF_SOME(doId, id.tryGet>()) { - KJ_IF_SOME(name, doId->getName()) { - // To emulate production, we preserve the name on the id, but only if it's <= 1024 bytes. - if (name.size() > 1024) { - auto* idImpl = dynamic_cast(doId.get()); - KJ_ASSERT(idImpl != nullptr, "Unexpected ActorId type?"); - idImpl->clearName(); - } - } - } - - return kj::rc(getActorContainer(kj::mv(id)), persistent); - } - - class ActorContainer; - using ActorMap = kj::HashMap>; - - // ActorContainer mostly serves as a wrapper around Worker::Actor. - // We use it to associate a HibernationManager with the Worker::Actor, since the - // Worker::Actor can be destroyed during periods of prolonged inactivity. - // - // We use a RequestTracker to track strong references to this ActorContainer's Worker::Actor. - // Once there are no Worker::Actor's left (excluding our own), `inactive()` is triggered and we - // initiate the eviction of the Durable Object. If no requests arrive in the next 10 seconds, - // the DO is evicted, otherwise we cancel the eviction task. - class ActorContainer final: public RequestTracker::Hooks, - public kj::Refcounted, - public Worker::Actor::FacetManager { - public: - // Information which is needed before start() can be called, but may not be available yet - // when the ActorContainer is constructed (especially in the case of facets). - struct ClassAndId { - kj::Rc actorClass; - Worker::Actor::Id id; - - ClassAndId(kj::Rc actorClass, Worker::Actor::Id id) - : actorClass(kj::mv(actorClass)), - id(kj::mv(id)) {} - }; - - ActorContainer(kj::String key, - ActorNamespace& ns, - kj::Maybe parent, - kj::OneOf> classAndIdParam, - kj::Timer& timer) - : key(kj::mv(key)), - tracker(kj::refcounted(*this)), - ns(ns), - root(parent.map([](ActorContainer& p) -> ActorContainer& { return p.root; }) - .orDefault(*this)), - parent(parent), - timer(timer), - lastAccess(makeLastAccess(parent, timer)) { - KJ_SWITCH_ONEOF(classAndIdParam) { - KJ_CASE_ONEOF(value, ClassAndId) { - // `classAndId` is immediately available. - classAndId = kj::mv(value); - } - KJ_CASE_ONEOF(promise, kj::Promise) { - // We are receiving a promise for a `ClassAndId` to come later. Arrange to initialize - // `classAndId` from the promise. Create a `ForkedPromise` that resolves when - // initialization is complete. - classAndId = promise - .then([this](ClassAndId value) { - auto& forked = KJ_ASSERT_NONNULL(classAndId.tryGet>()); - if (!forked.hasBranches()) { - // HACK: We're about to replace the ForkedPromise but it has no one waiting on it, - // so we'd end up cancelling ourselves. Add a branch and detach it so this doesn't - // happen. - forked.addBranch().detach([](auto&&) {}); - } - - classAndId = kj::mv(value); - }).fork(); - } - } - } - - ~ActorContainer() noexcept(false) { - // Shutdown the tracker so we don't use active/inactive hooks anymore. - tracker->shutdown(); - - for (auto& facet: facets) { - facet.value->abort(kj::none); - } - - KJ_IF_SOME(a, actor) { - // Unknown broken reason. - auto reason = 0; - a->shutdown(reason); - } - - // Drop the container client reference - // If setInactivityTimeout() was called, there's still a timer holding a reference - // If not, this may be the last reference and the ContainerClient destructor will run - containerClient = kj::none; - } - - void active() override { - // We're handling a new request, cancel the eviction promise. - shutdownTask = kj::none; - } - - void inactive() override { - for (auto& fulfiller: inactiveFulfillers) { - fulfiller->fulfill(); - } - inactiveFulfillers.clear(); - - if (ns.isEvictable()) { - KJ_IF_SOME(a, actor) { - KJ_IF_SOME(m, a->getHibernationManager()) { - // The hibernation manager needs to survive actor eviction and be passed to the actor - // constructor next time we create it. - manager = m.addRef(); - } - } - shutdownTask = - handleShutdown().eagerlyEvaluate([](kj::Exception&& e) { KJ_LOG(ERROR, e); }); - } - } - - kj::StringPtr getKey() { - return key; - } - RequestTracker& getTracker() { - return *tracker; - } - kj::Maybe> tryGetManagerRef() { - return manager.map( - [&](kj::Own& m) { return kj::addRef(*m); }); - } - void updateAccessTime() { - *lastAccess = timer.now(); - } - kj::TimePoint getLastAccess() { - return *lastAccess; - } - - bool hasClients() { - // If anyone holds a reference to the container other than the actor map, then it must be - // a client. - if (isShared()) return true; - for (auto& facet: facets) { - if (facet.value->hasClients()) return true; - } - return false; - } - kj::Own addRef() { - return kj::addRef(*this); - } - - // Get the actor, starting it if it's not already running. - kj::Promise> getActor() { - requireNotBroken(); - - if (actor == kj::none) { - KJ_IF_SOME(promise, classAndId.tryGet>()) { - co_await promise; - requireNotBroken(); - } - - auto& [actorClass, id] = KJ_ASSERT_NONNULL(classAndId.tryGet()); - - KJ_IF_SOME(promise, actorClass->whenReady()) { - co_await promise; - requireNotBroken(); - } - - // A concurrent request could have started the actor, so check again. - if (actor == kj::none) { - start(actorClass, id); - } - } - - co_return KJ_ASSERT_NONNULL(actor)->addRef(); - } - - // Callers should `attach` a self-ref to this promise as it can outlive `ActorContainer` - // The ForkBranch created by `co_await classAndId.tryGet>()` keeps - // the `.then([this])` continuation set up in the constructor alive independently of the - // `ActorContainer` refcount. Without this self-ref, the `ActorContainer` can be freed - // (via ctx.facets.abort() + Fetcher GC) while the `getActor()` coroutine is suspended - // and the continuation would later run on a dangling `this`. - kj::Promise> startRequest( - IoChannelFactory::SubrequestMetadata metadata) { - auto actor = co_await getActor(); - - if (ns.cleanupTask == kj::none) { - // Need to start the cleanup loop. - ns.cleanupTask = ns.cleanupLoop(); - } - - // Since `getActor()` completed, `classAndId` must be resolved. - auto& actorClass = KJ_ASSERT_NONNULL(classAndId.tryGet()).actorClass; - - if (parent == kj::none) { - // We're the root actor, not a facet. So, this ActorContainer is perfectly capable of - // constructing a token pointing to itself, and can set itself as the - // `restoredSelfTokenFactory`. In fact, we MUST NOT accept a `restoredSelfTokenFactory` - // passed in from the caller as this would potentially allow a malicious caller to read - // and manipulate the parameters to our own `[restore]()` method. - // - // We use a dedicated `ActorSelfTokenFactory` rather than an `ActorChannelImpl` so that we - // don't hold a reference back to the `ActorContainer`, which would create a cycle - // preventing actor eviction. - metadata.restoredSelfTokenFactory = kj::rc( - ns, Worker::Actor::cloneId(KJ_ASSERT_NONNULL(classAndId.tryGet()).id)); - } else { - // We're a facet. The only way we could have been called by anyone other than our direct - // parent is if the `ctx.restore()` mechanism was used (because otherwise, the facet stub - // is not serializable and therefore could not have been shared). In this case, we would - // have been called through a `RestoredSubrequestChannel` which sets - // `restoredSelfTokenFactory` to point at itself. Since this `RestoredSubrequestChannel` - // must have been set up by our parent actor, we can trust it as our `selfTokenFactory`. - } - - co_return actorClass->startRequest(kj::mv(metadata), kj::mv(actor)) - .attach(kj::defer([self = kj::addRef(*this)]() mutable { self->updateAccessTime(); })); - } - - // Abort this actor, shutting it down. - // - // It is the caller's responsibility to ensure that the aborted ActorContainer has been - // removed from any maps that would cause it to receive further traffic, since any further - // requests will be expected to fail. abort() does NOT attempt to remove the ActorContainer - // from the parent facet map since at most call sites it makes more sense to handle this - // directly. - void abort(kj::Maybe reason) { - if (brokenReason != kj::none) return; - - KJ_IF_SOME(a, actor) { - KJ_IF_SOME(r, reason) { - a->abort(r); - } else { - // Unknown broken reason. - a->shutdown(0, kj::none); - } - } - - for (auto& facet: facets) { - facet.value->abort(reason); - } - - onBrokenTask = kj::none; - shutdownTask = kj::none; - manager = kj::none; - tracker->shutdown(); - actor = kj::none; - containerClient = kj::none; - - KJ_IF_SOME(r, reason) { - brokenReason = r.clone(); - } else { - brokenReason = JSG_KJ_EXCEPTION(FAILED, Error, "Actor aborted for unknown reason."); - } - } - - // Resets the actor's SQLite database while the connection is still open, - // avoiding file-locking issues on Windows. - void resetStorage() { - KJ_IF_SOME(a, actor) { - KJ_IF_SOME(cache, a->getPersistent()) { - KJ_IF_SOME(db, cache.getSqliteDatabase()) { - kj::runCatchingExceptions([&]() { db.reset(); }); - } - } - } - } - - kj::Own getFacetContainer( - kj::String childKey, kj::Function()> getStartInfo) { - auto makeContainer = [&]() { - auto promise = callFacetStartCallback(kj::mv(getStartInfo)); - return kj::refcounted(kj::mv(childKey), ns, *this, kj::mv(promise), timer); - }; - - bool isNew = false; - - auto& entry = facets.findOrCreateEntry(childKey, [&]() mutable { - isNew = true; - auto container = makeContainer(); - return ActorMap::Entry{container->getKey().clone(), kj::mv(container)}; - }); - - return entry.value->addRef(); - } - - uint getDepth() const override { - KJ_IF_SOME(p, parent) { - return 1 + p.getDepth(); - } - return 0; - } - - kj::Rc getFacet( - kj::StringPtr name, kj::Function()> getStartInfo) override { - auto facet = getFacetContainer(kj::str(name), kj::mv(getStartInfo)); - return kj::rc(kj::mv(facet), Persistent::NO); - } - - void abortFacet(kj::StringPtr name, kj::Exception reason) override { - KJ_IF_SOME(entry, facets.findEntry(name)) { - entry.value->abort(reason); - facets.erase(entry); - } - } - - void deleteFacet(kj::StringPtr name) override { - // First, abort any running facets. - abortFacet(name, JSG_KJ_EXCEPTION(FAILED, Error, "Facet was deleted.")); - - // Then delete the underlying storage. - KJ_IF_SOME(as, ns.actorStorage) { - // Note that if there's no facet index then there couldn't possibly be any child storage. - KJ_IF_SOME(index, getFacetTreeIndexIfNotEmpty()) { - uint childId = index.getId(getFacetId(), name); - deleteFacetImpl(*as.directory, index, childId); - } - } - } - - void cloneFacet(kj::StringPtr src, kj::StringPtr dst) override { - // Replacing a facet implies aborting it. - abortFacet(dst, JSG_KJ_EXCEPTION(FAILED, Error, "Facet was cloned-over.")); - - if (src == dst) { - // Cloning a facet to itself is equivalent to replacing it with an exact copy of its own - // data. Aborting matches the observable semantics of delete(dst), but we leave the - // storage untouched (since src == dst, copying it onto itself would be a no-op anyway). - return; - } - - auto& as = KJ_UNWRAP_OR(ns.actorStorage, return); - - // If no index exists on disk, there can be no storage to delete or copy. - KJ_IF_SOME(index, getFacetTreeIndexIfNotEmpty()) { - uint parentId = getFacetId(); - - // Delete dst's existing storage first, mirroring the storage-side behavior of - // deleteFacet() (the abort was already handled above). - uint dstId = index.getId(parentId, dst); - deleteFacetImpl(*as.directory, index, dstId); - - // Now copy src to dst. If src's DB file does not exist, then src has no data, which - // in the Durable Objects model is indistinguishable from src never having run. In - // that case dst should also have no data, which it already does (we just deleted it). - uint srcId = index.getId(parentId, src); - cloneFacetImpl(*as.directory, index, srcId, dstId); - } - } - - void requireTransferrableStub() { - JSG_REQUIRE(parent == kj::none, DOMDataCloneError, - "Stubs pointing to Durable Object facets are not serializable."); - JSG_REQUIRE(ns.getConfig().is(), DOMDataCloneError, - "Stubs pointing to ephemeral objects are not serializable."); - } - - kj::OneOf, kj::Promise>> getChannelToken( - IoChannelFactory::ChannelTokenUsage usage, Persistent persistent) { - requireTransferrableStub(); - - KJ_SWITCH_ONEOF(classAndId) { - KJ_CASE_ONEOF(c, ClassAndId) { - return getChannelTokenImpl(usage, c.id, persistent); - } - KJ_CASE_ONEOF(promise, kj::ForkedPromise) { - return promise.addBranch().then([self = addWeakToThis(), usage, persistent]() { - auto& container = self.assertLive(); - return container.getChannelTokenImpl( - usage, KJ_ASSERT_NONNULL(container.classAndId.tryGet()).id, persistent); - }); - } - } - - KJ_UNREACHABLE; - } - - private: - // The actor is constructed after the ActorContainer so it starts off empty. - kj::Maybe> actor; - - kj::String key; - kj::Own tracker; - ActorNamespace& ns; - ActorContainer& root; - kj::Maybe parent; - kj::Timer& timer; - // Namespace expiration is tracked at the root, so the whole facet tree shares this timestamp. - kj::Rc lastAccess; - kj::Maybe> manager; - kj::Maybe> shutdownTask; - kj::Maybe> onBrokenTask; - kj::Maybe brokenReason; - kj::Vector>> inactiveFulfillers; - - // Reference to the ContainerClient (if container is enabled for this actor) - kj::Maybe> containerClient; - - // If this is a `ForkedPromise`, await the promise. When it has resolved, then - // `classAndId` will have been replaced with the resolved `ClassAndId` value. - kj::OneOf> classAndId; - - // FacetTreeIndex for this actor. Only initialized on the root. - kj::Maybe> facetTreeIndex; - - // ID of this facet. Initialized when getFacetId() is first called. - kj::Maybe facetId; - - ActorMap facets; - - static kj::Rc makeLastAccess( - kj::Maybe parent, kj::Timer& timer) { - KJ_IF_SOME(p, parent) { - return p.lastAccess.addRef(); - } - return kj::rc(timer.now()); - } - - // Get the facet ID for this facet. The root facet always has ID zero, but all other facets - // need to be looked up in the index to make sure they are assigned consistent IDs. - uint getFacetId() { - KJ_IF_SOME(f, facetId) { - return f; - } - - ActorContainer& parent = KJ_UNWRAP_OR(this->parent, return 0); - - FacetTreeIndex& index = root.ensureFacetTreeIndex(); - return index.getId(parent.getFacetId(), key); - } - - // Get the facet tree index, opening the file if it hasn't been opened yet, and creating it - // if it hasn't been created yet. - FacetTreeIndex& ensureFacetTreeIndex() { - KJ_REQUIRE(parent == kj::none, "only 'root' may ensureFacetTreeIndex()"); - - KJ_IF_SOME(i, facetTreeIndex) { - return *i; - } else { - // Facet tree index hasn't been initialized yet. Do that now (opening the existing file, - // or creating it if it doesn't exist). - auto& as = KJ_REQUIRE_NONNULL( - ns.actorStorage, "can't call getFacetId() when there's no backing storage"); - auto indexFile = as.directory->openFile( - kj::Path({kj::str(key, ".facets")}), kj::WriteMode::CREATE | kj::WriteMode::MODIFY); - return *facetTreeIndex.emplace(kj::heap(kj::mv(indexFile))); - } - } - - // Like ensureFacetTreeIndex() but if the index doesn't exist on disk, return kj::none. - kj::Maybe getFacetTreeIndexIfNotEmpty() { - KJ_REQUIRE(parent == kj::none); - - KJ_IF_SOME(i, facetTreeIndex) { - return *i; - } else { - // Facet tree index hasn't been initialized yet. If the file exists, open it. Otherwise, - // assume empty and return none. - auto& as = KJ_UNWRAP_OR(ns.actorStorage, return kj::none); - auto indexFile = KJ_UNWRAP_OR( - as.directory->tryOpenFile(kj::Path({kj::str(key, ".facets")}), kj::WriteMode::MODIFY), - return kj::none); - return *facetTreeIndex.emplace(kj::heap(kj::mv(indexFile))); - } - } - - // Get the path to the facet's sqlite database, within the actor namespace directory. - // - // `suffix` can be e.g. "-wal" or "-shm". - kj::Path getSqlitePathForId(uint id, kj::StringPtr suffix = ""_kj) { - if (id == 0) { - return kj::Path({kj::str(root.key, ".sqlite", suffix)}); - } else { - return kj::Path({kj::str(root.key, '.', id, ".sqlite", suffix)}); - } - } - - void deleteFacetImpl(const kj::Directory& dir, FacetTreeIndex& index, uint facetId) { - deleteDescendantStorage(dir, index, facetId); - - // Remove the database, WAL, and SHM files, if present. Note that the database may not - // exist at all if this facet didn't exist before delete() was called on it. - dir.tryRemove(getSqlitePathForId(facetId)); - dir.tryRemove(getSqlitePathForId(facetId, "-wal")); - dir.tryRemove(getSqlitePathForId(facetId, "-shm")); - } - - void deleteDescendantStorage(const kj::Directory& dir, uint parentId) { - KJ_IF_SOME(index, getFacetTreeIndexIfNotEmpty()) { - deleteDescendantStorage(dir, index, parentId); - } else { - // There's no index, so there must be no facets (other than the root). - KJ_ASSERT(parentId == 0); - } - } - - void deleteDescendantStorage(const kj::Directory& dir, FacetTreeIndex& index, uint parentId) { - index.forEachChild(parentId, - [&](uint childId, kj::StringPtr childName) { deleteFacetImpl(dir, index, childId); }); - } - - // Recursively copy the subtree rooted at the facet with ID `srcParentId` to a new subtree - // rooted at the facet with ID `dstParentId`. - void cloneFacetImpl(const kj::Directory& dir, FacetTreeIndex& index, uint srcId, uint dstId) { - // Snapshot src's children before recursing, because the recursion mutates the index by - // allocating new IDs for the destination subtree, which would interfere with a live - // forEachChild iteration. - struct Child { - uint id; - kj::String name; - }; - kj::Vector children; - index.forEachChild(srcId, [&](uint childId, kj::StringPtr childName) { - children.add(Child{childId, kj::str(childName)}); - }); - - for (auto& child: children) { - uint newChildId = index.getId(dstId, child.name); - cloneFacetImpl(dir, index, child.id, newChildId); - } - - // Now that the children are copied, copy the main facet. - auto srcDb = getSqlitePathForId(srcId); - - // It's possible there's no backing file on disk, if the facet existed previously but was - // deleted. If the source facet has no data, then leaving the destination with no data - // is correct. - if (!dir.exists(srcDb)) return; - - // Copy the database. Use KJ's Directory::transfer() which will use copy-on-write where - // available (e.g. FICLONE on Linux, if the FS supports it). - auto dstDb = getSqlitePathForId(dstId); - dir.transfer(dstDb, kj::WriteMode::CREATE, srcDb, kj::TransferMode::COPY); - - // Copy the WAL if it exists. We can't rely on the source's WAL having been checkpointed - // and truncated at close time -- e.g., a previous process may have crashed leaving a - // valid WAL. Copying the WAL alongside the DB preserves any unmerged data. - auto srcWal = getSqlitePathForId(srcId, "-wal"); - if (!dir.exists(srcWal)) return; - auto dstWal = getSqlitePathForId(dstId, "-wal"); - dir.transfer(dstWal, kj::WriteMode::CREATE, srcWal, kj::TransferMode::COPY); - - // Finally copy the SHM file if present. This is not strictly necessary but if the WAL is - // large this helps SQLite start up faster. - auto srcShm = getSqlitePathForId(srcId, "-shm"); - if (!dir.exists(srcShm)) return; - auto dstShm = getSqlitePathForId(dstId, "-shm"); - dir.transfer(dstShm, kj::WriteMode::CREATE, srcShm, kj::TransferMode::COPY); - } - - void requireNotBroken() { - KJ_IF_SOME(e, brokenReason) { - kj::throwFatalException(e.clone()); - } - } - - kj::Promise monitorOnBroken(Worker::Actor& actor) { - try { - // It's possible for this to never resolve if the actor never breaks, - // in which case the returned promise will just be canceled. - co_await actor.onBroken(); - KJ_FAIL_ASSERT("actor.onBroken() resolved normally?"); - } catch (...) { - brokenReason = kj::getCaughtExceptionAsKj(); - } - - for (auto& facet: facets) { - facet.value->abort(brokenReason); - } - facets.clear(); - - // HACK: Dropping the ActorContainer will delete onBrokenTask, cancelling ourselves. This - // would crash. To avoid the problem, detach ourselves. This is safe because we know that - // once we return there's nothing left for this promise to do anyway. - KJ_ASSERT_NONNULL(onBrokenTask).detach([](kj::Exception&& e) {}); - - // Hollow out the object, so that if it still has references, they won't keep these parts - // alive. Since any further calls to `getActor()` will throw, we don't have to worry about - // the actor being recreated. - auto actorToDrop = kj::mv(this->actor); - tracker->shutdown(); - auto managerToDrop = kj::mv(manager); - - // Note that we remove the entire ActorContainer from the map -- this drops the - // HibernationManager so any connected hibernatable websockets will be disconnected. - KJ_IF_SOME(p, parent) { - p.facets.erase(key); - } else { - ns.actors.erase(key); - } - - // WARNING: `this` MAY HAVE BEEN DELETED as a result of the above `erase()`. Do not access - // it again here. - } - - // Processes the eviction of the Durable Object and hibernates active websockets. - kj::Promise handleShutdown() { - // After 10 seconds of inactivity, we destroy the Worker::Actor and hibernate any active - // JS WebSockets. - // TODO(someday): We could make this timeout configurable to make testing less burdensome. - co_await timer.afterDelay(10 * kj::SECONDS); - // Cancel the onBroken promise, since we're about to destroy the actor anyways and don't - // want to trigger it. - onBrokenTask = kj::none; - KJ_IF_SOME(a, actor) { - if (a->isShared()) { - // Our ActiveRequest refcounting has broken somewhere. This is likely because we're - // `addRef`-ing an actor that has had an ActiveRequest attached to its kj::Own (in other - // words, the ActiveRequest count is less than it should be). - // - // Rather than dropping our actor and possibly ending up with split-brain, - // we should opt out of the deferred proxy optimization and log the error to Sentry. - KJ_LOG(ERROR, - "Detected internal bug in hibernation: Durable Object has strong references " - "when hibernation timeout expired."); - - co_return; - } - KJ_IF_SOME(m, manager) { - auto& worker = a->getWorker(); - auto workerStrongRef = kj::atomicAddRef(worker); - // Take an async lock, we can't use `takeAsyncLock(RequestObserver&)` since we don't - // have an `IncomingRequest` at this point. - // - // Note that we do not have a race here because this is part of the `shutdownTask` - // promise. If a new request comes in while we're waiting to get the lock then we will - // cancel this promise. - Worker::AsyncLock asyncLock = co_await worker.takeAsyncLockWithoutRequest(nullptr); - workerStrongRef->runInLockScope( - asyncLock, [&](Worker::Lock& lock) { m->hibernateWebSockets(lock); }); - } - a->shutdown(0, KJ_EXCEPTION(DISCONNECTED, "broken.dropped; Actor freed due to inactivity")); - } - // Destroy the last strong Worker::Actor reference. - actor = kj::none; - - // Drop our reference to the ContainerClient - // If setInactivityTimeout() was called, the timer still holds a reference - // so the container stays alive until the timeout expires - containerClient = kj::none; - } - - // Test-only eviction body, used by the evict hooks (evictWhenIdle). Destroys the Worker::Actor - // while keeping durable storage alive, so the DO is rebuilt on its next request. Depending on - // webSocketMode, hibernatable WebSockets are either hibernated first or closed. `reason` is - // recorded as the actor's disconnect reason. - // - // Returns false without evicting if the actor has acquired strong references by the time we - // hold the isolate lock (i.e. a new request raced in). Unlike the inactivity-timer path - // (handleShutdown), which is cancelled by active() when a request arrives, the test-only evict - // path is not cancellable, so it relies on this re-check to avoid tearing down a live actor. - // For the same reason, we only cancel onBrokenTask once we're committed to the shutdown -- - // otherwise an early `false` return would leave the actor running with no broken-detection. - kj::Promise tryEvict( - kj::StringPtr reason, IoChannelFactory::EvictWebSocketMode webSocketMode) { - KJ_IF_SOME(a, actor) { - if (a->isShared()) { - co_return false; - } - - if (manager != kj::none && - webSocketMode == IoChannelFactory::EvictWebSocketMode::HIBERNATE) { - auto& worker = a->getWorker(); - auto workerStrongRef = kj::atomicAddRef(worker); - // Take an async lock, we can't use `takeAsyncLock(RequestObserver&)` since we don't - // have an `IncomingRequest` at this point. - auto asyncLock = co_await worker.takeAsyncLockWithoutRequest(nullptr); - - // Re-check the actor slot now that we've awaited the lock. `a` is the actor we observed - // before suspending, but while we waited another path may have replaced it, or a new - // request may have grabbed a strong reference. - KJ_IF_SOME(current, actor) { - if (&*current != &*a) { - co_return false; - } - if (current->isShared()) { - co_return false; - } - - KJ_IF_SOME(m, manager) { - workerStrongRef->runInLockScope( - asyncLock, [&](Worker::Lock& lock) { m->hibernateWebSockets(lock); }); - } - - // Note: wrap `reason` in kj::str() so KJ_EXCEPTION doesn't prefix the description with - // "reason = " (it only omits the label for string literals and kj::str(...) args). - current->shutdown(0, KJ_EXCEPTION(DISCONNECTED, kj::str(reason))); - } else { - co_return true; - } - } else { - // Note: wrap `reason` in kj::str() so KJ_EXCEPTION doesn't prefix the description with - // "reason = " (it only omits the label for string literals and kj::str(...) args). - a->shutdown(0, KJ_EXCEPTION(DISCONNECTED, kj::str(reason))); - } - } - - // Cancel the onBroken promise, since we're committed to destroying the actor and don't want - // to trigger it. - onBrokenTask = kj::none; - - // Destroy the last strong Worker::Actor reference. - actor = kj::none; - - if (webSocketMode == IoChannelFactory::EvictWebSocketMode::CLOSE) { - manager = kj::none; - } - - // Drop our reference to the ContainerClient. If setInactivityTimeout() was called, the timer - // still holds a reference so the container stays alive until the timeout expires. - containerClient = kj::none; - co_return true; - } - - public: - // Test-only: evict this actor, bypassing the inactivity timer. Throws if the actor isn't - // currently running (never instantiated, or already evicted/hibernated). If the actor has - // in-flight requests, waits for them to drain before evicting. - kj::Promise evictForTest(IoChannelFactory::EvictWebSocketMode webSocketMode) { - JSG_REQUIRE(ns.isEvictable(), Error, - "Cannot evict Durable Object: its namespace has preventEviction set."); - JSG_REQUIRE( - actor != kj::none, Error, "Cannot evict Durable Object: it is not currently running."); - return evictWhenIdle(webSocketMode); - } - - // Test-only: evict this actor and all of its facets if they are running, otherwise do nothing. - // Used by the bulk evictAllDurableObjects() path, which must not error on actors that aren't - // running. - kj::Promise evictTreeForTestIfRunning( - IoChannelFactory::EvictWebSocketMode webSocketMode) { - kj::Vector> promises(facets.size() + 1); - for (auto& facet: facets) { - // Pin the ActorContainer for the duration of its eviction. The map entry is normally - // retained, but the onBroken path can erase it mid-eviction; the addRef keeps the - // coroutine's `this` valid regardless. - promises.add( - facet.value->evictTreeForTestIfRunning(webSocketMode).attach(facet.value->addRef())); - } - - if (actor != kj::none) { - // Pin ourselves just like facet/root map callers do. The join below can be canceled by its - // caller, but the coroutine may still be suspended on request inactivity. - promises.add(evictWhenIdle(webSocketMode).attach(addRef())); - } - - return kj::joinPromises(promises.releaseAsArray()); - } - - private: - // Waits until the actor is idle, then evicts it. We never abort a live request, so while there - // are in-flight requests -- or a just-completed request still holding a transient strong - // reference during teardown (e.g. RPC/fetch teardown) -- we poll until the actor can be torn - // down. To avoid hanging a test forever (e.g. a request that never completes), we give up - // after a fixed deadline. - kj::Promise evictWhenIdle(IoChannelFactory::EvictWebSocketMode webSocketMode) { - constexpr auto EVICT_TIMEOUT = 30 * kj::SECONDS; - auto deadline = timer.now() + EVICT_TIMEOUT; - for (;;) { - if (co_await tryEvict("broken.dropped; Actor evicted by test"_kj, webSocketMode)) { - shutdownTask = kj::none; - co_return; - } - - auto now = timer.now(); - JSG_REQUIRE(now < deadline, Error, - "Timed out waiting to evict Durable Object: it still has active references."); - - if (tracker->isActive()) { - auto paf = kj::newPromiseAndFulfiller(); - auto promise = kj::mv(paf.promise); - inactiveFulfillers.add(kj::mv(paf.fulfiller)); - - co_await kj::mv(promise).exclusiveJoin(timer.afterDelay(deadline - now).then([]() { - JSG_FAIL_REQUIRE(Error, - "Timed out waiting to evict Durable Object: it still has active references."); - })); - } else { - // The actor can briefly have non-request strong refs during teardown, after the tracker - // has already reported inactivity. Yield before re-checking, but don't poll for the full - // request-drain duration. - co_await timer.afterDelay(1 * kj::MILLISECONDS); - } - } - } - - void start(kj::Rc& actorClass, Worker::Actor::Id& id) { - KJ_REQUIRE(actor == kj::none); - - // Capture the actor's name (if it was created via `idFromName()`/`getByName()`) so the alarm - // scheduler can persist it and restore `ctx.id.name` when the alarm later fires, even if this - // actor has been evicted from memory by then. - kj::Maybe actorName; - KJ_IF_SOME(actorId, id.tryGet>()) { - KJ_IF_SOME(n, actorId->getName()) { - actorName = kj::str(n); - } - } - - auto makeActorCache = [this, actorName = kj::mv(actorName)]( - const ActorCache::SharedLru& sharedLru, OutputGate& outputGate, - ActorCache::Hooks& hooks, SqliteObserver& sqliteObserver) mutable { - return ns.config.tryGet().map( - [&](const Durable& d) -> kj::Own { - KJ_IF_SOME(as, ns.actorStorage) { - kj::Own sqliteHooks; - if (parent == kj::none) { - KJ_IF_SOME(a, ns.alarmScheduler) { - auto actorKey = ActorKey(key.clone(), actorName.clone()); - sqliteHooks = kj::heap(a, kj::mv(actorKey)); - } else { - // No alarm scheduler available, use default hooks instance. - sqliteHooks = fakeOwn(ActorSqlite::Hooks::getDefaultHooks()); - } - } else { - // A non-null `parent` means this is a facet, which has no alarm scheduler. - sqliteHooks = kj::heap(); - } - - uint selfId = getFacetId(); - auto path = getSqlitePathForId(selfId); - auto db = kj::heap( - as.vfs, kj::mv(path), kj::WriteMode::CREATE | kj::WriteMode::MODIFY); - - // Before we do anything, make sure the database is in WAL mode. We also need to - // do this after reset() is used, so register a callback for that. - db->run("PRAGMA journal_mode=WAL;"); - - db->afterReset([this, &dir = *as.directory, selfId](SqliteDatabase& db) { - db.run("PRAGMA journal_mode=WAL;"); - - // reset() is used when the app called deleteAll(), in which case we also want to - // delete all child facets. - // TODO(someday): Arguably this should be transactional somehow so if we fail here - // we don't leave the facets still there after the parent has already been reset. - // But most filesystems do not support transactions, so we'd have to do something - // like store a flag in the parent DB saying "reset pending" so that on a restart - // we retry the deletions. Note that in production on SRS, this is actually - // transactional -- there's only a problem when running locally with workerd. - deleteDescendantStorage(dir, selfId); - }); - - return kj::heap(kj::mv(db), outputGate, - [](SpanParent) -> kj::Promise { return kj::READY_NOW; }, *sqliteHooks) - .attach(kj::mv(sqliteHooks)); - } else { - // Create an ActorCache backed by a fake, empty storage. Elsewhere, we configure - // ActorCache never to flush, so this effectively creates in-memory storage. - return kj::heap( - newEmptyReadOnlyActorStorage(), sharedLru, outputGate, hooks); - } - }); - }; - - bool enableSql = true; - kj::Maybe containerOptions = - kj::none; - kj::Maybe uniqueKey; - KJ_SWITCH_ONEOF(ns.config) { - KJ_CASE_ONEOF(c, Durable) { - enableSql = c.enableSql; - containerOptions = c.containerOptions; - uniqueKey = c.uniqueKey; - } - KJ_CASE_ONEOF(c, Ephemeral) { - enableSql = c.enableSql; - } - } - - auto makeStorage = - [enableSql = enableSql](jsg::Lock& js, const Worker::Api& api, - ActorCacheInterface& actorCache) -> jsg::Ref { - return js.alloc( - js, IoContext::current().addObject(actorCache), enableSql); - }; - - auto loopback = kj::refcounted(*this); - - kj::Maybe container = kj::none; - jsg::Dict containerImages; - KJ_IF_SOME(config, containerOptions) { - kj::Maybe imageName = kj::none; - if (config.hasImageName() && config.getImageName().size() > 0) { - imageName = config.getImageName(); - } - containerImages.fields = KJ_MAP(image, config.getImages()) { - return jsg::Dict::Field{ - .name = kj::str(image.getName()), - .value = kj::str(image.getImage()), - }; - }; - auto privilegeConfig = config.getPrivileges(); - auto capabilities = - kj::heapArrayBuilder(privilegeConfig.getCapabilities().size()); - for (auto capability: privilegeConfig.getCapabilities()) { - capabilities.add(kj::str(capability)); - } - auto devices = - kj::heapArrayBuilder(privilegeConfig.getDevices().size()); - for (auto device: privilegeConfig.getDevices()) { - devices.add(ContainerPrivileges::Device{ - .pathOnHost = kj::str(device.getPathOnHost()), - .pathInContainer = kj::str(device.getPathInContainer()), - .cgroupPermissions = kj::str(device.getCgroupPermissions()), - }); - } - auto securityOpt = - kj::heapArrayBuilder(privilegeConfig.getSecurityOpt().size()); - for (auto option: privilegeConfig.getSecurityOpt()) { - securityOpt.add(kj::str(option)); - } - ContainerPrivileges privileges{ - .capabilities = capabilities.finish(), - .devices = devices.finish(), - .securityOpt = securityOpt.finish(), - }; - kj::String containerId; - KJ_SWITCH_ONEOF(id) { - KJ_CASE_ONEOF(globalId, kj::Own) { - containerId = globalId->toString(); - } - KJ_CASE_ONEOF(existingId, kj::String) { - containerId = kj::str(existingId); - } - } - - container = ns.getContainerClient( - kj::str("workerd-", KJ_ASSERT_NONNULL(uniqueKey), "-", containerId), imageName, - kj::mv(privileges)); - } - - auto actor = actorClass->newActor(getTracker(), Worker::Actor::cloneId(id), - kj::mv(makeActorCache), kj::mv(makeStorage), kj::mv(loopback), tryGetManagerRef(), - kj::mv(container), kj::mv(containerImages), *this); - onBrokenTask = monitorOnBroken(*actor); - this->actor = kj::mv(actor); - } - - // Helper coroutine to call `getStartInfo()`, the start callback for a facet, while making - // sure the function stays alive until the returned promise resolves. - static kj::Promise callFacetStartCallback( - kj::Function()> getStartInfo) { - auto info = co_await getStartInfo(); - co_await info.ensureAllResolved(); - co_return ClassAndId(info.actorClass.downcast(), kj::mv(info.id)); - } - - kj::Array getChannelTokenImpl(IoChannelFactory::ChannelTokenUsage usage, - const Worker::Actor::Id& id, - Persistent persistent) { - kj::StringPtr uniqueKey = KJ_ASSERT_NONNULL(ns.getConfig().tryGet()).uniqueKey; - auto& abstractId = *KJ_ASSERT_NONNULL(id.tryGet>()); - auto& idImpl = - KJ_ASSERT_NONNULL(kj::tryDowncast(abstractId)); - return ns.channelTokenHandler.encodeActorChannelToken( - usage, uniqueKey, idImpl.getRaw(), idImpl.getName(), persistent); - } - }; - - kj::Own getActorContainer(Worker::Actor::Id id) { - kj::String key; - - KJ_SWITCH_ONEOF(id) { - KJ_CASE_ONEOF(obj, kj::Own) { - KJ_REQUIRE(config.is()); - key = obj->toString(); - } - KJ_CASE_ONEOF(str, kj::String) { - KJ_REQUIRE(config.is()); - key = kj::str(str); - } - } - - return actors - .findOrCreate(key, [&]() mutable { - auto container = kj::refcounted(kj::mv(key), *this, kj::none, - ActorContainer::ClassAndId(actorClass->addRef(), kj::mv(id)), timer); - - return ActorMap::Entry{container->getKey().clone(), kj::mv(container)}; - })->addRef(); - } - - kj::Own getContainerClient(kj::StringPtr containerId, - kj::Maybe imageName, - ContainerPrivileges privileges) { - KJ_REQUIRE(!containerShutdownStarted, - "cannot acquire a container client after graceful shutdown has begun"); - - KJ_IF_SOME(existingClient, containerClients.find(containerId)) { - return existingClient->addRef(); - } - - // No existing container in the map, create a new one - auto& dockerPathRef = KJ_ASSERT_NONNULL( - dockerPath, "dockerPath must be defined to enable containers on this Durable Object."); - - // Grab a branch of any pending cleanup from a previous ContainerClient for this - // container. If it exists, pass it to the container client so it knows that it has to sync. - kj::Promise previousCleanup = kj::READY_NOW; - KJ_IF_SOME(state, containerCleanupState.find(containerId)) { - previousCleanup = state.promise.addBranch(); - } - - // Upsert the cleanup state for this container ID. Replacing the - // canceler auto-cancels any in-flight cleanup tasks from the previous - // client's destructor. The generation counter is bumped on replacement - // so the cleanup callback can detect stale ownership without relying - // on raw pointer identity (which is vulnerable to address reuse). - auto canceler = kj::heap(); - uint64_t capturedGeneration = 0; - containerCleanupState.upsert(kj::str(containerId), - ContainerCleanupState{.canceler = kj::mv(canceler)}, - [&capturedGeneration](ContainerCleanupState& existing, ContainerCleanupState&& incoming) { - existing.canceler = kj::mv(incoming.canceler); - capturedGeneration = ++existing.generation; - }); - - // Cleanup callback: invoked when ContainerClient shutdown begins with a cleanup promise. - kj::Function)> cleanupCallback = - [this, containerId = kj::str(containerId), capturedGeneration]( - kj::Promise cleanupPromise) mutable { - KJ_IF_SOME(state, containerCleanupState.find(containerId)) { - if (state.generation != capturedGeneration) { - // A newer ContainerClient has already taken ownership of cleanup for this ID. - return; - } - - containerClients.erase(containerId); - // Wrap with the canceler so a future client creation can cancel these - // tasks - auto cancellable = - state.canceler->wrap(kj::mv(cleanupPromise)).catch_([](kj::Exception&&) {}); - - auto forked = kj::mv(cancellable).fork(); - waitUntilTasks.add(forked.addBranch()); - state.promise = kj::mv(forked); - } - }; - - auto client = kj::refcounted(byteStreamFactory, timer, dockerNetwork, - kj::str(dockerPathRef), kj::str(containerId), - imageName.map([](kj::StringPtr image) { return kj::str(image); }), - kj::str(KJ_ASSERT_NONNULL(containerEgressInterceptorImage, - "containerEgressInterceptorImage must be configured for containers.")), - waitUntilTasks, kj::mv(previousCleanup), kj::mv(cleanupCallback), channelTokenHandler, - kj::mv(privileges)); - - // Store raw pointer in map (does not own) - containerClients.insert(kj::str(containerId), client.get()); - - return kj::mv(client); - } - - void abortAll(kj::Maybe reason) { - for (auto& actor: actors) { - actor.value->abort(reason); - } - actors.clear(); - } - - void beginContainerCleanup() { - bool hasContainer = false; - KJ_IF_SOME(durable, config.tryGet()) { - hasContainer = durable.containerOptions != kj::none; - } - if (!hasContainer || containerShutdownStarted) return; - - containerShutdownStarted = true; - abortAll(KJ_EXCEPTION(DISCONNECTED, "Server shutting down.")); - - auto clients = kj::heapArrayBuilder(containerClients.size()); - for (auto& entry: containerClients) { - clients.add(entry.value); - } - for (auto* client: clients.finish()) { - client->shutdown(); - } - } - - kj::Promise waitForContainerCleanup() { - auto cleanups = kj::heapArrayBuilder>(containerCleanupState.size()); - for (auto& entry: containerCleanupState) { - cleanups.add(entry.value.promise.addBranch()); - } - return kj::joinPromises(cleanups.finish()); - } - - // Test-only: gracefully evict every currently-running actor in this namespace. Depending on - // webSocketMode, hibernatable WebSockets are either hibernated first or closed. - // Namespaces with preventEviction are skipped. - // Idle/non-running actors are skipped (not an error). The actor map entries are retained so the - // DO rebuilds on its next request. See IoChannelFactory::evictAllActorsForTest(). - kj::Promise evictAllForTest(IoChannelFactory::EvictWebSocketMode webSocketMode) { - if (!isEvictable()) return kj::READY_NOW; - - kj::Vector> promises(actors.size()); - for (auto& actor: actors) { - // Pin the ActorContainer for the duration of its eviction. The map entry is normally - // retained, but the onBroken path can erase it mid-eviction; the addRef keeps the coroutine's - // `this` valid regardless. - promises.add( - actor.value->evictTreeForTestIfRunning(webSocketMode).attach(actor.value->addRef())); - } - return kj::joinPromises(promises.releaseAsArray()); - } - - // Resets all actor databases, aborts all actors, and cancels all alarms so DOs - // can be recreated with clean state. - void deleteAll(kj::Maybe reason) { - // Reset databases before aborting so connections are still open (avoids - // Windows file-locking issues with deferred handle release). - for (auto& actor: actors) { - actor.value->resetStorage(); - } - - abortAll(reason); - - KJ_IF_SOME(scheduler, ownAlarmScheduler) { - scheduler->deleteAll(); - } - } - - private: - kj::Rc actorClass; - const ActorConfig& config; - const kj::Clock& clock; - - struct ActorStorage { - kj::Own directory; - SqliteDatabase::Vfs vfs; - - ActorStorage(kj::Own directoryParam) - : directory(kj::mv(directoryParam)), - vfs(*directory) {} - }; - - // Note: The Vfs, actorStorage, and ownAlarmScheduler must not be torn down until all actors - // have been torn down, so we declare them before `actors`. - kj::Maybe actorStorage; - kj::Maybe> ownAlarmScheduler; - - // Tracks the canceler and cleanup promise for a Docker container's lifecycle cleanup. - // Useful to await on async calls of a ContainerClient destructor when the new - // one appears before they've been resolved. - struct ContainerCleanupState { - // Canceler that wraps the promise fired in ~ContainerClient. Replacing - // it cancels any pending cleanup, which resolves the promise immediately. - kj::Own canceler; - - // Forked cleanup promise. A branch is added to waitUntilTasks to keep the I/O alive, - // and another branch is passed to the next ContainerClient so its status() can await. - kj::ForkedPromise promise = kj::Promise(kj::READY_NOW).fork(); - - // Monotonically increasing counter, bumped each time the canceler is replaced - // via upsert. The cleanup callback captures the generation at creation time and - // compares it to detect whether a newer ContainerClient has taken ownership, - // avoiding a raw-pointer identity check that is vulnerable to address reuse. - uint64_t generation = 0; - }; - - // Per-container cleanup state: canceler + forked cleanup promise. - kj::HashMap containerCleanupState; - - // Map of container IDs to ContainerClients (for reconnection support with inactivity timeouts). - // The map holds raw pointers (not ownership) - ContainerClients are owned by actors and timers. - // When the last reference is dropped, the destructor removes the entry from this map. - kj::HashMap containerClients; - bool containerShutdownStarted = false; - - // If the actor is broken, we remove it from the map. However, if it's just evicted due to - // inactivity, we keep the ActorContainer in the map but drop the Own. When a new - // request comes in, we recreate the Own. - ActorMap actors; - - kj::Maybe> cleanupTask; - kj::Timer& timer; - capnp::ByteStreamFactory& byteStreamFactory; - ChannelTokenHandler& channelTokenHandler; - kj::Network& dockerNetwork; - kj::Maybe dockerPath; - kj::Maybe containerEgressInterceptorImage; - kj::TaskSet& waitUntilTasks; - - // Whether the worker owning this actor namespace has `allow_irrevocable_stub_storage` enabled, - // and therefore self-tokens for root actors should be treated as persistent. - Persistent selfTokensArePersistent; - - kj::Maybe alarmScheduler; - - // Removes actors from `actors` after 70 seconds of last access. - kj::Promise cleanupLoop() { - constexpr auto EXPIRATION = 70 * kj::SECONDS; - - // Don't bother running the loop if the config doesn't allow eviction. - KJ_SWITCH_ONEOF(config) { - KJ_CASE_ONEOF(c, Durable) { - if (!c.isEvictable) co_return; - } - KJ_CASE_ONEOF(c, Ephemeral) { - if (!c.isEvictable) co_return; - } - } - - while (true) { - auto now = timer.now(); - actors.eraseAll([&](auto&, kj::Own& entry) { - // Check getLastAccess() before hasClients() since it's faster. - if ((now - entry->getLastAccess()) <= EXPIRATION) { - // Used recently; don't evict. - return false; - } - - if (entry->hasClients()) { - // There's still an active client; don't evict. - return false; - } - - // No clients and not used in a while, evict this actor. - return true; - }); - - co_await timer.atTime(now + EXPIRATION); - } - } - - class ActorChannelImpl final: public IoChannelFactory::ActorChannel { - public: - ActorChannelImpl(kj::Own actorContainer, Persistent persistent) - : actorContainer(kj::mv(actorContainer)), - persistent(persistent) {} - ~ActorChannelImpl() noexcept(false) { - actorContainer->updateAccessTime(); - } - - kj::Own startRequest(IoChannelFactory::SubrequestMetadata metadata) override { - // If this channel was reconstructed from a persistent (stored) stub, signal the target so it - // can re-verify that it still allows persistent stubs. - metadata.fromPersistentStub = metadata.fromPersistentStub || persistent; - return newPromisedWorkerInterface( - actorContainer->startRequest(kj::mv(metadata)).attach(actorContainer->addRef())); - } - - kj::Promise evictForTest(IoChannelFactory::EvictWebSocketMode webSocketMode) override { - return actorContainer->evictForTest(webSocketMode).attach(actorContainer->addRef()); - } - - void requireAllowsTransfer() override { - actorContainer->requireTransferrableStub(); - } - - kj::OneOf, kj::Promise>> getTokenMaybeSync( - IoChannelFactory::ChannelTokenUsage usage) override { - return actorContainer->getChannelToken(usage, persistent); - } - - private: - kj::Own actorContainer; - Persistent persistent; - }; - - // `SelfTokenFactory` for a root (non-facet) Durable Object instance. Note that unlike - // `StaticServiceSelfTokenFactory`, this does not simply hold a reference to the entrypoint, - // because that would mean holding a reference to the `ActorChannelImpl` or `ActorContainer`, - // which would block eviction of the actor while the request is still running. All we actually - // need to construct the self-token is the actor ID (and namespace info). - class ActorSelfTokenFactory final: public ChannelTokenHandler::ServerSelfTokenFactory { - public: - ActorSelfTokenFactory(ActorNamespace& ns, Worker::Actor::Id id): ns(ns), id(kj::mv(id)) {} - - kj::OneOf, kj::Promise>> getSelfToken( - IoChannelFactory::ChannelTokenUsage usage) override { - // This factory is only ever created for root (non-facet) actors, so the only - // transferrability check we need is that the namespace is durable (ephemeral objects are - // not serializable). This throws lazily, i.e. only if `ctx.restore()` is actually used. - JSG_REQUIRE(ns.getConfig().is(), DOMDataCloneError, - "Stubs pointing to ephemeral objects are not serializable."); - - kj::StringPtr uniqueKey = ns.getConfig().get().uniqueKey; - const ActorIdFactory::ActorId& abstractId = - *KJ_ASSERT_NONNULL(id.tryGet>()); - auto& idImpl = - KJ_ASSERT_NONNULL(kj::tryDowncast(abstractId)); - return ns.channelTokenHandler.encodeActorChannelToken( - usage, uniqueKey, idImpl.getRaw(), idImpl.getName(), ns.selfTokensArePersistent); - } - - private: - ActorNamespace& ns; - Worker::Actor::Id id; - }; - - // Implements actor loopback, which is used by websocket hibernation to deliver events to the - // actor from the websocket's read loop. - class Loopback: public Worker::Actor::Loopback, public kj::Refcounted { - public: - Loopback(ActorContainer& actorContainer): actorContainer(actorContainer) {} - - kj::Own getWorker(IoChannelFactory::SubrequestMetadata metadata) override { - return newPromisedWorkerInterface( - actorContainer.startRequest(kj::mv(metadata)).attach(actorContainer.addRef())); - } - - kj::Own addRef() override { - return kj::addRef(*this); - } - - private: - ActorContainer& actorContainer; - }; - - class ActorSqliteHooks final: public ActorSqlite::Hooks { - public: - ActorSqliteHooks(AlarmScheduler& alarmScheduler, ActorKey actor) - : alarmScheduler(alarmScheduler), - actor(kj::mv(actor)) {} - - // We ignore the priorTask in workerd because everything should run synchronously. - kj::Promise scheduleRun( - kj::Maybe newAlarmTime, kj::Promise priorTask) override { - KJ_IF_SOME(scheduledTime, newAlarmTime) { - alarmScheduler.setAlarm(actor, scheduledTime); - } else { - alarmScheduler.deleteAlarm(actor); - } - return kj::READY_NOW; - } - - private: - AlarmScheduler& alarmScheduler; - ActorKey actor; - }; - - // Hooks used by facets, which have their own storage but no way to schedule alarms: the alarm - // scheduler only knows how to deliver alarms to the root actor of a namespace. - // - // TODO(someday): Support alarms in facets, somehow. - class FacetAlarmHooks final: public ActorSqlite::Hooks { - public: - kj::Promise scheduleRun( - kj::Maybe newAlarmTime, kj::Promise priorTask) override { - // Keep this message in sync with the equivalent error produced in actor-storage-factory.c++ - // in the internal codebase. - JSG_FAIL_REQUIRE(Error, "Facets currently cannot set alarms."); - } - }; -}; - -// ======================================================================================= - -kj::Own Server::makeTlsContext(config::TlsOptions::Reader conf) { - kj::TlsContext::Options options; - - struct Attachments { - kj::Maybe keypair; - kj::Array trustedCerts; - }; - auto attachments = kj::heap(); - - if (conf.hasKeypair()) { - auto pairConf = conf.getKeypair(); - options.defaultKeypair = attachments->keypair.emplace( - kj::TlsKeypair{.privateKey = kj::TlsPrivateKey(pairConf.getPrivateKey()), - .certificate = kj::TlsCertificate(pairConf.getCertificateChain())}); - } - - options.verifyClients = conf.getRequireClientCerts(); - options.useSystemTrustStore = conf.getTrustBrowserCas(); - - auto trustList = conf.getTrustedCertificates(); - if (trustList.size() > 0) { - attachments->trustedCerts = KJ_MAP(cert, trustList) { return kj::TlsCertificate(cert); }; - options.trustedCertificates = attachments->trustedCerts; - } - - switch (conf.getMinVersion()) { - case config::TlsOptions::Version::GOOD_DEFAULT: - // Don't change. - goto validVersion; - case config::TlsOptions::Version::SSL3: - options.minVersion = kj::TlsVersion::SSL_3; - goto validVersion; - case config::TlsOptions::Version::TLS1_DOT0: - options.minVersion = kj::TlsVersion::TLS_1_0; - goto validVersion; - case config::TlsOptions::Version::TLS1_DOT1: - options.minVersion = kj::TlsVersion::TLS_1_1; - goto validVersion; - case config::TlsOptions::Version::TLS1_DOT2: - options.minVersion = kj::TlsVersion::TLS_1_2; - goto validVersion; - case config::TlsOptions::Version::TLS1_DOT3: - options.minVersion = kj::TlsVersion::TLS_1_3; - goto validVersion; - } - reportConfigError(kj::str("Encountered unknown TlsOptions::minVersion setting. Was the " - "config compiled with a newer version of the schema?")); - -validVersion: - if (conf.hasCipherList()) { - options.cipherList = conf.getCipherList(); - } - - return kj::heap(kj::mv(options)).attach(kj::mv(attachments)); -} - -kj::Promise> Server::makeTlsNetworkAddress( - config::TlsOptions::Reader conf, - kj::StringPtr addrStr, - kj::Maybe certificateHost, - uint defaultPort) { - auto context = makeTlsContext(conf); - - KJ_IF_SOME(h, certificateHost) { - auto parsed = co_await network.parseAddress(addrStr, defaultPort); - co_return context->wrapAddress(kj::mv(parsed), h).attach(kj::mv(context)); - } - - // Wrap the `Network` itself so we can use the TLS implementation's `parseAddress()` to extract - // the authority from the address. - auto tlsNetwork = context->wrapNetwork(network); - auto parsed = co_await network.parseAddress(addrStr, defaultPort); - co_return parsed.attach(kj::mv(context)); -} - -// ======================================================================================= - -// Helper to apply config::HttpOptions. -class Server::HttpRewriter { - // TODO(beta): Do we want to automatically add `Date`, `Server` (to outgoing responses), - // `User-Agent` (to outgoing requests), etc.? - - public: - HttpRewriter( - config::HttpOptions::Reader httpOptions, kj::HttpHeaderTable::Builder& headerTableBuilder) - : style(httpOptions.getStyle()), - requestInjector(httpOptions.getInjectRequestHeaders(), headerTableBuilder), - responseInjector(httpOptions.getInjectResponseHeaders(), headerTableBuilder) { - if (httpOptions.hasForwardedProtoHeader()) { - forwardedProtoHeader = headerTableBuilder.add(httpOptions.getForwardedProtoHeader()); - } - if (httpOptions.hasCfBlobHeader()) { - cfBlobHeader = headerTableBuilder.add(httpOptions.getCfBlobHeader()); - } - if (httpOptions.hasCapnpConnectHost()) { - capnpConnectHost = httpOptions.getCapnpConnectHost(); - } - } - - bool hasCfBlobHeader() { - return cfBlobHeader != kj::none; - } - - bool needsRewriteRequest() { - return style == config::HttpOptions::Style::HOST || hasCfBlobHeader() || - !requestInjector.empty(); - } - - // Attach this to the promise returned by request(). - struct Rewritten { - kj::Own headers; - kj::String ownUrl; - }; - - Rewritten rewriteOutgoingRequest( - kj::StringPtr& url, const kj::HttpHeaders& headers, kj::Maybe cfBlobJson) { - Rewritten result{kj::heap(headers.cloneShallow()), nullptr}; - - if (style == config::HttpOptions::Style::HOST) { - auto parsed = kj::Url::parse(url, kj::Url::HTTP_PROXY_REQUEST, - kj::Url::Options{.percentDecode = false, .allowEmpty = true}); - result.headers->set(kj::HttpHeaderId::HOST, kj::mv(parsed.host)); - KJ_IF_SOME(h, forwardedProtoHeader) { - result.headers->set(h, kj::mv(parsed.scheme)); - } - url = result.ownUrl = parsed.toString(kj::Url::HTTP_REQUEST); - } - - KJ_IF_SOME(h, cfBlobHeader) { - KJ_IF_SOME(b, cfBlobJson) { - result.headers->setPtr(h, b); - } else { - result.headers->unset(h); - } - } - - requestInjector.apply(*result.headers); - - return result; - } - - kj::Maybe rewriteIncomingRequest(kj::StringPtr& url, - kj::StringPtr physicalProtocol, - const kj::HttpHeaders& headers, - kj::Maybe& cfBlobJson) { - Rewritten result{kj::heap(headers.cloneShallow()), nullptr}; - - if (style == config::HttpOptions::Style::HOST) { - auto parsed = kj::Url::parse( - url, kj::Url::HTTP_REQUEST, kj::Url::Options{.percentDecode = false, .allowEmpty = true}); - parsed.host = kj::str(KJ_UNWRAP_OR_RETURN(headers.get(kj::HttpHeaderId::HOST), kj::none)); - - KJ_IF_SOME(h, forwardedProtoHeader) { - KJ_IF_SOME(s, headers.get(h)) { - parsed.scheme = kj::str(s); - result.headers->unset(h); - } - } - - if (parsed.scheme == nullptr) parsed.scheme = kj::str(physicalProtocol); - - url = result.ownUrl = parsed.toString(kj::Url::HTTP_PROXY_REQUEST); - } - - KJ_IF_SOME(h, cfBlobHeader) { - KJ_IF_SOME(b, headers.get(h)) { - cfBlobJson = kj::str(b); - result.headers->unset(h); - } - } - - requestInjector.apply(*result.headers); - - return result; - } - - bool needsRewriteResponse() { - return !responseInjector.empty(); - } - - void rewriteResponse(kj::HttpHeaders& headers) { - responseInjector.apply(headers); - } - - kj::Maybe getCapnpConnectHost() { - return capnpConnectHost; - } - - private: - config::HttpOptions::Style style; - kj::Maybe forwardedProtoHeader; - kj::Maybe cfBlobHeader; - kj::Maybe capnpConnectHost; - - class HeaderInjector { - public: - HeaderInjector(capnp::List::Reader headers, - kj::HttpHeaderTable::Builder& headerTableBuilder) - : injectedHeaders(KJ_MAP(header, headers) { - InjectedHeader result; - result.id = headerTableBuilder.add(header.getName()); - if (header.hasValue()) { - result.value = kj::str(header.getValue()); - } - return result; - }) {} - - bool empty() { - return injectedHeaders.size() == 0; - } - - void apply(kj::HttpHeaders& headers) { - for (auto& header: injectedHeaders) { - KJ_IF_SOME(v, header.value) { - headers.setPtr(header.id, v); - } else { - headers.unset(header.id); - } - } - } - - private: - struct InjectedHeader { - kj::HttpHeaderId id; - kj::Maybe value; - }; - kj::Array injectedHeaders; - }; - - HeaderInjector requestInjector; - HeaderInjector responseInjector; -}; - -// ======================================================================================= - -// Service used when the service's config is invalid. -class Server::InvalidConfigService final: public Service { - public: - kj::Own startRequest(IoChannelFactory::SubrequestMetadata metadata) override { - JSG_FAIL_REQUIRE(Error, "Service cannot handle requests because its config is invalid."); - } - - bool hasHandler(kj::StringPtr handlerName) override { - return false; - } - - kj::OneOf, kj::Promise>> getTokenMaybeSync( - IoChannelFactory::ChannelTokenUsage usage) override { - // Can't get here because workerd would have failed to start. - KJ_UNREACHABLE; - } -}; - -class Server::InvalidConfigActorClass final: public ActorClass { - public: - void requireAllowsTransfer() override { - // Can't get here because workerd would have failed to start. - KJ_UNREACHABLE; - } - kj::OneOf, kj::Promise>> getTokenMaybeSync( - IoChannelFactory::ChannelTokenUsage usage) override { - // Can't get here because workerd would have failed to start. - KJ_UNREACHABLE; - } - - kj::Own newActor(kj::Maybe tracker, - Worker::Actor::Id actorId, - Worker::Actor::MakeActorCacheFunc makeActorCache, - Worker::Actor::MakeStorageFunc makeStorage, - kj::Own loopback, - kj::Maybe> manager, - kj::Maybe container, - jsg::Dict containerImages, - kj::Maybe facetManager) override { - JSG_FAIL_REQUIRE( - Error, "Cannot instantiate Durable Object class because its config is invalid."); - } - - kj::Own startRequest( - IoChannelFactory::SubrequestMetadata metadata, kj::Own actor) override { - // Can't get here because creating the actor would have required calling the other method. - KJ_UNREACHABLE; - } -}; - -kj::Rc Server::makeInvalidConfigService() { - return invalidConfigServiceSingleton.addRef(); -} - -// A NetworkAddress whose connect() method waits for a Promise and then forwards -// to it. Used by ExternalHttpService so that we don't have to wait for DNS lookup before the -// server can start. -class PromisedNetworkAddress final: public kj::NetworkAddress { - // TODO(cleanup): kj::Network should be extended with a new version of parseAddress() which does - // not do DNS lookup immediately, and therefore can return a NetworkAddress synchronously. - // In fact, this version should be designed to redo the DNS lookup periodically to see if it - // changed, which would be nice for workerd when the remote address may change over time. - public: - PromisedNetworkAddress(kj::Promise> promise) - : promise(promise.then([this](kj::Own result) { addr = kj::mv(result); }) - .fork()) {} - - kj::Promise> connect() override { - KJ_IF_SOME(a, addr) { - co_return co_await a.get()->connect(); - } else { - co_await promise; - co_return co_await KJ_ASSERT_NONNULL(addr)->connect(); - } - } - - kj::Promise connectAuthenticated() override { - KJ_IF_SOME(a, addr) { - co_return co_await a.get()->connectAuthenticated(); - } else { - co_await promise; - co_return co_await KJ_ASSERT_NONNULL(addr)->connectAuthenticated(); - } - } - - // We don't use any other methods, and they seem kinda annoying to implement. - kj::Own listen() override { - KJ_UNIMPLEMENTED("PromisedNetworkAddress::listen() not implemented"); - } - kj::Own clone() override { - KJ_UNIMPLEMENTED("PromisedNetworkAddress::clone() not implemented"); - } - kj::String toString() override { - KJ_UNIMPLEMENTED("PromisedNetworkAddress::toString() not implemented"); - } - - private: - kj::ForkedPromise promise; - kj::Maybe> addr; -}; - -class Server::ExternalTcpService final: public Service, private WorkerInterface { - public: - ExternalTcpService(kj::Own addrParam): addr(kj::mv(addrParam)) {} - - kj::Own startRequest(IoChannelFactory::SubrequestMetadata metadata) override { - return {this, kj::NullDisposer::instance}; - } - - bool hasHandler(kj::StringPtr handlerName) override { - return handlerName == "fetch"_kj || handlerName == "connect"_kj; - } - - kj::OneOf, kj::Promise>> getTokenMaybeSync( - IoChannelFactory::ChannelTokenUsage usage) override { - JSG_FAIL_REQUIRE(DOMDataCloneError, "ExternalService can't be passed over RPC."); - } - - private: - kj::Own addr; - - kj::Promise request(kj::HttpMethod method, - kj::StringPtr url, - const kj::HttpHeaders& headers, - kj::AsyncInputStream& requestBody, - kj::HttpService::Response& response) override { - throwUnsupported(); - } - - kj::Promise connect(kj::StringPtr host, - const kj::HttpHeaders& headers, - kj::AsyncIoStream& connection, - ConnectResponse& tunnel, - kj::HttpConnectSettings settings) override { - TRACE_EVENT("workerd", "ExternalTcpService::connect()", "host", host.cStr()); - auto io_stream = co_await addr->connect(); - - auto promises = kj::heapArrayBuilder>(2); - - promises.add(connection.pumpTo(*io_stream).then([&io_stream = *io_stream](uint64_t size) { - io_stream.shutdownWrite(); - })); - - promises.add(io_stream->pumpTo(connection).then([&connection](uint64_t size) { - connection.shutdownWrite(); - })); - - tunnel.accept(200, "OK", kj::HttpHeaders(kj::HttpHeaderTable{})); - - co_await kj::joinPromisesFailFast(promises.finish()).attach(kj::mv(io_stream)); - } - - kj::Promise prewarm(kj::StringPtr url) override { - return kj::READY_NOW; - } - kj::Promise runScheduled(kj::Date scheduledTime, kj::StringPtr cron) override { - throwUnsupported(); - } - kj::Promise runAlarm(kj::Date scheduledTime, uint32_t retryCount) override { - throwUnsupported(); - } - kj::Promise customEvent(kj::Own event) override { - return event->notSupported(); - } - - [[noreturn]] void throwUnsupported() { - JSG_FAIL_REQUIRE(Error, "External TCP servers don't support this event type."); - } -}; - -// Service used when the service is configured as external HTTP service. -class Server::ExternalHttpService final: public Service { - public: - ExternalHttpService(kj::Own addrParam, - kj::Own rewriter, - kj::HttpHeaderTable& headerTable, - kj::Timer& timer, - kj::EntropySource& entropySource, - capnp::ByteStreamFactory& byteStreamFactory, - capnp::HttpOverCapnpFactory& httpOverCapnpFactory) - : addr(kj::mv(addrParam)), - webSocketErrorHandler(kj::heap()), - inner(kj::newHttpClient(timer, - headerTable, - *addr, - {.entropySource = entropySource, - .webSocketCompressionMode = kj::HttpClientSettings::MANUAL_COMPRESSION, - .webSocketErrorHandler = *webSocketErrorHandler})), - serviceAdapter(kj::newHttpService(*inner)), - rewriter(kj::mv(rewriter)), - headerTable(headerTable), - byteStreamFactory(byteStreamFactory), - httpOverCapnpFactory(httpOverCapnpFactory) {} - - kj::Own startRequest(IoChannelFactory::SubrequestMetadata metadata) override { - return kj::heap(*this, kj::mv(metadata)); - } - - bool hasHandler(kj::StringPtr handlerName) override { - return handlerName == "fetch"_kj || handlerName == "connect"_kj; - } - - kj::OneOf, kj::Promise>> getTokenMaybeSync( - IoChannelFactory::ChannelTokenUsage usage) override { - JSG_FAIL_REQUIRE(DOMDataCloneError, "ExternalService can't be passed over RPC."); - } - - private: - kj::Own addr; - - kj::Own webSocketErrorHandler; - kj::Own inner; - kj::Own serviceAdapter; - - kj::Own rewriter; - - kj::HttpHeaderTable& headerTable; - capnp::ByteStreamFactory& byteStreamFactory; - capnp::HttpOverCapnpFactory& httpOverCapnpFactory; - - struct CapnpClient { - kj::Own connection; - capnp::TwoPartyClient rpcSystem; - - CapnpClient(kj::Own connectionParam) - : connection(kj::mv(connectionParam)), - rpcSystem(*connection) {} - }; - - // capnpClient is created on-demand when RPC is needed. - kj::Maybe capnpClient; - - // This task nulls out `capnpClient` when the connection is lost. - kj::Promise clearCapnpClientTask = nullptr; - - // Get an WorkerdBootstrap representing the service on the other end of an HTTP connection. May - // reuse an existing connection, or form a new one over `client`. - rpc::WorkerdBootstrap::Client getOutgoingCapnp(kj::HttpClient& client) { - KJ_IF_SOME(c, capnpClient) { - return c.rpcSystem.bootstrap().castAs(); - } - - // No existing client, need to create a new one. - kj::StringPtr host = KJ_UNWRAP_OR(rewriter->getCapnpConnectHost(), - { return JSG_KJ_EXCEPTION(FAILED, Error, "This ExternalServer not configured for RPC."); }); - - auto req = client.connect(host, kj::HttpHeaders(headerTable), {}); - auto& c = capnpClient.emplace(kj::mv(req.connection)); - - // Arrange that when the connection is lost, we'll null out `capnpClient`. This ensures that - // on the next event, we'll attempt to reconnect. - // - // TODO(perf): Time out idle connections? - clearCapnpClientTask = - c.rpcSystem.onDisconnect().attach(kj::defer([this]() { - capnpClient = kj::none; - })).eagerlyEvaluate(nullptr); - - return c.rpcSystem.bootstrap().castAs(); - } - - class WorkerInterfaceImpl final: public WorkerInterface, private kj::HttpService::Response { - public: - WorkerInterfaceImpl(ExternalHttpService& parent, IoChannelFactory::SubrequestMetadata metadata) - : parent(parent.addRef()), - metadata(kj::mv(metadata)) {} - - kj::Promise request(kj::HttpMethod method, - kj::StringPtr url, - const kj::HttpHeaders& headers, - kj::AsyncInputStream& requestBody, - kj::HttpService::Response& response) override { - TRACE_EVENT("workerd", "ExternalHttpServer::request()"); - KJ_REQUIRE(wrappedResponse == kj::none, "object should only receive one request"); - wrappedResponse = response; - if (parent->rewriter->needsRewriteRequest()) { - auto rewrite = parent->rewriter->rewriteOutgoingRequest(url, headers, metadata.cfBlobJson); - return parent->serviceAdapter->request(method, url, *rewrite.headers, requestBody, *this) - .attach(kj::mv(rewrite)); - } else { - return parent->serviceAdapter->request(method, url, headers, requestBody, *this); - } - } - - kj::Promise connect(kj::StringPtr host, - const kj::HttpHeaders& headers, - kj::AsyncIoStream& connection, - ConnectResponse& tunnel, - kj::HttpConnectSettings settings) override { - TRACE_EVENT("workerd", "ExternalHttpServer::connect()"); - return parent->serviceAdapter->connect(host, headers, connection, tunnel, kj::mv(settings)); - } - - kj::Promise prewarm(kj::StringPtr url) override { - return kj::READY_NOW; - } - kj::Promise runScheduled(kj::Date scheduledTime, kj::StringPtr cron) override { - throwUnsupported(); - } - kj::Promise runAlarm(kj::Date scheduledTime, uint32_t retryCount) override { - throwUnsupported(); - } - - kj::Promise customEvent(kj::Own event) override { - // We'll use capnp RPC for custom events. - auto bootstrap = parent->getOutgoingCapnp(*parent->inner); - auto dispatcher = - bootstrap.startEventRequest(capnp::MessageSize{4, 0}).send().getDispatcher(); - // NOTE: We don't support restore() over workerd-to-workerd RPC so we can use - // getUnsupportedFrankenvalueHandler() here for now. - return event - ->sendRpc(parent->httpOverCapnpFactory, parent->byteStreamFactory, - getUnsupportedFrankenvalueHandler(), kj::mv(dispatcher)) - .attach(kj::mv(event)); - } - - private: - kj::Rc parent; - IoChannelFactory::SubrequestMetadata metadata; - kj::Maybe wrappedResponse; - - [[noreturn]] void throwUnsupported() { - JSG_FAIL_REQUIRE(Error, "External HTTP servers don't support this event type."); - } - - kj::Own send(uint statusCode, - kj::StringPtr statusText, - const kj::HttpHeaders& headers, - kj::Maybe expectedBodySize) override { - TRACE_EVENT("workerd", "ExternalHttpService::send()", "status", statusCode); - auto& response = KJ_ASSERT_NONNULL(wrappedResponse); - if (parent->rewriter->needsRewriteResponse()) { - auto rewrite = headers.cloneShallow(); - parent->rewriter->rewriteResponse(rewrite); - return response.send(statusCode, statusText, rewrite, expectedBodySize); - } else { - return response.send(statusCode, statusText, headers, expectedBodySize); - } - } - - kj::Own acceptWebSocket(const kj::HttpHeaders& headers) override { - TRACE_EVENT("workerd", "ExternalHttpService::acceptWebSocket()"); - auto& response = KJ_ASSERT_NONNULL(wrappedResponse); - if (parent->rewriter->needsRewriteResponse()) { - auto rewrite = headers.cloneShallow(); - parent->rewriter->rewriteResponse(rewrite); - return response.acceptWebSocket(rewrite); - } else { - return response.acceptWebSocket(headers); - } - } - }; -}; - -kj::Rc Server::makeExternalService(kj::StringPtr name, - config::ExternalServer::Reader conf, - kj::HttpHeaderTable::Builder& headerTableBuilder) { - TRACE_EVENT("workerd", "Server::makeExternalService()", "name", name.cStr()); - kj::StringPtr addrStr = nullptr; - kj::String ownAddrStr = nullptr; - - KJ_IF_SOME(override, externalOverrides.findEntry(name)) { - addrStr = ownAddrStr = kj::mv(override.value); - externalOverrides.erase(override); - } else if (conf.hasAddress()) { - addrStr = conf.getAddress(); - } else { - reportConfigError(kj::str("External service \"", name, - "\" has no address in the config, so must be specified " - "on the command line with `--external-addr`.")); - return makeInvalidConfigService(); - } - - switch (conf.which()) { - case config::ExternalServer::HTTP: { - // We have to construct the rewriter upfront before waiting on any promises, since the - // HeaderTable::Builder is only available synchronously. - auto rewriter = kj::heap(conf.getHttp(), headerTableBuilder); - auto addr = kj::heap(network.parseAddress(addrStr, 80)); - return kj::rc(kj::mv(addr), kj::mv(rewriter), - headerTableBuilder.getFutureTable(), timer, entropySource, - globalContext->byteStreamFactory, globalContext->httpOverCapnpFactory); - } - case config::ExternalServer::HTTPS: { - auto httpsConf = conf.getHttps(); - kj::Maybe certificateHost; - if (httpsConf.hasCertificateHost()) { - certificateHost = httpsConf.getCertificateHost(); - } - auto rewriter = kj::heap(httpsConf.getOptions(), headerTableBuilder); - auto addr = kj::heap( - makeTlsNetworkAddress(httpsConf.getTlsOptions(), addrStr, certificateHost, 443)); - return kj::rc(kj::mv(addr), kj::mv(rewriter), - headerTableBuilder.getFutureTable(), timer, entropySource, - globalContext->byteStreamFactory, globalContext->httpOverCapnpFactory); - } - case config::ExternalServer::TCP: { - auto tcpConf = conf.getTcp(); - auto addr = kj::heap(network.parseAddress(addrStr, 80)); - if (tcpConf.hasTlsOptions()) { - kj::Maybe certificateHost; - if (tcpConf.hasCertificateHost()) { - certificateHost = tcpConf.getCertificateHost(); - } - addr = kj::heap( - makeTlsNetworkAddress(tcpConf.getTlsOptions(), addrStr, certificateHost, 0)); - } - return kj::rc(kj::mv(addr)); - } - } - reportConfigError(kj::str("External service named \"", name, - "\" has unrecognized protocol. Was the config " - "compiled with a newer version of the schema?")); - return makeInvalidConfigService(); -} - -// Service used when the service is configured as network service. -class Server::NetworkService final: public Service, private WorkerInterface { - public: - NetworkService(kj::HttpHeaderTable& headerTable, - kj::Timer& timer, - kj::EntropySource& entropySource, - kj::Own networkParam, - kj::Maybe> tlsNetworkParam, - kj::Maybe tlsContext) - : network(kj::mv(networkParam)), - tlsNetwork(kj::mv(tlsNetworkParam)), - webSocketErrorHandler(kj::heap()), - inner(kj::newHttpClient(timer, - headerTable, - *network, - tlsNetwork, - {.entropySource = entropySource, - .webSocketCompressionMode = kj::HttpClientSettings::MANUAL_COMPRESSION, - .webSocketErrorHandler = *webSocketErrorHandler, - .tlsContext = tlsContext})), - serviceAdapter(kj::newHttpService(*inner)) {} - - kj::Own startRequest(IoChannelFactory::SubrequestMetadata metadata) override { - return {this, kj::NullDisposer::instance}; - } - - bool hasHandler(kj::StringPtr handlerName) override { - return handlerName == "fetch"_kj || handlerName == "connect"_kj; - } - - kj::OneOf, kj::Promise>> getTokenMaybeSync( - IoChannelFactory::ChannelTokenUsage usage) override { - JSG_FAIL_REQUIRE(DOMDataCloneError, "NetworkService can't be passed over RPC."); - } - - private: - kj::Own network; - kj::Maybe> tlsNetwork; - kj::Own webSocketErrorHandler; - kj::Own inner; - kj::Own serviceAdapter; - - kj::Promise request(kj::HttpMethod method, - kj::StringPtr url, - const kj::HttpHeaders& headers, - kj::AsyncInputStream& requestBody, - kj::HttpService::Response& response) override { - TRACE_EVENT("workerd", "NetworkService::request()"); - return serviceAdapter->request(method, url, headers, requestBody, response); - } - - kj::Promise connect(kj::StringPtr host, - const kj::HttpHeaders& headers, - kj::AsyncIoStream& connection, - ConnectResponse& tunnel, - kj::HttpConnectSettings settings) override { - TRACE_EVENT("workerd", "NetworkService::connect()"); - // This code is hit when the global `connect` function is called in a JS worker script. - // It represents a proxy-less TCP connection, which means we can simply defer the handling of - // the connection to the service adapter (likely NetworkHttpClient). Its behavior will be to - // connect directly to the host over TCP. - return serviceAdapter->connect(host, headers, connection, tunnel, kj::mv(settings)); - } - - kj::Promise prewarm(kj::StringPtr url) override { - return kj::READY_NOW; - } - kj::Promise runScheduled(kj::Date scheduledTime, kj::StringPtr cron) override { - throwUnsupported(); - } - kj::Promise runAlarm(kj::Date scheduledTime, uint32_t retryCount) override { - throwUnsupported(); - } - kj::Promise customEvent(kj::Own event) override { - return event->notSupported(); - } - - [[noreturn]] void throwUnsupported() { - JSG_FAIL_REQUIRE(Error, "External HTTP servers don't support this event type."); - } -}; - -kj::Rc Server::makeNetworkService(config::Network::Reader conf) { - TRACE_EVENT("workerd", "Server::makeNetworkService()"); - auto restrictedNetwork = network.restrictPeers( KJ_MAP(a, conf.getAllow()) -> kj::StringPtr { - return a; - }, KJ_MAP(a, conf.getDeny()) -> kj::StringPtr { return a; }); - - kj::Maybe> tlsNetwork; - kj::Maybe tlsContext; - if (conf.hasTlsOptions()) { - auto ownedTlsContext = makeTlsContext(conf.getTlsOptions()); - tlsContext = ownedTlsContext; - tlsNetwork = ownedTlsContext->wrapNetwork(*restrictedNetwork).attach(kj::mv(ownedTlsContext)); - } - - return kj::rc(globalContext->headerTable, timer, entropySource, - kj::mv(restrictedNetwork), kj::mv(tlsNetwork), tlsContext); -} - -// Service used when the service is configured as disk directory service. -class Server::DiskDirectoryService final: public Service, private WorkerInterface { - public: - DiskDirectoryService(config::DiskDirectory::Reader conf, - kj::Own dir, - kj::HttpHeaderTable::Builder& headerTableBuilder) - : writable(*dir), - readable(kj::mv(dir)), - headerTable(headerTableBuilder.getFutureTable()), - hLastModified(headerTableBuilder.add("Last-Modified")), - allowDotfiles(conf.getAllowDotfiles()) {} - DiskDirectoryService(config::DiskDirectory::Reader conf, - kj::Own dir, - kj::HttpHeaderTable::Builder& headerTableBuilder) - : readable(kj::mv(dir)), - headerTable(headerTableBuilder.getFutureTable()), - hLastModified(headerTableBuilder.add("Last-Modified")), - allowDotfiles(conf.getAllowDotfiles()) {} - - kj::Own startRequest(IoChannelFactory::SubrequestMetadata metadata) override { - return {this, kj::NullDisposer::instance}; - } - - kj::Maybe getWritable() { - return writable; - } - - bool hasHandler(kj::StringPtr handlerName) override { - return handlerName == "fetch"_kj; - } - - kj::OneOf, kj::Promise>> getTokenMaybeSync( - IoChannelFactory::ChannelTokenUsage usage) override { - JSG_FAIL_REQUIRE(DOMDataCloneError, "DiskDirectoryService can't be passed over RPC."); - } - - private: - kj::Maybe writable; - kj::Own readable; - kj::HttpHeaderTable& headerTable; - kj::HttpHeaderId hLastModified; - bool allowDotfiles; - - kj::Promise request(kj::HttpMethod method, - kj::StringPtr urlStr, - const kj::HttpHeaders& requestHeaders, - kj::AsyncInputStream& requestBody, - kj::HttpService::Response& response) override { - TRACE_EVENT("workerd", "DiskDirectoryService::request()", "url", urlStr.cStr()); - auto url = kj::Url::parse(urlStr); - - bool blockedPath = false; - kj::Path path = nullptr; - KJ_IF_SOME(exception, - kj::runCatchingExceptions([&]() { path = kj::Path(url.path.releaseAsArray()); })) { - (void)exception; // squash compiler warning about unused var - // If the Path constructor throws, this path is not valid (e.g. it contains ".."). - blockedPath = true; - } - - if (!blockedPath && !allowDotfiles) { - for (auto& part: path) { - if (part.startsWith(".")) { - blockedPath = true; - break; - } - } - } - - if (method == kj::HttpMethod::GET || method == kj::HttpMethod::HEAD) { - if (blockedPath) { - co_return co_await response.sendError(404, "Not Found", headerTable); - } - - auto file = KJ_UNWRAP_OR(readable->tryOpenFile(path), - { co_return co_await response.sendError(404, "Not Found", headerTable); }); - - auto meta = file->stat(); - - switch (meta.type) { - case kj::FsNode::Type::FILE: { - // If this is a GET request with a Range header, return partial content if a single - // satisfiable range is specified. - // TODO(someday): consider supporting multiple ranges with multipart/byteranges - kj::Maybe range; - if (method == kj::HttpMethod::GET) { - KJ_IF_SOME(header, requestHeaders.get(kj::HttpHeaderId::RANGE)) { - KJ_SWITCH_ONEOF(kj::tryParseHttpRangeHeader(header.asArray(), meta.size)) { - KJ_CASE_ONEOF(ranges, kj::Array) { - KJ_ASSERT(ranges.size() > 0); - if (ranges.size() == 1) range = ranges[0]; - } - KJ_CASE_ONEOF(_, kj::HttpEverythingRange) {} - KJ_CASE_ONEOF(_, kj::HttpUnsatisfiableRange) { - kj::HttpHeaders headers(headerTable); - headers.set(kj::HttpHeaderId::CONTENT_RANGE, kj::str("bytes */", meta.size)); - co_return co_await response.sendError(416, "Range Not Satisfiable", headers); - } - } - } - } - - kj::HttpHeaders headers(headerTable); - headers.set(kj::HttpHeaderId::CONTENT_TYPE, MimeType::OCTET_STREAM.toString()); - headers.set(hLastModified, httpTime(meta.lastModified)); - - // We explicitly set the Content-Length header because if we don't, and we were called - // by a local Worker (without an actual HTTP connection in between), then the Worker - // will not see a Content-Length header, but being able to query the content length - // (especially with HEAD requests) is quite useful. - // TODO(cleanup): Arguably the implementation of `fetch()` should be adjusted so that - // if no `Content-Length` header is returned, but the body size is known via the KJ - // HTTP API, then the header should be filled in automatically. Unclear if this is safe - // to change without a compat flag. - - if (method == kj::HttpMethod::HEAD) { - headers.set(kj::HttpHeaderId::CONTENT_LENGTH, kj::str(meta.size)); - response.send(200, "OK", headers, meta.size); - co_return; - } else KJ_IF_SOME(r, range) { - KJ_ASSERT(r.start <= r.end); - auto rangeSize = r.end - r.start + 1; - headers.set(kj::HttpHeaderId::CONTENT_LENGTH, kj::str(rangeSize)); - headers.set(kj::HttpHeaderId::CONTENT_RANGE, - kj::str("bytes ", r.start, "-", r.end, "/", meta.size)); - auto out = response.send(206, "Partial Content", headers, rangeSize); - - auto in = kj::heap(*file, r.start); - co_await in->pumpTo(*out, rangeSize); - co_return; - } else { - headers.set(kj::HttpHeaderId::CONTENT_LENGTH, kj::str(meta.size)); - auto out = response.send(200, "OK", headers, meta.size); - - auto in = kj::heap(*file); - co_await in->pumpTo(*out, meta.size); - co_return; - } - } - case kj::FsNode::Type::DIRECTORY: { - // Whoooops, we opened a directory. Back up and start over. - - auto dir = readable->openSubdir(path); - - kj::HttpHeaders headers(headerTable); - headers.set(kj::HttpHeaderId::CONTENT_TYPE, MimeType::JSON.toString()); - headers.set(hLastModified, httpTime(meta.lastModified)); - - // We intentionally don't provide the expected size here in order to reserve the right - // to switch to streaming directory listing in the future. - auto out = response.send(200, "OK", headers); - - if (method == kj::HttpMethod::HEAD) { - co_return; - } else { - auto entries = dir->listEntries(); - kj::Vector jsonEntries(entries.size()); - for (auto& entry: entries) { - if (!allowDotfiles && entry.name.startsWith(".")) { - continue; - } - - kj::StringPtr type = "other"; - switch (entry.type) { - case kj::FsNode::Type::FILE: - type = "file"; - break; - case kj::FsNode::Type::DIRECTORY: - type = "directory"; - break; - case kj::FsNode::Type::SYMLINK: - type = "symlink"; - break; - case kj::FsNode::Type::BLOCK_DEVICE: - type = "blockDevice"; - break; - case kj::FsNode::Type::CHARACTER_DEVICE: - type = "characterDevice"; - break; - case kj::FsNode::Type::NAMED_PIPE: - type = "namedPipe"; - break; - case kj::FsNode::Type::SOCKET: - type = "socket"; - break; - case kj::FsNode::Type::OTHER: - type = "other"; - break; - } - - jsonEntries.add( - kj::str("{\"name\":", escapeJsonString(entry.name), ",\"type\":\"", type, "\"}")); - }; - - auto content = kj::str('[', kj::strArray(jsonEntries, ","), ']'); - - co_return co_await out->write(content.asBytes()); - } - } - default: - co_return co_await response.sendError(406, "Not Acceptable", headerTable); - } - } else if (method == kj::HttpMethod::PUT) { - auto& w = KJ_UNWRAP_OR(writable, - { co_return co_await response.sendError(405, "Method Not Allowed", headerTable); }); - - if (blockedPath || path.size() == 0) { - co_return co_await response.sendError(403, "Unauthorized", headerTable); - } - - auto replacer = w.replaceFile( - path, kj::WriteMode::CREATE | kj::WriteMode::MODIFY | kj::WriteMode::CREATE_PARENT); - auto stream = kj::heap(replacer->get()); - - co_await requestBody.pumpTo(*stream); - - replacer->commit(); - kj::HttpHeaders headers(headerTable); - response.send(204, "No Content", headers); - co_return; - } else if (method == kj::HttpMethod::DELETE) { - auto& w = KJ_UNWRAP_OR(writable, - { co_return co_await response.sendError(405, "Method Not Allowed", headerTable); }); - - if (blockedPath || path.size() == 0) { - co_return co_await response.sendError(403, "Unauthorized", headerTable); - } - - auto found = w.tryRemove(path); - - kj::HttpHeaders headers(headerTable); - if (found) { - response.send(204, "No Content", headers); - co_return; - } else { - co_return co_await response.sendError(404, "Not Found", headers); - } - } else { - co_return co_await response.sendError(501, "Not Implemented", headerTable); - } - } - - kj::Promise connect(kj::StringPtr host, - const kj::HttpHeaders& headers, - kj::AsyncIoStream& connection, - kj::HttpService::ConnectResponse& response, - kj::HttpConnectSettings settings) override { - throwUnsupported(); - } - kj::Promise prewarm(kj::StringPtr url) override { - return kj::READY_NOW; - } - kj::Promise runScheduled(kj::Date scheduledTime, kj::StringPtr cron) override { - throwUnsupported(); - } - kj::Promise runAlarm(kj::Date scheduledTime, uint32_t retryCount) override { - throwUnsupported(); - } - kj::Promise customEvent(kj::Own event) override { - return event->notSupported(); - } - - [[noreturn]] void throwUnsupported() { - JSG_FAIL_REQUIRE(Error, "Disk directory services don't support this event type."); - } -}; - -kj::Rc Server::makeDiskDirectoryService(kj::StringPtr name, - config::DiskDirectory::Reader conf, - kj::HttpHeaderTable::Builder& headerTableBuilder) { - TRACE_EVENT("workerd", "Server::makeDiskDirectoryService()"); - kj::StringPtr pathStr = nullptr; - kj::String ownPathStr; - - KJ_IF_SOME(override, directoryOverrides.findEntry(name)) { - pathStr = ownPathStr = kj::mv(override.value); - directoryOverrides.erase(override); - } else if (conf.hasPath()) { - pathStr = conf.getPath(); - } else { - reportConfigError(kj::str("Directory \"", name, - "\" has no path in the config, so must be specified on the " - "command line with `--directory-path`.")); - return makeInvalidConfigService(); - } - - auto path = fs.getCurrentPath().evalNative(pathStr); - - if (conf.getWritable()) { - auto openDir = KJ_UNWRAP_OR(fs.getRoot().tryOpenSubdir(kj::mv(path), kj::WriteMode::MODIFY), { - reportConfigError(kj::str("Directory named \"", name, "\" not found: ", pathStr)); - return makeInvalidConfigService(); - }); - - return kj::rc(conf, kj::mv(openDir), headerTableBuilder); - } else { - auto openDir = KJ_UNWRAP_OR(fs.getRoot().tryOpenSubdir(kj::mv(path)), { - reportConfigError(kj::str("Directory named \"", name, "\" not found: ", pathStr)); - return makeInvalidConfigService(); - }); - - return kj::rc(conf, kj::mv(openDir), headerTableBuilder); - } -} - -// ======================================================================================= - -// This class exists to update the InspectorService's table of isolates when a config -// has multiple services. The InspectorService exists on the stack of its own thread and -// initializes state that is bound to the thread, e.g. a http server and an event loop. -// This class provides a small thread-safe interface to the InspectorService so : -// mappings can be added after the InspectorService has started. -// -// The Cloudflare devtools only show the first service in workerd configuration. This service -// is always contains a users code. However, in packaging user code wrangler may add -// additional services that also have code. If using Chrome devtools to inspect a workerd, -// instance all services are visible and can be debugged. -class Server::InspectorServiceIsolateRegistrar final { - public: - InspectorServiceIsolateRegistrar() {} - ~InspectorServiceIsolateRegistrar() noexcept(true); - - void registerIsolate(kj::StringPtr name, Worker::Isolate* isolate); - - KJ_DISALLOW_COPY_AND_MOVE(InspectorServiceIsolateRegistrar); - - private: - void attach(const Server::InspectorService* anInspectorService) { - *inspectorService.lockExclusive() = anInspectorService; - } - - void detach() { - *inspectorService.lockExclusive() = nullptr; - } - - kj::MutexGuarded inspectorService; - friend class Server::InspectorService; -}; - -// Implements the interface for the devtools inspector protocol. -// -// The InspectorService is created when workerd serve is called using the -i option -// to define the inspector socket. -class Server::InspectorService final: public kj::HttpService, public kj::HttpServerErrorHandler { - public: - InspectorService(kj::Own isolateThreadExecutor, - kj::Timer& timer, - kj::HttpHeaderTable::Builder& headerTableBuilder, - InspectorServiceIsolateRegistrar& registrar) - : isolateThreadExecutor(kj::mv(isolateThreadExecutor)), - timer(timer), - headerTable(headerTableBuilder.getFutureTable()), - server(timer, headerTable, *this, kj::HttpServerSettings{.errorHandler = *this}), - registrar(registrar) { - registrar.attach(this); - } - - ~InspectorService() { - KJ_IF_SOME(r, registrar) { - r.detach(); - } - } - - void invalidateRegistrar() { - registrar = kj::none; - } - - kj::Promise handleApplicationError( - kj::Exception exception, kj::Maybe response) override { - if (exception.getType() == kj::Exception::Type::DISCONNECTED) { - // Don't send a response, just close connection. - co_return; - } - KJ_LOG(ERROR, kj::str("Uncaught exception: ", exception)); - KJ_IF_SOME(r, response) { - co_return co_await r.sendError(500, "Internal Server Error", headerTable); - } - } - - kj::Promise request(kj::HttpMethod method, - kj::StringPtr url, - const kj::HttpHeaders& headers, - kj::AsyncInputStream& requestBody, - kj::HttpService::Response& response) override { - // The inspector protocol starts with the debug client sending ordinary HTTP GET requests - // to /json/version and then to /json or /json/list. These must respond with valid JSON - // documents that list the details of what isolates are available for inspection. Each - // isolate must be listed separately. In the advertisement for each isolate is a URL - // and a unique ID. The client will use the URL and ID to open a WebSocket request to - // actually connect the debug session. - kj::HttpHeaders responseHeaders(headerTable); - if (headers.isWebSocket()) { - KJ_IF_SOME(pos, url.findLast('/')) { - auto id = url.slice(pos + 1); - - KJ_IF_SOME(isolate, isolates.find(id)) { - // If getting the strong ref doesn't work it means that the Worker::Isolate - // has already been cleaned up. We use a weak ref here in order to keep from - // having the Worker::Isolate itself having to know anything at all about the - // IsolateService and the registration process. So instead of having Isolate - // explicitly clean up after itself we lazily evaluate the weak ref and clean - // up when necessary. - KJ_IF_SOME(ref, isolate->tryAddStrongRef()) { - // When using --verbose, we'll output some logging to indicate when the - // inspector client is attached/detached. - KJ_LOG(INFO, kj::str("Inspector client attaching [", id, "]")); - auto webSocket = response.acceptWebSocket(responseHeaders); - kj::Duration timerOffset = 0 * kj::MILLISECONDS; - try { - co_return co_await ref->attachInspector( - isolateThreadExecutor->addRef(), timer, timerOffset, *webSocket); - } catch (...) { - auto exception = kj::getCaughtExceptionAsKj(); - if (exception.getType() == kj::Exception::Type::DISCONNECTED) { - // This likely just means that the inspector client was closed. - // Nothing to do here but move along. - KJ_LOG(INFO, "Inspector client detached"_kj); - co_return; - } else { - // If it's any other kind of error, propagate it! - kj::throwFatalException(kj::mv(exception)); - } - } - } else { - // If we can't get a strong ref to the isolate here, it's been cleaned - // up. The only thing we're going to do is clean up here and act like - // nothing happened. - isolates.erase(id); - } - } - - KJ_LOG(INFO, kj::str("Unknown worker session [", id, "]")); - co_return co_await response.sendError(404, "Unknown worker session", responseHeaders); - } - - // No / in url!? That's weird - co_return co_await response.sendError(400, "Invalid request", responseHeaders); - } - - // If the request is not a WebSocket request, it must be a GET to fetch details - // about the implementation. - if (method != kj::HttpMethod::GET) { - co_return co_await response.sendError(501, "Unsupported Operation", responseHeaders); - } - - if (url.endsWith("/json/version")) { - responseHeaders.set(kj::HttpHeaderId::CONTENT_TYPE, MimeType::JSON.toString()); - auto content = kj::str("{\"Browser\": \"workerd\", \"Protocol-Version\": \"1.3\" }"); - auto out = response.send(200, "OK", responseHeaders, content.size()); - co_return co_await out->write(content.asBytes()); - } else if (url.endsWith("/json") || url.endsWith("/json/list") || - url.endsWith("/json/list?for_tab")) { - responseHeaders.set(kj::HttpHeaderId::CONTENT_TYPE, MimeType::JSON.toString()); - - auto baseWsUrl = KJ_UNWRAP_OR(headers.get(kj::HttpHeaderId::HOST), - { co_return co_await response.sendError(400, "Bad Request", responseHeaders); }); - - kj::Vector entries(isolates.size()); - kj::Vector toRemove; - for (auto& entry: isolates) { - // While we don't actually use the strong ref here we still attempt to acquire it - // in order to determine if the isolate is actually still around. If the isolate - // has been destroyed the weak ref will be cleared. We do it this way to keep from - // having the Worker::Isolate know anything at all about the InspectorService. - // We'll lazily clean up whenever we detect that the ref has been invalidated. - // - // TODO(cleanup): If we ever enable reloading of isolates for live services, we may - // want to refactor this such that the WorkerService holds a handle to the registration - // as opposed to using this lazy cleanup mechanism. For now, however, this is - // sufficient. - KJ_IF_SOME(ref, entry.value->tryAddStrongRef()) { - (void)ref; // squash compiler warning about unused ref - kj::Vector fields(9); - fields.add(kj::str("\"id\":\"", entry.key, "\"")); - fields.add(kj::str("\"title\":\"workerd: worker ", entry.key, "\"")); - fields.add(kj::str("\"type\":\"node\"")); - fields.add(kj::str("\"description\":\"workerd worker\"")); - fields.add(kj::str("\"webSocketDebuggerUrl\":\"ws://", baseWsUrl, "/", entry.key, "\"")); - fields.add(kj::str( - "\"devtoolsFrontendUrl\":\"devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=", - baseWsUrl, "/\"")); - fields.add(kj::str( - "\"devtoolsFrontendUrlCompat\":\"devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=", - baseWsUrl, "/\"")); - fields.add(kj::str("\"faviconUrl\":\"https://workers.cloudflare.com/favicon.ico\"")); - fields.add(kj::str("\"url\":\"https://workers.dev\"")); - entries.add(kj::str('{', kj::strArray(fields, ","), '}')); - } else { - // If we're not able to get a reference to the isolate here, it's - // been cleaned up and we should remove it from the list. We do this - // after iterating to make sure we don't invalidate the iterator. - toRemove.add(kj::str(entry.key)); - } - } - // Clean up if necessary - for (auto& key: toRemove) { - isolates.erase(key); - } - - auto content = kj::str('[', kj::strArray(entries, ","), ']'); - - auto out = response.send(200, "OK", responseHeaders, content.size()); - co_return co_await out->write(content.asBytes()).attach(kj::mv(content), kj::mv(out)); - } - - co_return co_await response.sendError(500, "Not yet implemented", responseHeaders); - } - - kj::Promise listen(kj::Own listener) { - // Note that we intentionally do not make inspector connections be part of the usual drain() - // procedure. Inspector connections are always long-lived WebSockets, and we do not want the - // existence of such a connection to hold the server open. We do, however, want the connection - // to stay open until all other requests are drained, for debugging purposes. - // - // Thus: - // * We let connection loop tasks live on `HttpServer`'s own `TaskSet`, rather than our - // server's main `TaskSet` which we wait to become empty on drain. - // * We do not add this `HttpServer` to the server's `httpServers` list, so it will not receive - // drain() requests. (However, our caller does cancel listening on the server port as soon - // as we begin draining, since we may want new connections to go to a new instance of the - // server.) - co_return co_await server.listenHttp(*listener); - } - - void registerIsolate(kj::StringPtr name, Worker::Isolate* isolate) { - isolates.insert(kj::str(name), isolate->getWeakRef()); - } - - private: - kj::Own isolateThreadExecutor; - kj::Timer& timer; - kj::HttpHeaderTable& headerTable; - kj::HashMap> isolates; - kj::HttpServer server; - kj::Maybe registrar; -}; - -Server::InspectorServiceIsolateRegistrar::~InspectorServiceIsolateRegistrar() noexcept(true) { - auto lockedInspectorService = this->inspectorService.lockExclusive(); - if (lockedInspectorService != nullptr) { - auto is = const_cast(*lockedInspectorService); - is->invalidateRegistrar(); - } -} - -void Server::InspectorServiceIsolateRegistrar::registerIsolate( - kj::StringPtr name, Worker::Isolate* isolate) { - auto lockedInspectorService = this->inspectorService.lockExclusive(); - if (lockedInspectorService != nullptr) { - auto is = const_cast(*lockedInspectorService); - is->registerIsolate(name, isolate); - } -} - -// ======================================================================================= -namespace { -class RequestObserverWithTracer final: public RequestObserver, public WorkerInterface { - public: - RequestObserverWithTracer(kj::Maybe> tracer, kj::TaskSet& waitUntilTasks) - : tracer(kj::mv(tracer)) {} - - ~RequestObserverWithTracer() noexcept(false) { - KJ_IF_SOME(t, tracer) { - // for a more precise end time, set the end timestamp now, if available - KJ_IF_SOME(ioContext, IoContext::tryCurrent()) { - auto time = ioContext.now(); - t->recordTimestamp(time); - } - t->setOutcome( - outcome, 0 * kj::MILLISECONDS /* cpu time */, 0 * kj::MILLISECONDS /* wall time */); - } - } - - WorkerInterface& wrapWorkerInterface(WorkerInterface& worker) override { - if (tracer != kj::none) { - inner = worker; - return *this; - } - return worker; - } - - void reportFailure( - const kj::Exception& exception, FailureSource source = FailureSource::OTHER) override { - if (outcome == EventOutcome::OK) { - outcome = RequestObserver::outcomeFromException(exception, source); - } - } - - // WorkerInterface - kj::Promise request(kj::HttpMethod method, - kj::StringPtr url, - const kj::HttpHeaders& headers, - kj::AsyncInputStream& requestBody, - kj::HttpService::Response& response) override { - try { - co_await KJ_ASSERT_NONNULL(inner).request(method, url, headers, requestBody, response); - } catch (...) { - auto exception = kj::getCaughtExceptionAsKj(); - reportFailure(exception); - kj::throwFatalException(kj::mv(exception)); - } - } - - kj::Promise connect(kj::StringPtr host, - const kj::HttpHeaders& headers, - kj::AsyncIoStream& connection, - ConnectResponse& response, - kj::HttpConnectSettings settings) override { - try { - co_return co_await KJ_ASSERT_NONNULL(inner).connect( - host, headers, connection, response, settings); - } catch (...) { - auto exception = kj::getCaughtExceptionAsKj(); - reportFailure(exception); - kj::throwFatalException(kj::mv(exception)); - } - } - - kj::Promise prewarm(kj::StringPtr url) override { - try { - co_return co_await KJ_ASSERT_NONNULL(inner).prewarm(url); - } catch (...) { - auto exception = kj::getCaughtExceptionAsKj(); - reportFailure(exception); - kj::throwFatalException(kj::mv(exception)); - } - } - - kj::Promise runScheduled(kj::Date scheduledTime, kj::StringPtr cron) override { - try { - WorkerInterface::ScheduledResult result = - co_await KJ_ASSERT_NONNULL(inner).runScheduled(scheduledTime, cron); - if (outcome == EventOutcome::OK) { - // Haven't set an outcome yet from `reportFailure()` or some other pathway. - outcome = result.outcome; - } - co_return result; - } catch (...) { - auto exception = kj::getCaughtExceptionAsKj(); - reportFailure(exception); - kj::throwFatalException(kj::mv(exception)); - } - } - - kj::Promise runAlarm(kj::Date scheduledTime, uint32_t retryCount) override { - try { - WorkerInterface::AlarmResult result = - co_await KJ_ASSERT_NONNULL(inner).runAlarm(scheduledTime, retryCount); - if (outcome == EventOutcome::OK) { - // Haven't set an outcome yet from `reportFailure()` or some other pathway. - outcome = result.outcome; - } - co_return result; - } catch (...) { - auto exception = kj::getCaughtExceptionAsKj(); - reportFailure(exception); - kj::throwFatalException(kj::mv(exception)); - } - } - - kj::Promise test() override { - try { - co_return co_await KJ_ASSERT_NONNULL(inner).test(); - } catch (...) { - auto exception = kj::getCaughtExceptionAsKj(); - reportFailure(exception); - kj::throwFatalException(kj::mv(exception)); - } - } - - kj::Promise customEvent(kj::Own event) override { - try { - WorkerInterface::CustomEvent::Result result = - co_await KJ_ASSERT_NONNULL(inner).customEvent(kj::mv(event)); - if (outcome == EventOutcome::OK) { - // Haven't set an outcome yet from `reportFailure()` or some other pathway. - outcome = result.outcome; - } - co_return result; - } catch (...) { - auto exception = kj::getCaughtExceptionAsKj(); - reportFailure(exception); - kj::throwFatalException(kj::mv(exception)); - } - } - - kj::Promise> abandonAlarm(kj::Date scheduledTime) override { - co_return co_await KJ_ASSERT_NONNULL(inner).abandonAlarm(scheduledTime); - } - - private: - kj::Maybe> tracer; - kj::Maybe inner; - EventOutcome outcome = EventOutcome::OK; -}; - -class SequentialSpanSubmitter final: public SpanSubmitter { - public: - SequentialSpanSubmitter(kj::Own weakTracer, kj::EntropySource& entropySource) - : weakTracer(kj::mv(weakTracer)), - entropySource(entropySource) {} - void submitSpanClose( - tracing::SpanId spanId, kj::Date startTime, kj::Date endTime, Span::TagMap&& tags) override { - weakTracer->runIfAlive([&](BaseTracer& tracer) { - tracing::SpanEndData spanEnd(spanId, endTime, kj::mv(tags)); - if (isPredictableModeForTest()) { - startTime = spanEnd.endTime = kj::UNIX_EPOCH; - } - - tracer.addSpanClose(kj::mv(spanEnd), startTime); - }); - } - - void submitSpanUpdate(tracing::SpanId spanId, tracing::SpanUpdate&& update) override { - weakTracer->runIfAlive( - [&](BaseTracer& tracer) { tracer.addSpanUpdate(spanId, kj::mv(update)); }); - } - - void submitSpanException(tracing::SpanId spanId, - kj::Date timestamp, - kj::Maybe code, - kj::String name, - kj::String message, - kj::Maybe stack) override { - weakTracer->runIfAlive([&](BaseTracer& tracer) { - if (isPredictableModeForTest()) { - timestamp = kj::UNIX_EPOCH; - } - tracer.addSpanException( - spanId, timestamp, kj::mv(code), kj::mv(name), kj::mv(message), kj::mv(stack)); - }); - } - - bool submitSpanOpen(tracing::SpanId spanId, - tracing::SpanId parentSpanId, - kj::ConstString operationName, - kj::Date startTime) override { - bool submitted = false; - weakTracer->runIfAlive([&](BaseTracer& tracer) { - if (isPredictableModeForTest()) { - startTime = kj::UNIX_EPOCH; - } - tracer.addSpanOpen(spanId, parentSpanId, kj::mv(operationName), startTime); - submitted = true; - }); - return submitted; - } - - tracing::SpanId makeSpanId() override { - if (isPredictableModeForTest()) { - return tracing::SpanId(nextSpanId++); - } - return tracing::SpanId::fromEntropy(entropySource); - } - KJ_DISALLOW_COPY_AND_MOVE(SequentialSpanSubmitter); - - private: - uint64_t nextSpanId = 1; - kj::Own weakTracer; - kj::EntropySource& entropySource; -}; - -// IsolateLimitEnforcer that enforces no limits. -class NullIsolateLimitEnforcer final: public IsolateLimitEnforcer { - public: - v8::Isolate::CreateParams getCreateParams() override { - return {}; - } - - void customizeIsolate(v8::Isolate* isolate) override {} - - ActorCacheSharedLruOptions getActorCacheLruOptions() override { - // TODO(someday): Make this configurable? - return {.softLimit = 16 * (1ull << 20), // 16 MiB - .hardLimit = 128 * (1ull << 20), // 128 MiB - .staleTimeout = 30 * kj::SECONDS, - .dirtyListByteLimit = 8 * (1ull << 20), // 8 MiB - .maxKeysPerRpc = 128, - - // For now, we use `neverFlush` to implement in-memory-only actors. - // See WorkerService::getActor(). - .neverFlush = true}; - } - - kj::Own enterStartupJs( - jsg::Lock& lock, kj::OneOf&) const override { - return {}; - } - - kj::Own enterStartupPython( - jsg::Lock& lock, kj::OneOf&) const override { - return {}; - } - - kj::Own enterDynamicImportJs( - jsg::Lock& lock, kj::OneOf&) const override { - return {}; - } - - kj::Own enterLoggingJs( - jsg::Lock& lock, kj::OneOf&) const override { - return {}; - } - - kj::Own enterInspectorJs( - jsg::Lock& loc, kj::OneOf&) const override { - return {}; - } - - void completedRequest(kj::StringPtr id) const override {} - - bool exitJs(jsg::Lock& lock) const override { - return false; - } - - void reportMetrics(IsolateObserver& isolateMetrics) const override {} - - kj::Maybe checkPbkdfIterations(jsg::Lock& lock, size_t iterations) const override { - // No limit on the number of iterations in workerd - return kj::none; - } - - bool hasExcessivelyExceededHeapLimit() const override { - return false; - } - - const TrackedWasmInstanceList& getTrackedWasmInstances() const override { - return trackedWasmInstances; - } - - private: - TrackedWasmInstanceList trackedWasmInstances; -}; - -} // namespace - -// Shared ErrorReporter base implemnetation. The logic to collect entrypoint information is the -// same regardless of where the code came from. -struct Server::ErrorReporter: public Worker::ValidationErrorReporter { - // The `HashSet`s are the set of exported handlers, like `fetch`, `test`, etc. - kj::HashMap> namedEntrypoints; - kj::Maybe> defaultEntrypoint; - kj::HashSet actorClasses; - kj::HashSet workflowClasses; - - void addEntrypoint(kj::Maybe exportName, kj::Array methods) override { - kj::HashSet set; - for (auto& method: methods) { - set.insert(kj::mv(method)); - } - KJ_IF_SOME(e, exportName) { - namedEntrypoints.insert(kj::str(e), kj::mv(set)); - } else { - defaultEntrypoint = kj::mv(set); - } - } - - void addActorClass(kj::StringPtr exportName) override { - actorClasses.insert(kj::str(exportName)); - } - - void addWorkflowClass(kj::StringPtr exportName, kj::Array methods) override { - // At runtime, we need to add it into the normal namedEntrypoints for Workflows to appear - // in `WorkerService`. This is a different method compared to `addEntrypoint` because we need to - // check for `WorkflowEntrypoint` inheritance at validation time. - kj::HashSet set; - for (auto& method: methods) { - set.insert(kj::mv(method)); - } - namedEntrypoints.insert(kj::str(exportName), kj::mv(set)); - workflowClasses.insert(kj::str(exportName)); - } -}; - -// Implementation of ErrorReporter specifically for reporting errors in the top-level workerd -// config. -struct Server::ConfigErrorReporter final: public ErrorReporter { - ConfigErrorReporter(Server& server, kj::StringPtr name): server(server), name(name) {} - - Server& server; - kj::StringPtr name; - - void addError(kj::String error) override { - server.handleReportConfigError(kj::str("service ", name, ": ", error)); - } - - void addWarning(kj::String warning) override { - server.handleReportConfigWarning(kj::str("service ", name, ": ", warning)); - } -}; - -// Implementation of ErrorReporter for dynamically-loaded Workers. We'll collect the errors and -// report them in an exception at the end. -struct Server::DynamicErrorReporter final: public ErrorReporter { - kj::Vector errors; - - void addError(kj::String error) override { - errors.add(kj::mv(error)); - } - - void throwIfErrors() { - if (!errors.empty()) { - JSG_FAIL_REQUIRE(Error, "Failed to start Worker:\n", kj::strArray(errors, "\n")); - } - } -}; - -class Server::WorkerService final: public Service, - private kj::TaskSet::ErrorHandler, - public IoChannelFactory, - private TimerChannel, - private LimitEnforcer { - public: - // I/O channels, delivered when link() is called. - struct LinkedIoChannels { - kj::Array> subrequest; - kj::Array> actor; // null = configuration error - kj::Array> actorClass; - kj::Array> rpc; - kj::Maybe> cache; - kj::Maybe actorStorage; - kj::Array> tails; - kj::Array> streamingTails; - kj::Array> workerLoaders; - kj::Maybe workerdDebugPortNetwork; - kj::Maybe workerdDebugPortServer; - }; - using LinkCallback = - kj::Function; - using AbortActorsCallback = kj::Function reason)>; - using DeleteActorsCallback = kj::Function reason)>; - - WorkerService(ChannelTokenHandler& channelTokenHandler, - kj::Maybe serviceName, - ThreadContext& threadContext, - const kj::MonotonicClock& monotonicClock, - kj::Own worker, - kj::Maybe> defaultEntrypointHandlers, - kj::HashMap> namedEntrypoints, - kj::HashSet actorClassEntrypointsParam, - kj::HashSet workflowClassEntrypointsParam, - LinkCallback linkCallback, - AbortActorsCallback abortActorsCallback, - DeleteActorsCallback deleteActorsCallback, - kj::Maybe dockerPathParam, - kj::Maybe containerEgressInterceptorImageParam, - ActorStorageIsLocalDisk actorStorageIsLocalDisk, - bool isDynamic, - kj::Maybe> abortIsolateCallback = kj::none, - kj::Maybe accessBlobHeaderNameParam = kj::none) - : channelTokenHandler(channelTokenHandler), - serviceName(serviceName), - threadContext(threadContext), - monotonicClock(monotonicClock), - ioChannels(kj::mv(linkCallback)), - worker(kj::mv(worker)), - defaultEntrypointHandlers(kj::mv(defaultEntrypointHandlers)), - namedEntrypoints(kj::mv(namedEntrypoints)), - actorClassEntrypoints(kj::mv(actorClassEntrypointsParam)), - workflowClassEntrypoints(kj::mv(workflowClassEntrypointsParam)), - waitUntilTasks(*this), - abortActorsCallback(kj::mv(abortActorsCallback)), - deleteActorsCallback(kj::mv(deleteActorsCallback)), - dockerPath(kj::mv(dockerPathParam)), - containerEgressInterceptorImage(kj::mv(containerEgressInterceptorImageParam)), - actorStorageIsLocalDisk(actorStorageIsLocalDisk), - isDynamic(isDynamic), - abortIsolateCallback(kj::mv(abortIsolateCallback)), - accessBlobHeaderName(kj::mv(accessBlobHeaderNameParam)) {} - - // Call immediately after the constructor to set up `actorNamespaces`. This can't happen during - // the constructor itself since it sets up cyclic references, which will throw an exception if - // done during the constructor. - void initActorNamespaces(const kj::HashMap& actorClasses, - kj::HashMap& actorNamespacesByUniqueKey, - kj::Network& network) { - actorNamespaces.reserve(actorClasses.size()); - for (auto& entry: actorClasses) { - // Workflow-backing namespaces are set up separately in `initWorkflowActorNamespace()`, which - // runs after all services exist: they bind to an external engine's actor class and take their - // storage from another Worker. Skip them here so we don't create a normal namespace for them. - KJ_IF_SOME(durable, entry.value.tryGet()) { - if (durable.isWorkflow) continue; - } - - if (!actorClassEntrypoints.contains(entry.key)) { - KJ_LOG(WARNING, - kj::str("A DurableObjectNamespace in the config referenced the class \"", entry.key, - "\", but no such Durable Object class is exported from the worker. Please make " - "sure the class name matches, it is exported, and the class extends " - "'DurableObject'. Attempts to call to this Durable Object class will fail at " - "runtime, but historically this was not a startup-time error. Future versions of " - "workerd may make this a startup-time error.")); - } - - auto actorClass = kj::rc(*this, entry.key, Frankenvalue()); - auto ns = kj::heap(kj::mv(actorClass), entry.value, - kj::systemPreciseCalendarClock(), threadContext.getUnsafeTimer(), - threadContext.getByteStreamFactory(), channelTokenHandler, network, dockerPath, - containerEgressInterceptorImage, waitUntilTasks, selfTokensArePersistent()); - KJ_IF_SOME(d, entry.value.tryGet()) { - actorNamespacesByUniqueKey.insert(d.uniqueKey, ns.get()); - } - actorNamespaces.insert(entry.key, kj::mv(ns)); - } - } - - // Creates the synthetic actor namespace that backs one configured Workflow. Unlike a normal - // namespace, this one is owned by the application Worker but instantiates actors from an external - // engine's `actorClass`, and its on-disk storage comes from `storageService` (the Workflow's - // `bindingService` Worker) rather than from this Worker. The actual storage link happens later in - // `linkActorNamespaces()`; here we only record which service provides it. `selfTokensArePersistent` - // is taken from the engine Worker so restore-token persistence follows the engine's compat flags, - // not the application's. - void initWorkflowActorNamespace(const ActorConfig& config, - kj::Rc actorClass, - WorkerService& storageService, - Persistent selfTokensArePersistent, - kj::HashMap& actorNamespacesByUniqueKey, - kj::Network& network) { - auto ns = kj::heap(kj::mv(actorClass), config, kj::systemPreciseCalendarClock(), - threadContext.getUnsafeTimer(), threadContext.getByteStreamFactory(), channelTokenHandler, - network, dockerPath, containerEgressInterceptorImage, waitUntilTasks, - selfTokensArePersistent); - auto& durable = KJ_ASSERT_NONNULL(config.tryGet()); - actorNamespacesByUniqueKey.insert(durable.uniqueKey, ns.get()); - workflowActorStorageSources.insert(kj::str(durable.uniqueKey), storageService.addRef()); - actorNamespaces.insert(durable.uniqueKey, kj::mv(ns)); - } - - void requireAllowsTransfer() override { - if (isDynamic) throwDynamicEntrypointTransferError(); - } - - kj::OneOf, kj::Promise>> getTokenMaybeSync( - IoChannelFactory::ChannelTokenUsage usage) override { - requireAllowsTransfer(); - - // encodeSubrequestChannelToken wants a reference to the props. It needs this reference to - // be non-const because it might refcount things. But if it's an empty object then there's - // nothing to refcount. So we can just declare this statically... - static Frankenvalue EMPTY_PROPS; - - // If requireAllowsTransfer() passed, then we are not dynamic so should have a service name. - // We use Persistent::NO here because simple service bindings are not persistent -- only - // ctx.exports loopback bindings are, and those are always implemetned by EntrypointService. - return channelTokenHandler.encodeSubrequestChannelToken( - usage, KJ_ASSERT_NONNULL(serviceName), kj::none, EMPTY_PROPS, Persistent::NO); - } - - kj::Maybe> getEntrypoint( - kj::Maybe name, Frankenvalue props, Persistent persistent = Persistent::NO) { - const kj::HashSet* handlers; - KJ_IF_SOME(n, name) { - KJ_IF_SOME(entry, namedEntrypoints.findEntry(n)) { - name = entry.key; // replace with more-permanent string - handlers = &entry.value; - } else KJ_IF_SOME(className, actorClassEntrypoints.find(n)) { - // TODO(soon): Restore this warning once miniflare no longer generates config that causes - // it to log spuriously. - // - // KJ_LOG(WARNING, - // kj::str("A ServiceDesignator in the config referenced the entrypoint \"", n, - // "\", but this class does not extend 'WorkerEntrypoint'. Attempts to call this " - // "entrypoint will fail at runtime, but historically this was not a startup-time " - // "error. Future versions of workerd may make this a startup-time error.")); - - static const kj::HashSet EMPTY_HANDLERS; - name = className; // replace with more-permanent string - handlers = &EMPTY_HANDLERS; - } else { - return kj::none; - } - } else { - KJ_IF_SOME(d, defaultEntrypointHandlers) { - handlers = &d; - } else { - // It would appear that there is no default export, therefore this refers to an entrypoint - // that doesn't exist! However, this was historically allowed. For backwards-compatibility, - // we preserve this behavior, by returning a reference to the WorkerService itself, whose - // startRequest() will fail. - // - // What will happen if you invoke this entrypoint? Not what you think. Check out the - // test case in server-test.c++ entitled "referencing non-extant default entrypoint is not - // an error" for the sordid details. - return addRefToThis(); - } - } - return kj::rc(*this, name, kj::mv(props), *handlers, persistent); - } - - // Like getEntrypoint() but used specifically to get the entrypoint for use in ctx.exports, - // where it can be used raw (props are empty), or can be specialized with props. - kj::Rc getLoopbackEntrypoint(kj::Maybe name) { - const kj::HashSet* handlers; - KJ_IF_SOME(n, name) { - KJ_IF_SOME(entry, namedEntrypoints.findEntry(n)) { - name = entry.key; // replace with more-permanent string - handlers = &entry.value; - } else { - KJ_FAIL_REQUIRE("getLoopbackEntrypoint() called for entrypoint that doesn't exist"); - } - } else { - KJ_IF_SOME(d, defaultEntrypointHandlers) { - handlers = &d; - } else { - KJ_FAIL_REQUIRE("getLoopbackEntrypoint() called for entrypoint that doesn't exist"); - } - } - // This is a ctx.exports loopback channel, but note that this represent the "template" for - // the channel, which is not in itself allowed to be sent over RPC, much less persisted. - // The application must specialize it by calling `ctx.exports.Whatever({props})` to get a - // transferrable and persistable stub (which will call the entrypoint's `forProps()` method). - return kj::rc(*this, name, kj::none, *handlers); - } - - kj::Maybe> getActorClass( - kj::Maybe name, Frankenvalue props, Persistent persistent = Persistent::NO) { - KJ_IF_SOME(className, actorClassEntrypoints.find(KJ_UNWRAP_OR(name, return kj::none))) { - return kj::rc(*this, className, kj::mv(props), persistent); - } else { - return kj::none; - } - } - - kj::Rc getLoopbackActorClass(kj::StringPtr name) { - // Look up a more permanent class name string. (Also validates this is actually an export.) - kj::StringPtr className = KJ_REQUIRE_NONNULL(actorClassEntrypoints.find(name), - "getLoopbackActorClass() called for actor class that doesn't exist"); - - return kj::rc(*this, className, kj::none); - } - - bool hasDefaultEntrypoint() { - return defaultEntrypointHandlers != kj::none; - } - - // Whether this Worker exports a plain stateless entrypoint (a `WorkerEntrypoint`, or the default - // handler when `name` is null). Workflow classes are stored in `namedEntrypoints` for runtime - // dispatch but are deliberately excluded here: a `WorkflowEntrypoint` is not a valid target for - // things that require a stateless entrypoint, so, e.g., one Workflow cannot act as another - // Workflow's `bindingService`. - bool hasStatelessEntrypoint(kj::Maybe name) { - KJ_IF_SOME(n, name) { - return namedEntrypoints.find(n) != kj::none && !workflowClassEntrypoints.contains(n); - } - return hasDefaultEntrypoint(); - } - - void setAccessBindingServiceChannel(kj::uint channel) { - accessBindingServiceChannel = channel; - } - - kj::Array getEntrypointNames() { - return KJ_MAP(e, namedEntrypoints) -> kj::StringPtr { return e.key; }; - } - - kj::Array getActorClassNames() { - return KJ_MAP(name, actorClassEntrypoints) -> kj::StringPtr { return name; }; - } - - bool hasWorkflowClass(kj::StringPtr name) { - return workflowClassEntrypoints.contains(name); - } - - bool hasActorClass(kj::StringPtr name) { - return actorClassEntrypoints.contains(name); - } - - // Whether this Worker's Durable Object storage is backed by local disk. Used to validate that a - // Workflow's `bindingService` Worker can provide on-disk storage for the Workflow's namespace. - bool hasLocalDiskActorStorage() { - return actorStorageIsLocalDisk.toBool(); - } - - // The local-disk directory backing this Worker's actor storage, or null if none is configured. - // Consumed during `linkActorNamespaces()` to point a Workflow namespace at its bindingService - // Worker's storage. Requires `link()` to have already run. - kj::Maybe getActorStorage() { - return KJ_REQUIRE_NONNULL(ioChannels.tryGet(), "link() has not been called") - .actorStorage; - } - - void link(Worker::ValidationErrorReporter& errorReporter) override { - LinkCallback callback = - kj::mv(KJ_REQUIRE_NONNULL(ioChannels.tryGet(), "already called link()")); - auto linked = callback(*this, errorReporter); - - ioChannels = kj::mv(linked); - } - - void linkActorNamespaces(Worker::ValidationErrorReporter& errorReporter) override { - auto& linked = - KJ_REQUIRE_NONNULL(ioChannels.tryGet(), "link() has not been called"); - for (auto& ns: actorNamespaces) { - // A Workflow namespace stores its data in its bindingService Worker's storage rather than - // this Worker's. We deferred resolving that storage to this second pass precisely because the - // other Worker's `link()` must have run first for `getActorStorage()` to be available. - KJ_IF_SOME(storageService, workflowActorStorageSources.find(ns.key)) { - auto storage = storageService->getActorStorage(); - if (storage == kj::none) { - errorReporter.addError(kj::str("Workflow ActorNamespace \"", ns.key, - "\" could not resolve its bindingService's durableObjectStorage.localDisk.")); - } - ns.value->link(storage); - } else { - ns.value->link(linked.actorStorage); - } - } - } - - void unlink() override { - // Need to remove all waited until tasks before destroying `ioChannels` - waitUntilTasks.clear(); - - // Need to tear down all actors before tearing down `ioChannels.actorStorage`. - actorNamespaces.clear(); - workflowActorStorageSources.clear(); - - // OK, now we can unlink. - ioChannels = {}; - } - - kj::Maybe getActorNamespace(kj::StringPtr name) { - KJ_IF_SOME(a, actorNamespaces.find(name)) { - return *a; - } else { - return kj::none; - } - } - - kj::HashMap>& getActorNamespaces() { - return actorNamespaces; - } - - void beginContainerCleanup() { - for (auto& [className, ns]: actorNamespaces) { - ns->beginContainerCleanup(); - } - } - - kj::Own startRequest(IoChannelFactory::SubrequestMetadata metadata) override { - // Same logic as in EntrypointService::startRequest(). - if (!isDynamic) { - metadata.restoredSelfTokenFactory = - kj::rc(addRefToThis(), kj::none); - } - - return startRequest(kj::mv(metadata), kj::none, {}, kj::none, false); - } - - // Get whether self-tokens should be persistent, which is the case if the - // `allow_irrevocable_stub_storage` compat flag is set. - Persistent selfTokensArePersistent() { - return Persistent( - worker->getIsolate().getApi().getFeatureFlags().getAllowIrrevocableStubStorage()); - } - - bool hasHandler(kj::StringPtr handlerName) override { - KJ_IF_SOME(h, defaultEntrypointHandlers) { - return h.contains(handlerName); - } else { - return false; - } - } - - // AccessInfo constructed from a per-request JSON blob extracted from the accessBlobHeader HTTP - // header. The blob matches the production Access struct: - // { "app_aud": "...", "jwt_claims": {...} } - // - // Holds the subrequest channel index for the access binding worker (if configured). Per-request - // props (aud, jwtClaims) are injected by startSubrequest() when the channel is dispatched, - // mirroring the production channel-token mechanism. - class BlobAccessInfo final: public AccessInfo { - public: - BlobAccessInfo(kj::String audience, - kj::Maybe jwtClaimsJson, - kj::Maybe identityServiceChannel) - : audience(kj::mv(audience)), - jwtClaimsJson(kj::mv(jwtClaimsJson)), - identityServiceChannel(identityServiceChannel) {} - - kj::StringPtr getAudience() override { - return audience; - } - - kj::Maybe getIdentityServiceChannel() override { - return identityServiceChannel; - } - - // The per-request JWT claims JSON, used by startSubrequest() to build the props - // Frankenvalue for the access binding worker. - kj::Maybe getJwtClaimsJson() { - return jwtClaimsJson.map([](kj::String& s) -> kj::StringPtr { return s; }); - } - - private: - kj::String audience; - kj::Maybe jwtClaimsJson; - kj::Maybe identityServiceChannel; - }; - - // Wraps a lazily-created WorkerInterface to extract the Cloudflare Access blob header from - // incoming HTTP requests before the real entrypoint is constructed. This allows the parsed - // AccessInfo to be passed into newWorkerEntrypoint() at construction time (where it flows - // into IncomingRequest), rather than requiring post-hoc mutation. - // - // Only the `request()` path inspects headers; all other WorkerInterface methods (connect, - // prewarm, scheduled, alarm, test, customEvent) create the inner entrypoint without access - // info. - class AccessHeaderExtractor final: public WorkerInterface { - public: - AccessHeaderExtractor(kj::String accessBlobHeaderName, - kj::Maybe identityServiceChannel, - kj::Function(kj::Maybe>)> factory) - : accessBlobHeaderName(kj::mv(accessBlobHeaderName)), - identityServiceChannel(identityServiceChannel), - factory(kj::mv(factory)) {} - - kj::Promise request(kj::HttpMethod method, - kj::StringPtr url, - const kj::HttpHeaders& headers, - kj::AsyncInputStream& requestBody, - kj::HttpService::Response& response) override { - kj::Maybe blobJson; - headers.forEach([&](kj::StringPtr name, kj::StringPtr value) { - if (workerd::strcaseeq(name, accessBlobHeaderName)) { - blobJson = kj::str(value); - } - }); - - kj::Maybe> accessInfo; - KJ_IF_SOME(fields, parseAccessBlob(kj::mv(blobJson))) { - accessInfo = kj::refcounted( - kj::mv(fields.appAud), kj::mv(fields.jwtClaimsJson), identityServiceChannel); - } - - KJ_ASSERT(cachedInner == kj::none, "request() called more than once"); - cachedInner = factory(kj::mv(accessInfo)); - co_await KJ_ASSERT_NONNULL(cachedInner)->request(method, url, headers, requestBody, response); - } - - kj::Promise connect(kj::StringPtr host, - const kj::HttpHeaders& headers, - kj::AsyncIoStream& connection, - ConnectResponse& response, - kj::HttpConnectSettings settings) override { - auto inner = getOrCreateInner(); - co_await inner->connect(host, headers, connection, response, kj::mv(settings)); - } - - kj::Promise prewarm(kj::StringPtr url) override { - co_await getOrCreateInner()->prewarm(url); - } - - kj::Promise runScheduled(kj::Date scheduledTime, kj::StringPtr cron) override { - co_return co_await getOrCreateInner()->runScheduled(scheduledTime, cron); - } - - kj::Promise runAlarm(kj::Date scheduledTime, uint32_t retryCount) override { - co_return co_await getOrCreateInner()->runAlarm(scheduledTime, retryCount); - } - - kj::Promise> abandonAlarm(kj::Date scheduledTime) override { - co_return co_await getOrCreateInner()->abandonAlarm(scheduledTime); - } - - kj::Promise test() override { - co_return co_await getOrCreateInner()->test(); - } - - kj::Promise customEvent(kj::Own event) override { - co_return co_await getOrCreateInner()->customEvent(kj::mv(event)); - } - - private: - kj::String accessBlobHeaderName; - kj::Maybe identityServiceChannel; - kj::Function(kj::Maybe>)> factory; - kj::Maybe> cachedInner; - - WorkerInterface* getOrCreateInner() { - KJ_IF_SOME(w, cachedInner) { - return w.get(); - } - cachedInner = factory(kj::none); - return KJ_ASSERT_NONNULL(cachedInner).get(); - } - }; - - kj::Own startRequest(IoChannelFactory::SubrequestMetadata metadata, - kj::Maybe entrypointName, - Frankenvalue props, - kj::Maybe> actor = kj::none, - bool isTracer = false) { - TRACE_EVENT("workerd", "Server::WorkerService::startRequest()"); - - KJ_IF_SOME(headerName, accessBlobHeaderName) { - // This worker has an accessBlobHeader configured. Defer entrypoint creation until - // request() is called, so the access blob header can be extracted from the HTTP headers - // and parsed into an AccessInfo before the entrypoint (and its IncomingRequest) is - // constructed. - return kj::heap(kj::str(headerName), accessBindingServiceChannel, - [this, metadata = kj::mv(metadata), entrypointName, props = kj::mv(props), - actor = kj::mv(actor), isTracer]( - kj::Maybe> accessInfo) mutable -> kj::Own { - return createEntrypoint(kj::mv(metadata), entrypointName, kj::mv(props), kj::mv(actor), - isTracer, kj::mv(accessInfo)); - }); - } - - return createEntrypoint( - kj::mv(metadata), entrypointName, kj::mv(props), kj::mv(actor), isTracer, kj::none); - } - - kj::Own createEntrypoint(IoChannelFactory::SubrequestMetadata metadata, - kj::Maybe entrypointName, - Frankenvalue props, - kj::Maybe> actor, - bool isTracer, - kj::Maybe> accessInfo) { - auto& channels = KJ_ASSERT_NONNULL(ioChannels.tryGet()); - - kj::Vector> bufferedTailWorkers(channels.tails.size()); - kj::Vector> streamingTailWorkers(channels.streamingTails.size()); - auto addWorkerIfNotRecursiveTracer = [this, isTracer]( - kj::Vector>& workers, - IoChannelFactory::SubrequestChannel& channel) { - // Caution here... if the tail worker ends up having a circular dependency - // on the worker we'll end up with an infinite loop trying to initialize. - // We can test this directly but it's more difficult to test indirect - // loops (dependency of dependency, etc). Here we're just going to keep - // it simple and just check the direct dependency. - // If service refers to an EntrypointService, we need to compare with the underlying - // WorkerService to match this. - auto& service = KJ_UNWRAP_OR(kj::tryDowncast(channel), { - // Not a Service, probably not self-referential. - workers.add(channel.startRequest({})); - return; - }); - - if (service.service() == this) { - if (!isTracer) { - // This is a self-reference. Create a request with isTracer=true. - KJ_IF_SOME(s, kj::tryDowncast(service)) { - workers.add(s.startRequest({}, kj::none, {}, kj::none, true)); - } else KJ_IF_SOME(s, kj::tryDowncast(service)) { - workers.add(s.startRequest({}, true)); - } else { - KJ_FAIL_ASSERT("Unexpected service type in recursive tail worker declaration"); - } - } else { - // Intentionally left empty to prevent infinite recursion with tail workers tailing - // themselves - } - } else { - workers.add(service.startRequest({})); - } - }; - - // Do not add tracers for worker interfaces with the "test" entrypoint – we generally do not - // need to trace the test event, although this is useful to test that span tracing works, so - // we are not implementing a (more complex) mechanism to disable tracing for all test() events - // here. - if (entrypointName.orDefault("") != "test"_kj) { - for (auto& service: channels.tails) { - addWorkerIfNotRecursiveTracer(bufferedTailWorkers, *service); - } - for (auto& service: channels.streamingTails) { - addWorkerIfNotRecursiveTracer(streamingTailWorkers, *service); - } - } - - kj::Maybe> workerTracer = kj::none; - - if (!bufferedTailWorkers.empty() || !streamingTailWorkers.empty()) { - // Setting up buffered tail workers support, but only if we actually have tail workers - // configured. - auto executionModel = - actor == kj::none ? ExecutionModel::STATELESS : ExecutionModel::DURABLE_OBJECT; - kj::Maybe durableObjectId = kj::none; - KJ_IF_SOME(a, actor) { - KJ_SWITCH_ONEOF(a->getId()) { - KJ_CASE_ONEOF(id, kj::Own) { - durableObjectId = id->toString(); - } - KJ_CASE_ONEOF(id, kj::String) { - durableObjectId = kj::str(id); - } - } - } - auto tailStreamWriter = tracing::initializeTailStreamWriter( - streamingTailWorkers.releaseAsArray(), waitUntilTasks); - auto trace = kj::refcounted(kj::none /* stableId */, kj::none /* scriptName */, - kj::none /* scriptVersion */, kj::none /* dispatchNamespace */, kj::none /* scriptId */, - nullptr /* scriptTags */, entrypointName.clone(), executionModel, - kj::mv(durableObjectId)); - kj::Rc tracer = kj::rc( - kj::none, kj::mv(trace), PipelineLogLevel::FULL, kj::none, kj::mv(tailStreamWriter)); - - // When the tracer is complete, deliver traces to any buffered tail workers. We end up - // creating two references to the WorkerTracer, one held by the observer and one that will be - // passed to the IoContext. This ensures that the tracer lives long enough to receive all - // events. - if (!bufferedTailWorkers.empty()) { - waitUntilTasks.add(tracer->onComplete().then( - kj::coCapture([tailWorkers = bufferedTailWorkers.releaseAsArray()]( - kj::Own trace) mutable -> kj::Promise { - for (auto& worker: tailWorkers) { - auto event = kj::heap( - workerd::api::TraceCustomEvent::TYPE, kj::arr(kj::addRef(*trace))); - co_await worker->customEvent(kj::mv(event)); - } - co_return; - }))); - } - workerTracer = kj::mv(tracer); - } - - KJ_IF_SOME(w, workerTracer) { - w->setMakeUserRequestSpanFunc( - [&w = *w, &entropySource = threadContext.getEntropySource()]( - tracing::TraceId traceId, kj::Maybe traceFlags) { - return SpanParent(kj::rc( - kj::refcounted(w.getWeakRef(), entropySource), kj::mv(traceId), - traceFlags)); - }); - } - kj::Own observer = - kj::refcounted(workerTracer.clone(), waitUntilTasks); - - kj::Maybe triggerContext; - KJ_IF_SOME(ctx, metadata.userSpanParent.toSpanContext()) { - KJ_IF_SOME(spanId, ctx.getSpanId()) { - triggerContext = tracing::InvocationSpanContext( - ctx.getTraceId(), tracing::TraceId::nullId, spanId, ctx.getTraceFlags()); - } - } - - return newWorkerEntrypoint(threadContext, kj::atomicAddRef(*worker), entrypointName.clone(), - kj::mv(props), kj::mv(actor), - kj::attachRef(static_cast(*this), kj::addRef(*this)), - {}, // ioContextDependency - addRefToThis(), kj::mv(observer), waitUntilTasks, - true, // tunnelExceptions - kj::mv(workerTracer), // workerTracer - kj::mv(metadata.cfBlobJson), - kj::none, // versionInfo - kj::mv(triggerContext), IsDynamicDispatch::NO, kj::mv(accessInfo), - kj::mv(metadata.restoredSelfTokenFactory), metadata.fromPersistentStub, - kj::mv(metadata.clientAddress)); - } - - private: - class EntrypointService; - - // `SelfTokenFactory` for a static (non-dynamic) worker entrypoint. This is a little wonky - // because self-tokens need to be encoded as persistent as long as the worker itself has - // allow_irrevocable_stub_storage, regardless of whether the `EntrypointService` used to reach - // it was itself persistent. (The `EntrypointService` is only persistent if it was created by - // the worker itself using ctx.exports.) - class StaticServiceSelfTokenFactory final: public ChannelTokenHandler::ServerSelfTokenFactory { - public: - StaticServiceSelfTokenFactory( - kj::Rc worker, kj::Maybe> entrypoint) - : worker(kj::mv(worker)), - entrypoint(kj::mv(entrypoint)) {} - - kj::OneOf, kj::Promise>> getSelfToken( - IoChannelFactory::ChannelTokenUsage usage) override { - kj::Maybe entrypointName; - Frankenvalue emptyProps; - auto& propsRef = [&]() -> Frankenvalue& { - KJ_IF_SOME(ep, entrypoint) { - entrypointName = ep->entrypoint; - KJ_IF_SOME(p, ep->props) { - return p; - } - } - return emptyProps; - }(); - - return worker->channelTokenHandler.encodeSubrequestChannelToken(usage, - KJ_ASSERT_NONNULL(worker->serviceName), entrypointName, propsRef, - worker->selfTokensArePersistent()); - } - - private: - kj::Rc worker; - kj::Maybe> entrypoint; - }; - - class EntrypointService final: public Service { - public: - EntrypointService(WorkerService& worker, - kj::Maybe entrypoint, - kj::Maybe props, - const kj::HashSet& handlers, - Persistent persistent = Persistent::NO) - : worker(worker.addRef()), - entrypoint(entrypoint), - handlers(handlers), - props(kj::mv(props)), - persistent(persistent) {} - - kj::Own startRequest(IoChannelFactory::SubrequestMetadata metadata) override { - return startRequest(kj::mv(metadata), false); - } - - kj::Own startRequest( - IoChannelFactory::SubrequestMetadata metadata, bool isTracer) { - Frankenvalue props; - KJ_IF_SOME(p, this->props) { - props = p.clone(); - } else { - // Calling ctx.exports loopback without specifying props. Use empty props. - } - - // If this channel was reconstructed from a persistent (stored) stub, signal the target so it - // can re-verify that it still allows persistent stubs. Preserve any incoming bit (e.g. set - // by an outer restored channel) so it propagates through loopback hops. - metadata.fromPersistentStub = metadata.fromPersistentStub || persistent; - - // Figure out the self-token factory, used for restore tokens. - if (worker->isDynamic) { - // We're a dynamic worker. The only way we could have been called by anyone other than our - // own creator is if the `ctx.restore()` mechanism was used (because otherwise, the stub - // is not serializable and therefore could not have been shared). In this case, we would - // have been called through a `RestoredSubrequestChannel` which sets - // `restoredSelfTokenFactory` to point at itself. Since this `RestoredSubrequestChannel` - // must have been set up by our creating worker, we can trust it as our `selfTokenFactory`. - } else { - // We're the static worker. So, this EntrypointService is perfectly capable of constructing - // a token pointing to itself, and can set itself as the `restoredSelfTokenFactory`. In - // fact, we MUST NOT accept a `restoredSelfTokenFactory` passed in from the caller as this - // would potentially allow a malicious caller to read and manipulate the parameters to our - // own `[restore]()` method. - metadata.restoredSelfTokenFactory = - kj::rc(worker->addRef(), addRefToThis()); - } - - return worker->startRequest(kj::mv(metadata), entrypoint, kj::mv(props), kj::none, isTracer); - } - - bool hasHandler(kj::StringPtr handlerName) override { - return handlers.contains(handlerName); - } - - // Return underlying WorkerService. - virtual Service* service() override { - return worker.get(); - } - - kj::Rc forProps(Frankenvalue props, Persistent persistent) override { - if (this->props != kj::none) { - // This entrypoint is already specialized. Delegate to the default implementation (which - // will throw an exception). - return Service::forProps(kj::mv(props), persistent); - } - - return kj::rc(*worker, entrypoint, kj::mv(props), handlers, persistent); - } - - void requireAllowsTransfer() override { - worker->requireAllowsTransfer(); - } - - kj::OneOf, kj::Promise>> getTokenMaybeSync( - ChannelTokenUsage usage) override { - worker->requireAllowsTransfer(); - - // If requireAllowsTransfer() passed, then we are not dynamic so should have a service name. - Frankenvalue emptyProps; - return worker->channelTokenHandler.encodeSubrequestChannelToken(usage, - KJ_ASSERT_NONNULL(worker->serviceName), entrypoint, props.orDefault(emptyProps), - persistent); - } - - private: - kj::Rc worker; - kj::Maybe entrypoint; - const kj::HashSet& handlers; - kj::Maybe props; - - // `persistent` is only `YES` when either: - // - This EntrypointService came from a ctx.exports loopback binding and the worker has the - // allow_irrevocable_stub_storage flag. - // - This EntrypointService was constructed from a channel token that originated from - // serializing a persistent EntrypointService. - Persistent persistent; - - friend class StaticServiceSelfTokenFactory; - }; - - class ActorClassImpl final: public ActorClass { - public: - ActorClassImpl(WorkerService& service, - kj::StringPtr className, - kj::Maybe props, - Persistent persistent = Persistent::NO) - : service(service.addRef()), - className(className), - props(kj::mv(props)), - persistent(persistent) {} - - void requireAllowsTransfer() override { - service->requireAllowsTransfer(); - } - - kj::Own newActor(kj::Maybe tracker, - Worker::Actor::Id actorId, - Worker::Actor::MakeActorCacheFunc makeActorCache, - Worker::Actor::MakeStorageFunc makeStorage, - kj::Own loopback, - kj::Maybe> manager, - kj::Maybe container, - jsg::Dict containerImages, - kj::Maybe facetManager) override { - TimerChannel& timerChannel = *service; - - // We define this event ID in the internal codebase, but to have WebSocket Hibernation - // work for local development we need to pass an event type. - static constexpr uint16_t hibernationEventTypeId = 8; - - Frankenvalue props; - KJ_IF_SOME(p, this->props) { - props = p.clone(); - } else { - // Using ctx.exports class loopback without specifying props. Use empty props. - } - - return kj::refcounted(*service->worker, tracker, kj::mv(actorId), true, - kj::mv(makeActorCache), className, kj::mv(props), kj::mv(makeStorage), kj::mv(loopback), - timerChannel, kj::refcounted(), kj::mv(manager), hibernationEventTypeId, - kj::mv(container), kj::mv(containerImages), facetManager); - } - - kj::Own startRequest( - IoChannelFactory::SubrequestMetadata metadata, kj::Own actor) override { - // The `props` parameter is empty here because props are not passed per-request, they are - // passed at Actor construction time. - return service->startRequest(kj::mv(metadata), className, {}, kj::mv(actor)); - } - - kj::Rc forProps(Frankenvalue props, Persistent persistent) override { - if (this->props != kj::none) { - // This entrypoint is already specialized. Delegate to the default implementation (which - // will throw an exception). - return ActorClass::forProps(kj::mv(props), persistent); - } - - return kj::rc(*service, className, kj::mv(props), persistent); - } - - kj::OneOf, kj::Promise>> getTokenMaybeSync( - ChannelTokenUsage usage) override { - service->requireAllowsTransfer(); - - // If requireAllowsTransfer() passed, then we are not dynamic so should have a service name. - // Unspecialized loopback entrypoints are not serializable, so if we get here we must have - // props. - return service->channelTokenHandler.encodeActorClassChannelToken(usage, - KJ_ASSERT_NONNULL(service->serviceName), className, KJ_ASSERT_NONNULL(props), persistent); - } - - private: - kj::Rc service; - kj::StringPtr className; - kj::Maybe props; - Persistent persistent; - }; - - ChannelTokenHandler& channelTokenHandler; - - // This service's name as defined in the original config, or null if it's a dynamic isolate. - // Used only for serialization. - kj::Maybe serviceName; - - ThreadContext& threadContext; - const kj::MonotonicClock& monotonicClock; - - // LinkedIoChannels owns the SqliteDatabase::Vfs, so make sure it is destroyed last. - kj::OneOf ioChannels; - - kj::Own worker; - kj::Maybe> defaultEntrypointHandlers; - kj::HashMap> namedEntrypoints; - kj::HashSet actorClassEntrypoints; - // Exported `WorkflowEntrypoint` class names. Kept separate from `actorClassEntrypoints` and - // `namedEntrypoints` so Workflow classes can be recognized when building `ctx.exports` and - // excluded from places that require a plain stateless/actor entrypoint. - kj::HashSet workflowClassEntrypoints; - kj::HashMap> actorNamespaces; - // For each Workflow-backing namespace (keyed by its unique key), the Worker whose local-disk - // storage backs it -- i.e. the Workflow's `bindingService` Worker. Resolved into an actual - // storage link in `linkActorNamespaces()`. - kj::HashMap> workflowActorStorageSources; - kj::TaskSet waitUntilTasks; - AbortActorsCallback abortActorsCallback; - DeleteActorsCallback deleteActorsCallback; - kj::Maybe dockerPath; - kj::Maybe containerEgressInterceptorImage; - // Whether this Worker's own actor storage is local-disk backed; queried by - // `hasLocalDiskActorStorage()` when validating Workflow bindingService storage. - ActorStorageIsLocalDisk actorStorageIsLocalDisk; - bool isDynamic; - kj::Maybe> abortIsolateCallback; - kj::Maybe accessBlobHeaderName; - kj::Maybe accessBindingServiceChannel; - - // --------------------------------------------------------------------------- - // implements kj::TaskSet::ErrorHandler - - void taskFailed(kj::Exception&& exception) override { - KJ_LOG(ERROR, exception); - } - - // --------------------------------------------------------------------------- - // implements IoChannelFactory - - kj::Own startSubrequest(uint channel, SubrequestMetadata metadata) override { - auto& channels = - KJ_REQUIRE_NONNULL(ioChannels.tryGet(), "link() has not been called"); - - KJ_REQUIRE(channel < channels.subrequest.size(), "invalid subrequest channel number"); - - // Intercept the access binding channel: inject per-request props (aud, jwtClaims) from - // the current request's AccessInfo via forProps(), mirroring what the production embedder - // does with channel tokens in its IoChannelFactory. - KJ_IF_SOME(abChannel, accessBindingServiceChannel) { - if (channel == abChannel) { - auto& context = IoContext::current(); - KJ_IF_SOME(info, context.getAccessInfo()) { - auto& blobInfo = kj::downcast(info); - auto escapedAud = escapeJsonString(blobInfo.getAudience()); - kj::String propsJson; - KJ_IF_SOME(claims, blobInfo.getJwtClaimsJson()) { - propsJson = kj::str("{\"aud\":", escapedAud, ",\"jwtClaims\":", claims, "}"); - } else { - propsJson = kj::str("{\"aud\":", escapedAud, "}"); - } - SubrequestChannel& channelRef = *channels.subrequest[channel]; - auto& service = KJ_REQUIRE_NONNULL(kj::tryDowncast(channelRef), - "access binding service channel is not a Service"); - return service.forProps(Frankenvalue::fromJson(kj::mv(propsJson)), Persistent::NO) - ->startRequest(kj::mv(metadata)); - } - } - } - - return channels.subrequest[channel]->startRequest(kj::mv(metadata)); - } - - capnp::Capability::Client getCapability(uint channel) override { - KJ_FAIL_REQUIRE("no capability channels"); - } - class CacheClientImpl final: public CacheClient { - public: - CacheClientImpl( - IoChannelFactory::SubrequestChannel& cacheService, kj::HttpHeaderId cacheNamespaceHeader) - : cacheService(cacheService.addRef()), - cacheNamespaceHeader(cacheNamespaceHeader) {} - - kj::Own getDefault(CacheClient::SubrequestMetadata metadata) override { - return kj::heap(*cacheService, cacheNamespaceHeader, kj::none, - kj::mv(metadata.cfBlobJson), kj::mv(metadata.parentSpan)); - } - - kj::Own getNamespace( - kj::StringPtr cacheName, CacheClient::SubrequestMetadata metadata) override { - auto encodedName = kj::encodeUriComponent(cacheName); - return kj::heap(*cacheService, cacheNamespaceHeader, kj::mv(encodedName), - kj::mv(metadata.cfBlobJson), kj::mv(metadata.parentSpan)); - } - - private: - kj::Rc cacheService; - kj::HttpHeaderId cacheNamespaceHeader; - }; - - class CacheHttpClientImpl final: public kj::HttpClient { - public: - CacheHttpClientImpl(IoChannelFactory::SubrequestChannel& parent, - kj::HttpHeaderId cacheNamespaceHeader, - kj::Maybe cacheName, - kj::Maybe cfBlobJson, - SpanParent parentSpan) - : client(asHttpClient(parent.startRequest({kj::mv(cfBlobJson), kj::mv(parentSpan)}))), - cacheName(kj::mv(cacheName)), - cacheNamespaceHeader(cacheNamespaceHeader) {} - - Request request(kj::HttpMethod method, - kj::StringPtr url, - const kj::HttpHeaders& headers, - kj::Maybe expectedBodySize = kj::none) override { - - return client->request(method, url, addCacheNameHeader(headers, cacheName), expectedBodySize); - } - - private: - kj::Own client; - kj::Maybe cacheName; - kj::HttpHeaderId cacheNamespaceHeader; - - kj::HttpHeaders addCacheNameHeader( - const kj::HttpHeaders& headers, kj::Maybe cacheName) { - auto headersCopy = headers.cloneShallow(); - KJ_IF_SOME(name, cacheName) { - headersCopy.setPtr(cacheNamespaceHeader, name); - } - - return headersCopy; - } - }; - - kj::Own getCache() override { - auto& channels = - KJ_REQUIRE_NONNULL(ioChannels.tryGet(), "link() has not been called"); - auto& cache = *JSG_REQUIRE_NONNULL(channels.cache, Error, "No Cache was configured"); - return kj::heap(cache, threadContext.getHeaderIds().cfCacheNamespace); - } - - TimerChannel& getTimer() override { - return *this; - } - - kj::Promise writeLogfwdr( - uint channel, kj::FunctionParam buildMessage) override { - auto& context = IoContext::current(); - - auto headers = kj::HttpHeaders(context.getHeaderTable()); - auto client = context.getHttpClient(channel, true, kj::none, "writeLogfwdr"_kjc); - - auto urlStr = kj::str("https://fake-host"); - - capnp::MallocMessageBuilder requestMessage; - auto requestBuilder = requestMessage.initRoot(); - - buildMessage(requestBuilder); - capnp::JsonCodec json; - auto requestJson = json.encode(requestBuilder.getAs()); - - co_await context.waitForOutputLocks(); - - auto innerReq = client->request(kj::HttpMethod::POST, urlStr, headers, requestJson.size()); - auto request = attachToRequest(kj::mv(innerReq), kj::Rc(kj::mv(client))); - - co_await request.body->write(requestJson.asBytes()) - .attach(kj::mv(requestJson), kj::mv(request.body)); - auto response = co_await request.response; - - KJ_REQUIRE(response.statusCode >= 200 && response.statusCode < 300, - "writeLogfwdr request returned an error"); - co_await response.body->readAllBytes().attach(kj::mv(response.body)); - co_return; - } - - kj::Rc getSubrequestChannelResolved(uint channel, - kj::Maybe props, - kj::Maybe versionRequest, - Persistent persistent) override { - auto& channels = - KJ_REQUIRE_NONNULL(ioChannels.tryGet(), "link() has not been called"); - - KJ_REQUIRE(channel < channels.subrequest.size(), "invalid subrequest channel number"); - - SubrequestChannel& channelRef = *channels.subrequest[channel]; - - KJ_IF_SOME(p, props) { - // Requesting specialization of loopback (ctx.exports) entrypoint with props. - auto& service = KJ_REQUIRE_NONNULL( - kj::tryDowncast(channelRef), "referenced channel is not a loopback channel"); - return service.forProps(kj::mv(p), persistent); - } - - return channelRef.addRef(); - } - - kj::Rc getGlobalActor(uint channel, - const ActorIdFactory::ActorId& id, - kj::Maybe locationHint, - ActorGetMode mode, - bool enableReplicaRouting, - ActorRoutingMode routingMode, - SpanParent parentSpan, - kj::Maybe version, - Persistent persistent) override { - JSG_REQUIRE(mode == ActorGetMode::GET_OR_CREATE, Error, - "workerd only supports GET_OR_CREATE mode for getting actor stubs"); - JSG_REQUIRE(!enableReplicaRouting, Error, "workerd does not support replica routing."); - - // Compile-time assert that we have considered every routing mode here. - switch (routingMode) { - case ActorRoutingMode::PRIMARY_ONLY: - // Workerd-only configs only supports primaries anyway. - break; - case ActorRoutingMode::DEFAULT: - // In workerd-only configs, DEFAULT means PRIMARY_ONLY. - break; - } - - auto& channels = - KJ_REQUIRE_NONNULL(ioChannels.tryGet(), "link() has not been called"); - - KJ_REQUIRE(channel < channels.actor.size(), "invalid actor channel number"); - auto& ns = JSG_REQUIRE_NONNULL( - channels.actor[channel], Error, "Actor namespace configuration was invalid."); - KJ_REQUIRE(ns.getConfig().is()); // should have been verified earlier - return ns.getActorChannel(id.clone(), persistent); - } - - kj::Rc getColoLocalActor( - uint channel, kj::StringPtr id, SpanParent parentSpan) override { - auto& channels = - KJ_REQUIRE_NONNULL(ioChannels.tryGet(), "link() has not been called"); - - KJ_REQUIRE(channel < channels.actor.size(), "invalid actor channel number"); - auto& ns = JSG_REQUIRE_NONNULL( - channels.actor[channel], Error, "Actor namespace configuration was invalid."); - KJ_REQUIRE(ns.getConfig().is()); // should have been verified earlier - return ns.getActorChannel(kj::str(id)); - } - - kj::Rc getActorClassResolved( - uint channel, kj::Maybe props, Persistent persistent) override { - auto& channels = - KJ_REQUIRE_NONNULL(ioChannels.tryGet(), "link() has not been called"); - - KJ_REQUIRE(channel < channels.actorClass.size(), "invalid actor class channel number"); - - ActorClassChannel& cls = *channels.actorClass[channel]; - - KJ_IF_SOME(p, props) { - // Requesting specialization of loopback (ctx.exports) actor class with props. - auto& typed = KJ_REQUIRE_NONNULL( - kj::tryDowncast(cls), "referenced channel is not a loopback channel"); - return typed.forProps(kj::mv(p), persistent); - } - - return cls.addRef(); - } - - kj::Rc getRpcChannel(uint channel) override { - auto& channels = - KJ_REQUIRE_NONNULL(ioChannels.tryGet(), "link() has not been called"); - - KJ_REQUIRE(channel < channels.rpc.size(), "invalid RPC channel number"); - return channels.rpc[channel]->addRef(); - } - - void abortAllActors(kj::Maybe reason) override { - abortActorsCallback(reason); - } - - void deleteAllActors(kj::Maybe reason) override { - deleteActorsCallback(reason); - } - - kj::Promise evictAllActorsForTest( - IoChannelFactory::EvictWebSocketMode webSocketMode) override { - auto& channels = - KJ_REQUIRE_NONNULL(ioChannels.tryGet(), "link() has not been called"); - - kj::Vector> promises(channels.actor.size()); - for (auto& maybeNs: channels.actor) { - KJ_IF_SOME(ns, maybeNs) { - promises.add(ns.evictAllForTest(webSocketMode)); - } - } - return kj::joinPromises(promises.releaseAsArray()); - } - - // For now, in workerd just abort the process for non-dynamic workers. - void abortIsolate(kj::StringPtr reason) noexcept override { - KJ_IF_SOME(cb, abortIsolateCallback) { - // Removes the isolate from the isolates map. - // - // TODO: Should abort all outstanding calls to the isolate causing them to - // throw the reason as the error. - cb(); - } else { - // Otherwise, abort the process. Throwing from a noexcept function will call - // std::terminate, which produces a nicer error message than ::abort(). - if (reason == nullptr) { - KJ_FAIL_REQUIRE("abortIsolate() called, terminating process"); - } else { - KJ_FAIL_REQUIRE("abortIsolate() called, terminating process", reason); - } - } - } - - kj::Own loadIsolate(uint loaderChannel, - kj::Maybe name, - kj::Function()> fetchSource) override; - - kj::Network& getWorkerdDebugPortNetwork() override { - auto& channels = - KJ_REQUIRE_NONNULL(ioChannels.tryGet(), "link() has not been called"); - return KJ_REQUIRE_NONNULL(channels.workerdDebugPortNetwork, - "workerdDebugPort binding is not enabled for this worker"); - } - - rpc::WorkerdDebugPort::Client getWorkerdDebugPort() override { - auto& channels = - KJ_REQUIRE_NONNULL(ioChannels.tryGet(), "link() has not been called"); - return KJ_REQUIRE_NONNULL( - channels.workerdDebugPortServer, "workerdDebugPort binding is not enabled for this worker") - .makeWorkerdDebugPortClient(); - } - - kj::Rc subrequestChannelFromToken( - ChannelTokenUsage usage, kj::ArrayPtr token) override { - return channelTokenHandler.decodeSubrequestChannelToken(usage, token); - } - - kj::Rc actorClassFromToken( - ChannelTokenUsage usage, kj::ArrayPtr token) override { - return channelTokenHandler.decodeActorClassChannelToken(usage, token); - } - - kj::Rc rpcChannelFromToken( - ChannelTokenUsage usage, kj::ArrayPtr token) override { - return channelTokenHandler.decodeRpcChannelToken(usage, token); - } - - kj::Rc makeRestoredSubrequestChannelResolved( - kj::Rc selfTokenFactory, - Frankenvalue restoreParams, - kj::Rc inner, - Persistent persistent) override { - return channelTokenHandler.makeRestoredSubrequestChannel( - kj::mv(selfTokenFactory), kj::mv(restoreParams), kj::mv(inner), persistent); - } - - kj::Rc makeRestoredRpcChannelResolved(kj::Rc selfTokenFactory, - Frankenvalue restoreParams, - Persistent persistent) override { - return channelTokenHandler.makeRestoredRpcChannel( - kj::mv(selfTokenFactory), kj::mv(restoreParams), persistent); - } - - // --------------------------------------------------------------------------- - // implements TimerChannel - - void syncTime() override { - // Nothing to do - } - - kj::Date now(kj::Maybe) override { - return kj::systemPreciseCalendarClock().now(); - } - - kj::Promise atTime(kj::Date when) override { - auto delay = when - now(kj::none); - // We can't use `afterDelay(delay)` here because kj::Timer::afterDelay() is equivalent to - // `atTime(timer.now() + delay)`, and kj::Timer::now() only advances when the event loop - // polls for I/O. If JavaScript executed for a significant amount of time since the last - // poll (e.g. compiling/running a script before the first setTimeout), timer.now() will be - // stale and the delay will effectively be shortened by that staleness, causing the timer - // to fire too early. Instead, we compute the target time using a fresh reading from the - // monotonic clock so the delay is measured from the actual present. - return threadContext.getUnsafeTimer().atTime(monotonicClock.now() + delay); - } - - kj::Promise afterLimitTimeout(kj::Duration t) override { - return threadContext.getUnsafeTimer().afterDelay(t); - } - - kj::TimePoint nowForLimitTimeout() override { - return monotonicClock.now(); - } - - // --------------------------------------------------------------------------- - // implements LimitEnforcer - // - // No limits are enforced. - - kj::Own enterJs(jsg::Lock& lock, IoContext& context) override { - return {}; - } - void topUpActor() override {} - void newSubrequest(bool isInHouse) override {} - void newKvRequest(KvOpType op) override {} - void newAnalyticsEngineRequest() override {} - kj::Promise limitDrain() override { - return kj::NEVER_DONE; - } - kj::Promise limitScheduled() override { - return kj::NEVER_DONE; - } - kj::Duration getAlarmLimit() override { - return 15 * kj::MINUTES; - } - size_t getBufferingLimit() override { - return kj::maxValue; - } - kj::Maybe getLimitsExceeded() override { - return kj::none; - } - kj::Promise onLimitsExceeded() override { - return kj::NEVER_DONE; - } - void setCpuLimitNearlyExceededCallback(kj::Function cb) override {} - void requireLimitsNotExceeded() override {} - void reportMetrics(RequestObserver& requestMetrics) override {} - kj::Duration consumeTimeElapsedForPeriodicLogging() override { - return 0 * kj::SECONDS; - } - size_t getSqliteMemoryUsage() const override { - return 0; - } -}; - -struct FutureSubrequestChannel { - kj::OneOf> - designator; - kj::String errorContext; - - kj::Rc lookup(Server& server) && { - KJ_SWITCH_ONEOF(designator) { - KJ_CASE_ONEOF(conf, config::ServiceDesignator::Reader) { - return server.lookupService(conf, kj::mv(errorContext)); - } - KJ_CASE_ONEOF(channel, kj::Rc) { - return kj::mv(channel); - } - } - KJ_UNREACHABLE; - } -}; - -struct FutureActorChannel { - config::Worker::Binding::DurableObjectNamespaceDesignator::Reader designator; - kj::String errorContext; -}; - -struct FutureActorClassChannel { - kj::OneOf> - designator; - kj::String errorContext; - - kj::Rc lookup(Server& server) && { - KJ_SWITCH_ONEOF(designator) { - KJ_CASE_ONEOF(conf, config::ServiceDesignator::Reader) { - return server.lookupActorClass(conf, kj::mv(errorContext)); - } - KJ_CASE_ONEOF(channel, kj::Rc) { - return kj::mv(channel); - } - } - KJ_UNREACHABLE; - } -}; - -struct FutureWorkerLoaderChannel { - kj::String name; // for error logging, not necessarily unique - kj::Maybe id; -}; - -static kj::Maybe createBinding(kj::StringPtr workerName, - config::Worker::Reader conf, - config::Worker::Binding::Reader binding, - Worker::ValidationErrorReporter& errorReporter, - kj::Vector& subrequestChannels, - kj::Vector& actorChannels, - kj::Vector& actorClassChannels, - kj::Vector& workerLoaderChannels, - bool& hasWorkerdDebugPortBinding, - kj::HashMap>& actorConfigs, - bool experimental) { - // creates binding object or returns null and reports an error - using Global = WorkerdApi::Global; - kj::StringPtr bindingName = binding.getName(); - TRACE_EVENT("workerd", "Server::WorkerService::createBinding()", "name", workerName.cStr(), - "binding", bindingName.cStr()); - auto makeGlobal = [&](auto&& value) { - return Global{.name = kj::str(bindingName), .value = kj::mv(value)}; - }; - - auto errorContext = kj::str("Worker \"", workerName, "\"'s binding \"", bindingName, "\""); - - switch (binding.which()) { - case config::Worker::Binding::UNSPECIFIED: - errorReporter.addError(kj::str(errorContext, " does not specify any binding value.")); - return kj::none; - - case config::Worker::Binding::PARAMETER: - KJ_UNIMPLEMENTED("TODO(beta): parameters"); - - case config::Worker::Binding::TEXT: - return makeGlobal(kj::str(binding.getText())); - case config::Worker::Binding::DATA: - return makeGlobal(kj::heapArray(binding.getData())); - case config::Worker::Binding::JSON: - return makeGlobal(Global::Json{kj::str(binding.getJson())}); - - case config::Worker::Binding::WASM_MODULE: - if (conf.isServiceWorkerScript()) { - // Already handled earlier. - } else { - errorReporter.addError(kj::str(errorContext, - " is a Wasm binding, but Wasm bindings are not allowed in " - "modules-based scripts. Use Wasm modules instead.")); - } - return kj::none; - - case config::Worker::Binding::CRYPTO_KEY: { - auto keyConf = binding.getCryptoKey(); - Global::CryptoKey keyGlobal; - - switch (keyConf.which()) { - case config::Worker::Binding::CryptoKey::RAW: - keyGlobal.format = kj::str("raw"); - keyGlobal.keyData = kj::heapArray(keyConf.getRaw()); - goto validFormat; - case config::Worker::Binding::CryptoKey::HEX: { - keyGlobal.format = kj::str("raw"); - auto decoded = kj::decodeHex(keyConf.getHex()); - if (decoded.hadErrors) { - errorReporter.addError( - kj::str("CryptoKey binding \"", binding.getName(), "\" contained invalid hex.")); - } - keyGlobal.keyData = kj::Array(kj::mv(decoded)); - goto validFormat; - } - case config::Worker::Binding::CryptoKey::BASE64: { - keyGlobal.format = kj::str("raw"); - auto decoded = kj::decodeBase64(keyConf.getBase64()); - if (decoded.hadErrors) { - errorReporter.addError( - kj::str("CryptoKey binding \"", binding.getName(), "\" contained invalid base64.")); - } - keyGlobal.keyData = kj::Array(kj::mv(decoded)); - goto validFormat; - } - case config::Worker::Binding::CryptoKey::PKCS8: { - keyGlobal.format = kj::str("pkcs8"); - auto pem = KJ_UNWRAP_OR(decodePem(keyConf.getPkcs8()), { - errorReporter.addError(kj::str( - "CryptoKey binding \"", binding.getName(), "\" contained invalid PEM format.")); - return kj::none; - }); - if (pem.type != "PRIVATE KEY") { - errorReporter.addError(kj::str("CryptoKey binding \"", binding.getName(), - "\" contained wrong PEM type, " - "expected \"PRIVATE KEY\" but got \"", - pem.type, "\".")); - return kj::none; - } - keyGlobal.keyData = kj::mv(pem.data); - goto validFormat; - } - case config::Worker::Binding::CryptoKey::SPKI: { - keyGlobal.format = kj::str("spki"); - auto pem = KJ_UNWRAP_OR(decodePem(keyConf.getSpki()), { - errorReporter.addError(kj::str( - "CryptoKey binding \"", binding.getName(), "\" contained invalid PEM format.")); - return kj::none; - }); - if (pem.type != "PUBLIC KEY") { - errorReporter.addError(kj::str("CryptoKey binding \"", binding.getName(), - "\" contained wrong PEM type, " - "expected \"PUBLIC KEY\" but got \"", - pem.type, "\".")); - return kj::none; - } - keyGlobal.keyData = kj::mv(pem.data); - goto validFormat; - } - case config::Worker::Binding::CryptoKey::JWK: - keyGlobal.format = kj::str("jwk"); - keyGlobal.keyData = Global::Json{kj::str(keyConf.getJwk())}; - goto validFormat; - } - errorReporter.addError(kj::str("Encountered unknown CryptoKey type for binding \"", - binding.getName(), "\". Was the config compiled with a newer version of the schema?")); - return kj::none; - validFormat: - - auto algorithmConf = keyConf.getAlgorithm(); - switch (algorithmConf.which()) { - case config::Worker::Binding::CryptoKey::Algorithm::NAME: - keyGlobal.algorithm = Global::Json{escapeJsonString(algorithmConf.getName())}; - goto validAlgorithm; - case config::Worker::Binding::CryptoKey::Algorithm::JSON: - keyGlobal.algorithm = Global::Json{kj::str(algorithmConf.getJson())}; - goto validAlgorithm; - } - errorReporter.addError(kj::str("Encountered unknown CryptoKey algorithm type for binding \"", - binding.getName(), "\". Was the config compiled with a newer version of the schema?")); - return kj::none; - validAlgorithm: - - keyGlobal.extractable = keyConf.getExtractable(); - keyGlobal.usages = KJ_MAP(usage, keyConf.getUsages()) { return kj::str(usage); }; - - return makeGlobal(kj::mv(keyGlobal)); - return kj::none; - } - - case config::Worker::Binding::SERVICE: { - uint channel = static_cast(subrequestChannels.size()) + - IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT; - subrequestChannels.add(FutureSubrequestChannel{binding.getService(), kj::mv(errorContext)}); - return makeGlobal( - Global::Fetcher{.channel = channel, .requiresHost = true, .isInHouse = false}); - } - - case config::Worker::Binding::DURABLE_OBJECT_NAMESPACE: { - auto actorBinding = binding.getDurableObjectNamespace(); - kj::Maybe userDefinedRetryPolicy; - if (actorBinding.hasRetryPolicy()) { - auto retryPolicy = actorBinding.getRetryPolicy(); - auto retryTimeout = retryPolicy.getTimeoutMs() * kj::MILLISECONDS; - if (retryPolicy.getMaxAttempts() > api::UserDefinedRetryPolicy::MAX_CONFIGURABLE_ATTEMPTS || - retryTimeout < api::UserDefinedRetryPolicy::MIN_CONFIGURABLE_TIMEOUT || - retryTimeout > api::UserDefinedRetryPolicy::MAX_CONFIGURABLE_TIMEOUT) { - errorReporter.addError(kj::str(errorContext, - " has a Durable Object retry policy outside " - "the system limits.")); - return kj::none; - } - userDefinedRetryPolicy = api::UserDefinedRetryPolicy{ - .maxAttempts = retryPolicy.getMaxAttempts(), - .timeout = retryTimeout, - }; - } - - const Server::ActorConfig* actorConfig; - if (actorBinding.hasServiceName()) { - auto& svcMap = KJ_UNWRAP_OR(actorConfigs.find(actorBinding.getServiceName()), { - errorReporter.addError(kj::str(errorContext, " refers to a service \"", - actorBinding.getServiceName(), "\", but no such service is defined.")); - return kj::none; - }); - - actorConfig = &KJ_UNWRAP_OR(svcMap.find(actorBinding.getClassName()), { - errorReporter.addError( - kj::str(errorContext, " refers to a Durable Object namespace named \"", - actorBinding.getClassName(), "\" in service \"", actorBinding.getServiceName(), - "\", but no such Durable Object namespace is defined by that service.")); - return kj::none; - }); - } else { - auto& localActorConfigs = KJ_ASSERT_NONNULL(actorConfigs.find(workerName)); - actorConfig = &KJ_UNWRAP_OR(localActorConfigs.find(actorBinding.getClassName()), { - errorReporter.addError(kj::str(errorContext, - " refers to a Durable Object namespace named \"", actorBinding.getClassName(), - "\", but no such Durable Object namespace is defined " - "by this Worker.")); - return kj::none; - }); - } - - uint channel = static_cast(actorChannels.size()); - actorChannels.add(FutureActorChannel{actorBinding, kj::mv(errorContext)}); - - KJ_SWITCH_ONEOF(*actorConfig) { - KJ_CASE_ONEOF(durable, Server::Durable) { - return makeGlobal(Global::DurableActorNamespace{ - .actorChannel = channel, - .uniqueKey = durable.uniqueKey, - .userDefinedRetryPolicy = userDefinedRetryPolicy, - }); - } - KJ_CASE_ONEOF(_, Server::Ephemeral) { - return makeGlobal(Global::EphemeralActorNamespace{.actorChannel = channel}); - } - } - - return kj::none; - } - - case config::Worker::Binding::KV_NAMESPACE: { - uint channel = static_cast(subrequestChannels.size()) + - IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT; - subrequestChannels.add( - FutureSubrequestChannel{binding.getKvNamespace(), kj::mv(errorContext)}); - - return makeGlobal(Global::KvNamespace{ - .subrequestChannel = channel, .bindingName = kj::str(binding.getName())}); - } - - case config::Worker::Binding::R2_BUCKET: { - uint channel = static_cast(subrequestChannels.size()) + - IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT; - subrequestChannels.add(FutureSubrequestChannel{binding.getR2Bucket(), kj::mv(errorContext)}); - return makeGlobal(Global::R2Bucket{.subrequestChannel = channel, - .bucket = kj::str(binding.getR2Bucket().getName()), - .bindingName = kj::str(binding.getName())}); - } - - case config::Worker::Binding::OBSOLETE0: - errorReporter.addError(kj::str(errorContext, " uses an obsolete binding type.")); - return kj::none; - - case config::Worker::Binding::QUEUE: { - uint channel = static_cast(subrequestChannels.size()) + - IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT; - subrequestChannels.add(FutureSubrequestChannel{binding.getQueue(), kj::mv(errorContext)}); - - return makeGlobal(Global::QueueBinding{.subrequestChannel = channel}); - } - - case config::Worker::Binding::WRAPPED: { - auto wrapped = binding.getWrapped(); - kj::Vector innerGlobals; - for (const auto& innerBinding: wrapped.getInnerBindings()) { - KJ_IF_SOME(global, - createBinding(workerName, conf, innerBinding, errorReporter, subrequestChannels, - actorChannels, actorClassChannels, workerLoaderChannels, hasWorkerdDebugPortBinding, - actorConfigs, experimental)) { - innerGlobals.add(kj::mv(global)); - } else { - // we've already communicated the error - return kj::none; - } - } - return makeGlobal(Global::Wrapped{ - .moduleName = kj::str(wrapped.getModuleName()), - .entrypoint = kj::str(wrapped.getEntrypoint()), - .innerBindings = innerGlobals.releaseAsArray(), - }); - } - - case config::Worker::Binding::FROM_ENVIRONMENT: { - const char* value = getenv(binding.getFromEnvironment().cStr()); - if (value == nullptr) { - // TODO(cleanup): Maybe make a Global::Null? (Can't use nullptr_t in OneOf.) For now, - // using JSON gets the job done hackily. - return makeGlobal(Global::Json{kj::str("null")}); - } else { - return makeGlobal(kj::str(value)); - } - } - - case config::Worker::Binding::ANALYTICS_ENGINE: { - if (!experimental) { - errorReporter.addError(kj::str( - "AnalyticsEngine bindings are an experimental feature which may change or go away in the future." - "You must run workerd with `--experimental` to use this feature.")); - } - - uint channel = static_cast(subrequestChannels.size()) + - IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT; - subrequestChannels.add( - FutureSubrequestChannel{binding.getAnalyticsEngine(), kj::mv(errorContext)}); - - return makeGlobal(Global::AnalyticsEngine{ - .subrequestChannel = channel, - .dataset = kj::str(binding.getAnalyticsEngine().getName()), - .version = 0, - }); - } - case config::Worker::Binding::HYPERDRIVE: { - uint channel = static_cast(subrequestChannels.size()) + - IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT; - subrequestChannels.add( - FutureSubrequestChannel{binding.getHyperdrive().getDesignator(), kj::mv(errorContext)}); - return makeGlobal(Global::Hyperdrive{ - .subrequestChannel = channel, - .database = kj::str(binding.getHyperdrive().getDatabase()), - .user = kj::str(binding.getHyperdrive().getUser()), - .password = kj::str(binding.getHyperdrive().getPassword()), - .scheme = kj::str(binding.getHyperdrive().getScheme()), - }); - } - case config::Worker::Binding::UNSAFE_EVAL: { - if (!experimental) { - errorReporter.addError(kj::str("Unsafe eval is an experimental feature. ", - "You must run workerd with `--experimental` to use this feature.")); - return kj::none; - } - return makeGlobal(Global::UnsafeEval{}); - } - case config::Worker::Binding::MEMORY_CACHE: { - if (!experimental) { - errorReporter.addError(kj::str( - "MemoryCache bindings are an experimental feature which may change or go away " - "in the future. You must run workerd with `--experimental` to use this feature.")); - return kj::none; - } - auto cache = binding.getMemoryCache(); - // TODO(cleanup): Should we have some reasonable default for these so they can - // be optional? - if (!cache.hasLimits()) { - errorReporter.addError( - kj::str("MemoryCache bindings must specify limits. Please " - "update the binding in the worker configuration and try again.")); - return kj::none; - } - Global::MemoryCache cacheCopy; - // The id is optional. If provided, then multiple bindings with the same id will - // share the same cache. Otherwise, a unique id is generated for the cache. - if (cache.hasId()) { - cacheCopy.cacheId = kj::str(cache.getId()); - } - auto limits = cache.getLimits(); - cacheCopy.maxKeys = limits.getMaxKeys(); - cacheCopy.maxValueSize = limits.getMaxValueSize(); - cacheCopy.maxTotalValueSize = limits.getMaxTotalValueSize(); - return makeGlobal(kj::mv(cacheCopy)); - } - - case config::Worker::Binding::DURABLE_OBJECT_CLASS: { - if (!experimental) { - errorReporter.addError(kj::str( - "Durable Object class bindings are an experimental feature which may change or go away " - "in the future. You must run workerd with `--experimental` to use this feature.")); - return kj::none; - } - uint channel = actorClassChannels.size(); - actorClassChannels.add( - FutureActorClassChannel{binding.getDurableObjectClass(), kj::mv(errorContext)}); - return makeGlobal(Global::ActorClass{.channel = channel}); - } - - case config::Worker::Binding::WORKER_LOADER: { - if (!experimental) { - errorReporter.addError(kj::str( - "Worker loader bindings are an experimental feature which may change or go away " - "in the future. You must run workerd with `--experimental` to use this feature.")); - return kj::none; - } - - auto loaderConf = binding.getWorkerLoader(); - - FutureWorkerLoaderChannel channel; - if (loaderConf.hasId()) { - channel.name = kj::str(loaderConf.getId()); - channel.id = kj::str(channel.name); - } else { - channel.name = kj::str(bindingName); - } - - uint channelNumber = workerLoaderChannels.size(); - workerLoaderChannels.add(kj::mv(channel)); - return makeGlobal(Global::WorkerLoader{.channel = channelNumber}); - } - - case config::Worker::Binding::WORKERD_DEBUG_PORT: { - if (!experimental) { - errorReporter.addError(kj::str( - "workerdDebugPort bindings are an experimental feature which may change or go away " - "in the future. You must run workerd with `--experimental` to use this feature.")); - return kj::none; - } - - hasWorkerdDebugPortBinding = true; - return makeGlobal(Global::WorkerdDebugPort{}); - } - } - errorReporter.addError(kj::str(errorContext, - "has unrecognized type. Was the config compiled with a newer version of " - "the schema?")); -} - -uint startInspector( - kj::StringPtr inspectorAddress, Server::InspectorServiceIsolateRegistrar& registrar); - -void Server::abortAllActors(kj::Maybe reason) { - for (auto& service: services) { - KJ_IF_SOME(worker, kj::tryDowncast(*service.value)) { - for (auto& [className, ns]: worker.getActorNamespaces()) { - if (ns->isEvictable()) ns->abortAll(reason); - } - } - } -} - -void Server::deleteAllActors(kj::Maybe reason) { - for (auto& service: services) { - KJ_IF_SOME(worker, kj::tryDowncast(*service.value)) { - for (auto& [className, ns]: worker.getActorNamespaces()) { - if (ns->isEvictable()) ns->deleteAll(reason); - } - } - } -} - -// WorkerDef is an intermediate representation of everything from `config::Worker::Reader` that -// `Server::makeWorkerImpl()` needs. Similar to `WorkerSource`, we factor out this intermediate -// representation so that we can potentially build it dynamically from input that isn't a -// workerd config file. -struct Server::WorkerDef { - CompatibilityFlags::Reader featureFlags; - WorkerSource source; - kj::Maybe moduleFallback; - const kj::HashMap& localActorConfigs; - bool isDynamic; - - FutureSubrequestChannel globalOutbound; - kj::Maybe cacheApiOutbound; - kj::Vector subrequestChannels; - // Maps a configured Workflow's class name to the subrequest channel (an index into - // `subrequestChannels`) that targets its `bindingService`. Used when building the wrapped - // `ctx.exports` binding for that Workflow, whose inner fetcher points at this channel. - kj::HashMap workflowBindingChannels; - kj::Vector actorChannels; - kj::Vector actorClassChannels; - kj::Vector> rpcChannels; - kj::Vector workerLoaderChannels; - bool hasWorkerdDebugPortBinding = false; - kj::Array tails; - kj::Array streamingTails; - - // Dynamically-loaded isolates can't directly have storage, so for now I'm using a raw capnp - // Reader here. A default-constructed Reader will have type `none` which is appropriate for - // dynamically-loaded workers. Same story for ContainerEngine. - config::Worker::DurableObjectStorage::Reader actorStorageConf; - config::Worker::ContainerEngine::Reader containerEngineConf; - - // Similar to the `compileBindings` callback passed into `Worker`'s constructor, except that - // `ctx.exports` is taken care of separately. This is provided as a callback since `env` is - // constructed in a vastly different way for dynamically-loaded workers. - kj::Function target)> - compileBindings; - - // If the WorkerDef was created from a DymamicWorkerSource and that - // source contains a clone of the source bundle, this will take ownership. - kj::Maybe> maybeOwnedSourceCode; - - // Callback invoked when abortIsolate() is called. Used by dynamic workers to remove - // themselves from the loader's isolate map. - kj::Maybe> abortIsolateCallback; - - // HTTP header name from which to extract the per-request Cloudflare Access JSON blob. - // Set from Worker.accessBlobHeader in the config; kj::none for most workers. - kj::Maybe accessBlobHeaderName; - - // ServiceDesignator for the access binding worker. Resolved during linkCallback. - kj::Maybe accessBindingServiceDesignator; -}; - -class Server::WorkerLoaderNamespace: public kj::Refcounted, private kj::TaskSet::ErrorHandler { - public: - WorkerLoaderNamespace(Server& server, kj::String namespaceName) - : server(server), - namespaceName(kj::mv(namespaceName)), - startupTasks(*this) {} - - void unlink() { - for (auto& isolate: isolates) { - isolate.value->unlink(); - } - } - - kj::Own loadIsolate( - kj::Maybe name, kj::Function()> fetchSource) { - KJ_IF_SOME(n, name) { - return isolates - .findOrCreate(n, - [&]() -> decltype(isolates)::Entry { - // This name isn't actually used in any maps nor is it ever revealed back to the app, but it - // may be used in error logs. - auto isolateName = kj::str(namespaceName, ':', n); - - // On abort, remove the entry from this namespace's isolates map so - // subsequent loadIsolate() calls with the same name will create a fresh - // isolate. - kj::Function onAborted = [this, mapKey = kj::str(n)]() { removeIsolate(mapKey); }; - - return {.key = kj::mv(n), - .value = kj::rc( - server, kj::mv(isolateName), kj::mv(onAborted), kj::mv(fetchSource))}; - }) - .addRef() - .toOwn(); - } else { - auto isolateName = kj::str(namespaceName, ":dynamic:", randomUUID(server.entropySource)); - auto stub = - kj::rc(server, kj::mv(isolateName), kj::none, kj::mv(fetchSource)); - // Unnamed workers have no entry in the isolates map, so the JS-side - // IoOwn would be the sole owner. Retain an extra ref so that GC of the - // JS handle during the getCode re-entry callback cannot destroy the - // object while its start() coroutine is still running. The extra ref - // is held in a task on the namespace (NOT on the WorkerStubImpl itself) - // so that when the task completes and drops the ref, the destruction - // does not re-enter a firing Event. The named-load path is safe because - // the isolates map already holds an additional kj::Rc. - auto selfRef = stub.addRef(); - startupTasks.add( - stub->whenStartupDone().then([prevent = kj::mv(selfRef)]() { /* prevent dropped here */ }, - [](kj::Exception&&) { /* startup failed; prevent dropped here */ })); - return kj::mv(stub).toOwn(); - } - } - - void removeIsolate(kj::StringPtr name) { - // This is called by abortIsolate() - isolates.erase(name); - } - - private: - Server& server; - kj::String namespaceName; - - class WorkerStubImpl; - kj::HashMap> isolates; - - // Holds tasks that keep unnamed WorkerStubImpl instances alive while their - // start() coroutines are running. See the unnamed branch of loadIsolate(). - kj::TaskSet startupTasks; - - void taskFailed(kj::Exception&& exception) override { - // Startup failures are already handled by the WorkerStubImpl's - // startupTask (callers get the exception when they await the stub). - // Nothing to do here. - } - - class NullGlobalOutboundChannel final: public IoChannelFactory::SubrequestChannel { - public: - kj::Own startRequest(IoChannelFactory::SubrequestMetadata metadata) override { - JSG_FAIL_REQUIRE(Error, - "This worker is not permitted to access the internet via global functions like fetch(). " - "It must use capabilities (such as bindings in 'env') to talk to the outside world."); - } - - void requireAllowsTransfer() override { - // It's difficult to get here, because the null outbound is not normally something you can - // reference. That said, it is possible to get a `Fetcher` representing the `next` outbound - // by pulling it off an incoming `Request` object, and in practice that points to the same - // thing as the null outbound. You could then try to transfer it. - // - // We disallow this for now because it's not clear why it would be needed. That said, if it - // is needed for some reason, it wouldn't be hard to support. But we might want to change - // the error message it throws from startRequest(), since the error would be somewhat - // misleading after the channel has been transferred. - JSG_FAIL_REQUIRE(DOMDataCloneError, "The null global outbound is not transferrable."); - } - - kj::OneOf, kj::Promise>> getTokenMaybeSync( - IoChannelFactory::ChannelTokenUsage usage) override { - JSG_FAIL_REQUIRE(DOMDataCloneError, "The null global outbound is not transferrable."); - } - }; - - class WorkerStubImpl final: public WorkerStubChannel { - public: - WorkerStubImpl(Server& server, - kj::String isolateName, - kj::Maybe> onAborted, - kj::Function()> fetchSource) - : onAborted(kj::mv(onAborted)), - startupTask(start(server, kj::mv(isolateName), kj::mv(fetchSource)).fork()), - cleanupTaskSet(server.tasks) {} - - // Returns a branch of the startup task promise. Used by the namespace to - // hold an extra reference to unnamed stubs until startup completes. - kj::Promise whenStartupDone() { - return startupTask.addBranch(); - } - - ~WorkerStubImpl() { - // Defer unlink and destruction of `WorkerService` to the next turn of the event loop. This - // is needed because worker stubs are typically destroyed while some other isolate is - // current, and so we cannot enter the dynamic worker's isolate to tear it down. It's even - // possible that the stub is held by an IoContext that is inside the dynamic isolate itself - // (particularly happens when using ctx.restore()) in which case unlinking it synchronously - // would likely lead to promise self-cancellation. - // - // However, don't do this if we're already unlinked, as in this case we're likely in the - // midst of destroying the `Server` and `cleanupTaskSet` may already be destroyed. - if (!unlinked) { - KJ_IF_SOME(s, service) { - cleanupTaskSet.add(kj::evalLater([service = kj::mv(s)]() mutable { service->unlink(); })); - } - } - } - - void unlink() { - KJ_IF_SOME(s, service) { - s->unlink(); - } - unlinked = true; - } - - kj::Rc getEntrypointResolved( - kj::Maybe name, Frankenvalue props, kj::Maybe limits) override { - return kj::rc(addRefToThis(), kj::mv(name), kj::mv(props)); - } - - kj::Rc getActorClassResolved( - kj::Maybe name, Frankenvalue props, kj::Maybe limits) override { - return kj::rc(addRefToThis(), kj::mv(name), kj::mv(props)); - } - - private: - // Callback to remove the worker stub from the isolates map. None for - // unnamed dynamic isolates. - kj::Maybe> onAborted; - - kj::Maybe> service; // null if still starting up - kj::ForkedPromise startupTask; // resolves when `service` is non-null - - kj::TaskSet& cleanupTaskSet; - bool unlinked = false; - - void onAbortIsolate() { - KJ_IF_SOME(cb, onAborted) { - auto callback = kj::mv(cb); - onAborted = kj::none; - callback(); - } - } - - kj::Promise start(Server& server, - kj::String isolateName, - kj::Function()> fetchSource) { - auto source = co_await fetchSource(); - co_await source.ensureAllResolved(); - static const kj::HashMap EMPTY_ACTOR_CONFIGS; - - // Rewrite the capabilities in `env` in order to build the I/O channel table. - kj::Vector subrequestChannels; - kj::Vector actorClassChannels; - kj::Vector> rpcChannels; - source.env.rewriteCaps([&](kj::Own entry) { - KJ_IF_SOME(channel, kj::tryDowncast(*entry)) { - uint channelNumber = - subrequestChannels.size() + IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT; - subrequestChannels.add(FutureSubrequestChannel{ - .designator = channel.addRef(), - .errorContext = kj::str("Worker's env"), - }); - return kj::heap( - IoChannelCapTableEntry::SUBREQUEST, channelNumber); - } else KJ_IF_SOME(channel, kj::tryDowncast(*entry)) { - uint channelNumber = actorClassChannels.size(); - actorClassChannels.add(FutureActorClassChannel{ - .designator = channel.addRef(), - .errorContext = kj::str("Worker's env"), - }); - return kj::heap( - IoChannelCapTableEntry::ACTOR_CLASS, channelNumber); - } else KJ_IF_SOME(channel, kj::tryDowncast(*entry)) { - uint channelNumber = rpcChannels.size(); - rpcChannels.add(channel.addRef()); - return kj::heap(IoChannelCapTableEntry::RPC, channelNumber); - } else { - // Generally, it shouldn't be possible to get here, but just in case, let's at least - // provide some sort of error, although it's a vague one. - JSG_FAIL_REQUIRE(DOMDataCloneError, - "Dynamic 'env' contains one or more objects that are not supported for use in " - "'env', although they would be supported in 'props'."); - } - }); - - WorkerDef def{ - .featureFlags = source.compatibilityFlags, - .source = kj::mv(source.source), - .moduleFallback = kj::none, - .localActorConfigs = EMPTY_ACTOR_CONFIGS, - .isDynamic = true, - - // clang-format off - .globalOutbound{ - .designator = kj::mv(source.globalOutbound) - .orDefault([]() -> kj::Rc { - return kj::rc(); - }), - .errorContext = kj::str("Worker's globalOutbound"), - }, - - .subrequestChannels = kj::mv(subrequestChannels), - .actorClassChannels = kj::mv(actorClassChannels), - .rpcChannels = kj::mv(rpcChannels), - - .tails = KJ_MAP(tail, source.tails) -> FutureSubrequestChannel { - return { - .designator = kj::mv(tail), - .errorContext = kj::str("Worker's tail"), - }; - }, - .streamingTails = KJ_MAP(tail, source.streamingTails) -> FutureSubrequestChannel { - return { - .designator = kj::mv(tail), - .errorContext = kj::str("Worker's streaming tail"), - }; - }, - - .compileBindings = [env = kj::mv(source.env)]( - jsg::Lock& js, const Worker::Api& api, v8::Local target) mutable { - env.populateJsObject(js, jsg::JsObject(target)); - }, - - // Note here that we always keep the ownContent from the source, even if - // ownContentIsRpcResponse is true. This is safe in workerd because we - // are single-threaded here and we don't need to worry about the cross-thread - // ownership issues. For the downstream use, however, we need to be careful - // to not copy the ownContent if it is an RPC response. - .maybeOwnedSourceCode = kj::mv(source.ownContent), - // The callback is owned by the WorkerService, which is owned by `this`, so a raw - // pointer is safe. - .abortIsolateCallback = kj::Function([this]() { onAbortIsolate(); }), - // clang-format on - }; - - DynamicErrorReporter errorReporter; - - auto service = co_await server.makeWorkerImpl(isolateName, kj::mv(def), {}, errorReporter); - errorReporter.throwIfErrors(); - - service->link(errorReporter); - service->linkActorNamespaces(errorReporter); - errorReporter.throwIfErrors(); - - this->service = kj::mv(service); - } - - class SubrequestChannelImpl final: public IoChannelFactory::SubrequestChannel { - public: - SubrequestChannelImpl( - kj::Rc isolate, kj::Maybe entrypointName, Frankenvalue props) - : isolate(kj::mv(isolate)), - entrypointName(kj::mv(entrypointName)), - props(kj::mv(props)) {} - - kj::Own startRequest( - IoChannelFactory::SubrequestMetadata metadata) override { - if (isolate->service == kj::none) { - // Capture a refcounted reference rather than a raw `this` pointer so that the - // SubrequestChannelImpl is kept alive until the startup task resolves, even if the - // owning Fetcher is garbage-collected while the deferred promise is pending. - return newPromisedWorkerInterface(isolate->startupTask.addBranch().then( - [self = kj::addRef(*this), metadata = kj::mv(metadata)]() mutable { - return self->startRequestImpl(kj::mv(metadata)); - })); - } else { - return startRequestImpl(kj::mv(metadata)); - } - } - - void requireAllowsTransfer() override { - throwDynamicEntrypointTransferError(); - } - - kj::OneOf, kj::Promise>> getTokenMaybeSync( - IoChannelFactory::ChannelTokenUsage usage) override { - throwDynamicEntrypointTransferError(); - } - - private: - kj::Rc isolate; - kj::Maybe entrypointName; - Frankenvalue props; // moved away when `entrypointService` is initialized - - kj::Maybe> entrypointService; - - kj::Own startRequestImpl(IoChannelFactory::SubrequestMetadata metadata) { - auto& service = KJ_ASSERT_NONNULL(isolate->service); - if (entrypointService == kj::none) { - entrypointService = service->getEntrypoint(entrypointName, kj::mv(props)); - } - KJ_IF_SOME(ep, entrypointService) { - // Attach a refcounted reference to `this` (SubrequestChannelImpl) to the returned - // WorkerInterface. This keeps the SubrequestChannelImpl alive for the duration of - // the request, which in turn keeps the WorkerStubImpl alive (via Rc), preventing - // WorkerStubImpl::unlink() from destroying the WorkerService's I/O channels while - // the request's IoContext still holds raw pointers to the WorkerService. - // - // Without this, if the JS Fetcher object is garbage-collected mid-request (e.g. - // because it was a temporary expression), the SubrequestChannelImpl is destroyed, - // the WorkerStubImpl refcount drops to zero, unlink() clears the WorkerService's - // LinkedIoChannels, and the child worker's IoContext crashes accessing them. - return ep->startRequest(kj::mv(metadata)).attach(kj::addRef(*this)); - } else { - KJ_IF_SOME(en, entrypointName) { - JSG_FAIL_REQUIRE(Error, "Worker has no such entrypoint: ", en); - } else { - JSG_FAIL_REQUIRE(Error, "Worker has no default entrypoint."); - } - } - } - }; - - class ActorClassImpl final: public ActorClass { - public: - ActorClassImpl( - kj::Rc isolate, kj::Maybe entrypointName, Frankenvalue props) - : isolate(kj::mv(isolate)), - entrypointName(kj::mv(entrypointName)), - props(kj::mv(props)) {} - - void requireAllowsTransfer() override { - throwDynamicEntrypointTransferError(); - } - - kj::OneOf, kj::Promise>> getTokenMaybeSync( - IoChannelFactory::ChannelTokenUsage usage) override { - throwDynamicEntrypointTransferError(); - } - - kj::Maybe> whenReady() override { - if (inner != kj::none) return kj::none; - - KJ_IF_SOME(service, isolate->service) { - inner = service->getActorClass(entrypointName, kj::mv(props)); - return kj::none; - } - - // Have to wait for the isolate to start up. Capture a refcounted reference rather than - // a raw `this` pointer so that the ActorClassImpl stays alive until the startup task - // resolves, even if the owning object is garbage-collected while waiting. - return isolate->startupTask.addBranch().then([self = kj::addRef(*this)]() mutable { - if (self->inner == kj::none) { - self->inner = KJ_ASSERT_NONNULL(self->isolate->service) - ->getActorClass(self->entrypointName, kj::mv(self->props)); - } - }); - } - - kj::Own newActor(kj::Maybe tracker, - Worker::Actor::Id actorId, - Worker::Actor::MakeActorCacheFunc makeActorCache, - Worker::Actor::MakeStorageFunc makeStorage, - kj::Own loopback, - kj::Maybe> manager, - kj::Maybe container, - jsg::Dict containerImages, - kj::Maybe facetManager) override { - return getInner().newActor(tracker, kj::mv(actorId), kj::mv(makeActorCache), - kj::mv(makeStorage), kj::mv(loopback), kj::mv(manager), kj::mv(container), - kj::mv(containerImages), facetManager); - } - - kj::Own startRequest( - IoChannelFactory::SubrequestMetadata metadata, kj::Own actor) override { - return getInner().startRequest(kj::mv(metadata), kj::mv(actor)); - } - - private: - kj::Rc isolate; - kj::Maybe entrypointName; - Frankenvalue props; // moved away when `inner` is initialized - - kj::Maybe> inner; - - ActorClass& getInner() { - return *KJ_ASSERT_NONNULL( - inner, "ActorClassChannel is not ready yet; should have awaited whenReady()"); - } - }; - }; -}; - -void Server::unlinkWorkerLoaders() { - for (auto& loader: workerLoaderNamespaces) { - loader.value->unlink(); - } - for (auto& loader: anonymousWorkerLoaderNamespaces) { - loader->unlink(); - } -} - -kj::Own Server::WorkerService::loadIsolate(uint loaderChannel, - kj::Maybe name, - kj::Function()> fetchSource) { - auto& channels = - KJ_REQUIRE_NONNULL(ioChannels.tryGet(), "link() has not been called"); - KJ_REQUIRE(loaderChannel < channels.workerLoaders.size(), "invalid worker loader channel number"); - - return channels.workerLoaders[loaderChannel]->loadIsolate(kj::mv(name), kj::mv(fetchSource)); -} - -static MainModuleIsPython isPythonMainModule(config::Worker::Reader conf) { - if (!conf.isModules()) { - // Service workers syntax has no main module. - return MainModuleIsPython::NO; - } - auto modules = conf.getModules(); - if (modules.size() == 0) { - // An empty module list is a config error, reported elsewhere. - return MainModuleIsPython::NO; - } - // The first module is the main module. - return modules[0].isPythonModule() ? MainModuleIsPython::YES : MainModuleIsPython::NO; -} - -kj::Promise> Server::makeWorker(kj::StringPtr name, - config::Worker::Reader conf, - capnp::List::Reader extensions) { - TRACE_EVENT("workerd", "Server::makeWorker()", "name", name.cStr()); - auto& localActorConfigs = KJ_ASSERT_NONNULL(actorConfigs.find(name)); - - ConfigErrorReporter errorReporter(*this, name); - - capnp::MallocMessageBuilder arena; - // TODO(beta): Factor out FeatureFlags from WorkerBundle. - auto featureFlags = arena.initRoot(); - - KJ_IF_SOME(overrideDate, testCompatibilityDateOverride) { - // When testCompatibilityDateOverride is set, the config must NOT specify compatibilityDate. - if (conf.hasCompatibilityDate()) { - errorReporter.addError(kj::str( - "Worker specifies compatibilityDate but --compat-date was provided. " - "When using --compat-date, workers must not specify compatibilityDate in the config. " - "Use compatibilityFlags to enable/disable specific flags if needed.")); - } - // Use FUTURE_FOR_TEST to allow any valid date (including far future like 2999-12-31) - // without validation against CODE_VERSION or current date. - compileCompatibilityFlags(overrideDate, conf.getCompatibilityFlags(), featureFlags, - errorReporter, experimental, CompatibilityDateValidation::FUTURE_FOR_TEST, nullptr, - isPythonMainModule(conf)); - } else if (conf.hasCompatibilityDate()) { - compileCompatibilityFlags(conf.getCompatibilityDate(), conf.getCompatibilityFlags(), - featureFlags, errorReporter, experimental, CompatibilityDateValidation::CODE_VERSION, - nullptr, isPythonMainModule(conf)); - } else { - errorReporter.addError(kj::str("Worker must specify compatibilityDate.")); - } - - kj::Vector subrequestChannels; - kj::Vector actorChannels; - kj::Vector actorClassChannels; - kj::Vector workerLoaderChannels; - bool hasWorkerdDebugPortBinding = false; - - auto confBindings = conf.getBindings(); - kj::Vector globals(confBindings.size()); - for (auto binding: confBindings) { - KJ_IF_SOME(global, - createBinding(name, conf, binding, errorReporter, subrequestChannels, actorChannels, - actorClassChannels, workerLoaderChannels, hasWorkerdDebugPortBinding, actorConfigs, - experimental)) { - globals.add(kj::mv(global)); - } - } - - // Allocate one subrequest channel per configured Workflow, targeting that Workflow's - // `bindingService`. These channels become the inner fetcher of the wrapped `ctx.exports` binding - // built later in `makeWorkerImpl()`; the class->channel map lets that step find the right channel. - // We only allocate a channel for Workflows whose synthetic namespace config actually exists (it - // was validated and created during `startServices()`); duplicates by class or name are skipped so - // numbering stays in lockstep with the ctx.exports build loop. - kj::HashMap workflowBindingChannels; - if (conf.hasWorkflowsEngine()) { - kj::HashSet workflowClasses; - kj::HashSet workflowNames; - for (auto workflow: conf.getWorkflowsEngine().getWorkflows()) { - auto className = workflow.getClassName(); - auto workflowName = workflow.getName(); - if (workflowClasses.contains(className) || workflowNames.contains(workflowName)) continue; - workflowClasses.insert(kj::str(className)); - workflowNames.insert(kj::str(workflowName)); - - auto namespaceKey = kj::str(WORKFLOW_NAMESPACE_KEY_PREFIX, workflowName); - auto& actorConfig = KJ_UNWRAP_OR(localActorConfigs.find(namespaceKey), continue); - auto& durable = KJ_UNWRAP_OR(actorConfig.tryGet(), continue); - if (!durable.isWorkflow) continue; - - uint channel = static_cast(subrequestChannels.size()) + - IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT; - subrequestChannels.add(FutureSubrequestChannel{workflow.getBindingService(), - kj::str("Worker \"", name, "\"'s Workflow \"", workflowName, "\"'s bindingService")}); - workflowBindingChannels.insert(kj::str(className), channel); - } - } - - // Construct `WorkerDef` from `conf`. - WorkerDef def{ - .featureFlags = featureFlags.asReader(), - .source = WorkerdApi::extractSource(name, conf, featureFlags.asReader(), errorReporter), - .moduleFallback = conf.hasModuleFallback() ? kj::some(conf.getModuleFallback()) : kj::none, - .localActorConfigs = localActorConfigs, - .isDynamic = false, - - .globalOutbound{ - .designator = conf.getGlobalOutbound(), - .errorContext = kj::str("Worker \"", name, "\"'s globalOutbound"), - }, - - .cacheApiOutbound = conf.hasCacheApiOutbound() - ? kj::some(FutureSubrequestChannel{ - .designator = conf.getCacheApiOutbound(), - .errorContext = kj::str("Worker \"", name, "\"'s cacheApiOutbound"), - }) - : kj::none, - - .subrequestChannels = kj::mv(subrequestChannels), - .workflowBindingChannels = kj::mv(workflowBindingChannels), - .actorChannels = kj::mv(actorChannels), - .actorClassChannels = kj::mv(actorClassChannels), - .workerLoaderChannels = kj::mv(workerLoaderChannels), - .hasWorkerdDebugPortBinding = hasWorkerdDebugPortBinding, - - // clang-format off - .tails = KJ_MAP(tail, conf.getTails()) -> FutureSubrequestChannel { - return { - .designator = tail, - .errorContext = kj::str("Worker \"", name, "\"'s tails"), - }; - }, - - .streamingTails = KJ_MAP(streamingTail, conf.getStreamingTails()) -> FutureSubrequestChannel { - return { - .designator = streamingTail, - .errorContext = kj::str("Worker \"", name, "\"'s streaming tails"), - }; - }, - - .actorStorageConf = conf.getDurableObjectStorage(), - .containerEngineConf = conf.getContainerEngine(), - - .compileBindings = [globals = kj::mv(globals)]( - jsg::Lock& lock, const Worker::Api& api, v8::Local target) { - return WorkerdApi::from(api).compileGlobals(lock, globals, target, 1); - }, - // clang-format on - - .accessBlobHeaderName = [&]() -> kj::Maybe { - if (!conf.hasAccessBlobHeader()) return kj::none; - if (!experimental) { - errorReporter.addError(kj::str("Worker \"", name, - "\" has accessBlobHeader configured but this is an experimental " - "feature. You must run workerd with `--experimental` to use this feature.")); - return kj::none; - } - return kj::str(conf.getAccessBlobHeader()); - }(), - - .accessBindingServiceDesignator = [&]() -> kj::Maybe { - if (!conf.hasAccessBindingService()) return kj::none; - return conf.getAccessBindingService(); - }(), - }; - - co_return co_await makeWorkerImpl(name, kj::mv(def), extensions, errorReporter); -} - -kj::Own Server::makeWorkerIsolate(kj::StringPtr name, - kj::StringPtr inboundListenersKey, - const WorkerDef& def, - capnp::List::Reader extensions, - Worker::Isolate::InspectorPolicy inspectorPolicy, - kj::Maybe snapshotConfig) { - auto jsgobserver = kj::atomicRefcounted(); - auto observer = kj::atomicRefcounted(); - auto limitEnforcer = kj::refcounted(); - auto isolateGroup = jsg::newIsolateGroup(); - - kj::Array listeners; - KJ_IF_SOME(l, inboundListeners.find(inboundListenersKey)) { - listeners = KJ_MAP(listener, l) { - return Worker::Api::InboundListener{ - .protocol = kj::str(listener.protocol), - .address = kj::str(listener.address), - .port = listener.port, - }; - }; - } - - auto api = kj::heap(globalContext->v8System, def.featureFlags, extensions, - limitEnforcer->getCreateParams(), isolateGroup, kj::mv(jsgobserver), *memoryCacheProvider, - pythonConfig, kj::mv(listeners), kj::mv(snapshotConfig)); - - Worker::LoggingOptions isolateLoggingOptions = loggingOptions; - isolateLoggingOptions.consoleMode = def.source.variant.is() && - !isNewModuleRegistryEnabled(def.featureFlags) - ? Worker::ConsoleMode::INSPECTOR_ONLY - : loggingOptions.consoleMode; - - return kj::atomicRefcounted(kj::mv(api), kj::mv(observer), name, - kj::mv(limitEnforcer), inspectorPolicy, kj::mv(isolateLoggingOptions)); -} - -namespace { - -bool hasWasmModules(const WorkerSource& source) { - KJ_IF_SOME(modules, source.variant.tryGet()) { - for (auto& module: modules.modules) { - if (module.content.is()) return true; - } - } - return false; -} - -bool supportsStartupSnapshot(CompatibilityFlags::Reader featureFlags, const WorkerSource& source) { - return !featureFlags.getPythonWorkers() && !featureFlags.getNewModuleRegistry() && - !source.variant.is() && !hasWasmModules(source); -} - -} // namespace - -kj::Maybe> Server::makeSnapshot( - kj::StringPtr name, WorkerDef& def, capnp::List::Reader extensions) { - // Build a throwaway zygote Worker in PREPARE_SNAPSHOT mode just to extract a V8 startup - // snapshot; the caller then builds the real Worker in START_FROM_SNAPSHOT mode using it. - KJ_REQUIRE( - supportsStartupSnapshot(def.featureFlags, def.source), "snapshot PoC: unsupported Worker"); - - // The zygote reports into a reporter of its own: its failures must never surface as the - // Worker's. - DynamicErrorReporter zygoteErrors; - - auto snapshotArtifact = kj::atomicRefcounted(); - auto zygoteName = kj::str(name, "-snapshot"); - auto zygoteIsolate = makeWorkerIsolate(zygoteName, name, def, extensions, - Worker::Isolate::InspectorPolicy::DISALLOW, - jsg::SnapshotConfig(jsg::MutableSnapshot{.artifact = kj::mv(snapshotArtifact)})); - - auto zygoteWorkerFs = newWorkerFileSystem(kj::heap(), getBundleDirectory(def.source)); - auto zygoteArtifactBundler = workerd::api::pyodide::ArtifactBundler::makeDisabledBundler(); - - auto zygoteScript = zygoteIsolate->newScript(name, def.source, IsolateObserver::StartType::COLD, - SpanParent(nullptr), kj::mv(zygoteWorkerFs), false, zygoteErrors, - kj::mv(zygoteArtifactBundler)); - - // Same as for a regular worker, except we ignore ctxExports: pinning it would create a - // v8::Global that outlives the zygote isolate and breaks snapshot creation. - auto zygoteCompileBindings = - [&](jsg::Lock& lock, const Worker::Api& api, v8::Local target, - v8::Local /*ctxExports*/) { def.compileBindings(lock, api, target); }; - - auto zygoteWorker = - kj::atomicRefcounted(kj::mv(zygoteScript), kj::atomicRefcounted(), - kj::mv(zygoteCompileBindings), IsolateObserver::StartType::COLD, SpanParent(nullptr), - Worker::Lock::TakeSynchronously(kj::none), zygoteErrors); - - if (!zygoteErrors.errors.empty()) { - auto errors = kj::strArray(zygoteErrors.errors, "\n"); - KJ_LOG(INFO, "startup snapshot skipped: the zygote Worker failed to start", name, errors); - return kj::none; - } - - kj::Own extractedArtifact; - zygoteIsolate->runInLockScope(Worker::Lock::TakeSynchronously(kj::none), [&](jsg::Lock& lock) { - extractedArtifact = jsg::IsolateBase::from(lock.v8Isolate).extractSnapshotArtifact(); - }); - return kj::mv(extractedArtifact); -} - -kj::Promise> Server::makeWorkerImpl(kj::StringPtr name, - WorkerDef def, - capnp::List::Reader extensions, - ErrorReporter& errorReporter) { - // Load Python artifacts if this is a Python worker. - co_await preloadPython(name, def, errorReporter); - - // Create the FsMap that will be used to map known file system - // roots to configurable locations. - // TODO(node-fs): This is set up to allow users to configure the "mount" - // points for known roots but we currently do not expose that in the - // config. So for now this just uses the defaults. - auto workerFs = newWorkerFileSystem(kj::heap(), getBundleDirectory(def.source)); - - // Note: Python workers do not support the new module registry; - // isNewModuleRegistryEnabled() returns false for them regardless of the - // new_module_registry flag, so they always take the legacy path below. - bool usingNewModuleRegistry = isNewModuleRegistryEnabled(def.featureFlags); - kj::Maybe> newModuleRegistry; - if (usingNewModuleRegistry) { - KJ_REQUIRE(experimental, - "The new ModuleRegistry implementation is an experimental feature. " - "You must run workerd with `--experimental` to use this feature."); - - // We use the same path for modules that the virtual file system uses. - // For instance, if the user specifies a bundle path of "/foo/bar" and - // there is a module in the bundle at "/foo/bar/baz.js", then the module's - // import specifier url will be "file:///foo/bar/baz.js". - const jsg::Url& bundleBase = workerFs->getBundleRoot(); - - // In workerd the module registry is always associated with just a single - // worker instance, so we initialize it here. In production, however, a - // single instance may be shared across multiple replicas. - kj::Maybe maybeFallbackService; - KJ_IF_SOME(moduleFallback, def.moduleFallback) { - maybeFallbackService = kj::str(moduleFallback); - } - - using workerd::api::pyodide::ArtifactBundler; - - KJ_IF_SOME(exception, kj::runCatchingExceptions([&]() { - newModuleRegistry = WorkerdApi::newWorkerdModuleRegistry( - def.source.variant.tryGet(), def.featureFlags, - pythonConfig, bundleBase, extensions, kj::mv(maybeFallbackService), - ArtifactBundler::makeDisabledBundler()); - })) { - // Building the module registry from the worker's source failed. This is - // a worker configuration problem (e.g. duplicate or invalid module - // names, or python modules without the python_workers flag), so report - // it through the error reporter like any other config error rather than - // letting the exception abort server startup. To let worker - // construction proceed far enough for the error to be the one that is - // reported, substitute an inert empty script and a registry containing - // only the built-in modules -- the same stub extractSource() substitutes - // for source-level config errors. The worker never runs: config errors - // prevent the server from serving. - errorReporter.addError(kj::str(exception.getDescription())); - def.source = WorkerSource(Worker::Script::ScriptSource{""_kj, name, nullptr}); - newModuleRegistry = WorkerdApi::newWorkerdModuleRegistry(kj::none, def.featureFlags, - pythonConfig, bundleBase, capnp::List::Reader{}, kj::none, - ArtifactBundler::makeDisabledBundler()); - } - } - - const bool snapshotEnabled = util::Autogate::isEnabled(util::AutogateKey::STARTUP_SNAPSHOT) && - supportsStartupSnapshot(def.featureFlags, def.source); - kj::Maybe snapshotConfig; - if (snapshotEnabled) { - KJ_IF_SOME(snapshotArtifact, makeSnapshot(name, def, extensions)) { - // TODO(soon): use snapshot artefact. - (void)snapshotArtifact; - } - } - - auto inspectorPolicy = Worker::Isolate::InspectorPolicy::DISALLOW; - if (inspectorOverride != kj::none) { - // For workerd, if the inspector is enabled, it is always fully trusted. - inspectorPolicy = Worker::Isolate::InspectorPolicy::ALLOW_FULLY_TRUSTED; - } - auto isolate = - makeWorkerIsolate(name, name, def, extensions, inspectorPolicy, kj::mv(snapshotConfig)); - - // If we are using the inspector, we need to register the Worker::Isolate - // with the inspector service. - KJ_IF_SOME(isolateRegistrar, inspectorIsolateRegistrar) { - isolateRegistrar->registerIsolate(name, isolate.get()); - } - - if (!usingNewModuleRegistry) { - KJ_IF_SOME(moduleFallback, def.moduleFallback) { - KJ_REQUIRE(experimental, - "The module fallback service is an experimental feature. " - "You must run workerd with `--experimental` to use the module fallback service."); - // If the config has the moduleFallback option, then we are going to set up the ability - // to load certain modules from a fallback service. This is generally intended for local - // dev/testing purposes only. - auto& apiIsolate = isolate->getApi(); - auto fallbackClient = - kj::heap(kj::str(moduleFallback)); - apiIsolate.setModuleFallbackCallback( - [client = kj::mv(fallbackClient), featureFlags = apiIsolate.getFeatureFlags()]( - jsg::Lock& js, kj::StringPtr specifier, kj::Maybe referrer, - jsg::CompilationObserver& observer, jsg::ModuleRegistry::ResolveMethod method, - kj::Maybe rawSpecifier) mutable - -> kj::Maybe> { - kj::HashMap attributes; - KJ_IF_SOME(moduleOrRedirect, - client->tryResolve(workerd::fallback::Version::V1, - method == jsg::ModuleRegistry::ResolveMethod::IMPORT - ? workerd::fallback::ImportType::IMPORT - : workerd::fallback::ImportType::REQUIRE, - specifier, rawSpecifier.orDefault(nullptr), referrer.orDefault(kj::String()), - attributes)) { - KJ_SWITCH_ONEOF(moduleOrRedirect) { - KJ_CASE_ONEOF(redirect, kj::String) { - // If a string is returned, then the fallback service returned a 301 redirect. - // The value is the specifier of the new target module. - return kj::Maybe(kj::mv(redirect)); - } - KJ_CASE_ONEOF(module, kj::Own) { - KJ_IF_SOME(module, - WorkerdApi::tryCompileModule(js, *module, observer, featureFlags)) { - return kj::Maybe(kj::mv(module)); - } - KJ_LOG(ERROR, "Fallback service does not support this module type", module->which()); - } - } - } - - return kj::none; - }); - } - } - - using workerd::api::pyodide::ArtifactBundler; - auto artifactBundler = ArtifactBundler::makeDisabledBundler(); - - auto script = isolate->newScript(name, def.source, IsolateObserver::StartType::COLD, - SpanParent(nullptr), workerFs.attach(kj::mv(def.maybeOwnedSourceCode)), false, errorReporter, - kj::mv(artifactBundler), kj::mv(newModuleRegistry)); - - using Global = WorkerdApi::Global; - jsg::V8Ref ctxExportsHandle = nullptr; - auto compileBindings = [&](jsg::Lock& lock, const Worker::Api& api, v8::Local target, - v8::Local ctxExports) { - // We can't fill in ctx.exports yet because we need to run the validator first to discover - // entrypoints, which we cannot do until after the Worker constructor completes. We are - // permitted to hold a handle until then, though. - ctxExportsHandle = lock.v8Ref(ctxExports); - - return def.compileBindings(lock, api, target); - }; - auto worker = kj::atomicRefcounted(kj::mv(script), kj::atomicRefcounted(), - kj::mv(compileBindings), IsolateObserver::StartType::COLD, SpanParent(nullptr), - Worker::Lock::TakeSynchronously(kj::none), errorReporter); - - uint totalActorChannels = 0; - - worker->runInLockScope(Worker::Lock::TakeSynchronously(kj::none), [&](Worker::Lock& lock) { - lock.validateHandlers(errorReporter); - - // Build `ctx.exports` based on the entrypoints reported by `validateHandlers()`. - kj::Vector ctxExports( - errorReporter.namedEntrypoints.size() + def.localActorConfigs.size()); - - // Start numbering loopback channels for stateless entrypoints after the last subrequest - // channel used by bindings. - uint nextSubrequestChannel = - def.subrequestChannels.size() + IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT; - if (errorReporter.defaultEntrypoint != kj::none) { - ctxExports.add(Global{.name = kj::str("default"), - .value = Global::LoopbackServiceStub{.channel = nextSubrequestChannel++}}); - } - for (auto& ep: errorReporter.namedEntrypoints) { - // A configured Workflow is exposed on `ctx.exports` as a wrapped binding: the - // `cloudflare-internal:workflows-api` module wraps an inner `fetcher` pointing at the - // Workflow's `bindingService` channel, producing the public `Workflow` API. Exported - // `WorkflowEntrypoint` classes with no allocated channel (i.e. not listed under - // `workflowsEngine.workflows`) are intentionally omitted from `ctx.exports`. The inner - // fetcher uses requiresHost/!isInHouse so it behaves like a normal external service binding. - if (errorReporter.workflowClasses.contains(ep.key)) { - KJ_IF_SOME(channel, def.workflowBindingChannels.find(ep.key)) { - auto innerBindings = kj::heapArray(1); - innerBindings[0] = Global{.name = kj::str("fetcher"), - .value = Global::Fetcher{.channel = channel, .requiresHost = true, .isInHouse = false}}; - ctxExports.add(Global{.name = kj::str(ep.key), - .value = Global::Wrapped{.moduleName = kj::str("cloudflare-internal:workflows-api"), - .entrypoint = kj::str("default"), - .innerBindings = kj::mv(innerBindings)}}); - } - } else { - ctxExports.add(Global{.name = kj::str(ep.key), - .value = Global::LoopbackServiceStub{.channel = nextSubrequestChannel++}}); - } - } - - // Start numbering loopback channels for actor classes after the last actor channel and actor - // class channel used by bindings. Note that every exported actor class will have a ctx.exports - // entry, but only the ones that have storage configured will be namespace bindings; the others - // will be simply actor class bindings, which can be used with facets. We will iterate over - // the exported class names and cross-reference with the storage config. Note that if the - // storage config contains a class name that isn't among the exports, we won't create a - // ctx.exports entry for it (it wouldn't work anyway). - uint nextActorChannel = def.actorChannels.size(); - uint nextActorClassChannel = def.actorClassChannels.size(); - for (auto& className: errorReporter.actorClasses) { - uint actorClassChannel = nextActorClassChannel++; - - decltype(Global::value) value; - KJ_IF_SOME(ns, def.localActorConfigs.find(className)) { - // This class has storage attached. We'll create a loopback actor namespace binding. - KJ_SWITCH_ONEOF(ns) { - KJ_CASE_ONEOF(durable, Durable) { - value = Global::LoopbackDurableActorNamespace{ - .actorChannel = nextActorChannel++, - .uniqueKey = durable.uniqueKey, - .classChannel = actorClassChannel, - }; - } - KJ_CASE_ONEOF(ephemeral, Ephemeral) { - value = Global::LoopbackEphemeralActorNamespace{ - .actorChannel = nextActorChannel++, - .classChannel = actorClassChannel, - }; - } - } - } else { - // No storage attached. We'll create an actual class binding (for use with facets). - value = Global::LoopbackActorClass{.channel = actorClassChannel}; - } - ctxExports.add(Global{.name = kj::str(className), .value = kj::mv(value)}); - } - totalActorChannels = nextActorChannel; - - JSG_WITHIN_CONTEXT_SCOPE(lock, lock.getContext(), [&](jsg::Lock& js) { - WorkerdApi::from(worker->getIsolate().getApi()) - .compileGlobals(lock, ctxExports, ctxExportsHandle.getHandle(js), 1); - }); - - // As an optimization, drop this now while we have the lock. - { auto drop = kj::mv(ctxExportsHandle); } - }); - - // These fields are needed after def is moved into the linkCallback lambda, so they must be - // extracted beforehand. - auto abortIsolateCallback = kj::mv(def.abortIsolateCallback); - auto accessBlobHeaderName = kj::mv(def.accessBlobHeaderName); - auto containerEngineConf = def.containerEngineConf; - bool isDynamic = def.isDynamic; - auto& localActorConfigs = def.localActorConfigs; - auto actorStorageIsLocalDisk = ActorStorageIsLocalDisk(def.actorStorageConf.isLocalDisk()); - - auto linkCallback = [this, def = kj::mv(def), totalActorChannels](WorkerService& workerService, - Worker::ValidationErrorReporter& errorReporter) mutable { - WorkerService::LinkedIoChannels result; - - auto entrypointNames = workerService.getEntrypointNames(); - auto actorClassNames = workerService.getActorClassNames(); - // Workflow classes don't get a self-referential loopback channel here: on `ctx.exports` they - // are exposed as wrapped Workflow bindings targeting their bindingService, not plain loopback - // entrypoints. This skip must stay in lockstep with the channel-adding loop below so the - // reserved count matches the channels actually appended. - uint loopbackEntrypointCount = 0; - for (auto& ep: entrypointNames) { - if (!workerService.hasWorkflowClass(ep)) ++loopbackEntrypointCount; - } - - bool hasAccessBinding = def.accessBindingServiceDesignator != kj::none; - auto services = kj::heapArrayBuilder>( - def.subrequestChannels.size() + IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT + - loopbackEntrypointCount + workerService.hasDefaultEntrypoint() + - (hasAccessBinding ? 1 : 0)); - - auto globalService = kj::mv(def.globalOutbound).lookup(*this); - - // Bind both "next" and "null" to the global outbound. (The difference between these is a - // legacy artifact that no one should be depending on.) - static_assert(IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT == 2); - services.add(globalService->addRef()); - services.add(kj::mv(globalService)); - - for (auto& channel: def.subrequestChannels) { - services.add(kj::mv(channel).lookup(*this)); - } - - // Link the ctx.exports self-referential channels. Note that it's important these are added - // in exactyl the same order as the channels were allocated earlier when we compiled the - // ctx.exports bindings. - if (workerService.hasDefaultEntrypoint()) { - services.add(workerService.getLoopbackEntrypoint(/*name=*/kj::none)); - } - for (auto& ep: entrypointNames) { - // Skip Workflow classes here for the same reason as the count loop above, keeping the two in - // agreement so channel indices line up with what was reserved. - if (!workerService.hasWorkflowClass(ep)) { - services.add(workerService.getLoopbackEntrypoint(ep)); - } - } - - // Add the access binding service as a subrequest channel slot. Per-request props (aud, - // jwtClaims) are injected by startSubrequest() at dispatch time, mirroring the production - // channel-token mechanism. getLoopbackEntrypoint() creates an un-specialized EntrypointService - // (props = kj::none) that supports per-request forProps() calls. - KJ_IF_SOME(designator, def.accessBindingServiceDesignator) { - kj::StringPtr targetName = designator.getName(); - KJ_IF_SOME(svc, this->services.find(targetName)) { - KJ_IF_SOME(worker, kj::tryDowncast(*svc)) { - kj::Maybe epName; - if (designator.hasEntrypoint()) { - epName = designator.getEntrypoint(); - } - auto channelIndex = static_cast(services.size()); - services.add(worker.getLoopbackEntrypoint(epName)); - workerService.setAccessBindingServiceChannel(channelIndex); - } else { - reportConfigError(kj::str("Worker accessBindingService refers to service \"", targetName, - "\", but it is not a Worker.")); - } - } else { - reportConfigError(kj::str("Worker accessBindingService refers to a service \"", targetName, - "\", but no such service is defined.")); - } - } - - result.subrequest = services.finish(); - - // Set up actor class channels - auto actorClasses = kj::heapArrayBuilder>( - def.actorClassChannels.size() + actorClassNames.size()); - - for (auto& channel: def.actorClassChannels) { - actorClasses.add(kj::mv(channel).lookup(*this)); - } - - auto linkedActorChannels = kj::heapArrayBuilder>(totalActorChannels); - - for (auto& channel: def.actorChannels) { - WorkerService* targetService = &workerService; - if (channel.designator.hasServiceName()) { - auto& svc = KJ_UNWRAP_OR(this->services.find(channel.designator.getServiceName()), { - // error was reported earlier - linkedActorChannels.add(kj::none); - continue; - }); - targetService = &KJ_UNWRAP_OR(kj::tryDowncast(*svc), { - // error was reported earlier - linkedActorChannels.add(kj::none); - continue; - }); - } - - // (If getActorNamespace() returns null, an error was reported earlier.) - linkedActorChannels.add(targetService->getActorNamespace(channel.designator.getClassName())); - }; - - // Link the ctx.exports self-referential actor channels. Again, it's important that these - // be added in the same order as before. kj::HashMap iteration order is deterministic, and - // is exactly insertion order as long as no entries have been removed, so we can expect that - // `workerService.getActorClassNames()` iterates in the same order as - // `errorReporter.actorClasses` did earlier. As before, every exported class gets an actor - // class channel, but only the ones with configured storage will also get namespace channels. - auto& selfActorNamespaces = workerService.getActorNamespaces(); - for (auto& className: actorClassNames) { - actorClasses.add(workerService.getLoopbackActorClass(className)); - KJ_IF_SOME(ns, selfActorNamespaces.find(className)) { - linkedActorChannels.add(*ns); - } - } - - result.actor = linkedActorChannels.finish(); - result.actorClass = actorClasses.finish(); - result.rpc = kj::mv(def.rpcChannels).releaseAsArray(); - - KJ_IF_SOME(out, def.cacheApiOutbound) { - result.cache = kj::mv(out).lookup(*this); - } - - if (def.actorStorageConf.isLocalDisk()) { - kj::StringPtr diskName = def.actorStorageConf.getLocalDisk(); - KJ_IF_SOME(svc, this->services.find(def.actorStorageConf.getLocalDisk())) { - KJ_IF_SOME(diskSvc, kj::tryDowncast(*svc)) { - KJ_IF_SOME(dir, diskSvc.getWritable()) { - result.actorStorage = dir; - } else { - errorReporter.addError( - kj::str("durableObjectStorage config refers to the disk service \"", diskName, - "\", but that service is defined read-only.")); - } - } else { - errorReporter.addError(kj::str("durableObjectStorage config refers to the service \"", - diskName, "\", but that service is not a local disk service.")); - } - } else { - errorReporter.addError(kj::str("durableObjectStorage config refers to a service \"", - diskName, "\", but no such service is defined.")); - } - } - - result.tails = KJ_MAP(tail, def.tails) { return kj::mv(tail).lookup(*this); }; - - result.streamingTails = KJ_MAP(tail, def.streamingTails) { return kj::mv(tail).lookup(*this); }; - - result.workerLoaders = KJ_MAP(il, def.workerLoaderChannels) { - KJ_IF_SOME(id, il.id) { - return workerLoaderNamespaces - .findOrCreate(id, [&]() -> decltype(workerLoaderNamespaces)::Entry { - return { - .key = kj::mv(id), - .value = kj::rc(*this, kj::mv(il.name)), - }; - }).addRef(); - } else { - return anonymousWorkerLoaderNamespaces - .add(kj::rc(*this, kj::mv(il.name))) - .addRef(); - } - }; - - if (def.hasWorkerdDebugPortBinding) { - result.workerdDebugPortNetwork = network; - result.workerdDebugPortServer = *this; - } - - return result; - }; - - kj::Maybe dockerPath = kj::none; - kj::Maybe containerEgressInterceptorImage = kj::none; - switch (containerEngineConf.which()) { - case config::Worker::ContainerEngine::NONE: - // No container engine configured - break; - case config::Worker::ContainerEngine::LOCAL_DOCKER: { - auto dockerConf = containerEngineConf.getLocalDocker(); - dockerPath = kj::str(dockerConf.getSocketPath()); - if (dockerConf.hasContainerEgressInterceptorImage()) { - containerEgressInterceptorImage = kj::str(dockerConf.getContainerEgressInterceptorImage()); - } - break; - } - } - - kj::Maybe serviceName; - if (!isDynamic) serviceName = name; - - auto result = - kj::rc(channelTokenHandler, serviceName, globalContext->threadContext, - monotonicClock, kj::mv(worker), kj::mv(errorReporter.defaultEntrypoint), - kj::mv(errorReporter.namedEntrypoints), kj::mv(errorReporter.actorClasses), - kj::mv(errorReporter.workflowClasses), kj::mv(linkCallback), - KJ_BIND_METHOD(*this, abortAllActors), KJ_BIND_METHOD(*this, deleteAllActors), - kj::mv(dockerPath), kj::mv(containerEgressInterceptorImage), actorStorageIsLocalDisk, - isDynamic, kj::mv(abortIsolateCallback), kj::mv(accessBlobHeaderName)); - result->initActorNamespaces(localActorConfigs, actorNamespacesByUniqueKey, network); - co_return result; -} - -// ======================================================================================= - -kj::Promise> Server::makeService(config::Service::Reader conf, - kj::HttpHeaderTable::Builder& headerTableBuilder, - capnp::List::Reader extensions) { - kj::StringPtr name = conf.getName(); - - switch (conf.which()) { - case config::Service::UNSPECIFIED: - reportConfigError(kj::str("Service named \"", name, "\" does not specify what to serve.")); - co_return makeInvalidConfigService(); - - case config::Service::EXTERNAL: - co_return makeExternalService(name, conf.getExternal(), headerTableBuilder); - - case config::Service::NETWORK: - co_return makeNetworkService(conf.getNetwork()); - - case config::Service::WORKER: - co_return co_await makeWorker(name, conf.getWorker(), extensions); - - case config::Service::DISK: - co_return makeDiskDirectoryService(name, conf.getDisk(), headerTableBuilder); - } - - reportConfigError(kj::str("Service named \"", name, - "\" has unrecognized type. Was the config compiled with a " - "newer version of the schema?")); - co_return makeInvalidConfigService(); -} - -void Server::taskFailed(kj::Exception&& exception) { - fatalFulfiller->reject(kj::mv(exception)); -} - -kj::Rc Server::lookupService( - config::ServiceDesignator::Reader designator, kj::String errorContext) { - kj::StringPtr targetName = designator.getName(); - Service* service = KJ_UNWRAP_OR(services.find(targetName), { - reportConfigError(kj::str(errorContext, " refers to a service \"", targetName, - "\", but no such service is defined.")); - return invalidConfigServiceSingleton.addRef(); - }).get(); - - kj::Maybe entrypointName; - if (designator.hasEntrypoint()) { - entrypointName = designator.getEntrypoint(); - } - - auto props = [&]() -> Frankenvalue { - auto props = designator.getProps(); - switch (props.which()) { - case config::ServiceDesignator::Props::EMPTY: - return {}; - case config::ServiceDesignator::Props::JSON: - return Frankenvalue::fromJson(kj::str(props.getJson())); - } - reportConfigError(kj::str(errorContext, - " has unrecognized props type. Was the config compiled with a " - "newer version of the schema?")); - return {}; - }(); - - KJ_IF_SOME(worker, kj::tryDowncast(*service)) { - KJ_IF_SOME(ep, worker.getEntrypoint(entrypointName, kj::mv(props))) { - return kj::mv(ep); - } else KJ_IF_SOME(ep, entrypointName) { - reportConfigError(kj::str(errorContext, " refers to service \"", targetName, - "\" with a named entrypoint \"", ep, "\", but \"", targetName, - "\" has no such named entrypoint.")); - return invalidConfigServiceSingleton.addRef(); - } else { - reportConfigError(kj::str(errorContext, " refers to service \"", targetName, - "\", but does not specify an entrypoint, and the service does not have a " - "default entrypoint.")); - return invalidConfigServiceSingleton.addRef(); - } - } else { - KJ_IF_SOME(ep, entrypointName) { - reportConfigError(kj::str(errorContext, " refers to service \"", targetName, - "\" with a named entrypoint \"", ep, "\", but \"", targetName, - "\" is not a Worker, so does not have any named entrypoints.")); - } else if (!props.empty()) { - reportConfigError(kj::str(errorContext, " refers to service \"", targetName, - "\" and provides a `props` value, but \"", targetName, - "\" is not a Worker, so cannot accept `props`")); - } - - return service->addRef(); - } -} - -kj::Rc Server::lookupActorClass( - config::ServiceDesignator::Reader designator, kj::String errorContext) { - // TODO(cleanup): There's a lot of repeated code with lookupService(), should it be refactored? - - kj::StringPtr targetName = designator.getName(); - Service* service = KJ_UNWRAP_OR(services.find(targetName), { - reportConfigError(kj::str(errorContext, " refers to a service \"", targetName, - "\", but no such service is defined.")); - return invalidConfigActorClassSingleton.addRef(); - }).get(); - - kj::Maybe entrypointName; - if (designator.hasEntrypoint()) { - entrypointName = designator.getEntrypoint(); - } - - auto props = [&]() -> Frankenvalue { - auto props = designator.getProps(); - switch (props.which()) { - case config::ServiceDesignator::Props::EMPTY: - return {}; - case config::ServiceDesignator::Props::JSON: - return Frankenvalue::fromJson(kj::str(props.getJson())); - } - reportConfigError(kj::str(errorContext, - " has unrecognized props type. Was the config compiled with a " - "newer version of the schema?")); - return {}; - }(); - - KJ_IF_SOME(worker, kj::tryDowncast(*service)) { - KJ_IF_SOME(ep, worker.getActorClass(entrypointName, kj::mv(props))) { - return kj::mv(ep); - } else KJ_IF_SOME(ep, entrypointName) { - reportConfigError(kj::str(errorContext, " refers to service \"", targetName, - "\" with a Durable Object entrypoint \"", ep, "\", but \"", targetName, - "\" has no such exported entrypoint class.")); - return invalidConfigActorClassSingleton.addRef(); - } else { - reportConfigError(kj::str(errorContext, " refers to service \"", targetName, - "\", but does not specify an entrypoint, and the service does export a " - "Durable Object class as its default entrypoint.")); - return invalidConfigActorClassSingleton.addRef(); - } - } else { - KJ_IF_SOME(ep, entrypointName) { - reportConfigError(kj::str(errorContext, " refers to service \"", targetName, - "\" with a named Durable Object entrypoint \"", ep, "\", but \"", targetName, - "\" is not a Worker, so does not have any named entrypoints.")); - } else { - reportConfigError(kj::str(errorContext, " refers to service \"", targetName, - "\" as a Durable Object class, but \"", targetName, - "\" is not a Worker, so cannot be used as a class.")); - } - - return invalidConfigActorClassSingleton.addRef(); - } -} - -kj::Rc Server::resolveEntrypoint(kj::StringPtr serviceName, - kj::Maybe entrypoint, - Frankenvalue props, - Persistent persistent) { - auto& service = *JSG_REQUIRE_NONNULL(services.find(serviceName), Error, - "Stub refers to a service that doesn't exist: ", serviceName); - - auto& worker = JSG_REQUIRE_NONNULL(kj::tryDowncast(service), Error, - "Stub refers to a service that is not a Worker: ", serviceName); - - return JSG_REQUIRE_NONNULL(worker.getEntrypoint(entrypoint, kj::mv(props), persistent), Error, - "Stub refers to a an entrypoint of the target service that doesn't exist: ", - entrypoint.orDefault("default")); -} - -kj::Rc Server::resolveActorClass(kj::StringPtr serviceName, - kj::Maybe entrypoint, - Frankenvalue props, - Persistent persistent) { - auto& service = *JSG_REQUIRE_NONNULL(services.find(serviceName), Error, - "Stub refers to a service that doesn't exist: ", serviceName); - - auto& worker = JSG_REQUIRE_NONNULL(kj::tryDowncast(service), Error, - "Stub refers to a service that is not a Worker: ", serviceName); - - return JSG_REQUIRE_NONNULL(worker.getActorClass(entrypoint, kj::mv(props), persistent), Error, - "Stub refers to a an entrypoint of the target service that doesn't exist: ", - entrypoint.orDefault("default")); -} - -kj::Rc Server::resolveActor(kj::StringPtr namespaceKey, - kj::ArrayPtr id, - kj::Maybe name, - Persistent persistent) { - auto& ns = *KJ_REQUIRE_NONNULL(actorNamespacesByUniqueKey.find(namespaceKey), - "couldn't deserialize actor stub pointing at unknown namespace", namespaceKey); - - auto idFactory = kj::heap(namespaceKey); - auto idObj = idFactory->idFromRaw(id, name.clone()); - - return ns.getActorChannel(kj::mv(idObj), persistent); -} - -// ======================================================================================= - -class Server::WorkerdBootstrapImpl final: public rpc::WorkerdBootstrap::Server { - public: - WorkerdBootstrapImpl(kj::Rc service, - capnp::HttpOverCapnpFactory& httpOverCapnpFactory) - : service(kj::mv(service)), - httpOverCapnpFactory(httpOverCapnpFactory) {} - - kj::Promise startEvent(StartEventContext context) override { - // Extract the optional cf blob from the RPC params and pass it along with the - // service channel to EventDispatcherImpl. The cf blob will be included in - // SubrequestMetadata when creating the WorkerInterface for HTTP events. - kj::Maybe cfBlobJson; - auto params = context.getParams(); - if (params.hasCfBlobJson()) { - cfBlobJson = kj::str(params.getCfBlobJson()); - } - context.initResults(capnp::MessageSize{4, 1}) - .setDispatcher(kj::heap(httpOverCapnpFactory, service->addRef(), - kj::mv(cfBlobJson), Persistent(params.getFromPersistentStub()))); - return kj::READY_NOW; - } - - private: - kj::Rc service; - capnp::HttpOverCapnpFactory& httpOverCapnpFactory; - - class EventDispatcherImpl final: public rpc::EventDispatcher::Server { - public: - EventDispatcherImpl(capnp::HttpOverCapnpFactory& httpOverCapnpFactory, - kj::Rc service, - kj::Maybe cfBlobJson, - Persistent fromPersistentStub) - : httpOverCapnpFactory(httpOverCapnpFactory), - service(kj::mv(service)), - cfBlobJson(kj::mv(cfBlobJson)), - fromPersistentStub(fromPersistentStub) {} - - kj::Promise getHttpService(GetHttpServiceContext context) override { - // Create WorkerInterface with cf blob metadata (if provided via startEvent). - IoChannelFactory::SubrequestMetadata metadata; - KJ_IF_SOME(cf, cfBlobJson) { - metadata.cfBlobJson = kj::str(cf); - } - metadata.fromPersistentStub = fromPersistentStub; - auto worker = getService()->startRequest(kj::mv(metadata)); - context.initResults(capnp::MessageSize{4, 1}) - .setHttp(httpOverCapnpFactory.kjToCapnp(kj::mv(worker))); - return kj::READY_NOW; - } - - kj::Promise sendTraces(SendTracesContext context) override { - auto traces = - KJ_MAP(trace, context.getParams().getTraces()){ return kj::refcounted(trace); }; - auto event = kj::heap(api::TraceCustomEvent::TYPE, kj::mv(traces)); - auto worker = getWorker(); - auto result = co_await worker->customEvent(kj::mv(event)); - auto resp = context.getResults().getResult(); - resp.setOutcome(result.outcome); - } - - kj::Promise prewarm(PrewarmContext context) override { - throwUnsupported(); - } - - kj::Promise runScheduled(RunScheduledContext context) override { - throwUnsupported(); - } - - kj::Promise runAlarm(RunAlarmContext context) override { - throwUnsupported(); - } - - kj::Promise queue(QueueContext context) override { - throwUnsupported(); - } - - kj::Promise jsRpcSession(JsRpcSessionContext context) override { - return api::JsRpcSessionCustomEvent::receiveRpc(context, getWorker()); - } - - kj::Promise udpConnect(UdpConnectContext context) override { - auto worker = getWorker(); - auto& workerRef = *worker; - return api::UdpConnectCustomEvent::receiveRpc(context, workerRef).attach(kj::mv(worker)); - } - - kj::Promise tailStreamSession(TailStreamSessionContext context) override { - auto customEvent = kj::heap(); - auto cap = customEvent->getCap(); - capnp::PipelineBuilder pipelineBuilder; - pipelineBuilder.setTopLevel(cap); - context.setPipeline(pipelineBuilder.build()); - context.getResults().setTopLevel(kj::mv(cap)); - - auto worker = getWorker(); - auto result = co_await worker->customEvent(kj::mv(customEvent)).attach(kj::mv(worker)); - auto response = context.getResults(); - response.setResult(result.outcome); - } - - private: - capnp::HttpOverCapnpFactory& httpOverCapnpFactory; - kj::Maybe> service; - kj::Maybe cfBlobJson; - Persistent fromPersistentStub; - - kj::Rc getService() { - auto result = - kj::mv(KJ_ASSERT_NONNULL(service, "EventDispatcher can only be used for one request")); - service = kj::none; - return result; - } - - kj::Own getWorker() { - // For non-HTTP events (RPC, traces, etc.), create WorkerInterface with - // empty metadata since there's no HTTP request to extract cf from. - return getService()->startRequest({}); - } - - [[noreturn]] void throwUnsupported() { - JSG_FAIL_REQUIRE(Error, "RPC connections don't yet support this event type."); - } - }; -}; - -class Server::HttpListener final: public kj::Refcounted { - public: - HttpListener(Server& owner, - kj::Own listener, - kj::Rc service, - kj::StringPtr physicalProtocol, - kj::Own rewriter, - kj::HttpHeaderTable& headerTable, - kj::Timer& timer, - capnp::HttpOverCapnpFactory& httpOverCapnpFactory) - : owner(owner), - listener(kj::mv(listener)), - service(kj::mv(service)), - headerTable(headerTable), - timer(timer), - httpOverCapnpFactory(httpOverCapnpFactory), - physicalProtocol(physicalProtocol), - rewriter(kj::mv(rewriter)) {} - - kj::Promise run() { - TRACE_EVENT("workerd", "HttpListener::run"); - for (;;) { - kj::AuthenticatedStream stream = co_await listener->acceptAuthenticated(); - TRACE_EVENT("workerd", "HTTPListener handle connection"); - - kj::Maybe cfBlobJson; - if (!rewriter->hasCfBlobHeader()) { - // Construct a cf blob describing the client identity. - - kj::PeerIdentity* peerId; - - KJ_IF_SOME(tlsId, kj::tryDowncast(*stream.peerIdentity)) { - peerId = &tlsId.getNetworkIdentity(); - - // TODO(someday): Add client certificate info to the cf blob? At present, KJ only - // supplies the common name, but that doesn't even seem to be one of the fields that - // Cloudflare-hosted Workers receive. We should probably try to match those. - } else { - peerId = stream.peerIdentity; - } - - KJ_IF_SOME(remote, kj::tryDowncast(*peerId)) { - cfBlobJson = kj::str("{\"clientIp\": ", escapeJsonString(remote.toString()), "}"); - } else KJ_IF_SOME(local, kj::tryDowncast(*peerId)) { - auto creds = local.getCredentials(); - - kj::Vector parts; - KJ_IF_SOME(p, creds.pid) { - parts.add(kj::str("\"clientPid\":", p)); - } - KJ_IF_SOME(u, creds.uid) { - parts.add(kj::str("\"clientUid\":", u)); - } - - cfBlobJson = kj::str("{", kj::strArray(parts, ","), "}"); - } - } - - auto conn = kj::heap(*this, kj::mv(cfBlobJson)); - - static auto constexpr listen = [](kj::Own self, kj::Own conn, - kj::Own stream) -> kj::Promise { - try { - co_await conn->listedHttp.httpServer.listenHttp(kj::mv(stream)); - } catch (...) { - KJ_LOG(ERROR, kj::getCaughtExceptionAsKj()); - } - }; - - // Run the connection handler loop in the global task set, so that run() waits for open - // connections to finish before returning, even if the listener loop is canceled. However, - // do not consider exceptions from a specific connection to be fatal. - owner.tasks.add(listen(kj::addRef(*this), kj::mv(conn), kj::mv(stream.stream))); - } - } - - private: - Server& owner; - kj::Own listener; - kj::Rc service; - kj::HttpHeaderTable& headerTable; - kj::Timer& timer; - capnp::HttpOverCapnpFactory& httpOverCapnpFactory; - kj::StringPtr physicalProtocol; - kj::Own rewriter; - - kj::Maybe capnpServer; - - kj::Promise acceptCapnpConnection(kj::AsyncIoStream& conn) { - KJ_IF_SOME(s, capnpServer) { - return s.accept(conn); - } - - // Capnp server not initialized. Create it now. - auto& s = capnpServer.emplace( - kj::heap(service->addRef(), httpOverCapnpFactory)); - return s.accept(conn); - } - - struct Connection final: public kj::HttpService, public kj::HttpServerErrorHandler { - Connection(HttpListener& parent, kj::Maybe cfBlobJson) - : parent(parent), - cfBlobJson(kj::mv(cfBlobJson)), - webSocketErrorHandler(kj::heap()), - listedHttp(parent.owner, - parent.timer, - parent.headerTable, - *this, - kj::HttpServerSettings{.errorHandler = *this, - .webSocketErrorHandler = *webSocketErrorHandler, - .webSocketCompressionMode = kj::HttpServerSettings::MANUAL_COMPRESSION}) {} - - HttpListener& parent; - kj::Maybe cfBlobJson; - kj::Own webSocketErrorHandler; - ListedHttpServer listedHttp; - - class ResponseWrapper final: public kj::HttpService::Response { - public: - ResponseWrapper(kj::HttpService::Response& inner, HttpRewriter& rewriter) - : inner(inner), - rewriter(rewriter) {} - - kj::Own send(uint statusCode, - kj::StringPtr statusText, - const kj::HttpHeaders& headers, - kj::Maybe expectedBodySize = kj::none) override { - TRACE_EVENT("workerd", "ResponseWrapper::send()"); - auto rewrite = headers.cloneShallow(); - rewriter.rewriteResponse(rewrite); - return inner.send(statusCode, statusText, rewrite, expectedBodySize); - } - - kj::Own acceptWebSocket(const kj::HttpHeaders& headers) override { - TRACE_EVENT("workerd", "ResponseWrapper::acceptWebSocket()"); - auto rewrite = headers.cloneShallow(); - rewriter.rewriteResponse(rewrite); - return inner.acceptWebSocket(rewrite); - } - - private: - kj::HttpService::Response& inner; - HttpRewriter& rewriter; - }; - - // --------------------------------------------------------------------------- - // implements kj::HttpService - - kj::Promise request(kj::HttpMethod method, - kj::StringPtr url, - const kj::HttpHeaders& headers, - kj::AsyncInputStream& requestBody, - kj::HttpService::Response& response) override { - TRACE_EVENT("workerd", "Connection:request()"); - IoChannelFactory::SubrequestMetadata metadata; - metadata.cfBlobJson = cfBlobJson.clone(); - - Response* wrappedResponse = &response; - kj::Own ownResponse; - if (parent.rewriter->needsRewriteResponse()) { - wrappedResponse = ownResponse = kj::heap(response, *parent.rewriter); - } - - if (parent.rewriter->needsRewriteRequest() || cfBlobJson != kj::none) { - auto rewrite = KJ_UNWRAP_OR(parent.rewriter->rewriteIncomingRequest( - url, parent.physicalProtocol, headers, metadata.cfBlobJson), - { co_return co_await response.sendError(400, "Bad Request", parent.headerTable); }); - auto worker = parent.service->startRequest(kj::mv(metadata)); - co_return co_await worker->request( - method, url, *rewrite.headers, requestBody, *wrappedResponse); - } else { - auto worker = parent.service->startRequest(kj::mv(metadata)); - co_return co_await worker->request(method, url, headers, requestBody, *wrappedResponse); - } - } - - kj::Promise connect(kj::StringPtr host, - const kj::HttpHeaders& headers, - kj::AsyncIoStream& connection, - ConnectResponse& response, - kj::HttpConnectSettings settings) override { - TRACE_EVENT("workerd", "Connection:connect()"); - KJ_IF_SOME(h, parent.rewriter->getCapnpConnectHost()) { - if (h == host) { - // Client is requesting to open a capnp session! - response.accept(200, "OK", kj::HttpHeaders(parent.headerTable)); - co_return co_await parent.acceptCapnpConnection(connection); - } - } - - IoChannelFactory::SubrequestMetadata metadata; - metadata.cfBlobJson = cfBlobJson.clone(); - - auto worker = parent.service->startRequest(kj::mv(metadata)); - co_return co_await worker->connect(host, headers, connection, response, kj::mv(settings)); - } - - // --------------------------------------------------------------------------- - // implements kj::HttpServerErrorHandler - - kj::Promise handleApplicationError( - kj::Exception exception, kj::Maybe response) override { - if (exception.getType() == kj::Exception::Type::DISCONNECTED) { - // Don't send a response, just close connection. - co_return; - } - KJ_LOG(ERROR, kj::str("Uncaught exception: ", exception)); - KJ_IF_SOME(r, response) { - co_return co_await r.sendError(500, "Internal Server Error", parent.headerTable); - } - } - }; -}; - -class Server::TcpListener final: public kj::Refcounted { - public: - TcpListener(Server& owner, - kj::Own listener, - kj::Rc service, - kj::HttpHeaderTable& headerTable, - kj::String authority) - : owner(owner), - listener(kj::mv(listener)), - service(kj::mv(service)), - headerTable(headerTable), - authority(kj::mv(authority)) {} - - kj::Promise run() { - TRACE_EVENT("workerd", "TcpListener::run"); - for (;;) { - kj::AuthenticatedStream stream = co_await listener->acceptAuthenticated(); - TRACE_EVENT("workerd", "TcpListener handle connection"); - - kj::PeerIdentity* peerId; - KJ_IF_SOME(tlsId, kj::tryDowncast(*stream.peerIdentity)) { - peerId = &tlsId.getNetworkIdentity(); - } else { - peerId = stream.peerIdentity; - } - - IoChannelFactory::SubrequestMetadata metadata; - KJ_IF_SOME(remote, kj::tryDowncast(*peerId)) { - metadata.clientAddress = remote.toString(); - } - - auto req = service->startRequest(kj::mv(metadata)); - auto response = kj::heap(); - kj::HttpHeaders headers(headerTable); - owner.tasks.add(req->connect(authority, headers, *stream.stream, *response, {}) - .attach(kj::mv(stream.stream), kj::mv(response)) - .attach(kj::mv(req)) - .catch_([](kj::Exception&& e) { - if (e.getType() != kj::Exception::Type::DISCONNECTED) { - KJ_LOG(ERROR, "TCP connect() handler threw", e); - } - })); - } - } - - private: - Server& owner; - kj::Own listener; - kj::Rc service; - kj::HttpHeaderTable& headerTable; - kj::String authority; - - struct ResponseWrapper final: public kj::HttpService::ConnectResponse { - void accept( - uint statusCode, kj::StringPtr statusText, const kj::HttpHeaders& headers) override { - // Ok.. we're accepting the connection... anything to do? - } - kj::Own reject(uint statusCode, - kj::StringPtr statusText, - const kj::HttpHeaders& headers, - kj::Maybe expectedBodySize = kj::none) override { - // Doh... we're rejecting the connection... anything to do? - return newNullOutputStream(); - } - }; -}; - -kj::Promise Server::listenHttp(kj::Own listener, - kj::Rc service, - kj::StringPtr physicalProtocol, - kj::Own rewriter) { - auto obj = - kj::refcounted(*this, kj::mv(listener), kj::mv(service), physicalProtocol, - kj::mv(rewriter), globalContext->headerTable, timer, globalContext->httpOverCapnpFactory); - co_return co_await obj->run(); -} - -kj::Promise Server::listenTcp( - kj::Own listener, kj::Rc service, kj::String authority) { - auto obj = kj::refcounted( - *this, kj::mv(listener), kj::mv(service), globalContext->headerTable, kj::mv(authority)); - co_return co_await obj->run(); -} - -// ======================================================================================= -// UdpListener - -class Server::UdpListener final: public kj::Refcounted { - public: - UdpListener(Server& owner, - kj::Own port, - kj::Rc service, - kj::StringPtr addrStr, - kj::Duration idleTimeout, - size_t maxPendingBytes) - : owner(owner), - port(kj::mv(port)), - service(kj::mv(service)), - addrStr(addrStr), - idleTimeout(idleTimeout), - maxPendingBytes(maxPendingBytes) {} - - kj::Promise run() { - TRACE_EVENT("workerd", "UdpListener::run"); - // Datagrams larger than this are truncated by the kernel with no way to recover the tail -- - // matching setupDatagramSocket()'s read-side buffer, and comfortably above the largest UDP - // payload a peer could ever send (65507 bytes plus headers). - static constexpr size_t MAX_DATAGRAM_SIZE = 65535; - - auto receiver = - port->makeReceiver(kj::DatagramReceiver::Capacity{.content = MAX_DATAGRAM_SIZE}); - for (;;) { - co_await receiver->receive(); - TRACE_EVENT("workerd", "UdpListener handle datagram"); - - auto content = receiver->getContent(); - if (content.isTruncated) { - continue; - } - auto key = receiver->getSource().toString(); - - kj::Rc flow = nullptr; - KJ_IF_SOME(existing, flows.find(key)) { - KJ_IF_SOME(strong, existing.upgrade()) { - flow = kj::mv(strong); - } else { - flows.erase(key); - } - } - if (flow == nullptr) { - auto newFlow = kj::rc( - *this, kj::str(key), receiver->getSource().clone(), idleTimeout, maxPendingBytes); - flow = newFlow.addRef(); - flows.insert(kj::str(key), newFlow.downgrade()); - dispatch(kj::mv(newFlow)); - } - flow->deliver(kj::heapArray(content.value)); - } - } - - private: - // A single logical UDP flow (all datagrams to/from one peer address, until idle). Implements - // DatagramChannel so it can be handed to a UdpConnectCustomEvent directly: receive() drains - // `pending`, blocking on `waitingReceiver` if empty; send() writes back to the peer via the - // listener's shared DatagramPort. - // - // Ownership: the dispatch task owns Flow (see dispatch()), same as TcpListener::run() tasks owning - // per-connection state. `flows` below holds weak references for routing datagrams. - class Flow final: public workerd::DatagramChannel, public kj::Refcounted { - public: - Flow(UdpListener& listener, - kj::String key, - kj::Own peerAddr, - kj::Duration idleTimeout, - size_t maxPendingBytes) - : listener(kj::addRef(listener)), - key(kj::mv(key)), - peerAddr(kj::mv(peerAddr)), - port(listener.port.addRef()), - idleTimeout(idleTimeout), - maxPendingBytes(maxPendingBytes) { - resetIdleTimer(); - } - - ~Flow() noexcept(false) { - // Stop routing further datagrams here if the dispatch task is ending before an idle timeout - // removed us already (e.g. the connect() handler returned without reading until EOF). - unregister(); - } - - // Called by UdpListener::run() when a new datagram arrives for this flow. The listener keeps - // draining the kernel socket regardless of whether this flow's queue has room, so one slow - // flow does not block delivery to other peers sharing the same socket. - // Once `maxPendingBytes` worth of data is queued, further arrivals for - // this flow are dropped rather than buffered. - void deliver(kj::Array datagram) { - if (ended) return; - resetIdleTimer(); - KJ_IF_SOME(fulfiller, waitingReceiver) { - fulfiller->fulfill(kj::mv(datagram)); - waitingReceiver = kj::none; - return; - } - auto queuedSize = datagram.size() + sizeof(datagram); - if (queuedSize > maxPendingBytes - pendingBytes) { - return; - } - pendingBytes += queuedSize; - pending.push_back(kj::mv(datagram)); - } - - kj::Promise>> receive() override { - if (!pending.empty()) { - auto result = kj::mv(pending.front()); - pending.pop_front(); - pendingBytes -= result.size() + sizeof(result); - return kj::Maybe>(kj::mv(result)); - } - if (ended) { - return kj::Maybe>(kj::none); - } - KJ_REQUIRE( - waitingReceiver == kj::none, "DatagramChannel::receive() already has a pending call"); - auto paf = kj::newPromiseAndFulfiller>>(); - waitingReceiver = kj::mv(paf.fulfiller); - return kj::mv(paf.promise); - } - - kj::Promise send(kj::ArrayPtr datagram) override { - co_await port->send(datagram, *peerAddr); - } - - private: - kj::Own listener; - kj::String key; - kj::Own peerAddr; - kj::Rc port; - kj::Duration idleTimeout; - size_t maxPendingBytes; - - workerd::RingBuffer> pending; - size_t pendingBytes = 0; - kj::Maybe>>>> waitingReceiver; - bool ended = false; - kj::Promise idleTask = kj::READY_NOW; - - void resetIdleTimer() { - // Dropping the old task (by overwriting idleTask) cancels its pending afterDelay(), so only - // the most recent datagram's timer can ever fire. eagerlyEvaluate() is required: a bare - // kj::Promise sitting in a member variable is never polled by the event loop unless - // something is actively waiting on it (a coroutine co_await, a TaskSet, or eager - // evaluation) -- without it this timer would simply never fire. - idleTask = listener->owner.timer.afterDelay(idleTimeout).then([this]() { - onIdleTimeout(); - }).eagerlyEvaluate(nullptr); - } - - void onIdleTimeout() { - ended = true; - KJ_IF_SOME(fulfiller, waitingReceiver) { - fulfiller->fulfill(kj::Maybe>(kj::none)); - waitingReceiver = kj::none; - } - // Stop routing further datagrams here; the Flow object itself isn't destroyed by this - // (it's owned by its dispatch task, not by `flows`), just no longer reachable for future - // deliver() calls. It's destroyed once that task's promise chain -- the connect() handler, - // plus connectUdp()'s own neutering -- completes. - unregister(); - } - - void unregister() { - KJ_IF_SOME(current, listener->flows.find(key)) { - KJ_IF_SOME(live, current.tryGet()) { - if (&live != this) { - return; - } - } - listener->flows.erase(key); - } - } - }; - - Server& owner; - kj::Rc port; - kj::Rc service; - kj::StringPtr addrStr; - kj::Duration idleTimeout; - size_t maxPendingBytes; - - // Flows keyed by the peer's address (as text), used to route a later datagram from the same peer - // to the Flow already dispatched for it. See Flow's class comment for the ownership model. - kj::HashMap> flows; - - void dispatch(kj::Rc flow) { - IoChannelFactory::SubrequestMetadata metadata; - auto worker = service->startRequest(kj::mv(metadata)); - auto event = kj::heap(kj::str(addrStr), *flow); - owner.tasks.add(worker->customEvent(kj::mv(event)) - .ignoreResult() - .attach(kj::mv(worker), kj::mv(flow)) - .catch_([](kj::Exception&& e) { - if (e.getType() != kj::Exception::Type::DISCONNECTED) { - KJ_LOG(ERROR, "UDP connect() handler threw", e); - } - })); - } -}; - -kj::Promise Server::listenUdp(kj::Own port, - kj::Rc service, - kj::StringPtr addrStr, - kj::Duration idleTimeout, - size_t maxPendingBytes) { - auto obj = kj::refcounted( - *this, kj::mv(port), kj::mv(service), addrStr, idleTimeout, maxPendingBytes); - co_return co_await obj->run(); -} - -// ======================================================================================= -// Debug port for exposing all services via RPC - -class Server::WorkerdDebugPortImpl final: public rpc::WorkerdDebugPort::Server { - public: - WorkerdDebugPortImpl( - workerd::server::Server& srv, capnp::HttpOverCapnpFactory& httpOverCapnpFactory) - : srv(srv), - httpOverCapnpFactory(httpOverCapnpFactory) {} - - kj::Promise getEntrypoint(GetEntrypointContext context) override { - auto params = context.getParams(); - auto serviceName = params.getService(); - auto propsReader = params.getProps(); - - // Look up the service. - auto& serviceEntry = KJ_ASSERT_NONNULL(srv.services.find(serviceName), - kj::str("jsg.Error: Worker \"", serviceName, "\" not found")); - auto service = serviceEntry->service(); - - // Convert props from Frankenvalue if provided - Frankenvalue props; - if (params.hasProps()) { - props = Frankenvalue::fromCapnp(propsReader); - } - - kj::Rc targetService; - - // Try to cast to WorkerService to support entrypoints and props - KJ_IF_SOME(workerService, kj::tryDowncast(*service)) { - // This is a WorkerService, use getEntrypoint which supports both entrypoints and props - kj::Maybe maybeEntrypoint; - if (params.hasEntrypoint()) { - maybeEntrypoint = params.getEntrypoint(); - } - - targetService = KJ_ASSERT_NONNULL(workerService.getEntrypoint(maybeEntrypoint, kj::mv(props)), - kj::str("jsg.Error: Worker does not export an entrypoint named \"", - maybeEntrypoint.orDefault("(default)"), "\"")); - } else { - // Not a WorkerService - KJ_ASSERT(!params.hasEntrypoint(), "jsg.Error: Worker does not support named entrypoints"); - - // Try to apply props if the service supports it - if (params.hasProps()) { - targetService = service->forProps(kj::mv(props), Persistent::NO); - } else { - // No props, just use the service as-is - targetService = service->addRef(); - } - } - - // Return a WorkerdBootstrap that wraps this service using the generic implementation. - context.initResults(capnp::MessageSize{4, 1}) - .setEntrypoint(kj::heap(kj::mv(targetService), httpOverCapnpFactory)); - return kj::READY_NOW; - } - - kj::Promise getActor(GetActorContext context) override { - auto params = context.getParams(); - auto serviceName = params.getService(); - auto entrypointName = params.getEntrypoint(); - auto actorIdStr = params.getActorId(); - - // Look up the service - auto& serviceEntry = KJ_ASSERT_NONNULL(srv.services.find(serviceName), - kj::str("jsg.Error: Worker \"", serviceName, "\" not found")); - auto service = serviceEntry->service(); - - // Try to cast to WorkerService - auto& workerService = KJ_REQUIRE_NONNULL(kj::tryDowncast(*service), - "jsg.Error: Worker does not support Durable Objects"); - - // Look up the actor namespace - auto& actorNamespace = KJ_ASSERT_NONNULL(workerService.getActorNamespace(entrypointName), - kj::str("jsg.Error: Worker does not export a Durable Object class named \"", entrypointName, - "\"")); - - // Create an actor ID - use the namespace config to determine if it's durable or ephemeral - Worker::Actor::Id actorId; - KJ_SWITCH_ONEOF(actorNamespace.getConfig()) { - KJ_CASE_ONEOF(c, Durable) { - // Durable Object ID (hex-encoded SHA256 hash) - auto decoded = kj::decodeHex(actorIdStr); - KJ_REQUIRE(decoded.size() == SHA256_DIGEST_LENGTH, - "Invalid Durable Object ID: expected 64 hex characters (32 bytes)", decoded.size()); - kj::Own id = - kj::heap(decoded.begin(), kj::none); - actorId = kj::mv(id); - } - KJ_CASE_ONEOF(c, Ephemeral) { - // Ephemeral actor ID (plain string) - actorId = kj::str(actorIdStr); - } - } - - // Wrap the actor channel using the generic WorkerdBootstrap implementation. - context.initResults(capnp::MessageSize{4, 1}) - .setActor(kj::heap( - actorNamespace.getActorChannel(kj::mv(actorId)), httpOverCapnpFactory)); - return kj::READY_NOW; - } - - private: - workerd::server::Server& srv; - capnp::HttpOverCapnpFactory& httpOverCapnpFactory; -}; - -class Server::DebugPortListener { - public: - DebugPortListener(Server& owner, kj::Own listener) - : owner(owner), - listener(kj::mv(listener)) {} - - kj::Promise run() { - capnp::TwoPartyServer server(owner.makeWorkerdDebugPortClient()); - co_return co_await server.listen(*listener); - } - - private: - Server& owner; - kj::Own listener; -}; - -rpc::WorkerdDebugPort::Client Server::makeWorkerdDebugPortClient() { - return rpc::WorkerdDebugPort::Client( - kj::heap(*this, globalContext->httpOverCapnpFactory)); -} - -kj::Promise Server::listenDebugPort(kj::Own listener) { - DebugPortListener obj(*this, kj::mv(listener)); - co_return co_await obj.run(); -} - -// ======================================================================================= -// Server::run() - -kj::Promise Server::handleDrain(kj::Promise drainWhen) { - co_await drainWhen; - TRACE_EVENT("workerd", "Server::handleDrain()"); - // Tell all HttpServers to drain. This causes them to disconnect any connections that don't - // have a request in-flight. - for (auto& httpServer: httpServers) { - // The promise returned by `drain()` resolves when all connections have ended. But, we need - // the promise returned by handleDrain() to resolve immediately when draining has started, - // since that's what signals us to stop accepting incoming connections. So, we should not - // co_await the promise returned by `drain()`. Technically, we don't actually have to wait - // on it at all -- `drain()` returns the promise end of a promise-and-fulfiller, so simply - // dropping it won't actually cancel anything. But since that's not documented in drain()'s - // doc comment, we instead add the promise to `tasks` to be safe. - tasks.add(httpServer.httpServer.drain()); - } -} - -kj::Promise Server::run( - jsg::V8System& v8System, config::Config::Reader config, kj::Promise drainWhen) { - TRACE_EVENT("workerd", "Server.run"); - - // Update logging settings from config (overridding structuredLogging when so) - if (config.hasLogging()) { - auto logging = config.getLogging(); - loggingOptions.structuredLogging = StructuredLogging(logging.getStructuredLogging()); - if (logging.hasStdoutPrefix()) { - loggingOptions.stdoutPrefix = kj::ConstString(kj::str(logging.getStdoutPrefix())); - } - if (logging.hasStderrPrefix()) { - loggingOptions.stderrPrefix = kj::ConstString(kj::str(logging.getStderrPrefix())); - } - } else { - loggingOptions.structuredLogging = StructuredLogging(config.getStructuredLogging()); - } - - kj::HttpHeaderTable::Builder headerTableBuilder; - globalContext = kj::heap(*this, v8System, headerTableBuilder); - invalidConfigServiceSingleton = kj::rc(); - invalidConfigActorClassSingleton = kj::rc(); - - auto [fatalPromise, fatalFulfiller] = kj::newPromiseAndFulfiller(); - this->fatalFulfiller = kj::mv(fatalFulfiller); - - auto forkedDrainWhen = handleDrain(kj::mv(drainWhen)).fork(); - - co_await bindSockets(config); - co_await startServices(v8System, config, headerTableBuilder, forkedDrainWhen); - - auto listenPromise = listenOnSockets(config, headerTableBuilder, forkedDrainWhen); - - // We should have registered all headers synchronously. This is important because we want to - // be able to start handling requests as soon as the services are available, even if some other - // services take longer to get ready. - auto ownHeaderTable = headerTableBuilder.build(); - - co_await listenPromise.exclusiveJoin(kj::mv(fatalPromise)); - - // All incoming requests have drained. Stop container-enabled actors so they cannot race their - // terminal Docker cleanup, then wait while their namespaces and Docker I/O remain available. - for (auto& service: services) { - KJ_IF_SOME(worker, kj::tryDowncast(*service.value)) { - worker.beginContainerCleanup(); - } - } - for (auto& service: services) { - KJ_IF_SOME(worker, kj::tryDowncast(*service.value)) { - for (auto& [className, ns]: worker.getActorNamespaces()) { - co_await ns->waitForContainerCleanup(); - } - } - } -} - -// Configure and start the inspector socket, returning the port the socket started on. -uint startInspector( - kj::StringPtr inspectorAddress, Server::InspectorServiceIsolateRegistrar& registrar) { - static constexpr uint UNASSIGNED_PORT = 0; - static constexpr uint DEFAULT_PORT = 9229; - kj::MutexGuarded inspectorPort(UNASSIGNED_PORT); - - // `startInspector()` is called on the Isolate thread. V8 requires CPU profiling to be started and - // stopped on the same thread which executes JavaScript -- that is, the Isolate thread -- which - // means we need to dispatch inspector messages on this thread. To help make that happen, we - // capture this thread's kj::Executor here, and pass it into the InspectorService below. Later, - // when the InspectorService receives a WebSocket connection, it calls - // `Isolate::attachInspector()`, which uses the kj::Executor we create here to create a - // XThreadNotifier and start a dispatch loop. The InspectorService reads subsequent WebSocket - // inspector messages and feeds them to that dispatch loop via the XThreadNotifier. - auto isolateThreadExecutor = kj::getCurrentThreadExecutor().addRef(); - - // Start the InspectorService thread. - kj::Thread thread([inspectorAddress, &inspectorPort, ®istrar, - isolateThreadExecutor = kj::mv(isolateThreadExecutor)]() mutable { - kj::AsyncIoContext io = kj::setupAsyncIo(); - - kj::HttpHeaderTable::Builder headerTableBuilder; - - // Create the special inspector service. - auto inspectorService(kj::heap( - kj::mv(isolateThreadExecutor), io.provider->getTimer(), headerTableBuilder, registrar)); - auto ownHeaderTable = headerTableBuilder.build(); - - // Configure and start the inspector socket. - - auto& network = io.provider->getNetwork(); - - // TODO(cleanup): There's an issue here that if listen fails, nothing notices. The - // server will continue running but will no longer accept inspector connections. - // This should be fixed by: - // 1. Replacing the kj::NEVER_DONE with listen - // 2. Making the thread's lambda `noexcept` so that if it throws the process crashes - // 3. Probably also throw if listen completes without an exception (even if unlikely to - // happen) - auto listen = (kj::coCapture( - [&network, &inspectorAddress, &inspectorPort, &inspectorService]() -> kj::Promise { - auto parsed = co_await network.parseAddress(inspectorAddress, DEFAULT_PORT); - auto listener = parsed->listen(); - // EW-7716: Signal to thread that started the inspector service that the inspector is ready. - *inspectorPort.lockExclusive() = listener->getPort(); - KJ_LOG(INFO, "Inspector is listening"); - co_await inspectorService->listen(kj::mv(listener)); - }))(); - - kj::NEVER_DONE.wait(io.waitScope); - }); - thread.detach(); - - // EW-7716: Wait for the InspectorService instance to be initialized before proceeding. - return inspectorPort.when([](const uint& port) { return port != UNASSIGNED_PORT; }, - [](const uint& port) { return port; }); -} - -kj::Promise Server::preloadPython( - kj::StringPtr workerName, const WorkerDef& workerDef, ErrorReporter& errorReporter) { - if (workerDef.featureFlags.getPythonWorkers()) { - auto pythonRelease = getPythonSnapshotRelease(workerDef.featureFlags); - KJ_IF_SOME(release, pythonRelease) { - auto version = getPythonBundleName(release); - - // Fetch the Pyodide bundle, verifying its integrity against the expected checksum. - co_await server::fetchPyodideBundle( - pythonConfig, kj::mv(version), release.getIntegrity(), network, timer); - } - } -} - -kj::Promise Server::startServices(jsg::V8System& v8System, - config::Config::Reader config, - kj::HttpHeaderTable::Builder& headerTableBuilder, - kj::ForkedPromise& forkedDrainWhen) { - // --------------------------------------------------------------------------- - // Configure services - TRACE_EVENT("workerd", "startServices"); - - // First pass: Extract actor namespace configs. - kj::HashSet durableNamespaceKeys; - for (auto serviceConf: config.getServices()) { - if (!serviceConf.isWorker()) continue; - for (auto ns: serviceConf.getWorker().getDurableObjectNamespaces()) { - if (ns.isUniqueKey()) durableNamespaceKeys.insert(kj::str(ns.getUniqueKey())); - } - } - - kj::HashSet workflowNamespaceKeys; - for (auto serviceConf: config.getServices()) { - kj::StringPtr name = serviceConf.getName(); - kj::HashMap serviceActorConfigs; - - if (serviceConf.isWorker()) { - auto workerConf = serviceConf.getWorker(); - bool hadDurable = false; - for (auto ns: workerConf.getDurableObjectNamespaces()) { - switch (ns.which()) { - case config::Worker::DurableObjectNamespace::UNIQUE_KEY: - hadDurable = true; - serviceActorConfigs.insert(kj::str(ns.getClassName()), - Durable{.uniqueKey = kj::str(ns.getUniqueKey()), - .isEvictable = !ns.getPreventEviction(), - .enableSql = ns.getEnableSql(), - .containerOptions = ns.hasContainer() ? kj::Maybe(ns.getContainer()) : kj::none}); - continue; - case config::Worker::DurableObjectNamespace::EPHEMERAL_LOCAL: - if (!experimental) { - reportConfigError(kj::str( - "Ephemeral objects (Durable Object namespaces with type 'ephemeralLocal') are an " - "experimental feature which may change or go away in the future. You must run " - "workerd with `--experimental` to use this feature.")); - } - serviceActorConfigs.insert(kj::str(ns.getClassName()), - Ephemeral{.isEvictable = !ns.getPreventEviction(), .enableSql = ns.getEnableSql()}); - continue; - } - reportConfigError(kj::str("Encountered unknown DurableObjectNamespace type in service \"", - name, "\", class \"", ns.getClassName(), - "\". Was the config compiled with a newer version " - "of the schema?")); - } - - // Each configured Workflow is backed by a synthetic Durable Object namespace owned by this - // (the app) Worker. This first pass validates the `workflowsEngine` config and, for every - // valid Workflow, synthesizes that namespace's `Durable` actor config keyed by the derived - // `miniflare-workflows-` key. Cross-service wiring (resolving the actorClass/engine and - // bindingService, building props) happens in a later pass once all services exist. - if (workerConf.hasWorkflowsEngine()) { - auto workflowsEngine = workerConf.getWorkflowsEngine(); - bool hasActorClass = - workflowsEngine.hasActorClass() && workflowsEngine.getActorClass().getName().size() > 0; - if (!hasActorClass) { - reportConfigError( - kj::str("Worker service \"", name, "\"'s workflowsEngine is missing actorClass.")); - } - - kj::HashSet workflowClasses; - kj::HashSet workflowNames; - for (auto workflow: workflowsEngine.getWorkflows()) { - auto className = workflow.getClassName(); - auto workflowName = workflow.getName(); - bool valid = hasActorClass; - - if (className.size() == 0) { - reportConfigError( - kj::str("Worker service \"", name, "\" configures a Workflow without className.")); - valid = false; - } else if (workflowClasses.contains(className)) { - reportConfigError(kj::str("Worker service \"", name, - "\" configures multiple Workflows for class \"", className, "\".")); - valid = false; - } else { - workflowClasses.insert(kj::str(className)); - } - - if (workflowName.size() == 0) { - reportConfigError( - kj::str("Worker service \"", name, "\" configures a Workflow without name.")); - valid = false; - } else if (workflowName.findFirst('/') != kj::none || - workflowName.findFirst('\\') != kj::none) { - reportConfigError(kj::str("Worker service \"", name, "\" configures Workflow name \"", - workflowName, "\" containing a path separator.")); - valid = false; - } else if (workflowNames.contains(workflowName)) { - reportConfigError(kj::str("Worker service \"", name, - "\" configures multiple Workflows named \"", workflowName, "\".")); - valid = false; - } else { - workflowNames.insert(kj::str(workflowName)); - } - - if (!workflow.hasBindingService() || workflow.getBindingService().getName().size() == 0) { - reportConfigError(kj::str("Worker service \"", name, "\"'s Workflow \"", workflowName, - "\" is missing bindingService.")); - valid = false; - } - - if (!valid) continue; - - auto namespaceKey = kj::str(WORKFLOW_NAMESPACE_KEY_PREFIX, workflowName); - if (serviceActorConfigs.find(namespaceKey) != kj::none) { - reportConfigError(kj::str("Worker service \"", name, - "\"'s Workflow namespace conflicts with Durable Object class \"", namespaceKey, - "\".")); - continue; - } - if (durableNamespaceKeys.contains(namespaceKey)) { - reportConfigError(kj::str("Workflow ActorNamespace key \"", namespaceKey, - "\" conflicts with a Durable Object namespace unique key.")); - continue; - } - if (workflowNamespaceKeys.contains(namespaceKey)) { - reportConfigError(kj::str("Workflow ActorNamespace key \"", namespaceKey, - "\" is configured by more than one Worker.")); - continue; - } - - workflowNamespaceKeys.insert(kj::str(namespaceKey)); - auto actorConfigKey = kj::str(namespaceKey); - serviceActorConfigs.insert(kj::mv(actorConfigKey), - Durable{.uniqueKey = kj::mv(namespaceKey), - // Workflow actors must not be evicted mid-run, and their state is SQL-backed. - .isEvictable = false, - .enableSql = true, - // Marks this as a synthetic Workflow namespace so later passes recognize it and - // route its storage to the bindingService Worker rather than this Worker. - .isWorkflow = true, - .containerOptions = kj::none}); - } - } - - switch (workerConf.getDurableObjectStorage().which()) { - case config::Worker::DurableObjectStorage::NONE: - if (hadDurable) { - reportConfigError(kj::str("Worker service \"", name, - "\" implements durable object classes but has " - "`durableObjectStorage` set to `none`.")); - } - goto validDurableObjectStorage; - case config::Worker::DurableObjectStorage::IN_MEMORY: - case config::Worker::DurableObjectStorage::LOCAL_DISK: - goto validDurableObjectStorage; - } - reportConfigError(kj::str("Encountered unknown durableObjectStorage type in service \"", name, - "\". Was the config compiled with a newer version of the schema?")); - - validDurableObjectStorage: - if (workerConf.hasDurableObjectUniqueKeyModifier()) { - // This should be implemented along with parameterized workers. It's not relevant - // otherwise, but let's make sure no one sets it accidentally. - KJ_UNIMPLEMENTED("durableObjectUniqueKeyModifier is not implemented yet"); - } - } - - actorConfigs.upsert(kj::str(name), kj::mv(serviceActorConfigs), [&](auto&&...) { - reportConfigError(kj::str("Config defines multiple services named \"", name, "\".")); - }); - } - - // If we are using the inspector, we need to register the Worker::Isolate - // with the inspector service. - KJ_IF_SOME(inspectorAddress, inspectorOverride) { - auto registrar = kj::heap(); - auto port = startInspector(inspectorAddress, *registrar); - KJ_IF_SOME(stream, controlOverride) { - auto message = kj::str("{\"event\":\"listen-inspector\",\"port\":", port, "}\n"); - try { - stream->write(message.asBytes()); - } catch (kj::Exception& e) { - KJ_LOG(ERROR, e); - } - } - inspectorIsolateRegistrar = kj::mv(registrar); - } - - // Second pass: Build services. - for (auto serviceConf: config.getServices()) { - kj::StringPtr name = serviceConf.getName(); - auto service = co_await makeService(serviceConf, headerTableBuilder, config.getExtensions()); - - services.upsert(kj::str(name), kj::mv(service), [&](auto&&...) { - reportConfigError(kj::str("Config defines multiple services named \"", name, "\".")); - }); - } - - // Make the default "internet" service if it's not there already. - services.findOrCreate("internet"_kj, [&]() { - auto publicNetwork = network.restrictPeers({"public"_kj}); - - kj::TlsContext::Options options; - options.useSystemTrustStore = true; - - kj::Own tls = kj::heap(kj::mv(options)); - kj::TlsContext& tlsRef = *tls; - auto tlsNetwork = tls->wrapNetwork(*publicNetwork).attach(kj::mv(tls)); - - auto service = kj::rc(globalContext->headerTable, timer, entropySource, - kj::mv(publicNetwork), kj::mv(tlsNetwork), tlsRef); - - return decltype(services)::Entry{kj::str("internet"_kj), kj::mv(service)}; - }); - - // Now that every service exists, wire up each app Worker's Workflows to the engine that runs - // them. For each configured Workflow this resolves the shared Engine actor class and the - // Workflow's bindingService Worker, builds the per-Workflow props identifying which class the - // Engine should run, and initializes the synthetic namespace (created in the first pass) as an - // external actor served by that Engine class. - for (auto serviceConf: config.getServices()) { - if (!serviceConf.isWorker() || !serviceConf.getWorker().hasWorkflowsEngine()) continue; - - auto name = serviceConf.getName(); - ConfigErrorReporter errorReporter(*this, name); - auto& appService = KJ_ASSERT_NONNULL(services.find(name)); - auto& appWorker = KJ_UNWRAP_OR(kj::tryDowncast(*appService), { - errorReporter.addError( - kj::str("Worker service \"", name, "\" could not initialize its workflowsEngine.")); - continue; - }); - auto workflowsEngine = serviceConf.getWorker().getWorkflowsEngine(); - if (!workflowsEngine.hasActorClass()) continue; - - auto actorClassDesignator = workflowsEngine.getActorClass(); - if (actorClassDesignator.getName().size() == 0) continue; - if (!actorClassDesignator.getProps().isEmpty()) { - errorReporter.addError( - kj::str("workflowsEngine.actorClass must not specify props; Workflow props are supplied " - "by the runtime.")); - continue; - } - - auto& engineService = KJ_UNWRAP_OR(services.find(actorClassDesignator.getName()), { - errorReporter.addError(kj::str("workflowsEngine.actorClass refers to a service \"", - actorClassDesignator.getName(), "\", but no such service is defined.")); - continue; - }); - auto& engineWorker = KJ_UNWRAP_OR(kj::tryDowncast(*engineService), { - errorReporter.addError(kj::str("workflowsEngine.actorClass refers to service \"", - actorClassDesignator.getName(), "\", but it is not a Worker.")); - continue; - }); - - kj::Maybe actorClassName; - if (actorClassDesignator.hasEntrypoint()) { - actorClassName = actorClassDesignator.getEntrypoint(); - } - - kj::HashSet workflowClasses; - kj::HashSet workflowNames; - for (auto workflow: workflowsEngine.getWorkflows()) { - auto className = workflow.getClassName(); - auto workflowName = workflow.getName(); - if (workflowClasses.contains(className) || workflowNames.contains(workflowName)) continue; - workflowClasses.insert(kj::str(className)); - workflowNames.insert(kj::str(workflowName)); - - auto namespaceKey = kj::str(WORKFLOW_NAMESPACE_KEY_PREFIX, workflowName); - auto& localActorConfigs = KJ_ASSERT_NONNULL(actorConfigs.find(name)); - auto& actorConfig = KJ_UNWRAP_OR(localActorConfigs.find(namespaceKey), continue); - auto& workflowActorConfig = KJ_UNWRAP_OR(actorConfig.tryGet(), continue); - if (!workflowActorConfig.isWorkflow) continue; - - if (appWorker.hasActorClass(namespaceKey)) { - errorReporter.addError(kj::str("Workflow \"", workflowName, "\" namespace \"", namespaceKey, - "\" conflicts with an exported Durable Object class.")); - continue; - } - if (!appWorker.hasWorkflowClass(className)) { - errorReporter.addError(kj::str("Workflow \"", workflowName, "\" refers to class \"", - className, "\", but the Worker exports no such WorkflowEntrypoint.")); - continue; - } - - auto& bindingService = KJ_UNWRAP_OR(services.find(workflow.getBindingService().getName()), { - errorReporter.addError( - kj::str("Workflow \"", workflowName, "\"'s bindingService refers to a service \"", - workflow.getBindingService().getName(), "\", but no such service is defined.")); - continue; - }); - auto& bindingWorker = KJ_UNWRAP_OR(kj::tryDowncast(*bindingService), { - errorReporter.addError( - kj::str("Workflow \"", workflowName, "\"'s bindingService refers to service \"", - workflow.getBindingService().getName(), "\", but it is not a Worker.")); - continue; - }); - kj::Maybe bindingEntrypointName; - if (workflow.getBindingService().hasEntrypoint()) { - bindingEntrypointName = workflow.getBindingService().getEntrypoint(); - } - if (!bindingWorker.hasStatelessEntrypoint(bindingEntrypointName)) { - errorReporter.addError(kj::str("Workflow \"", workflowName, - "\"'s bindingService Worker does not export WorkerEntrypoint \"", - bindingEntrypointName.orDefault("default"), "\".")); - continue; - } - if (!bindingWorker.hasLocalDiskActorStorage()) { - errorReporter.addError(kj::str("Workflow \"", workflowName, - "\"'s bindingService Worker must configure durableObjectStorage.localDisk; in-memory " - "and absent storage are unsupported.")); - continue; - } - - // The single Engine actor class serves every Workflow, so the per-Workflow identity is - // supplied through the actor's props rather than baked into the class. `workflowClass` is a - // loopback stub to the app Worker's WorkflowEntrypoint (the user code the Engine runs); - // `workflowClassName`/`workflowName` identify it. These are set here by the runtime, which is - // why `workflowsEngine.actorClass` is rejected earlier if it specifies props of its own. - Frankenvalue props; - props.setProperty(kj::str("workflowClass"), - Frankenvalue::fromCapability(static_cast(rpc::SerializationTag::SERVICE_STUB), - appWorker.getLoopbackEntrypoint(className).toOwn())); - props.setProperty( - kj::str("workflowClassName"), Frankenvalue::fromJson(escapeJsonString(className))); - props.setProperty( - kj::str("workflowName"), Frankenvalue::fromJson(escapeJsonString(workflowName))); - - auto actorClass = KJ_UNWRAP_OR(engineWorker.getActorClass(actorClassName, kj::mv(props)), { - errorReporter.addError(kj::str("workflowsEngine.actorClass refers to service \"", - actorClassDesignator.getName(), "\" with Durable Object entrypoint \"", - actorClassName.orDefault("default"), "\", but no such class is exported.")); - continue; - }); - appWorker.initWorkflowActorNamespace(actorConfig, kj::mv(actorClass), bindingWorker, - engineWorker.selfTokensArePersistent(), actorNamespacesByUniqueKey, network); - } - } - - // Third pass: Cross-link services. - for (auto& service: services) { - ConfigErrorReporter errorReporter(*this, service.key); - service.value->link(errorReporter); - } - - // Actor namespaces are linked in a separate final loop, after every service's `link()` has run. - // A Workflow namespace resolves its storage from its bindingService Worker's `getActorStorage()`, - // which is only valid once that Worker has been linked; doing this in the loop above could - // observe a not-yet-linked bindingService. - for (auto& service: services) { - ConfigErrorReporter errorReporter(*this, service.key); - service.value->linkActorNamespaces(errorReporter); - } -} - -namespace { - -// The host part of a "host[:port]" listen address ("*", "127.0.0.1", "[::1]"). Unix socket -// addresses have no host and are returned whole. -kj::String hostOfAddress(kj::StringPtr addrStr) { - if (addrStr.startsWith("unix:")) return kj::str(addrStr); - KJ_IF_SOME(colon, addrStr.findLast(':')) { - // A bare IPv6 literal without brackets contains colons but no port. - if (addrStr.startsWith("[") || addrStr.findFirst(':') == colon) { - return kj::str(addrStr.first(colon)); - } - } - return kj::str(addrStr); -} - -uint defaultPortFor(config::Socket::Reader sock) { - switch (sock.which()) { - case config::Socket::HTTP: - return 80; - case config::Socket::HTTPS: - return 443; - case config::Socket::TCP: - return 0; - case config::Socket::UDP: - return 0; - } - return 0; -} - -} // namespace - -kj::Maybe Server::parseSocketType( - config::Socket::Reader sock, kj::StringPtr name) { - switch (sock.which()) { - case config::Socket::HTTP: { - SocketTypeConfig result; - result.defaultPort = defaultPortFor(sock); - result.httpOptions = sock.getHttp(); - result.physicalProtocol = "http"; - return kj::mv(result); - } - case config::Socket::HTTPS: { - auto https = sock.getHttps(); - SocketTypeConfig result; - result.defaultPort = defaultPortFor(sock); - result.httpOptions = https.getOptions(); - result.tls = makeTlsContext(https.getTlsOptions()); - result.physicalProtocol = "https"; - return kj::mv(result); - } - case config::Socket::TCP: { - auto tcp = sock.getTcp(); - SocketTypeConfig result; - if (tcp.hasTlsOptions()) { - result.tls = makeTlsContext(tcp.getTlsOptions()); - } - return kj::mv(result); - } - case config::Socket::UDP: { - // listenOnSockets() handles UDP sockets in its own branch, before parseSocketType() is - // called, since UDP binds a DatagramPort rather than listening for connections. This case - // should be unreachable. - KJ_UNREACHABLE; - } - } - reportConfigError(kj::str("Encountered unknown socket type in \"", name, - "\". Was the config compiled with a newer version of the schema?")); - return kj::none; -} - -kj::Promise Server::bindSockets(config::Config::Reader config) { - TRACE_EVENT("workerd", "bindSockets"); - for (auto sock: config.getSockets()) { - kj::String name = kj::str(sock.getName()); - kj::String addrStr; - kj::Maybe> listenerOverride; - - KJ_IF_SOME(override, socketOverrides.findEntry(name)) { - KJ_SWITCH_ONEOF(override.value) { - KJ_CASE_ONEOF(str, kj::String) { - addrStr = kj::mv(str); - break; - } - KJ_CASE_ONEOF(l, kj::Own) { - listenerOverride = kj::mv(l); - break; - } - } - socketOverrides.erase(override); - } else if (sock.hasAddress()) { - addrStr = kj::str(sock.getAddress()); - } else { - reportConfigError(kj::str("Socket \"", name, - "\" has no address in the config, so must be specified on the " - "command line with `--socket-addr`.")); - boundSockets.add(kj::none); - continue; - } - - if (sock.which() == config::Socket::UDP) { - if (listenerOverride != kj::none) { - reportConfigError(kj::str("Socket \"", name, - "\" is a UDP socket; --socket-fd overrides (which pass a listening " - "connection-oriented socket) are not supported for it.")); - boundSockets.add(kj::none); - continue; - } - - auto parsed = co_await network.parseAddress(addrStr, defaultPortFor(sock)); - boundSockets.add(BoundSocket{parsed->bindDatagramPort(), kj::mv(addrStr)}); - continue; - } - - kj::Own listener; - KJ_IF_SOME(l, listenerOverride) { - listener = kj::mv(l); - } else { - auto parsed = co_await network.parseAddress(addrStr, defaultPortFor(sock)); - listener = parsed->listen(); - } - - if (sock.which() == config::Socket::TCP && sock.getService().hasName()) { - inboundListeners - .findOrCreate(sock.getService().getName(), - [&]() { - return decltype(inboundListeners)::Entry{ - kj::str(sock.getService().getName()), kj::Vector()}; - }) - .add(Worker::Api::InboundListener{ - .protocol = kj::str("tcp"), - .address = hostOfAddress(addrStr), - .port = static_cast(listener->getPort()), - }); - } - - boundSockets.add(BoundSocket{kj::mv(listener), kj::mv(addrStr)}); - } -} - -kj::Promise Server::listenOnSockets(config::Config::Reader config, - kj::HttpHeaderTable::Builder& headerTableBuilder, - kj::ForkedPromise& forkedDrainWhen, - bool forTest) { - // --------------------------------------------------------------------------- - // Start sockets - TRACE_EVENT("workerd", "listenOnSockets"); - auto sockets = config.getSockets(); - KJ_ASSERT(boundSockets.size() == sockets.size()); - for (auto i: kj::indices(sockets)) { - auto sock = sockets[i]; - kj::String name = kj::str(sock.getName()); - - // Sockets that failed to bind have already reported a config error. - kj::Own listener; - kj::Own datagramPort; - kj::String addrStr; - KJ_IF_SOME(bound, boundSockets[i]) { - KJ_SWITCH_ONEOF(bound.port) { - KJ_CASE_ONEOF(l, kj::Own) { - listener = kj::mv(l); - } - KJ_CASE_ONEOF(p, kj::Own) { - datagramPort = kj::mv(p); - } - } - addrStr = kj::mv(bound.addrStr); - boundSockets[i] = kj::none; - } else { - continue; - } - - kj::Rc service = lookupService(sock.getService(), kj::str("Socket \"", name, "\"")); - - if (sock.which() == config::Socket::UDP) { - auto idleTimeout = sock.getUdp().getIdleTimeoutMs() * kj::MILLISECONDS; - size_t maxPendingBytes = sock.getUdp().getMaxPendingBytes(); - - // Server owns and cancels its listener tasks before teardown, so `this` cannot outlive it. - auto handle = kj::coCapture([this, service = kj::mv(service), name = kj::mv(name), - addrStr = kj::mv(addrStr), idleTimeout, maxPendingBytes]( - kj::Own port) mutable -> kj::Promise { - TRACE_EVENT("workerd", "setup listenUdp"); - KJ_IF_SOME(stream, controlOverride) { - auto message = kj::str( - "{\"event\":\"listen\",\"socket\":\"", name, "\",\"port\":", port->getPort(), "}\n"); - try { - stream->write(message.asBytes()); - } catch (kj::Exception& e) { - KJ_LOG(ERROR, e); - } - } - - co_await listenUdp(kj::mv(port), kj::mv(service), addrStr, idleTimeout, maxPendingBytes); - }); - tasks.add(handle(kj::mv(datagramPort)).exclusiveJoin(forkedDrainWhen.addBranch())); - continue; - } - - auto maybeSocketConfig = parseSocketType(sock, name); - if (maybeSocketConfig == kj::none) continue; - auto& socketConfig = KJ_ASSERT_NONNULL(maybeSocketConfig); - - KJ_IF_SOME(t, socketConfig.tls) { - listener = t->wrapPort(kj::mv(listener)).attach(kj::mv(t)); - } - - // Need to create rewriter before waiting on anything since `headerTableBuilder` will no longer - // be available later. - auto rewriter = kj::heap(socketConfig.httpOptions, headerTableBuilder); - - auto handle = - kj::coCapture([this, service = kj::mv(service), rewriter = kj::mv(rewriter), - physicalProtocol = socketConfig.physicalProtocol, name = kj::mv(name), - isHttp = sock.which() != config::Socket::TCP, addrStr = kj::mv(addrStr)]( - kj::Own listener) mutable -> kj::Promise { - if (isHttp) { - TRACE_EVENT("workerd", "setup listenHttp"); - } else { - TRACE_EVENT("workerd", "setup listenTcp"); - } - - KJ_IF_SOME(stream, controlOverride) { - auto message = kj::str("{\"event\":\"listen\",\"socket\":\"", name, - "\",\"port\":", listener->getPort(), "}\n"); - try { - stream->write(message.asBytes()); - } catch (kj::Exception& e) { - KJ_LOG(ERROR, e); - } - } - - if (isHttp) { - co_await listenHttp(kj::mv(listener), kj::mv(service), physicalProtocol, kj::mv(rewriter)); - } else { - // The authority handed to the connect() handler is the endpoint as bound, so it is - // truthful for a configured port of 0. - auto authority = kj::str(hostOfAddress(addrStr), ":", listener->getPort()); - co_await listenTcp(kj::mv(listener), kj::mv(service), kj::mv(authority)); - } - }); - tasks.add(handle(kj::mv(listener)).exclusiveJoin(forkedDrainWhen.addBranch())); - } - - // Start debug port if configured - KJ_IF_SOME(addr, debugPortOverride) { - auto handle = kj::coCapture( - [this, addr = kj::str(addr)](kj::ForkedPromise& drain) mutable -> kj::Promise { - auto parsed = co_await network.parseAddress(addr, 0); - auto listener = parsed->listen(); - - KJ_IF_SOME(stream, controlOverride) { - auto message = kj::str("{\"event\":\"listen\",\"socket\":\"debug-port" - "\",\"port\":", - listener->getPort(), "}\n"); - try { - stream->write(message.asBytes()); - } catch (kj::Exception& e) { - KJ_LOG(ERROR, e); - } - } - - co_await listenDebugPort(kj::mv(listener)); - }); - tasks.add(handle(forkedDrainWhen).exclusiveJoin(forkedDrainWhen.addBranch())); - } - - for (auto& unmatched: socketOverrides) { - reportConfigError(kj::str("Config did not define any socket named \"", unmatched.key, - "\" to match the override " - "provided on the command line.")); - } - - for (auto& unmatched: externalOverrides) { - reportConfigError(kj::str("Config did not define any external service named \"", unmatched.key, - "\" to match the " - "override provided on the command line.")); - } - - for (auto& unmatched: directoryOverrides) { - if (forTest && unmatched.key == "TEST_TMPDIR") { - // Due to a historical bug, `workerd test` didn't check for the existence of unmatched - // overrides, and our own tests became dependent on the ability to override TEST_TMPDIR - // even if it was not used in the config. For now, we ignore this problem. - // - // TODO(cleanup): Figure out the right solution here. - continue; - } - - reportConfigError(kj::str("Config did not define any disk service named \"", unmatched.key, - "\" to match the " - "override provided on the command line.")); - } - - co_await tasks.onEmpty(); - - // Give a chance for any errors to bubble up before we return success. In particular - // Server::taskFailed() fulfills `fatalFulfiller`, which causes the server to exit with an error. - // But the `TaskSet` may have become empty at the same time. We want the error to win the race - // against the success. - // - // TODO(cleanup): A better solution would be for `TaskSet` to have a new variant of the - // `onEmpty()` method like `onEmptyOrException()`, which propagates any exception thrown by - // any task. - co_await kj::yieldUntilQueueEmpty(); -} - -// ======================================================================================= -// Server::test() - -kj::Promise Server::test(jsg::V8System& v8System, - config::Config::Reader config, - kj::StringPtr servicePattern, - kj::StringPtr entrypointPattern) { - - if (config.hasLogging()) { - auto logging = config.getLogging(); - loggingOptions.structuredLogging = StructuredLogging(logging.getStructuredLogging()); - if (logging.hasStdoutPrefix()) { - loggingOptions.stdoutPrefix = kj::ConstString(kj::str(logging.getStdoutPrefix())); - } - if (logging.hasStderrPrefix()) { - loggingOptions.stderrPrefix = kj::ConstString(kj::str(logging.getStderrPrefix())); - } - } else { - loggingOptions.structuredLogging = StructuredLogging(config.getStructuredLogging()); - } - - kj::HttpHeaderTable::Builder headerTableBuilder; - globalContext = kj::heap(*this, v8System, headerTableBuilder); - invalidConfigServiceSingleton = kj::rc(); - - auto [fatalPromise, fatalFulfiller] = kj::newPromiseAndFulfiller(); - this->fatalFulfiller = kj::mv(fatalFulfiller); - - auto forkedDrainWhen = kj::Promise(kj::NEVER_DONE).fork(); - - co_await bindSockets(config); - co_await startServices(v8System, config, headerTableBuilder, forkedDrainWhen); - - // Tests usually do not configure sockets, but they can, especially loopback sockets. Arrange - // to wait on them. Crash if listening fails. - auto listenPromise = - listenOnSockets(config, headerTableBuilder, forkedDrainWhen, - /* forTest = */ true) - .eagerlyEvaluate([](kj::Exception&& e) noexcept { kj::throwFatalException(kj::mv(e)); }); - - auto ownHeaderTable = headerTableBuilder.build(); - - // TODO(someday): If the inspector is enabled, pause and wait for an inspector connection before - // proceeding? - - kj::GlobFilter serviceGlob(servicePattern); - kj::GlobFilter entrypointGlob(entrypointPattern); - - uint passCount = 0, failCount = 0; - - auto doTest = [&](Service& service, kj::StringPtr name) -> kj::Promise { - // TODO(soon): Better way of reporting test results, KJ_LOG is ugly. We should probably have - // some sort of callback interface. It would be nice to report the exceptions thrown through - // that interface too... can we? Use a tracer maybe? - // HACK: We use DBG log level because INFO logging is optional, and warning/error would confuse - // people. Note that server-test.c++ actually tests for this logging, so simply writing to - // stderr wouldn't work. - KJ_LOG(DBG, kj::str("[ TEST ] "_kj, name)); - auto req = service.startRequest({}); - auto start = monotonicClock.now(); - - bool result = co_await req->test(); - if (result) { - ++passCount; - } else { - ++failCount; - } - - auto end = monotonicClock.now(); - auto duration = end - start; - - KJ_LOG(DBG, kj::str(result ? "[ PASS ] "_kj : "[ FAIL ] "_kj, name, " (", duration, ")")); - }; - - for (auto& service: services) { - if (serviceGlob.matches(service.key)) { - if (service.value->hasHandler("test"_kj) && entrypointGlob.matches("default"_kj)) { - co_await doTest(*service.value, service.key); - } - - KJ_IF_SOME(worker, kj::tryDowncast(*service.value)) { - for (auto& name: worker.getEntrypointNames()) { - if (entrypointGlob.matches(name)) { - kj::Rc ep = KJ_ASSERT_NONNULL(worker.getEntrypoint(name, /*props=*/{})); - if (ep->hasHandler("test"_kj)) { - co_await doTest(*ep, kj::str(service.key, ':', name)); - } - } - } - } - } - } - - if (passCount + failCount == 0) { - KJ_LOG(ERROR, "No tests found!"); - } - - co_return passCount > 0 && failCount == 0; -} - -} // namespace workerd::server diff --git a/src/workerd/server/server.h b/src/workerd/server/server.h deleted file mode 100644 index ce3f82fa451..00000000000 --- a/src/workerd/server/server.h +++ /dev/null @@ -1,407 +0,0 @@ -// Copyright (c) 2017-2022 Cloudflare, Inc. -// Licensed under the Apache 2.0 license found in the LICENSE file or at: -// https://opensource.org/licenses/Apache-2.0 - -#pragma once - -#include "channel-token.h" - -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace kj { -class TlsContext; -} - -namespace workerd::jsg { -class V8System; -} - -namespace workerd::server { - -using api::pyodide::PythonConfig; - -// Implements the single-tenant Workers Runtime server / CLI. -// -// The purpose of this class is to implement the core logic independently of the CLI itself, -// in such a way that it can be unit-tested. cli-main.c++ implements the CLI wrapper around this. -class Server final: private kj::TaskSet::ErrorHandler, private ChannelTokenHandler::Resolver { - public: - Server(kj::Filesystem& fs, - kj::Timer& timer, - const kj::MonotonicClock& monotonicClock, - kj::Network& network, - kj::EntropySource& entropySource, - Worker::LoggingOptions loggingOptions, - kj::Function reportConfigError, - kj::Function reportConfigWarning); - ~Server() noexcept; - - // Permit experimental features to be used. These features may break backwards compatibility - // in the future. - void allowExperimental() { - experimental = true; - } - - void overrideSocket(kj::String name, kj::Own port) { - socketOverrides.upsert(kj::mv(name), kj::mv(port)); - } - void overrideSocket(kj::String name, kj::String addr) { - socketOverrides.upsert(kj::mv(name), kj::mv(addr)); - } - void overrideDirectory(kj::String name, kj::String path) { - directoryOverrides.upsert(kj::mv(name), kj::mv(path)); - } - void overrideExternal(kj::String name, kj::String addr) { - externalOverrides.upsert(kj::mv(name), kj::mv(addr)); - } - void enableInspector(kj::String addr) { - inspectorOverride = kj::mv(addr); - } - void enableControl(uint fd) { - controlOverride = kj::heap(fd); - } - void enableDebugPort(kj::String addr) { - debugPortOverride = kj::mv(addr); - } - void setPackageDiskCacheRoot(kj::Maybe>&& dir) { - pythonConfig.packageDiskCacheRoot = kj::mv(dir); - } - void setPyodideDiskCacheRoot(kj::Maybe>&& dir) { - pythonConfig.pyodideDiskCacheRoot = kj::mv(dir); - } - void setPythonCreateSnapshot() { - pythonConfig.createSnapshot = true; - } - void setPythonCreateBaselineSnapshot() { - pythonConfig.createBaselineSnapshot = true; - } - void setPythonLoadSnapshot(kj::String snapshot) { - pythonConfig.loadSnapshotFromDisk = kj::mv(snapshot); - } - void setPythonSnapshotDirectory(kj::Maybe>&& dir) { - pythonConfig.snapshotDirectory = kj::mv(dir); - } - - // Set the compatibility date to use for all workers. When set, workers in the config must NOT - // specify compatibilityDate (an error is reported if they do). This is used for testing to - // ensure tests run with both old and new compat dates. - void setTestCompatibilityDateOverride(kj::String date) { - testCompatibilityDateOverride = kj::mv(date); - } - - // Runs the server using the given config. - kj::Promise run(jsg::V8System& v8System, - config::Config::Reader conf, - kj::Promise drainWhen = kj::NEVER_DONE); - - // Executes one or more tests. By default, all exported test handlers from all entrypoints to - // all services in the config are executed. Glob patterns can be specified to match specific - // service and entrypoint names. - // - // The returned promise resolves true if at least one test ran and no tests failed. - kj::Promise test(jsg::V8System& v8System, - config::Config::Reader conf, - kj::StringPtr servicePattern = "*"_kj, - kj::StringPtr entrypointPattern = "*"_kj); - - struct Durable { - kj::String uniqueKey; - bool isEvictable; - bool enableSql; - // True when this config was synthesized to back a Workflow (from `workflowsEngine`) rather - // than declared as a normal Durable Object namespace. Workflow-backing namespaces are created - // and linked through a separate code path (see `initWorkflowActorNamespace`) because they use - // an external engine's actor class and take their storage from a different Worker; this flag - // makes the normal Durable Object init/link paths skip them. - bool isWorkflow = false; - kj::Maybe containerOptions; - }; - struct Ephemeral { - bool isEvictable; - bool enableSql; - }; - using ActorConfig = kj::OneOf; - - class InspectorService; - class InspectorServiceIsolateRegistrar; - - void handleReportConfigError(kj::String error) { - reportConfigError(kj::mv(error)); - } - - void handleReportConfigWarning(kj::String warning) { - reportConfigWarning(kj::mv(warning)); - } - - private: - kj::Filesystem& fs; - kj::Timer& timer; - // monotonicClock must produce time values consistent with those produced by timer whenever - // timer updates, but monotonicClock updates continuously (not just when system I/O is polled). - const kj::MonotonicClock& monotonicClock; - kj::Network& network; - kj::EntropySource& entropySource; - kj::Function reportConfigError; - kj::Function reportConfigWarning; - PythonConfig pythonConfig = PythonConfig{.packageDiskCacheRoot = kj::none, - .pyodideDiskCacheRoot = kj::none, - .createSnapshot = false, - .createBaselineSnapshot = false, - .loadSnapshotFromDisk = kj::none}; - - bool experimental = false; - - // When set, overrides compatibilityDate for all workers and enforces that workers don't - // specify their own compatibilityDate. - kj::Maybe testCompatibilityDateOverride; - - Worker::LoggingOptions loggingOptions; - - kj::Own memoryCacheProvider; - - ChannelTokenHandler channelTokenHandler; - - kj::HashMap>> socketOverrides; - kj::HashMap directoryOverrides; - - // Sockets are bound before services start so that a worker can learn the actual port of each - // inbound listener that targets it (Worker::Api::getInboundListeners()). Indexed by position in - // the config's socket list (names may repeat); none for a socket that failed to bind. Consumed - // by listenOnSockets(). - struct BoundSocket { - kj::OneOf, kj::Own> port; - kj::String addrStr; - }; - kj::Vector> boundSockets; - // Bound TCP listeners by the name of the service they deliver to. - kj::HashMap> inboundListeners; - - // Overrides from the command line. - // - // String overrides are left as strings rather than parsed by the caller in order to reuse the - // code that parses strings from the config file. - kj::HashMap externalOverrides; - - kj::Maybe inspectorOverride; - kj::Maybe> inspectorIsolateRegistrar; - kj::Maybe> controlOverride; - kj::Maybe debugPortOverride; - - struct GlobalContext; - // General context needed to construct workers. Initialized early in run(). - kj::Own globalContext; - - class Service; - kj::Rc invalidConfigServiceSingleton; - - class ActorClass; - kj::Rc invalidConfigActorClassSingleton; - - // Information about all known actor namespaces. Maps serviceName -> className -> config. - // This needs to be populated in advance of constructing any services, in order to be able to - // correctly construct dependent services. - kj::HashMap> actorConfigs; - - kj::HashMap> services; - - class ActorNamespace; - kj::HashMap actorNamespacesByUniqueKey; - - class WorkerLoaderNamespace; - kj::HashMap> workerLoaderNamespaces; - kj::Vector> anonymousWorkerLoaderNamespaces; - - kj::Own> fatalFulfiller; - - // An HttpServer object maintained in a linked list. - struct ListedHttpServer { - Server& owner; - kj::HttpServer httpServer; - kj::ListLink link; - - template - ListedHttpServer(Server& owner, Params&&... params) - : owner(owner), - httpServer(kj::fwd(params)...) { - owner.httpServers.add(*this); - }; - ~ListedHttpServer() noexcept(false) { - owner.httpServers.remove(*this); - } - }; - - // All active HttpServer objects -- used to implement drain(). - kj::List httpServers; - - // Especially includes server loop tasks to listen on sockets. Any error is considered fatal. - kj::TaskSet tasks; - - // Reports an exception thrown by a task in `tasks`. - void taskFailed(kj::Exception&& exception) override; - - // Tell all HttpServers to drain once the drainWhen promise resolves. - // This causes them to disconnect any connections that do not have a - // request in flight. - kj::Promise handleDrain(kj::Promise drainWhen); - - kj::Own makeTlsContext(config::TlsOptions::Reader conf); - kj::Promise> makeTlsNetworkAddress(config::TlsOptions::Reader conf, - kj::StringPtr addrStr, - kj::Maybe certificateHost, - uint defaultPort = 0); - - class HttpRewriter; - - kj::Rc makeInvalidConfigService(); - kj::Rc makeExternalService(kj::StringPtr name, - config::ExternalServer::Reader conf, - kj::HttpHeaderTable::Builder& headerTableBuilder); - kj::Rc makeNetworkService(config::Network::Reader conf); - kj::Rc makeDiskDirectoryService(kj::StringPtr name, - config::DiskDirectory::Reader conf, - kj::HttpHeaderTable::Builder& headerTableBuilder); - kj::Promise> makeWorker(kj::StringPtr name, - config::Worker::Reader conf, - capnp::List::Reader extensions); - kj::Promise> makeService(config::Service::Reader conf, - kj::HttpHeaderTable::Builder& headerTableBuilder, - capnp::List::Reader extensions); - - // Aborts all actors in this server except those in namespaces marked with `preventEviction`. - void abortAllActors(kj::Maybe reason); - - // Aborts all actors, cancels all alarms, and deletes all underlying storage for evictable - // namespaces. After this, DOs can be recreated with clean state. Useful for test isolation. - void deleteAllActors(kj::Maybe reason); - - // Can only be called in the link stage. - // - // May return a new object or may return a fake-own around a long-lived object. - kj::Rc lookupService( - config::ServiceDesignator::Reader designator, kj::String errorContext); - - // Like lookupService() but looks up an actor class (especially for use as a facet class). - // Returns none on a config error. - kj::Rc lookupActorClass( - config::ServiceDesignator::Reader designator, kj::String errorContext); - - // Pretty similar to lookupService() and lookupActorClass(), but these callbacks are called by - // the `ChannelTokenHandler` when decoding tokens. - kj::Rc resolveEntrypoint(kj::StringPtr serviceName, - kj::Maybe entrypoint, - Frankenvalue props, - Persistent persistent) override; - kj::Rc resolveActorClass(kj::StringPtr serviceName, - kj::Maybe entrypoint, - Frankenvalue props, - Persistent persistent) override; - kj::Rc resolveActor(kj::StringPtr namespaceKey, - kj::ArrayPtr id, - kj::Maybe name, - Persistent persistent) override; - - kj::Array encodeChannelToken(IoChannelFactory::ChannelTokenUsage usage, - kj::StringPtr serviceName, - kj::Maybe entrypoint, - Frankenvalue& props); - - void decodeChannelToken(IoChannelFactory::ChannelTokenUsage usage, - kj::ArrayPtr token, - kj::FunctionParam entrypoint, Frankenvalue props)> - callback); - - kj::Promise listenHttp(kj::Own listener, - kj::Rc service, - kj::StringPtr physicalProtocol, - kj::Own rewriter); - - kj::Promise listenTcp( - kj::Own listener, kj::Rc service, kj::String authority); - - kj::Promise listenUdp(kj::Own port, - kj::Rc service, - kj::StringPtr addrStr, - kj::Duration idleTimeout, - size_t maxPendingBytes); - - kj::Promise listenDebugPort(kj::Own listener); - rpc::WorkerdDebugPort::Client makeWorkerdDebugPortClient(); - - class InvalidConfigService; - class InvalidConfigActorClass; - class ExternalHttpService; - class ExternalTcpService; - class NetworkService; - class DiskDirectoryService; - class WorkerService; - class WorkerEntrypointService; - class WorkerdBootstrapImpl; - class HttpListener; - class TcpListener; - class UdpListener; - class DebugPortListener; - class WorkerdDebugPortImpl; - - struct ErrorReporter; - struct ConfigErrorReporter; - struct DynamicErrorReporter; - struct WorkerDef; - kj::Promise> makeWorkerImpl(kj::StringPtr name, - WorkerDef def, - capnp::List::Reader extensions, - ErrorReporter& errorReporter); - - kj::Own makeWorkerIsolate(kj::StringPtr name, - kj::StringPtr inboundListenersKey, - const WorkerDef& def, - capnp::List::Reader extensions, - Worker::Isolate::InspectorPolicy inspectorPolicy, - kj::Maybe snapshotConfig); - - // Creates a throwaway zygote Worker in PREPARE_SNAPSHOT mode and returns the filled snapshot - // artifact, or kj::none when the zygote failed to start. - kj::Maybe> makeSnapshot( - kj::StringPtr name, WorkerDef& def, capnp::List::Reader extensions); - - kj::Promise startServices(jsg::V8System& v8System, - config::Config::Reader config, - kj::HttpHeaderTable::Builder& headerTableBuilder, - kj::ForkedPromise& forkedDrainWhen); - - kj::Promise bindSockets(config::Config::Reader config); - - kj::Promise listenOnSockets(config::Config::Reader config, - kj::HttpHeaderTable::Builder& headerTableBuilder, - kj::ForkedPromise& forkedDrainWhen, - bool forTest = false); - - // Parsed socket protocol/TLS config. Extracted from the switch in listenOnSockets() to avoid - // goto-over-initialization inside a coroutine, which triggers a clang optimizer crash. - struct SocketTypeConfig { - uint defaultPort = 0; - config::HttpOptions::Reader httpOptions; - kj::Maybe> tls; - kj::StringPtr physicalProtocol; - }; - kj::Maybe parseSocketType(config::Socket::Reader sock, kj::StringPtr name); - - void unlinkWorkerLoaders(); - - kj::Promise preloadPython( - kj::StringPtr workerName, const WorkerDef& workerDef, ErrorReporter& errorReporter); - - friend struct FutureSubrequestChannel; - friend struct FutureActorClassChannel; -}; - -} // namespace workerd::server diff --git a/src/workerd/server/server/BUILD.bazel b/src/workerd/server/server/BUILD.bazel new file mode 100644 index 00000000000..73add32e183 --- /dev/null +++ b/src/workerd/server/server/BUILD.bazel @@ -0,0 +1,58 @@ +load("//:build/wd_rust_crate.bzl", "wd_rust_crate") + +# The Rust server. Everything that needs the isolate is behind the C++ worker factory +# (//src/workerd/server/factory), reached through the cxx bridge in bridge.rs. +wd_rust_crate( + name = "workerd-server", + cxx_bridge_deps = [ + "//src/rust/cxx/kj-rs-tokio:kj-rs-tokio-lib", + "//src/workerd/server/factory:bootstrap-hdrs", + "//src/workerd/server/factory:worker-factory-hdrs", + ], + cxx_bridge_hdrs = [], + cxx_bridge_src = "bridge.rs", + # The factory's C++ includes the bridge header. + cxx_bridge_visibility = ["//src/workerd/server/factory:__pkg__"], + link_deps = [ + # The command line's bootstrap, called from entry.rs. + "//src/workerd/server/factory:bootstrap", + "//src/workerd/server/factory:worker-factory", + ], + proc_macro_deps = [ + "@crates_vendor//:async-trait", + ], + # The tests link C++ that reaches kj::setupAsyncIo(), the binary's tokio-backed one. + test_deps = [ + "//src/workerd/util:setup-async-io", + ], + visibility = ["//visibility:public"], + deps = [ + "//src/rust/cxx/kj-hyper", + "//src/rust/cxx/kj-rs-io", + "//src/rust/cxx/kj-rs-tokio", + "//src/rust/kj", + "//src/rust/worker", + "//src/workerd/server:compiled-bindings_capnp_rust", + "//src/workerd/server:workerd_capnp_rust", + "@crates_vendor//:base64", + "@crates_vendor//:capnp", + "@crates_vendor//:data-encoding", + "@crates_vendor//:futures", + "@crates_vendor//:glob", + "@crates_vendor//:hashlink", + "@crates_vendor//:http", + "@crates_vendor//:httpdate", + "@crates_vendor//:ipnet", + "@crates_vendor//:percent-encoding", + "@crates_vendor//:rand", + "@crates_vendor//:rustls", + "@crates_vendor//:serde_json", + "@crates_vendor//:socket2", + "@crates_vendor//:tokio", + "@crates_vendor//:tracing", + "@crates_vendor//:url", + ] + select({ + "@platforms//os:windows": ["@crates_vendor//:windows-sys"], + "//conditions:default": [], + }), +) diff --git a/src/workerd/server/server/actor.rs b/src/workerd/server/server/actor.rs new file mode 100644 index 00000000000..afeb9774aa1 --- /dev/null +++ b/src/workerd/server/server/actor.rs @@ -0,0 +1,1155 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! Durable Object namespaces: the actors of one class of one worker, their storage, facets and +//! eviction. A worker service owns one namespace per class its config gives storage to. +//! +//! An [`ActorNamespace`] files its actors by key (the hex id of a durable actor, the name of an +//! ephemeral one) in [`ActorContainer`]s. A container outlives the actor it holds: an actor idle +//! for ten seconds is shut down, its WebSockets hibernated, and rebuilt by the next request; a +//! container with no clients and no access for seventy seconds is dropped by the namespace's +//! cleanup loop; a broken actor removes its container at once. Facets are containers under a root +//! container, sharing its storage and its last-access time. +//! +//! Ownership: the namespace owns its root containers and a container its facets. Everything that +//! points the other way (a facet to its parent; an actor's hooks and a background task to their +//! container) holds a `Weak`, so that a container dropped from its map goes away with its actor. +//! Whatever else holds a container's `Rc` keeps it from the cleanup loop: a stub's channel and a +//! request in flight (each a [`Client`]), and an eviction in progress. + +use std::cell::Cell; +use std::cell::OnceCell; +use std::cell::RefCell; +use std::rc::Rc; +use std::rc::Weak; +use std::time::Duration; + +use futures::FutureExt; +use futures::future::Either; +use futures::future::LocalBoxFuture; +use futures::future::Shared; +use hashlink::LinkedHashMap; +use kj_rs::KjMaybe; +use kj_rs::KjOwn; +use kj_rs::KjRc; +use tokio::sync::Notify; +use worker::Interface; +use worker::PromisedInterface; + +use crate::Result; +use crate::bindings::ActorConfig; +use crate::bridge::ffi; +use crate::channels::AbortReason; +use crate::channels::ActorClass; +use crate::channels::ActorHooks; +use crate::channels::ActorIdHandle; +use crate::channels::ActorNamespaceHandle; +use crate::channels::Channel; +use crate::channels::FacetStart; +use crate::channels::NewActor; +use crate::channels::PendingToken; +use crate::channels::Persistent; +use crate::channels::RequestMetadata; +use crate::channels::SubrequestChannel; +use crate::channels::TokenUsage; +use crate::channels::WorkerInterface; +use crate::channels::attach; +use crate::config::Factory; +use crate::tasks::TaskHandle; + +/// How long an actor stays up after its last request ends before it is shut down. +const IDLE_SHUTDOWN_DELAY: Duration = Duration::from_secs(10); +/// How long a container with no clients stays in its namespace after its last access. +const CONTAINER_EXPIRATION: Duration = Duration::from_secs(70); +/// How long a test eviction waits for the actor's requests to drain. +const EVICT_TIMEOUT: Duration = Duration::from_secs(30); +/// How long a test eviction waits before re-checking an actor that has no requests but is still +/// referenced (a request tearing down). +const EVICT_RETRY_DELAY: Duration = Duration::from_millis(1); + +const EVICT_TIMEOUT_MESSAGE: &str = + "jsg.Error: Timed out waiting to evict Durable Object: it still has active references."; + +/// The namespace of one Durable Object class: its actors and their shared storage. The runtime's +/// `ActorNamespace`. +pub struct ActorNamespace { + this: Weak, + factory: Rc, + class_name: String, + config: ActorConfig, + actor_class: Rc, + /// Whether the actors' self tokens may be stored: the `allow_irrevocable_stub_storage` flag + /// of the worker whose class they are. + persistent_self_tokens: Persistent, + /// Opened by `link`; every actor of the namespace shares it. An ephemeral namespace gets + /// in-memory storage that its actors, being ephemeral, never use. + storage: OnceCell>, + /// A broken actor removes its container; an idle one keeps it, minus the actor, so that the + /// next request rebuilds the actor in place. The cleanup loop drops long-idle, clientless + /// containers. + actors: RefCell>>, + /// The cleanup loop, started by the first request; dropped with the namespace. + cleanup_task: RefCell>, +} + +impl ActorNamespace { + /// Builds the namespace of `class_name`, a class of a worker `factory` compiled. + /// + /// `actor_class` is the class every actor of the namespace is constructed from: the worker's + /// own, with no props bound, or for a Workflow's namespace the engine's class with the + /// Workflow's props. `persistent_self_tokens` is that class's worker's flag. + pub fn new( + factory: Rc, + class_name: String, + config: ActorConfig, + actor_class: Rc, + persistent_self_tokens: Persistent, + ) -> Rc { + Rc::new_cyclic(|this| Self { + this: Weak::clone(this), + factory, + class_name, + config, + actor_class, + persistent_self_tokens, + storage: OnceCell::new(), + actors: RefCell::new(LinkedHashMap::new()), + cleanup_task: RefCell::new(None), + }) + } + + fn storage(&self) -> Result<&ffi::ActorStorage> { + self.storage.get().map(|storage| &**storage).ok_or_else(|| { + kj::failed!( + "Durable Object class \"{}\": link() has not been called", + self.class_name + ) + }) + } + + fn is_durable(&self) -> bool { + matches!(self.config, ActorConfig::Durable { .. }) + } + + fn enable_sql(&self) -> bool { + match &self.config { + ActorConfig::Durable { enable_sql, .. } | ActorConfig::Ephemeral { enable_sql, .. } => { + *enable_sql + } + } + } + + /// The container of the actor with `id`, created if the namespace has none. + fn container_for(&self, id: KjOwn) -> Rc { + let key = ffi::actor_id_key(&id); + let mut actors = self.actors.borrow_mut(); + if let Some(container) = actors.get(&key) { + return Rc::clone(container); + } + let container = ActorContainer::new_root(key.clone(), self, id); + actors.insert(key, Rc::clone(&container)); + container + } + + /// Starts the cleanup loop if it is not running. A namespace with `preventEviction` never + /// drops a container, so it runs none. + fn ensure_cleanup_loop(&self) { + if !self.is_evictable() || self.cleanup_task.borrow().is_some() { + return; + } + let task = self.factory.spawn(cleanup_loop(Weak::clone(&self.this))); + *self.cleanup_task.borrow_mut() = Some(task); + } + + /// Removes and returns the containers the cleanup loop should drop: not accessed within + /// `CONTAINER_EXPIRATION` and without clients. + fn take_expired(&self, now: Duration) -> Vec> { + let mut actors = self.actors.borrow_mut(); + let keys: Vec = actors + .iter() + // Checking the access time first: it is cheaper than the client walk. + .filter(|(_, container)| { + now.saturating_sub(container.last_access.get()) > CONTAINER_EXPIRATION + && !container.has_clients() + }) + .map(|(key, _)| key.clone()) + .collect(); + keys.iter().filter_map(|key| actors.remove(key)).collect() + } +} + +/// Drops containers the namespace has not touched in `CONTAINER_EXPIRATION`, that often. +async fn cleanup_loop(namespace: Weak) { + loop { + let Some(ns) = namespace.upgrade() else { + return; + }; + let factory = Rc::clone(&ns.factory); + let expired = ns.take_expired(factory.now()); + drop(ns); + // Dropping a container shuts its actor down; that happens outside the map's borrow. + drop(expired); + factory.sleep(CONTAINER_EXPIRATION).await; + } +} + +impl ActorNamespace { + /// The channel of the actor with `id`, started on first use. `persistent` is recorded on the + /// channel: whether stubs to it may be stored. + pub fn channel(&self, id: KjOwn, persistent: Persistent) -> Rc { + Rc::new(ActorChannel { + container: Client(self.container_for(id)), + persistent, + }) + } + + /// The channel of the ephemeral actor named `name`. + pub fn channel_by_name(&self, name: &str, persistent: Persistent) -> Rc { + self.channel(ffi::actor_id_from_name(name), persistent) + } + + /// Whether `abortAllActors()`/`deleteAllActors()` may evict this namespace's actors + /// (`preventEviction` unset). + pub fn is_evictable(&self) -> bool { + match &self.config { + ActorConfig::Durable { evictable, .. } | ActorConfig::Ephemeral { evictable, .. } => { + *evictable + } + } + } + + /// `reason` becomes the error of the actors' in-flight requests. + pub fn abort_all(&self, reason: Option<&crate::Error>) { + let actors = std::mem::take(&mut *self.actors.borrow_mut()); + for container in actors.values() { + container.abort(reason.cloned()); + } + } + + /// Aborts every actor and deletes the namespace's storage and alarms. + pub fn delete_all(&self, reason: Option<&crate::Error>) -> Result<()> { + // The databases are reset while their connections are open, before the abort closes + // them: Windows releases file locks late. + for container in self.actors.borrow().values() { + container.reset_storage(); + } + self.abort_all(reason); + match self.storage.get() { + Some(storage) => Ok(ffi::actor_storage_delete_all(storage)?), + None => Ok(()), + } + } + + /// Test hook: shuts down every running actor of the namespace (hibernating or closing its + /// WebSockets), keeping its container so that the next request rebuilds it. Nothing for a + /// namespace with `preventEviction`. + pub async fn evict_all_for_test(&self, hibernate: bool) -> Result<()> { + if !self.is_evictable() { + return Ok(()); + } + // The containers stay in the map; a broken actor may remove its own mid-eviction, + // which the eviction's own `Rc` survives. + let containers: Vec> = self.actors.borrow().values().cloned().collect(); + let evictions = containers + .iter() + .map(|container| container.evict_tree_for_test_if_running(hibernate)); + futures::future::join_all(evictions) + .await + .into_iter() + .collect::>() + } + + /// Whether `link` has opened the namespace's storage. + pub fn is_linked(&self) -> bool { + self.storage.get().is_some() + } + + /// Opens the namespace's storage. `storage_path` is the directory of the worker's + /// `durableObjectStorage.localDisk` service; none for in-memory storage. + pub fn link(&self, storage_path: Option<&str>) -> Result<()> { + if self.is_linked() { + return Err(kj::failed!("already called link()")); + } + let (path, unique_key) = match &self.config { + ActorConfig::Durable { unique_key, .. } => { + (storage_path.unwrap_or_default(), unique_key.as_str()) + } + ActorConfig::Ephemeral { .. } => ("", ""), + }; + let storage = ffi::factory_new_actor_storage( + self.factory.raw(), + path, + unique_key, + Box::new(ActorNamespaceHandle(Weak::clone(&self.this))), + )?; + // Checked above; nothing else sets it. + let _ = self.storage.set(storage); + Ok(()) + } + + /// The `uniqueKey` of a durable namespace; none for an ephemeral one. + pub fn unique_key(&self) -> Option<&str> { + match &self.config { + ActorConfig::Durable { unique_key, .. } => Some(unique_key), + ActorConfig::Ephemeral { .. } => None, + } + } + + /// Where the config has the class's `container` options, if it has any. + fn container(&self) -> Option<&ffi::ContainerRef> { + match &self.config { + ActorConfig::Durable { container, .. } => container.as_ref(), + ActorConfig::Ephemeral { .. } => None, + } + } + + /// Stops the namespace's actors ahead of shutdown so that none can race its containers' + /// Docker cleanup. Nothing for a class without `container` options. + pub fn begin_container_cleanup(&self) { + if self.container().is_some() { + self.abort_all(Some(&kj::disconnected!("Server shutting down."))); + } + } +} + +impl ActorNamespaceHandle { + pub(crate) fn actor_for_alarm( + &self, + id: KjOwn, + ) -> Result> { + let namespace = self + .0 + .upgrade() + .ok_or_else(|| kj::disconnected!("the Durable Object namespace is gone"))?; + namespace + .container_for(id) + .start_request(ffi::new_request_metadata(KjMaybe::None, KjMaybe::None)) + } +} + +// ===================================================================================== +// Containers + +/// The class an actor is constructed from and its id: known up front for a root actor, resolved +/// through the parent actor's `FacetStart` for a facet. +enum ClassState { + Ready { + class: Rc, + id: KjOwn, + }, + /// Resolving. Every waiter awaits a clone; the future itself stores `Ready` into the + /// container when it completes. + Pending(Shared>>), +} + +/// The slot of one actor: its key in the namespace (or in its parent's facets) and the actor +/// while it runs. +/// +/// The container also holds what survives the actor's eviction: its hibernation manager, its +/// facets, and the broken reason once it broke. +pub struct ActorContainer { + this: Weak, + key: String, + namespace: Weak, + /// None for a root actor. + parent: Option>, + /// The key of the root actor whose storage this one shares: its own for a root. + root_key: String, + /// Shared by the whole facet tree: the namespace expires the tree as one. + last_access: Rc>, + class_and_id: RefCell, + /// The running actor: `None` until the first request, and again after an eviction. The `Rc` + /// is for the on-broken monitor and a request being started, which borrow the handle; it + /// counts no request, so it does not block the actor's shutdown. + actor: RefCell>>>, + /// The hibernation manager of the evicted actor, for the actor that replaces it. + manager: RefCell>>, + /// Set once the actor broke or was aborted; every later use of the container fails with it. + broken_reason: RefCell>, + facets: RefCell>>, + /// A facet's number within the root's storage, once looked up. The root has none. + facet_id: Cell>, + /// Whether a request holds the actor, per the actor's `ActorHooks`. + active: Cell, + /// Test evictions waiting for the actor to become inactive. + idle: Notify, + on_broken_task: RefCell>, + /// The idle shutdown, armed when the last request ends and cancelled by the next. + shutdown_task: RefCell>, +} + +impl ActorContainer { + fn new_root(key: String, ns: &ActorNamespace, id: KjOwn) -> Rc { + let class = Rc::clone(&ns.actor_class); + Rc::new_cyclic(|this| { + Self::init( + Weak::clone(this), + key.clone(), + Weak::clone(&ns.this), + None, + key, + Rc::new(Cell::new(ns.factory.now())), + ClassState::Ready { class, id }, + ) + }) + } + + /// A facet of `parent`, whose class and id `start` resolves. + fn new_facet(name: String, parent: &Self, start: KjOwn) -> Rc { + Rc::new_cyclic(|this| { + let pending = resolve_facet_start(Weak::clone(this), start) + .boxed_local() + .shared(); + Self::init( + Weak::clone(this), + name, + Weak::clone(&parent.namespace), + Some(Weak::clone(&parent.this)), + parent.root_key.clone(), + Rc::clone(&parent.last_access), + ClassState::Pending(pending), + ) + }) + } + + fn init( + this: Weak, + key: String, + namespace: Weak, + parent: Option>, + root_key: String, + last_access: Rc>, + class_and_id: ClassState, + ) -> Self { + Self { + this, + key, + namespace, + parent, + root_key, + last_access, + class_and_id: RefCell::new(class_and_id), + actor: RefCell::new(None), + manager: RefCell::new(None), + broken_reason: RefCell::new(None), + facets: RefCell::new(LinkedHashMap::new()), + facet_id: Cell::new(None), + active: Cell::new(false), + idle: Notify::new(), + on_broken_task: RefCell::new(None), + shutdown_task: RefCell::new(None), + } + } +} + +impl ActorContainer { + fn namespace(&self) -> Result> { + self.namespace + .upgrade() + .ok_or_else(|| kj::disconnected!("the Durable Object namespace is gone")) + } + + /// The parent of a facet. An error for a facet whose parent is gone; `None` for a root. + fn parent(&self) -> Result>> { + match &self.parent { + None => Ok(None), + Some(parent) => parent + .upgrade() + .map(Some) + .ok_or_else(|| kj::disconnected!("the facet's parent actor is gone")), + } + } + + fn is_root(&self) -> bool { + self.parent.is_none() + } + + fn require_not_broken(&self) -> Result<()> { + match &*self.broken_reason.borrow() { + Some(reason) => Err(reason.clone()), + None => Ok(()), + } + } + + /// The pending class resolution of a facet, if it has not resolved yet. + fn pending_class(&self) -> Option>>> { + match &*self.class_and_id.borrow() { + ClassState::Pending(pending) => Some(pending.clone()), + ClassState::Ready { .. } => None, + } + } + + fn class(&self) -> Result> { + match &*self.class_and_id.borrow() { + ClassState::Ready { class, .. } => Ok(Rc::clone(class)), + ClassState::Pending(_) => Err(kj::failed!("the facet's class is not resolved yet")), + } + } + + fn id(&self) -> Result> { + match &*self.class_and_id.borrow() { + ClassState::Ready { id, .. } => Ok(ffi::actor_id_clone(id)), + ClassState::Pending(_) => Err(kj::failed!("the facet's id is not resolved yet")), + } + } + + fn update_access_time(&self) { + if let Some(ns) = self.namespace.upgrade() { + self.last_access.set(ns.factory.now()); + } + } + + /// Whether anything but its map holds this container or one of its facets: a stub's + /// channel, a request in flight, an eviction in progress. + fn has_clients(&self) -> bool { + Weak::strong_count(&self.this) > 1 + || self + .facets + .borrow() + .values() + .any(|facet| facet.has_clients()) + } + + /// Whether a running actor exists. + fn is_running(&self) -> bool { + self.actor.borrow().is_some() + } + + /// Keeps the running actor's hibernation manager for the actor that replaces it. + fn save_manager(&self) { + if let Some(actor) = &*self.actor.borrow() + && let Some(manager) = Option::from(ffi::actor_hibernation_manager(actor)) + { + *self.manager.borrow_mut() = Some(manager); + } + } + + /// Spawns a background task of the container; none once its namespace is gone. + fn spawn(&self, future: impl Future + 'static) -> Option { + Some(self.namespace.upgrade()?.factory.spawn(future)) + } + + fn cancel_on_broken(&self) { + drop(self.on_broken_task.borrow_mut().take()); + } + + fn cancel_shutdown(&self) { + drop(self.shutdown_task.borrow_mut().take()); + } + + /// The running actor, started first if it is not running. + async fn get_actor(&self) -> Result>> { + self.require_not_broken()?; + if !self.is_running() { + if let Some(pending) = self.pending_class() { + pending.await?; + self.require_not_broken()?; + } + let class = self.class()?; + class.when_ready().await?; + self.require_not_broken()?; + // A concurrent request may have started the actor meanwhile. + if !self.is_running() { + self.start(&class)?; + } + } + self.actor + .borrow() + .clone() + .ok_or_else(|| kj::failed!("the actor did not start")) + } + + /// Constructs the actor and starts watching it for breakage. + fn start(&self, class: &Rc) -> Result<()> { + let ns = self.namespace()?; + let id = self.id()?; + let storage = ns.storage()?; + let spec = ffi::ActorStorageSpec { + durable: ns.is_durable(), + enable_sql: ns.enable_sql(), + root_key: self.root_key.clone(), + facet_id: self.facet_id()?.into(), + }; + let actor = Rc::new(class.new_actor(NewActor { + id, + storage, + spec, + hooks: Box::new(ActorHooks(Weak::clone(&self.this))), + container: ns.container(), + hibernation_manager: self.manager.borrow_mut().take(), + })?); + let monitor = self.spawn(monitor_on_broken( + Weak::clone(&self.this), + ffi::actor_on_broken(&actor), + )); + *self.on_broken_task.borrow_mut() = monitor; + *self.actor.borrow_mut() = Some(actor); + Ok(()) + } + + /// Starts a request on the actor, starting the actor first if it is not running. The request + /// is a client of the container from here until its interface is dropped. + pub fn start_request( + self: &Rc, + metadata: KjOwn, + ) -> Result> { + self.require_not_broken()?; + if let Some(ns) = self.namespace.upgrade() { + ns.ensure_cleanup_loop(); + } + let client = Client(Rc::clone(self)); + let running = self.actor.borrow().clone(); + if let Some(actor) = running { + return client.request_on(metadata, &actor); + } + Ok(PromisedInterface::new(async move { + let actor = client.get_actor().await?; + client.request_on(metadata, &actor) + }) + .into_kj()) + } +} + +impl Client { + /// Starts this client's request on `actor`, the container's running actor. + fn request_on( + self, + mut metadata: KjOwn, + actor: &ffi::ActorHandle, + ) -> Result> { + let ns = self.namespace()?; + let inner = { + let state = self.class_and_id.borrow(); + let ClassState::Ready { class, id } = &*state else { + return Err(kj::failed!("the facet's class is not resolved yet")); + }; + if self.is_root() { + // A root actor's requests get a self token that names the actor itself, and + // must not take one from the caller: a caller-supplied factory could read and + // manipulate the parameters of the actor's own `restore()`. The token names the + // namespace and id only, so that the request holds no reference back to the + // container that would block eviction. A facet is only ever called by its + // parent, or through a restored channel the parent set up, which brings its own + // trusted factory. + if let Some(unique_key) = ns.unique_key() { + ffi::request_metadata_set_actor_self_token( + ns.factory.raw(), + metadata.as_mut(), + unique_key, + id, + ns.persistent_self_tokens, + ); + } + } + class.start_request(metadata, actor)? + }; + // The interface is a client of the container while it lives. It does not keep the actor + // active: the actor's own reference accounting does that, so that an interface living on + // with an open WebSocket leaves the actor free to hibernate. + Ok(attach(inner, self)) + } +} + +impl ActorContainer { + /// The actor's first request started: it cancels the pending idle shutdown. + fn active(&self) { + self.active.set(true); + self.cancel_shutdown(); + } + + /// The actor's last request ended: arms the idle shutdown. + fn inactive(&self) { + self.active.set(false); + self.update_access_time(); + self.idle.notify_waiters(); + let Some(ns) = self.namespace.upgrade() else { + return; + }; + if !ns.is_evictable() { + return; + } + let factory = Rc::clone(&ns.factory); + let container = Weak::clone(&self.this); + let task = self.spawn(async move { + factory.sleep(IDLE_SHUTDOWN_DELAY).await; + if let Some(container) = container.upgrade() { + container.shutdown_after_idle().await; + } + }); + *self.shutdown_task.borrow_mut() = task; + } +} + +impl ActorContainer { + /// Evicts the actor after `IDLE_SHUTDOWN_DELAY` without requests: hibernates its WebSockets + /// and shuts it down. The container keeps its storage, facets and hibernation manager, so + /// the next request rebuilds the actor as it was. + async fn shutdown_after_idle(&self) { + match self + .try_evict("broken.dropped; Actor freed due to inactivity", true) + .await + { + Ok(true) => {} + // Something still holds the actor although its requests have all ended: the + // request accounting is off. Keeping the actor is the safe choice; dropping it + // could leave two instances of the same actor alive. + Ok(false) => tracing::error!( + "Detected internal bug in hibernation: Durable Object has strong references \ + when hibernation timeout expired." + ), + Err(error) => tracing::error!( + "shutting down an idle Durable Object failed: {}", + error.description() + ), + } + } + + /// The actor broke: `reason` becomes every later call's error, the facets are aborted and + /// the container leaves its map, dropping the hibernation manager (which disconnects the + /// hibernated WebSockets). + fn broken(self: &Rc, reason: &crate::Error) { + *self.broken_reason.borrow_mut() = Some(reason.clone()); + let facets = std::mem::take(&mut *self.facets.borrow_mut()); + for facet in facets.values() { + facet.abort(Some(reason.clone())); + } + drop(facets); + // The monitor is the task running this, and ends with it; its handle has nothing left to + // cancel. + drop(self.on_broken_task.borrow_mut().take()); + self.cancel_shutdown(); + // Hollow the container out: a stub still holding it must not keep these alive. + // `get_actor()` fails from now on, so nothing recreates the actor. + let actor = self.actor.borrow_mut().take(); + let manager = self.manager.borrow_mut().take(); + match self.parent.as_ref().and_then(Weak::upgrade) { + Some(parent) => { + parent.facets.borrow_mut().remove(&self.key); + } + None => { + if let Some(ns) = self.namespace.upgrade() { + ns.actors.borrow_mut().remove(&self.key); + } + } + } + drop(manager); + drop(actor); + } + + /// Aborts the actor and its facets; every request on them fails with `reason`. Without one + /// the actors are shut down, failing nothing in flight, and later requests fail for an + /// unknown reason. The caller removes the container from any map that could route traffic to + /// it: at most call sites the map is at hand, and `abort` on a facet's tree leaves the facets + /// filed under their parent. + fn abort(&self, reason: Option) { + if self.broken_reason.borrow().is_some() { + return; + } + let actor = self.actor.borrow_mut().take(); + if let Some(actor) = &actor { + ffi::actor_abort(actor, &AbortReason(reason.clone())); + } + for facet in self.facets.borrow().values() { + facet.abort(reason.clone()); + } + self.cancel_on_broken(); + self.cancel_shutdown(); + *self.manager.borrow_mut() = None; + *self.broken_reason.borrow_mut() = Some( + reason.unwrap_or_else(|| kj::failed!("jsg.Error: Actor aborted for unknown reason.")), + ); + drop(actor); + } + + /// Resets the actor's SQLite database while its connection is open, ahead of an abort that + /// closes it. + fn reset_storage(&self) { + if let Some(actor) = &*self.actor.borrow() { + ffi::actor_reset_storage(actor); + } + } + + /// Test hook: evicts the actor now, bypassing the idle delay. Fails if the actor is not + /// running (never started, or already evicted). Waits for in-flight requests to drain first. + fn evict_for_test(self: &Rc, hibernate: bool) -> LocalBoxFuture<'static, Result<()>> { + let this = Rc::clone(self); + Box::pin(async move { + if !this.namespace()?.is_evictable() { + return Err(kj::failed!( + "jsg.Error: Cannot evict Durable Object: its namespace has preventEviction set." + )); + } + if !this.is_running() { + return Err(kj::failed!( + "jsg.Error: Cannot evict Durable Object: it is not currently running." + )); + } + this.evict_when_idle(hibernate).await + }) + } + + /// Test hook: evicts the actor and its facets, those that are running; the bulk eviction + /// must not fail on actors that are not. + fn evict_tree_for_test_if_running( + self: &Rc, + hibernate: bool, + ) -> LocalBoxFuture<'static, Result<()>> { + let this = Rc::clone(self); + Box::pin(async move { + // Each eviction holds its own `Rc`: a broken actor may leave the map meanwhile. + let facets: Vec> = this.facets.borrow().values().cloned().collect(); + let mut evictions: Vec>> = facets + .iter() + .map(|facet| facet.evict_tree_for_test_if_running(hibernate)) + .collect(); + if this.is_running() { + let this = Rc::clone(&this); + evictions.push(Box::pin( + async move { this.evict_when_idle(hibernate).await }, + )); + } + futures::future::join_all(evictions) + .await + .into_iter() + .collect::>() + }) + } + + /// Waits for the actor to be idle, then evicts it. No live request is ever aborted, so while + /// requests are in flight, or a just-ended one still holds the actor during its teardown, + /// this polls; a fixed deadline keeps a request that never ends from hanging the test. + async fn evict_when_idle(&self, hibernate: bool) -> Result<()> { + let factory = Rc::clone(&self.namespace()?.factory); + let deadline = factory.now() + EVICT_TIMEOUT; + loop { + if self + .try_evict("broken.dropped; Actor evicted by test", hibernate) + .await? + { + self.cancel_shutdown(); + return Ok(()); + } + let now = factory.now(); + if now >= deadline { + return Err(kj::failed!("{EVICT_TIMEOUT_MESSAGE}")); + } + if self.active.get() { + let idle = std::pin::pin!(self.idle.notified()); + let timeout = std::pin::pin!(factory.sleep(deadline.saturating_sub(now))); + if let Either::Right(_) = futures::future::select(idle, timeout).await { + return Err(kj::failed!("{EVICT_TIMEOUT_MESSAGE}")); + } + } else { + factory.sleep(EVICT_RETRY_DELAY).await; + } + } + } + + /// Shuts the actor down, keeping its storage. Returns false without evicting if the actor + /// acquired a strong reference meanwhile (a request raced in): an idle shutdown is cancelled + /// by a new request, but a test eviction is not cancellable, so it relies on that re-check. + /// The only suspension is the wait for the isolate lock that hibernating WebSockets takes; + /// the shutdown and the clearing of the slot after it are one step, so no request finds a + /// shut-down actor in the slot. The on-broken monitor is cancelled only once the shutdown + /// is committed, so that an early `false` leaves the actor watched. + async fn try_evict(&self, reason: &str, hibernate: bool) -> Result { + let current = self.actor.borrow().clone(); + if let Some(actor) = ¤t { + if hibernate { + self.save_manager(); + } + let lock = if hibernate && self.manager.borrow().is_some() { + Some(ffi::actor_lock(actor).await?) + } else { + None + }; + // The slot may have changed while the lock was awaited. + match &*self.actor.borrow() { + Some(now) if Rc::ptr_eq(now, actor) => {} + Some(_) => return Ok(false), + None => return Ok(true), + } + if !ffi::actor_shutdown(actor, reason, lock.into())? { + return Ok(false); + } + } + self.cancel_on_broken(); + *self.actor.borrow_mut() = None; + if !hibernate { + *self.manager.borrow_mut() = None; + } + Ok(true) + } +} + +/// Watches the actor; when it breaks, the container records the reason and leaves its map. The +/// task holds no reference to the actor: a cancelled task's future is dropped only at its next +/// poll, and the actor must close its storage when the container drops it (a deleted facet's +/// files are removed in the same turn, which Windows refuses while they are open). +async fn monitor_on_broken(container: Weak, broken: KjOwn) { + // `broken` only ever rejects; an actor that never breaks leaves this task to be + // cancelled with the container. + let reason: crate::Error = match ffi::actor_broken(broken).await { + Ok(()) => kj::failed!("actor.onBroken() resolved normally?"), + Err(error) => error.into(), + }; + if let Some(container) = container.upgrade() { + container.broken(&reason); + } +} + +/// Resolves a facet's class and id through its parent's `FacetStart` and stores them in the +/// facet's container. +async fn resolve_facet_start( + container: Weak, + start: KjOwn, +) -> Result<()> { + let ffi::FacetStartInfo { id, actor_class } = ffi::facet_start_resolve(start).await?; + if let Some(container) = container.upgrade() { + let class = actor_class.0; + *container.class_and_id.borrow_mut() = ClassState::Ready { class, id }; + } + Ok(()) +} + +impl Drop for ActorContainer { + fn drop(&mut self) { + for facet in self.facets.get_mut().values() { + facet.abort(None); + } + if let Some(actor) = self.actor.get_mut().take() { + ffi::actor_abort(&actor, &AbortReason(None)); + } + } +} + +// ===================================================================================== +// Facets + +impl ActorContainer { + /// The facet `name`, created with `start` if new. An existing facet's `start` is dropped + /// unused. + fn facet_container(&self, name: &str, start: KjOwn) -> Rc { + let mut facets = self.facets.borrow_mut(); + if let Some(facet) = facets.get(name) { + return Rc::clone(facet); + } + let facet = Self::new_facet(name.to_owned(), self, start); + facets.insert(name.to_owned(), Rc::clone(&facet)); + facet + } + + /// This facet's number within the root's storage; `None` for the root, and for a facet of + /// an in-memory namespace, whose storage numbers nothing. Looked up (and allocated if new) on + /// first use. + fn facet_id(&self) -> Result> { + if let Some(id) = self.facet_id.get() { + return Ok(Some(id)); + } + let Some(parent) = self.parent()? else { + return Ok(None); + }; + let ns = self.namespace()?; + let id: Option = ffi::actor_storage_facet_id( + ns.storage()?, + &self.root_key, + parent.facet_id()?.into(), + &self.key, + )? + .into(); + self.facet_id.set(id); + Ok(id) + } + + /// Aborts the facet `name` with `reason` and drops it, if it is running. + fn abort_facet(&self, name: &str, reason: crate::Error) { + let facet = self.facets.borrow_mut().remove(name); + if let Some(facet) = facet { + facet.abort(Some(reason)); + } + } + + /// Whether a stub to this actor may be serialized: facets and ephemeral actors cannot be. + fn require_transferable(&self) -> Result<()> { + if !self.is_root() { + return Err(kj::failed!( + "jsg.DOMException(DataCloneError): Stubs pointing to Durable Object facets are \ + not serializable." + )); + } + if !self.namespace()?.is_durable() { + return Err(kj::failed!( + "jsg.DOMException(DataCloneError): Stubs pointing to ephemeral objects are not \ + serializable." + )); + } + Ok(()) + } + + /// The channel token that restores a stub to this actor, as the runtime encodes it. Only a + /// root actor has one, and a root's class and id are known from the start. + fn token(&self, usage: TokenUsage, persistent: Persistent) -> Result> { + self.require_transferable()?; + let ns = self.namespace()?; + let unique_key = ns + .unique_key() + .ok_or_else(|| kj::failed!("only durable actors have channel tokens"))?; + let id = self.id()?; + Ok(ffi::factory_encode_actor_token( + ns.factory.raw(), + unique_key, + &id, + persistent, + usage, + )?) + } +} + +impl ActorContainer { + fn depth(&self) -> u32 { + match self.parent.as_ref().and_then(Weak::upgrade) { + Some(parent) => 1 + parent.depth(), + None => 0, + } + } + + fn delete_facet(&self, name: &str) -> Result<()> { + self.abort_facet(name, kj::failed!("jsg.Error: Facet was deleted.")); + let ns = self.namespace()?; + ffi::actor_storage_delete_facet( + ns.storage()?, + &self.root_key, + self.facet_id()?.into(), + name, + ) + .map_err(Into::into) + } + + fn clone_facet(&self, src: &str, dst: &str) -> Result<()> { + // Replacing a facet implies aborting it. + self.abort_facet(dst, kj::failed!("jsg.Error: Facet was cloned-over.")); + if src == dst { + // Cloning a facet onto itself replaces it with an exact copy of its own data: the + // abort matches `delete(dst)`, and the storage stays as it is. + return Ok(()); + } + let ns = self.namespace()?; + ffi::actor_storage_clone_facet( + ns.storage()?, + &self.root_key, + self.facet_id()?.into(), + src, + dst, + ) + .map_err(Into::into) + } +} + +impl ActorHooks { + fn container(&self) -> Result> { + self.0 + .upgrade() + .ok_or_else(|| kj::disconnected!("the actor is gone")) + } + + /// Starts a request the actor raises for itself (an alarm, a hibernated WebSocket's event), + /// restarting the actor if it was evicted. + pub(crate) fn start_request( + &self, + metadata: KjOwn, + ) -> Result> { + self.container()?.start_request(metadata) + } + + pub(crate) fn depth(&self) -> u32 { + self.0.upgrade().map_or(0, |container| container.depth()) + } + + pub(crate) fn facet( + &self, + name: &str, + start: KjOwn, + ) -> Result> { + Ok(SubrequestChannel::new(Rc::new(ActorChannel { + container: Client(self.container()?.facet_container(name, start)), + persistent: false, + }))) + } + + pub(crate) fn abort_facet(&self, name: &str, reason: &ffi::Exception) { + if let Some(container) = self.0.upgrade() { + container.abort_facet(name, reason.into()); + } + } + + pub(crate) fn delete_facet(&self, name: &str) -> Result<()> { + self.container()?.delete_facet(name) + } + + pub(crate) fn clone_facet(&self, src: &str, dst: &str) -> Result<()> { + self.container()?.clone_facet(src, dst) + } + + pub(crate) fn active(&self) { + if let Some(container) = self.0.upgrade() { + container.active(); + } + } + + pub(crate) fn inactive(&self) { + if let Some(container) = self.0.upgrade() { + container.inactive(); + } + } +} + +// ===================================================================================== +// Channels + +/// A client's hold on a container: a stub's channel, or a request from its start until its +/// interface is dropped. The container's expiration is timed from when one last went away. +struct Client(Rc); + +impl std::ops::Deref for Client { + type Target = Rc; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Drop for Client { + fn drop(&mut self) { + self.0.update_access_time(); + } +} + +/// A stub's channel to an actor. +struct ActorChannel { + container: Client, + /// Whether the channel was restored from a stored stub, or may be stored. + persistent: Persistent, +} + +impl Channel for ActorChannel { + fn start_request( + &self, + mut metadata: KjOwn, + ) -> Result> { + ffi::request_metadata_set_from_persistent_stub(metadata.as_mut(), self.persistent); + self.container.start_request(metadata) + } + + fn require_allows_transfer(&self) -> Result<()> { + self.container.require_transferable() + } + + fn token(&self, usage: TokenUsage) -> Result> { + self.container.token(usage, self.persistent) + } + + fn evict_for_test(&self, hibernate: bool) -> LocalBoxFuture<'_, Result<()>> { + self.container.evict_for_test(hibernate) + } +} diff --git a/src/workerd/server/server/bindings-test.rs b/src/workerd/server/server/bindings-test.rs new file mode 100644 index 00000000000..748f9a3083d --- /dev/null +++ b/src/workerd/server/server/bindings-test.rs @@ -0,0 +1,671 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +use capnp::message::ReaderOptions; +use workerd_capnp::worker::binding::crypto_key::Usage; + +use super::*; + +/// A `Worker` message whose `bindings` `build` fills in. +fn worker_message( + binding_count: u32, + build: impl FnOnce(struct_list::Builder<'_, binding::Owned>), +) -> message::Builder { + let mut message = message::Builder::new_default(); + let worker = message.init_root::(); + build(worker.init_bindings(binding_count)); + message +} + +fn compile( + message: &message::Builder, + actor_configs: &ActorConfigs, + experimental: bool, +) -> CompiledBindings { + compile_bindings( + "main", + message.get_root_as_reader::().unwrap(), + actor_configs, + &actor_configs["main"], + experimental, + ) + .unwrap() +} + +/// Every global of an encoded `Globals` message, in capnp's text format. +fn rendered(words: &[u64]) -> Vec { + let bytes: Vec = words.iter().flat_map(|word| word.to_ne_bytes()).collect(); + let message = + capnp::serialize::read_message(&mut bytes.as_slice(), ReaderOptions::new()).unwrap(); + let globals = message.get_root::().unwrap(); + let globals = globals.get_globals().unwrap(); + globals.iter().map(|global| format!("{global:?}")).collect() +} + +fn no_actors() -> ActorConfigs { + ActorConfigs::from([("main".to_owned(), ActorConfigMap::new())]) +} + +fn durable(unique_key: &str) -> ActorConfig { + ActorConfig::Durable { + unique_key: unique_key.to_owned(), + evictable: false, + enable_sql: true, + workflow: None, + container: None, + } +} + +const EPHEMERAL: ActorConfig = ActorConfig::Ephemeral { + evictable: true, + enable_sql: false, +}; + +fn designator(service: &str) -> Designator { + Designator { + service: service.to_owned(), + entrypoint: None, + props_json: None, + error_context: String::new(), + } +} + +/// Designators compare without their error context. +fn without_context(designators: &[Designator]) -> Vec { + designators + .iter() + .map(|designator| Designator { + error_context: String::new(), + ..designator.clone() + }) + .collect() +} + +#[test] +fn subrequest_channels_start_after_the_special_ones() { + let message = worker_message(6, |mut bindings| { + let mut service = bindings.reborrow().get(0); + service.set_name("SVC"); + let mut designator = service.init_service(); + designator.set_name("target"); + designator.set_entrypoint("ep"); + designator.init_props().set_json("{\"x\":1}"); + let mut kv = bindings.reborrow().get(1); + kv.set_name("KV"); + kv.init_kv_namespace().set_name("kv-svc"); + let mut r2 = bindings.reborrow().get(2); + r2.set_name("R2"); + r2.init_r2_bucket().set_name("r2-svc"); + let mut queue = bindings.reborrow().get(3); + queue.set_name("Q"); + queue.init_queue().set_name("q-svc"); + let mut engine = bindings.reborrow().get(4); + engine.set_name("AE"); + engine.init_analytics_engine().set_name("ae-svc"); + let mut hyperdrive = bindings.get(5); + hyperdrive.set_name("HD"); + let mut hyperdrive = hyperdrive.init_hyperdrive(); + hyperdrive.reborrow().init_designator().set_name("hd-svc"); + hyperdrive.set_database("db"); + hyperdrive.set_user("user"); + hyperdrive.set_password("pw"); + hyperdrive.set_scheme("postgres"); + }); + let compiled = compile(&message, &no_actors(), true); + assert_eq!(compiled.errors, Vec::::new()); + assert_eq!( + rendered(&compiled.globals), + [ + "(name = \"SVC\", fetcher = 2)", + "(name = \"KV\", kvNamespace = 3)", + "(name = \"R2\", r2Bucket = (channel = 4, bucket = \"r2-svc\"))", + "(name = \"Q\", queue = 5)", + "(name = \"AE\", analyticsEngine = (channel = 6, dataset = \"ae-svc\"))", + "(name = \"HD\", hyperdrive = (channel = 7, database = \"db\", user = \"user\", \ + password = \"pw\", scheme = \"postgres\"))", + ] + ); + assert_eq!( + compiled.subrequest, + [ + Designator { + service: "target".to_owned(), + entrypoint: Some("ep".to_owned()), + props_json: Some("{\"x\":1}".to_owned()), + error_context: "Worker \"main\"'s binding \"SVC\"".to_owned(), + }, + Designator { + error_context: "Worker \"main\"'s binding \"KV\"".to_owned(), + ..designator("kv-svc") + }, + Designator { + error_context: "Worker \"main\"'s binding \"R2\"".to_owned(), + ..designator("r2-svc") + }, + Designator { + error_context: "Worker \"main\"'s binding \"Q\"".to_owned(), + ..designator("q-svc") + }, + Designator { + error_context: "Worker \"main\"'s binding \"AE\"".to_owned(), + ..designator("ae-svc") + }, + Designator { + error_context: "Worker \"main\"'s binding \"HD\"".to_owned(), + ..designator("hd-svc") + }, + ] + ); +} + +#[test] +fn durable_object_namespaces() { + let mut actor_configs = no_actors(); + let local = actor_configs.get_mut("main").unwrap(); + local.insert("Counter".to_owned(), durable("key-1")); + local.insert("Cache".to_owned(), EPHEMERAL); + actor_configs.insert( + "other".to_owned(), + ActorConfigMap::from_iter([("Remote".to_owned(), durable("key-2"))]), + ); + let message = worker_message(3, |mut bindings| { + let mut counter = bindings.reborrow().get(0); + counter.set_name("COUNTER"); + counter + .init_durable_object_namespace() + .set_class_name("Counter"); + let mut cache = bindings.reborrow().get(1); + cache.set_name("CACHE"); + cache + .init_durable_object_namespace() + .set_class_name("Cache"); + let mut remote = bindings.get(2); + remote.set_name("REMOTE"); + let mut designator = remote.init_durable_object_namespace(); + designator.set_class_name("Remote"); + designator.set_service_name("other"); + let mut policy = designator.init_retry_policy(); + policy.set_max_attempts(3); + policy.set_timeout_ms(5000); + }); + let compiled = compile(&message, &actor_configs, false); + assert_eq!(compiled.errors, Vec::::new()); + assert_eq!( + rendered(&compiled.globals), + [ + "(name = \"COUNTER\", durableActorNamespace = (actorChannel = 0, uniqueKey = \ + \"key-1\"))", + "(name = \"CACHE\", ephemeralActorNamespace = 1)", + "(name = \"REMOTE\", durableActorNamespace = (actorChannel = 2, uniqueKey = \ + \"key-2\", retryPolicy = (maxAttempts = 3, timeoutMs = 5000)))", + ] + ); + assert_eq!( + compiled.actors, + [ + ActorDesignator { + service: None, + class_name: "Counter".to_owned() + }, + ActorDesignator { + service: None, + class_name: "Cache".to_owned() + }, + ActorDesignator { + service: Some("other".to_owned()), + class_name: "Remote".to_owned() + }, + ] + ); +} + +#[test] +fn unknown_durable_object_namespaces_take_no_channel() { + let mut actor_configs = no_actors(); + actor_configs.insert("other".to_owned(), ActorConfigMap::new()); + let message = worker_message(4, |mut bindings| { + let mut local = bindings.reborrow().get(0); + local.set_name("LOCAL"); + local + .init_durable_object_namespace() + .set_class_name("Missing"); + let mut no_service = bindings.reborrow().get(1); + no_service.set_name("NO_SERVICE"); + let mut designator = no_service.init_durable_object_namespace(); + designator.set_class_name("Missing"); + designator.set_service_name("nowhere"); + let mut no_class = bindings.reborrow().get(2); + no_class.set_name("NO_CLASS"); + let mut designator = no_class.init_durable_object_namespace(); + designator.set_class_name("Missing"); + designator.set_service_name("other"); + let mut class = bindings.get(3); + class.set_name("CLASS"); + class.init_durable_object_class().set_name("other"); + }); + let compiled = compile(&message, &actor_configs, true); + assert_eq!( + compiled.errors, + [ + "Worker \"main\"'s binding \"LOCAL\" refers to a Durable Object namespace named \ + \"Missing\", but no such Durable Object namespace is defined by this Worker.", + "Worker \"main\"'s binding \"NO_SERVICE\" refers to a service \"nowhere\", but no \ + such service is defined.", + "Worker \"main\"'s binding \"NO_CLASS\" refers to a Durable Object namespace \ + named \"Missing\" in service \"other\", but no such Durable Object namespace is \ + defined by that service.", + ] + ); + assert!(compiled.actors.is_empty()); + assert_eq!( + rendered(&compiled.globals), + ["(name = \"CLASS\", actorClass = 0)"] + ); + assert_eq!( + without_context(&compiled.actor_classes), + [designator("other")] + ); +} + +#[test] +fn wrapped_bindings_share_the_channel_tables() { + let message = worker_message(3, |mut bindings| { + let mut wrapped = bindings.reborrow().get(0); + wrapped.set_name("WRAPPED"); + let mut wrapped = wrapped.init_wrapped(); + wrapped.set_module_name("my-module"); + wrapped.set_entrypoint("make"); + let mut inner = wrapped.init_inner_bindings(2); + let mut service = inner.reborrow().get(0); + service.set_name("INNER_SVC"); + service.init_service().set_name("inner-svc"); + let mut text = inner.get(1); + text.set_name("INNER_TEXT"); + text.set_text("t"); + + // One inner binding fails: the wrapped binding has no global, but the channel its + // first inner binding took stays allocated. + let mut failing = bindings.reborrow().get(1); + failing.set_name("FAILING"); + let mut inner = failing.init_wrapped().init_inner_bindings(2); + let mut service = inner.reborrow().get(0); + service.set_name("TAKEN"); + service.init_service().set_name("taken-svc"); + let mut unspecified = inner.get(1); + unspecified.set_name("BROKEN"); + unspecified.set_unspecified(()); + + let mut service = bindings.get(2); + service.set_name("AFTER"); + service.init_service().set_name("after-svc"); + }); + let compiled = compile(&message, &no_actors(), false); + assert_eq!( + compiled.errors, + ["Worker \"main\"'s binding \"BROKEN\" does not specify any binding value."] + ); + assert_eq!( + rendered(&compiled.globals), + [ + "(name = \"WRAPPED\", wrapped = (moduleName = \"my-module\", entrypoint = \ + \"make\", innerBindings = [(name = \"INNER_SVC\", fetcher = 2), (name = \ + \"INNER_TEXT\", text = \"t\")]))", + "(name = \"AFTER\", fetcher = 4)", + ] + ); + assert_eq!( + without_context(&compiled.subrequest), + [ + designator("inner-svc"), + designator("taken-svc"), + designator("after-svc"), + ] + ); +} + +#[test] +fn crypto_key_raw_formats() { + let message = worker_message(5, |mut bindings| { + let mut raw = bindings.reborrow().get(0); + raw.set_name("RAW"); + let mut key = raw.init_crypto_key(); + key.set_raw(&[9, 8]); + key.reborrow().init_algorithm().set_name("AES-GCM"); + key.set_extractable(true); + let mut usages = key.init_usages(2); + usages.set(0, Usage::Encrypt); + usages.set(1, Usage::DeriveBits); + + let mut hex = bindings.reborrow().get(1); + hex.set_name("HEX"); + let mut key = hex.init_crypto_key(); + key.set_hex("0aFF"); + key.init_algorithm().set_json("{\"name\":\"HMAC\"}"); + + let mut bad_hex = bindings.reborrow().get(2); + bad_hex.set_name("BAD_HEX"); + let mut key = bad_hex.init_crypto_key(); + key.set_hex("0g1"); + key.init_algorithm().set_name("HMAC"); + + let mut base64 = bindings.reborrow().get(3); + base64.set_name("B64"); + let mut key = base64.init_crypto_key(); + key.set_base64("AQID"); + key.init_algorithm().set_name("a\"b\n"); + + let mut bad_base64 = bindings.get(4); + bad_base64.set_name("BAD_B64"); + let mut key = bad_base64.init_crypto_key(); + key.set_base64("AQ!D"); + key.init_algorithm().set_name("HMAC"); + }); + let compiled = compile(&message, &no_actors(), false); + assert_eq!( + compiled.errors, + [ + "CryptoKey binding \"BAD_HEX\" contained invalid hex.", + "CryptoKey binding \"BAD_B64\" contained invalid base64.", + ] + ); + assert_eq!( + rendered(&compiled.globals), + [ + "(name = \"RAW\", cryptoKey = (format = \"raw\", keyData = (bytes = 0x\"0908\"), \ + algorithm = \"\\\"AES-GCM\\\"\", extractable = true, usages = [encrypt, \ + deriveBits]))", + "(name = \"HEX\", cryptoKey = (format = \"raw\", keyData = (bytes = 0x\"0aff\"), \ + algorithm = \"{\\\"name\\\":\\\"HMAC\\\"}\", extractable = false))", + "(name = \"B64\", cryptoKey = (format = \"raw\", keyData = (bytes = \ + 0x\"010203\"), algorithm = \"\\\"a\\\\\\\"b\\\\n\\\"\", extractable = false))", + ] + ); +} + +#[test] +fn crypto_key_pem_and_jwk() { + const PKCS8: &str = "some text before\n\ + -----BEGIN PRIVATE KEY-----\r\n\ + AQID\r\n\ + BAU=\r\n\ + -----END PRIVATE KEY-----\r\n"; + const SPKI: &str = "-----BEGIN PUBLIC KEY-----\nBgc=\n-----END PUBLIC KEY-----\n"; + let message = worker_message(5, |mut bindings| { + let mut pkcs8 = bindings.reborrow().get(0); + pkcs8.set_name("PKCS8"); + let mut key = pkcs8.init_crypto_key(); + key.set_pkcs8(PKCS8); + key.init_algorithm().set_name("RSA-PSS"); + + let mut spki = bindings.reborrow().get(1); + spki.set_name("SPKI"); + let mut key = spki.init_crypto_key(); + key.set_spki(SPKI); + key.init_algorithm().set_name("RSA-PSS"); + + let mut wrong = bindings.reborrow().get(2); + wrong.set_name("WRONG"); + let mut key = wrong.init_crypto_key(); + key.set_pkcs8(SPKI); + key.init_algorithm().set_name("RSA-PSS"); + + let mut invalid = bindings.reborrow().get(3); + invalid.set_name("INVALID"); + let mut key = invalid.init_crypto_key(); + key.set_spki("-----BEGIN PUBLIC KEY-----\nBgc=\n-----END PRIVATE KEY-----\n"); + key.init_algorithm().set_name("RSA-PSS"); + + let mut jwk = bindings.get(4); + jwk.set_name("JWK"); + let mut key = jwk.init_crypto_key(); + key.set_jwk("{\"kty\":\"oct\"}"); + key.init_algorithm().set_name("HMAC"); + }); + let compiled = compile(&message, &no_actors(), false); + assert_eq!( + compiled.errors, + [ + "CryptoKey binding \"WRONG\" contained wrong PEM type, expected PrivateKey but \ + got PublicKey.", + "CryptoKey binding \"INVALID\" contained invalid PEM format.", + ] + ); + assert_eq!( + rendered(&compiled.globals), + [ + "(name = \"PKCS8\", cryptoKey = (format = \"pkcs8\", keyData = (bytes = \ + 0x\"0102030405\"), algorithm = \"\\\"RSA-PSS\\\"\", extractable = false))", + "(name = \"SPKI\", cryptoKey = (format = \"spki\", keyData = (bytes = 0x\"0607\"), \ + algorithm = \"\\\"RSA-PSS\\\"\", extractable = false))", + "(name = \"JWK\", cryptoKey = (format = \"jwk\", keyData = (json = \ + \"{\\\"kty\\\":\\\"oct\\\"}\"), algorithm = \"\\\"HMAC\\\"\", extractable = \ + false))", + ] + ); +} + +#[test] +fn experimental_bindings_are_refused_without_the_flag() { + let build = |mut bindings: struct_list::Builder<'_, binding::Owned>| { + let mut eval = bindings.reborrow().get(0); + eval.set_name("EVAL"); + eval.set_unsafe_eval(()); + let mut cache = bindings.reborrow().get(1); + cache.set_name("CACHE"); + let mut cache = cache.init_memory_cache(); + cache.set_id("shared"); + let mut limits = cache.init_limits(); + limits.set_max_keys(10); + limits.set_max_value_size(20); + limits.set_max_total_value_size(30); + let mut class = bindings.reborrow().get(2); + class.set_name("CLASS"); + class.init_durable_object_class().set_name("svc"); + let mut loader = bindings.reborrow().get(3); + loader.set_name("LOADER"); + loader.init_worker_loader().set_id("loader-id"); + let mut anonymous_loader = bindings.reborrow().get(4); + anonymous_loader.set_name("ANON_LOADER"); + anonymous_loader.init_worker_loader(); + let mut debug = bindings.reborrow().get(5); + debug.set_name("DEBUG"); + debug.set_workerd_debug_port(()); + let mut engine = bindings.get(6); + engine.set_name("AE"); + engine.init_analytics_engine().set_name("ae-svc"); + }; + + let compiled = compile(&worker_message(7, build), &no_actors(), false); + assert_eq!( + compiled.errors, + [ + "Unsafe eval bindings are an experimental feature which may change or go away in \ + the future. You must run workerd with `--experimental` to use this feature.", + "MemoryCache bindings are an experimental feature which may change or go away in \ + the future. You must run workerd with `--experimental` to use this feature.", + "Durable Object class bindings are an experimental feature which may change or go \ + away in the future. You must run workerd with `--experimental` to use this \ + feature.", + "Worker loader bindings are an experimental feature which may change or go away \ + in the future. You must run workerd with `--experimental` to use this feature.", + "Worker loader bindings are an experimental feature which may change or go away \ + in the future. You must run workerd with `--experimental` to use this feature.", + "workerdDebugPort bindings are an experimental feature which may change or go \ + away in the future. You must run workerd with `--experimental` to use this \ + feature.", + "AnalyticsEngine bindings are an experimental feature which may change or go away \ + in the future. You must run workerd with `--experimental` to use this feature.", + ] + ); + assert!(rendered(&compiled.globals).is_empty()); + assert!(compiled.actor_classes.is_empty()); + assert!(compiled.worker_loaders.is_empty()); + assert!(!compiled.has_debug_port); + + let compiled = compile(&worker_message(7, build), &no_actors(), true); + assert_eq!(compiled.errors, Vec::::new()); + assert_eq!( + rendered(&compiled.globals), + [ + "(name = \"EVAL\", unsafeEval = ())", + "(name = \"CACHE\", memoryCache = (cacheId = \"shared\", maxKeys = 10, \ + maxValueSize = 20, maxTotalValueSize = 30))", + "(name = \"CLASS\", actorClass = 0)", + "(name = \"LOADER\", workerLoader = 0)", + "(name = \"ANON_LOADER\", workerLoader = 1)", + "(name = \"DEBUG\", workerdDebugPort = ())", + "(name = \"AE\", analyticsEngine = (channel = 2, dataset = \"ae-svc\"))", + ] + ); + assert_eq!( + without_context(&compiled.actor_classes), + [designator("svc")] + ); + assert_eq!( + compiled.worker_loaders, + [ + WorkerLoaderDesignator { + name: "loader-id".to_owned(), + id: Some("loader-id".to_owned()), + }, + WorkerLoaderDesignator { + name: "ANON_LOADER".to_owned(), + id: None, + }, + ] + ); + assert!(compiled.has_debug_port); +} + +#[test] +fn memory_cache_needs_limits() { + let message = worker_message(1, |bindings| { + let mut cache = bindings.get(0); + cache.set_name("CACHE"); + cache.init_memory_cache(); + }); + let compiled = compile(&message, &no_actors(), true); + assert_eq!( + compiled.errors, + [ + "MemoryCache bindings must specify limits. Please update the binding in the worker \ + configuration and try again." + ] + ); + assert!(rendered(&compiled.globals).is_empty()); +} + +#[test] +fn unsupported_bindings() { + let message = worker_message(3, |mut bindings| { + let mut unspecified = bindings.reborrow().get(0); + unspecified.set_name("NONE"); + unspecified.set_unspecified(()); + let mut wasm = bindings.reborrow().get(1); + wasm.set_name("WASM"); + wasm.set_wasm_module(&[0]); + let mut obsolete = bindings.get(2); + obsolete.set_name("OLD"); + obsolete.init_obsolete0().set_name("svc"); + }); + let compiled = compile(&message, &no_actors(), false); + assert_eq!( + compiled.errors, + [ + "Worker \"main\"'s binding \"NONE\" does not specify any binding value.", + "Worker \"main\"'s binding \"WASM\" is a Wasm binding, but Wasm bindings are not \ + allowed in modules-based scripts. Use Wasm modules instead.", + "Worker \"main\"'s binding \"OLD\" uses an obsolete binding type.", + ] + ); + assert!(rendered(&compiled.globals).is_empty()); +} + +#[test] +fn wasm_bindings_of_a_service_worker_script_are_skipped_silently() { + let mut message = message::Builder::new_default(); + let mut worker = message.init_root::(); + worker.set_service_worker_script("addEventListener('fetch', () => {})"); + let mut wasm = worker.init_bindings(1).get(0); + wasm.set_name("WASM"); + wasm.set_wasm_module(&[0]); + let compiled = compile(&message, &no_actors(), false); + assert_eq!(compiled.errors, Vec::::new()); + assert!(rendered(&compiled.globals).is_empty()); +} + +#[test] +fn loopbacks_are_numbered_after_the_bindings() { + let mut actor_configs = no_actors(); + let local = actor_configs.get_mut("main").unwrap(); + local.insert("Counter".to_owned(), durable("key-1")); + local.insert("Cache".to_owned(), EPHEMERAL); + local.insert("Unexported".to_owned(), durable("key-3")); + let message = worker_message(3, |mut bindings| { + let mut service = bindings.reborrow().get(0); + service.set_name("SVC"); + service.init_service().set_name("svc"); + let mut namespace = bindings.reborrow().get(1); + namespace.set_name("COUNTER"); + namespace + .init_durable_object_namespace() + .set_class_name("Counter"); + let mut class = bindings.get(2); + class.set_name("CLASS"); + class.init_durable_object_class().set_name("svc"); + }); + let bindings = compile(&message, &actor_configs, true); + assert_eq!(bindings.errors, Vec::::new()); + + let named = [ + "Api".to_owned(), + "MyWorkflow".to_owned(), + "Rpc".to_owned(), + "Unlisted".to_owned(), + ]; + let workflows = ["MyWorkflow".to_owned(), "Unlisted".to_owned()]; + let classes = ["Counter".to_owned(), "Facet".to_owned(), "Cache".to_owned()]; + let loopbacks = loopback_globals( + &Exports { + has_default_entrypoint: true, + named_entrypoints: &named, + workflow_classes: &workflows, + actor_classes: &classes, + }, + &actor_configs["main"], + // As `compile_bindings` allocates one for a Workflow the config lists. + |class| (class == "MyWorkflow").then_some(9), + len_u32(&bindings.subrequest), + len_u32(&bindings.actors), + len_u32(&bindings.actor_classes), + ) + .unwrap(); + assert_eq!( + rendered(&loopbacks.globals), + [ + "(name = \"default\", loopbackServiceStub = 3)", + "(name = \"Api\", loopbackServiceStub = 4)", + // A Workflow class takes no loopback channel: the configured one is a wrapped + // binding over its bindingService's channel, the unlisted one has no entry. + "(name = \"MyWorkflow\", wrapped = (moduleName = \ + \"cloudflare-internal:workflows-api\", entrypoint = \"default\", innerBindings = \ + [(name = \"fetcher\", fetcher = 9)]))", + "(name = \"Rpc\", loopbackServiceStub = 5)", + "(name = \"Counter\", loopbackDurableActorNamespace = (actorChannel = 1, \ + uniqueKey = \"key-1\", classChannel = 1))", + "(name = \"Facet\", loopbackActorClass = 2)", + "(name = \"Cache\", loopbackEphemeralActorNamespace = (actorChannel = 2, \ + classChannel = 3))", + ] + ); + assert_eq!( + loopbacks.subrequest_entrypoints, + [None, Some("Api".to_owned()), Some("Rpc".to_owned())] + ); + assert_eq!(loopbacks.actor_classes, classes); + assert_eq!( + loopbacks.actor_namespaces, + ["Counter".to_owned(), "Cache".to_owned()] + ); +} diff --git a/src/workerd/server/server/bindings.rs b/src/workerd/server/server/bindings.rs new file mode 100644 index 00000000000..acfa17e2bb6 --- /dev/null +++ b/src/workerd/server/server/bindings.rs @@ -0,0 +1,837 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! A worker's config bindings compiled into the `Globals` message (compiled-bindings.capnp) and +//! its channel tables. +//! +//! [`compile_bindings`] interprets `Worker.bindings`: every value becomes a `Global`, and every +//! capability gets the next channel number of its kind. The channel tables record what each +//! number must resolve to; the link stage fills them once every service exists. +//! [`loopback_globals`] does the same for `ctx.exports`, numbering the worker's own entrypoints and +//! actor classes after the bindings' channels. + +use std::collections::HashMap; +use std::env; + +use base64::Engine as _; +use base64::engine::DecodePaddingMode; +use base64::engine::GeneralPurpose; +use base64::engine::GeneralPurposeConfig; +use capnp::message; +use capnp::message::HeapAllocator; +use capnp::struct_list; +use compiled_bindings_capnp::global; +use compiled_bindings_capnp::globals; +use hashlink::LinkedHashMap; +use rustls::pki_types::pem::PemObject; +use rustls::pki_types::pem::SectionKind; +use workerd_capnp::service_designator; +use workerd_capnp::worker; +use workerd_capnp::worker::binding; + +/// The limits of a `durableObjectNamespace` binding's `retryPolicy`: the retries after the first +/// attempt, and the milliseconds from the call's start after which none may begin +/// (`api::UserDefinedRetryPolicy` in actor-call-retry.h). +const RETRY_MAX_CONFIGURABLE_ATTEMPTS: u32 = 10; +const RETRY_CONFIGURABLE_TIMEOUT_MS: std::ops::RangeInclusive = 500..=60_000; + +/// `IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT` (src/workerd/io/io-context.h): the subrequest +/// channels with a special meaning (the global outbound), ahead of the bindings' channels. +pub const SPECIAL_SUBREQUEST_CHANNEL_COUNT: u32 = 2; + +/// A `ServiceDesignator` from the config: what a subrequest or actor-class channel resolves to. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Designator { + pub service: String, + pub entrypoint: Option, + /// `props.json`; none for `props.empty`. + pub props_json: Option, + /// The prefix of the errors the link stage reports for this channel: + /// `Worker ""'s binding ""`. + pub error_context: String, +} + +impl Designator { + /// Reads a designator. An unrecognized `props` kind is reported to `errors` and read as empty. + pub fn from_reader( + reader: service_designator::Reader<'_>, + error_context: String, + errors: &mut Vec, + ) -> capnp::Result { + let service = reader.get_name()?.to_string()?; + let entrypoint = if reader.has_entrypoint() { + Some(reader.get_entrypoint()?.to_string()?) + } else { + None + }; + let props_json = match reader.get_props().which() { + Ok(service_designator::props::Which::Empty(())) => None, + Ok(service_designator::props::Which::Json(json)) => Some(json?.to_string()?), + Err(capnp::NotInSchema(_)) => { + errors.push(format!( + "{error_context} has unrecognized props type. Was the config compiled with a \ + newer version of the schema?" + )); + None + } + }; + Ok(Self { + service, + entrypoint, + props_json, + error_context, + }) + } +} + +/// A Durable Object namespace binding's target: a class of `service`, or of the worker itself +/// when there is none. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ActorDesignator { + pub service: Option, + pub class_name: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WorkerLoaderDesignator { + /// For error logging; the binding's name when the loader has no id, so not necessarily unique. + pub name: String, + /// Bindings with the same id share one loader. + pub id: Option, +} + +/// A configured Workflow (`workflowsEngine.workflows`) the config's first pass found valid. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Workflow { + pub name: String, + pub class_name: String, + pub binding_service: Designator, +} + +/// A Durable Object namespace's storage: durable with its unique key, or ephemeral. What the +/// config's first pass learns from `durableObjectNamespaces` for every worker. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ActorConfig { + Durable { + unique_key: String, + evictable: bool, + enable_sql: bool, + /// The Workflow a namespace was synthesized to back, rather than declared in + /// `durableObjectNamespaces`: its actors are the engine's class, and its storage the + /// Workflow's `bindingService` worker's, so the worker's own namespace setup skips it + /// (`WorkerService::init_workflow_namespace`). + workflow: Option>, + /// Where the config has the class's `container` options, if it has any. + container: Option, + }, + Ephemeral { + evictable: bool, + enable_sql: bool, + }, +} + +/// One worker's namespaces by class name, in config order. +pub type ActorConfigMap = LinkedHashMap; + +/// Every worker's namespaces, by service name. +pub type ActorConfigs = HashMap; + +/// The result of compiling a worker's bindings. +#[derive(Debug, Default)] +pub struct CompiledBindings { + /// The encoded `Globals` message (compiled-bindings.capnp), in words: the worker's `env`. + pub globals: Vec, + /// Subrequest channels: index `i` is channel `i + SPECIAL_SUBREQUEST_CHANNEL_COUNT`. After + /// the bindings' channels come one per configured Workflow, to its `bindingService`. + pub subrequest: Vec, + /// The subrequest channel of each configured Workflow's `bindingService`, by the Workflow's + /// class name: the inner fetcher of the Workflow's `ctx.exports` binding. + pub workflow_binding_channels: HashMap, + /// Actor channels, indexed by channel number. + pub actors: Vec, + /// Actor-class channels, indexed by channel number. + pub actor_classes: Vec, + /// Worker-loader channels, indexed by channel number. + pub worker_loaders: Vec, + pub has_debug_port: bool, + /// Config errors, without the `service : ` prefix (the caller adds it). + pub errors: Vec, +} + +/// What the worker exports, as its compilation reported them, in that order. +#[derive(Clone, Copy, Debug)] +pub struct Exports<'a> { + pub has_default_entrypoint: bool, + /// Every named entrypoint, workflow classes included. + pub named_entrypoints: &'a [String], + pub workflow_classes: &'a [String], + pub actor_classes: &'a [String], +} + +/// The `ctx.exports` globals and the loopback channels they use, in the order the link stage +/// appends them to the bindings' tables. +#[derive(Debug, Default, PartialEq, Eq)] +pub struct LoopbackGlobals { + /// The encoded `Globals` message: the worker's `ctx.exports`. + pub globals: Vec, + /// The subrequest channels after the bindings': the entrypoint each resolves to, none for the + /// default. + pub subrequest_entrypoints: Vec>, + /// The actor-class channels after the bindings': one per exported class. + pub actor_classes: Vec, + /// The actor channels after the bindings': the exported classes that have a namespace. + pub actor_namespaces: Vec, +} + +/// Compiles `worker`'s bindings. `local_actor_configs` are `worker_name`'s own. +/// +/// A malformed message fails; a binding the config gets wrong is reported in `errors` and left +/// out of the globals. A `parameter` binding is not implemented and fails the compilation. +pub fn compile_bindings( + worker_name: &str, + worker: worker::Reader<'_>, + actor_configs: &ActorConfigs, + local_actor_configs: &ActorConfigMap, + experimental: bool, +) -> capnp::Result { + let mut compiler = Compiler { + worker_name, + service_worker_script: worker.has_service_worker_script(), + actor_configs, + local_actor_configs, + experimental, + out: CompiledBindings::default(), + }; + let mut globals = Vec::new(); + for binding in worker.get_bindings()? { + if let Some(global) = compiler.global(binding)? { + globals.push(global); + } + } + let mut out = compiler.out; + out.globals = encode_globals(&globals)?; + // After the bindings' channels, one subrequest channel per Workflow, to its `bindingService`. + for config in local_actor_configs.values() { + if let ActorConfig::Durable { + workflow: Some(workflow), + .. + } = config + { + let channel = len_u32(&out.subrequest) + SPECIAL_SUBREQUEST_CHANNEL_COUNT; + out.subrequest.push(workflow.binding_service.clone()); + out.workflow_binding_channels + .insert(workflow.class_name.clone(), channel); + } + } + Ok(out) +} + +/// Miniflare's own Workflow engine namespaces use this key prefix, which determines actor IDs and +/// the storage subdirectory; matching it lets both reach the same local instances. +/// +/// TODO(cleanup): Make this configurable rather than hardcoding Miniflare's prefix; a configurable +/// key must keep this value for Miniflare. +pub const WORKFLOW_NAMESPACE_KEY_PREFIX: &str = "miniflare-workflows-"; + +/// The unique key of the namespace backing the Workflow `workflow_name`. +#[must_use] +pub fn workflow_namespace_key(workflow_name: &str) -> String { + format!("{WORKFLOW_NAMESPACE_KEY_PREFIX}{workflow_name}") +} + +/// The `ctx.exports` globals, numbered after the `subrequest_channels`, `actor_channels` and +/// `actor_class_channels` the worker's `env` took. +/// +/// Every exported entrypoint gets a loopback stub, except a Workflow class: a configured Workflow +/// is a wrapped binding (`cloudflare-internal:workflows-api` over a fetcher to the Workflow's +/// `bindingService` channel, which `workflow_binding_channel` gives for its class), and an +/// exported `WorkflowEntrypoint` the config does not list under `workflowsEngine.workflows` has +/// no entry. Every exported actor class gets a loopback namespace when `local_actor_configs` +/// gives the class one, and a loopback actor class otherwise. +pub fn loopback_globals( + exports: &Exports<'_>, + local_actor_configs: &ActorConfigMap, + workflow_binding_channel: impl Fn(&str) -> Option, + subrequest_channels: u32, + actor_channels: u32, + actor_class_channels: u32, +) -> capnp::Result { + let mut out = LoopbackGlobals::default(); + let mut globals = Vec::new(); + + let mut next_subrequest = subrequest_channels + SPECIAL_SUBREQUEST_CHANNEL_COUNT; + if exports.has_default_entrypoint { + globals.push(new_global("default", |mut global| { + global.set_loopback_service_stub(next_subrequest); + })); + next_subrequest += 1; + out.subrequest_entrypoints.push(None); + } + for name in exports.named_entrypoints { + if exports.workflow_classes.contains(name) { + if let Some(channel) = workflow_binding_channel(name) { + globals.push(new_global(name, |global| { + let mut wrapped = global.init_wrapped(); + wrapped.set_module_name("cloudflare-internal:workflows-api"); + wrapped.set_entrypoint("default"); + let mut fetcher = wrapped.init_inner_bindings(1).get(0); + fetcher.set_name("fetcher"); + fetcher.set_fetcher(channel); + })); + } + continue; + } + out.subrequest_entrypoints.push(Some(name.clone())); + globals.push(new_global(name, |mut global| { + global.set_loopback_service_stub(next_subrequest); + })); + next_subrequest += 1; + } + + let mut next_actor = actor_channels; + for (class_channel, class_name) in (actor_class_channels..).zip(exports.actor_classes) { + out.actor_classes.push(class_name.clone()); + let config = local_actor_configs.get(class_name); + if config.is_some() { + out.actor_namespaces.push(class_name.clone()); + } + globals.push(new_global(class_name, |mut global| match config { + Some(ActorConfig::Durable { unique_key, .. }) => { + let mut namespace = global.init_loopback_durable_actor_namespace(); + namespace.set_actor_channel(next_actor); + namespace.set_unique_key(unique_key.as_str()); + namespace.set_class_channel(class_channel); + } + Some(ActorConfig::Ephemeral { .. }) => { + let mut namespace = global.init_loopback_ephemeral_actor_namespace(); + namespace.set_actor_channel(next_actor); + namespace.set_class_channel(class_channel); + } + None => global.set_loopback_actor_class(class_channel), + })); + if config.is_some() { + next_actor += 1; + } + } + + out.globals = encode_globals(&globals)?; + Ok(out) +} + +/// One `Global` in a message of its own, so that a list of globals can be sized once it is known +/// which bindings compiled. +type GlobalMessage = message::Builder; + +fn new_global(name: &str, value: impl FnOnce(global::Builder<'_>)) -> GlobalMessage { + let mut message = message::Builder::new_default(); + let mut global = message.init_root::(); + global.set_name(name); + value(global); + message +} + +/// A `Globals` message of `globals`, encoded. +fn encode_globals(globals: &[GlobalMessage]) -> capnp::Result> { + let mut message = message::Builder::new_default(); + let list = message + .init_root::() + .init_globals(len_u32(globals)); + set_globals(list, globals)?; + Ok(capnp::serialize::write_message_to_words(&message) + .chunks_exact(8) + .map(|chunk| u64::from_ne_bytes(chunk.try_into().unwrap_or_default())) + .collect()) +} + +fn set_globals( + mut list: struct_list::Builder<'_, global::Owned>, + globals: &[GlobalMessage], +) -> capnp::Result<()> { + for (index, global) in (0..).zip(globals) { + list.set_with_caveats(index, global.get_root_as_reader::()?)?; + } + Ok(()) +} + +/// A table's length as the next channel number or a list length: there is one entry per config +/// binding, and a capnp list is shorter than 2^32. +pub fn len_u32(items: &[T]) -> u32 { + items.len() as u32 +} + +/// The state of one worker's compilation. +struct Compiler<'a> { + worker_name: &'a str, + /// Whether the worker is a service-worker-syntax script, whose Wasm bindings are modules. + service_worker_script: bool, + actor_configs: &'a ActorConfigs, + local_actor_configs: &'a ActorConfigMap, + experimental: bool, + out: CompiledBindings, +} + +impl Compiler<'_> { + fn error(&mut self, error: String) { + self.out.errors.push(error); + } + + /// The binding's `Global`, or none after reporting why it has none. + fn global(&mut self, binding: binding::Reader<'_>) -> capnp::Result> { + let name = binding.get_name()?.to_str()?; + let error_context = format!("Worker \"{}\"'s binding \"{name}\"", self.worker_name); + let mut message = message::Builder::new_default(); + let mut global = message.init_root::(); + global.set_name(name); + let Ok(which) = binding.which() else { + self.error(format!( + "{error_context}has unrecognized type. Was the config compiled with a newer \ + version of the schema?" + )); + return Ok(None); + }; + Ok(self + .value(which, name, error_context, global)? + .then_some(message)) + } + + /// Writes the binding's value into `global`; false after reporting why it has none. + fn value( + &mut self, + which: binding::WhichReader<'_>, + name: &str, + error_context: String, + mut global: global::Builder<'_>, + ) -> capnp::Result { + use binding::Which; + + match which { + Which::Unspecified(()) => { + self.error(format!( + "{error_context} does not specify any binding value." + )); + return Ok(false); + } + Which::Parameter(_) => { + return Err(capnp::Error::unimplemented( + "TODO(beta): parameters".to_owned(), + )); + } + Which::Text(text) => global.set_text(text?), + Which::Data(data) => global.set_data(data?), + Which::Json(json) => global.set_json(json?), + Which::WasmModule(_) => { + // A service worker script's Wasm bindings are compiled with its modules. + if !self.service_worker_script { + self.error(format!( + "{error_context} is a Wasm binding, but Wasm bindings are not allowed in \ + modules-based scripts. Use Wasm modules instead." + )); + } + return Ok(false); + } + Which::CryptoKey(key) => return self.crypto_key(name, key?, global.init_crypto_key()), + Which::Service(designator) => { + let channel = self.subrequest_channel(designator?, error_context)?; + global.set_fetcher(channel); + } + Which::DurableObjectNamespace(designator) => { + return self.actor_namespace(designator?, &error_context, global); + } + Which::KvNamespace(designator) => { + let channel = self.subrequest_channel(designator?, error_context)?; + global.set_kv_namespace(channel); + } + Which::R2Bucket(designator) => self.r2_bucket(designator?, error_context, global)?, + Which::Obsolete0(_) => { + self.error(format!("{error_context} uses an obsolete binding type.")); + return Ok(false); + } + Which::Queue(designator) => { + let channel = self.subrequest_channel(designator?, error_context)?; + global.set_queue(channel); + } + Which::Wrapped(wrapped) => return self.wrapped(wrapped?, global), + Which::FromEnvironment(variable) => match env::var_os(variable?.to_str()?) { + Some(value) => global.set_text(&*value.to_string_lossy()), + None => global.set_json("null"), + }, + Which::AnalyticsEngine(designator) => { + if !self.experimental_feature("AnalyticsEngine") { + return Ok(false); + } + self.analytics_engine(designator?, error_context, global)?; + } + Which::Hyperdrive(hyperdrive) => self.hyperdrive(hyperdrive, error_context, global)?, + Which::UnsafeEval(()) => { + if !self.experimental_feature("Unsafe eval") { + return Ok(false); + } + global.set_unsafe_eval(()); + } + Which::MemoryCache(cache) => { + if !self.experimental_feature("MemoryCache") { + return Ok(false); + } + return self.memory_cache(cache, global); + } + Which::DurableObjectClass(designator) => { + if !self.experimental_feature("Durable Object class") { + return Ok(false); + } + let channel = len_u32(&self.out.actor_classes); + let designator = + Designator::from_reader(designator?, error_context, &mut self.out.errors)?; + self.out.actor_classes.push(designator); + global.set_actor_class(channel); + } + Which::WorkerLoader(loader) => { + if !self.experimental_feature("Worker loader") { + return Ok(false); + } + self.worker_loader(loader, name, global)?; + } + Which::WorkerdDebugPort(()) => { + if !self.experimental_feature("workerdDebugPort") { + return Ok(false); + } + self.out.has_debug_port = true; + global.set_workerd_debug_port(()); + } + } + Ok(true) + } + + /// Whether `--experimental` is on; reports the `feature` binding's error when it is not. + fn experimental_feature(&mut self, feature: &str) -> bool { + if !self.experimental { + self.error(format!( + "{feature} bindings are an experimental feature which may change or go away in \ + the future. You must run workerd with `--experimental` to use this feature." + )); + } + self.experimental + } + + /// Writes a wrapped binding. Its inner bindings are compiled the way top-level ones are, + /// taking channels from the same tables; when one has no global, neither has the wrapper, and + /// the channels taken so far stay taken. + fn wrapped( + &mut self, + wrapped: binding::wrapped_binding::Reader<'_>, + global: global::Builder<'_>, + ) -> capnp::Result { + let mut inner = Vec::new(); + for binding in wrapped.get_inner_bindings()? { + match self.global(binding)? { + Some(global) => inner.push(global), + None => return Ok(false), + } + } + let mut out = global.init_wrapped(); + out.set_module_name(wrapped.get_module_name()?); + out.set_entrypoint(wrapped.get_entrypoint()?); + set_globals(out.init_inner_bindings(len_u32(&inner)), &inner)?; + Ok(true) + } + + fn r2_bucket( + &mut self, + designator: service_designator::Reader<'_>, + error_context: String, + global: global::Builder<'_>, + ) -> capnp::Result<()> { + let channel = self.subrequest_channel(designator, error_context)?; + let mut bucket = global.init_r2_bucket(); + bucket.set_channel(channel); + bucket.set_bucket(designator.get_name()?); + Ok(()) + } + + fn analytics_engine( + &mut self, + designator: service_designator::Reader<'_>, + error_context: String, + global: global::Builder<'_>, + ) -> capnp::Result<()> { + let channel = self.subrequest_channel(designator, error_context)?; + let mut engine = global.init_analytics_engine(); + engine.set_channel(channel); + engine.set_dataset(designator.get_name()?); + Ok(()) + } + + fn hyperdrive( + &mut self, + hyperdrive: binding::hyperdrive::Reader<'_>, + error_context: String, + global: global::Builder<'_>, + ) -> capnp::Result<()> { + let channel = self.subrequest_channel(hyperdrive.get_designator()?, error_context)?; + let mut out = global.init_hyperdrive(); + out.set_channel(channel); + out.set_database(hyperdrive.get_database()?); + out.set_user(hyperdrive.get_user()?); + out.set_password(hyperdrive.get_password()?); + out.set_scheme(hyperdrive.get_scheme()?); + Ok(()) + } + + /// Writes a memory cache binding; false after reporting that it has no limits. + fn memory_cache( + &mut self, + cache: binding::memory_cache::Reader<'_>, + global: global::Builder<'_>, + ) -> capnp::Result { + if !cache.has_limits() { + self.error( + "MemoryCache bindings must specify limits. Please update the binding in the \ + worker configuration and try again." + .to_owned(), + ); + return Ok(false); + } + let mut out = global.init_memory_cache(); + // Bindings with the same id share one cache; a binding without gets its own. + if cache.has_id() { + out.set_cache_id(cache.get_id()?); + } + let limits = cache.get_limits()?; + out.set_max_keys(limits.get_max_keys()); + out.set_max_value_size(limits.get_max_value_size()); + out.set_max_total_value_size(limits.get_max_total_value_size()); + Ok(true) + } + + /// Writes a worker loader binding, taking the next worker-loader channel. + fn worker_loader( + &mut self, + loader: binding::worker_loader::Reader<'_>, + name: &str, + mut global: global::Builder<'_>, + ) -> capnp::Result<()> { + let id = if loader.has_id() { + Some(loader.get_id()?.to_string()?) + } else { + None + }; + let channel = len_u32(&self.out.worker_loaders); + self.out.worker_loaders.push(WorkerLoaderDesignator { + name: id.clone().unwrap_or_else(|| name.to_owned()), + id, + }); + global.set_worker_loader(channel); + Ok(()) + } + + /// Takes the next subrequest channel for `designator`. + fn subrequest_channel( + &mut self, + designator: service_designator::Reader<'_>, + error_context: String, + ) -> capnp::Result { + let channel = len_u32(&self.out.subrequest) + SPECIAL_SUBREQUEST_CHANNEL_COUNT; + let designator = Designator::from_reader(designator, error_context, &mut self.out.errors)?; + self.out.subrequest.push(designator); + Ok(channel) + } + + /// Writes a Durable Object namespace binding, taking the next actor channel, when the class + /// has a namespace; reports the error otherwise. + fn actor_namespace( + &mut self, + designator: binding::durable_object_namespace_designator::Reader<'_>, + error_context: &str, + mut global: global::Builder<'_>, + ) -> capnp::Result { + let class_name = designator.get_class_name()?.to_str()?; + let service = if designator.has_service_name() { + Some(designator.get_service_name()?.to_str()?) + } else { + None + }; + let retry_policy = if designator.has_retry_policy() { + let policy = designator.get_retry_policy()?; + if policy.get_max_attempts() > RETRY_MAX_CONFIGURABLE_ATTEMPTS + || !RETRY_CONFIGURABLE_TIMEOUT_MS.contains(&policy.get_timeout_ms()) + { + self.error(format!( + "{error_context} has a Durable Object retry policy outside the system limits." + )); + return Ok(false); + } + Some(policy) + } else { + None + }; + let actor_configs = self.actor_configs; + let config = if let Some(service) = service { + let Some(classes) = actor_configs.get(service) else { + self.error(format!( + "{error_context} refers to a service \"{service}\", but no such service \ + is defined." + )); + return Ok(false); + }; + let Some(config) = classes.get(class_name) else { + self.error(format!( + "{error_context} refers to a Durable Object namespace named \ + \"{class_name}\" in service \"{service}\", but no such Durable Object \ + namespace is defined by that service." + )); + return Ok(false); + }; + config + } else { + let Some(config) = self.local_actor_configs.get(class_name) else { + self.error(format!( + "{error_context} refers to a Durable Object namespace named \ + \"{class_name}\", but no such Durable Object namespace is defined by \ + this Worker." + )); + return Ok(false); + }; + config + }; + + let channel = len_u32(&self.out.actors); + self.out.actors.push(ActorDesignator { + service: service.map(str::to_owned), + class_name: class_name.to_owned(), + }); + match config { + ActorConfig::Durable { unique_key, .. } => { + let mut namespace = global.init_durable_actor_namespace(); + namespace.set_actor_channel(channel); + namespace.set_unique_key(unique_key.as_str()); + if let Some(policy) = retry_policy { + namespace.set_retry_policy(policy)?; + } + } + ActorConfig::Ephemeral { .. } => global.set_ephemeral_actor_namespace(channel), + } + Ok(true) + } + + /// Writes a `CryptoKey` binding: the key material in the format `importKey()` takes, the + /// algorithm as JSON, and the usages. False after reporting an invalid key. + fn crypto_key( + &mut self, + name: &str, + key: binding::crypto_key::Reader<'_>, + mut out: global::crypto_key::Builder<'_>, + ) -> capnp::Result { + use binding::crypto_key::Which; + + let Ok(which) = key.which() else { + self.error(format!( + "Encountered unknown CryptoKey type for binding \"{name}\". Was the config \ + compiled with a newer version of the schema?" + )); + return Ok(false); + }; + match which { + Which::Raw(raw) => { + out.set_format("raw"); + out.reborrow().init_key_data().set_bytes(raw?); + } + Which::Hex(hex) => { + out.set_format("raw"); + let Ok(bytes) = data_encoding::HEXLOWER_PERMISSIVE.decode(hex?.as_bytes()) else { + self.error(format!( + "CryptoKey binding \"{name}\" contained invalid hex." + )); + return Ok(false); + }; + out.reborrow().init_key_data().set_bytes(&bytes); + } + Which::Base64(base64) => { + out.set_format("raw"); + let mut text = base64?.as_bytes().to_vec(); + text.retain(|c| !c.is_ascii_whitespace()); + let Ok(bytes) = BASE64.decode(text) else { + self.error(format!( + "CryptoKey binding \"{name}\" contained invalid base64." + )); + return Ok(false); + }; + out.reborrow().init_key_data().set_bytes(&bytes); + } + Which::Pkcs8(pem) => { + out.set_format("pkcs8"); + let Some(der) = self.pem_key(name, pem?, SectionKind::PrivateKey) else { + return Ok(false); + }; + out.reborrow().init_key_data().set_bytes(&der); + } + Which::Spki(pem) => { + out.set_format("spki"); + let Some(der) = self.pem_key(name, pem?, SectionKind::PublicKey) else { + return Ok(false); + }; + out.reborrow().init_key_data().set_bytes(&der); + } + Which::Jwk(jwk) => { + out.set_format("jwk"); + out.reborrow().init_key_data().set_json(jwk?); + } + } + + let Ok(algorithm) = key.get_algorithm().which() else { + self.error(format!( + "Encountered unknown CryptoKey algorithm type for binding \"{name}\". Was the \ + config compiled with a newer version of the schema?" + )); + return Ok(false); + }; + match algorithm { + binding::crypto_key::algorithm::Which::Name(name) => { + let quoted = serde_json::to_string(name?.to_str()?) + .map_err(|error| capnp::Error::failed(error.to_string()))?; + out.set_algorithm(quoted.as_str()); + } + binding::crypto_key::algorithm::Which::Json(json) => out.set_algorithm(json?), + } + + out.set_extractable(key.get_extractable()); + // An absent list stays absent: capnp copies one as a list of Void, which does not read + // back as a list of `Usage`. + if key.has_usages() { + out.set_usages(key.get_usages()?)?; + } + Ok(true) + } + + /// The DER of the first PEM section in `pem`, which must be of `kind`, or none after + /// reporting the error. + fn pem_key( + &mut self, + name: &str, + pem: capnp::text::Reader<'_>, + kind: SectionKind, + ) -> Option> { + match <(SectionKind, Vec)>::from_pem_slice(pem.as_bytes()) { + Ok((found, der)) if found == kind => return Some(der), + Ok((found, _)) => self.error(format!( + "CryptoKey binding \"{name}\" contained wrong PEM type, expected {kind:?} but \ + got {found:?}." + )), + Err(_) => self.error(format!( + "CryptoKey binding \"{name}\" contained invalid PEM format." + )), + } + None + } +} + +/// Base64 as `kj::decodeBase64()` accepts it: padding is optional. +const BASE64: GeneralPurpose = GeneralPurpose::new( + &base64::alphabet::STANDARD, + GeneralPurposeConfig::new() + .with_decode_padding_mode(DecodePaddingMode::Indifferent) + .with_decode_allow_trailing_bits(true), +); + +#[cfg(test)] +#[path = "bindings-test.rs"] +mod tests; diff --git a/src/workerd/server/server/bridge.rs b/src/workerd/server/server/bridge.rs new file mode 100644 index 00000000000..90f96986165 --- /dev/null +++ b/src/workerd/server/server/bridge.rs @@ -0,0 +1,851 @@ +//! The FFI between the Rust server and the C++ worker factory (worker-factory.h). +//! +//! The factory owns everything that needs the isolate: compiling a worker, starting a request on +//! it, constructing actors and their storage, the RPC bootstrap, channel tokens and the inspector. +//! The server owns everything else and reaches the factory through the `extern "C++"` block; the +//! factory reaches back through the `extern "Rust"` block, whose types are the server's channel +//! objects and its I/O channel factory. +//! +//! Ownership rules: a `KjOwn` moves ownership across the boundary in the direction of the call; +//! a `Box` of a Rust type handed to C++ is a cheap handle (an `Rc` inside), so C++ may hold it +//! for as long as the wrapping KJ object lives and clone it through `*_clone`. + +#![allow( + unsafe_code, + reason = "holds the cxx bridge, which expands to unsafe FFI glue, and `rewriting_response`, the safe wrapper over the bridge's `unsafe fn new_rewriting_response`" +)] + +pub use crate::channels::AbortReason; +pub use crate::channels::ActorClassChannel; +pub use crate::channels::ActorHooks; +pub use crate::channels::ActorNamespaceHandle; +pub use crate::channels::ChannelFactory; +pub use crate::channels::KeepAlive; +pub use crate::channels::ServerHandle; +pub use crate::channels::SubrequestChannel; +pub use crate::channels::WorkerInterfaceList; +pub use crate::channels::WorkerStub; +pub use crate::entry::PendingCommand; +pub use crate::entry::run_pending_command; +pub use crate::in_process::InProcessServer; +pub use crate::in_process::close_in_process_server; +pub use crate::in_process::new_in_process_server; +pub use crate::listen::udp::UdpFlow; +pub use crate::tasks::SpawnedTask; +pub use crate::tasks::task_run; + +#[cxx::bridge(namespace = "workerd::server")] +#[expect( + clippy::missing_safety_doc, + reason = "cxx bridge extern decls; safety is uniform" +)] +#[expect( + clippy::too_many_arguments, + reason = "the bridge declarations mirror the C++ signatures" +)] +#[expect(clippy::unnecessary_box_returns, reason = "cxx requires a Box")] +pub mod ffi { + #[namespace = "kj::rust"] + unsafe extern "C++" { + type AsyncIoStream = kj::io::ffi::AsyncIoStream; + type HttpHeaderTable = kj::http::ffi::HttpHeaderTable; + type HttpHeaders = kj::http::ffi::HttpHeaders; + type HttpServiceResponse = kj::http::ffi::HttpServiceResponse; + type ConnectResponse = kj::http::ffi::ConnectResponse; + } + + #[namespace = "workerd::rust::worker"] + unsafe extern "C++" { + type WorkerInterface = worker::ffi::bridge::WorkerInterface; + type CustomEvent = worker::ffi::bridge::CustomEvent; + type CustomEventResult = worker::ffi::bridge::CustomEventResult; + } + + #[namespace = "workerd::rust::kj_hyper"] + unsafe extern "C++" { + type WebSocketErrorHandler = kj_hyper::ffi::WebSocketErrorHandler; + } + + // ===================================================================================== + // Plain data + + /// A TCP socket bound before the services start, so a worker can learn its own address + /// (`Worker::Api::getInboundListeners()`). + struct InboundListener { + protocol: String, + address: String, + port: u16, + } + + /// An entrypoint the compiled worker exports. `name` is empty for the default export. + struct EntrypointInfo { + name: String, + is_default: bool, + handlers: Vec, + } + + /// What the factory learned while compiling a worker. + struct WorkerInfo { + entrypoints: Vec, + actor_classes: Vec, + workflow_classes: Vec, + /// Config errors and warnings, without the `service : ` prefix. + errors: Vec, + warnings: Vec, + /// The `allow_irrevocable_stub_storage` compatibility flag: whether channel tokens the + /// worker hands out may outlive the process. + persistent_self_tokens: bool, + /// A dynamic worker's env channel counts, which `ctx.exports` is numbered after: the + /// subrequest channels its `env` held (not counting the global-outbound slots) and its + /// actor classes. The factory serves these numbers itself. Zero for a config worker. + env_subrequest_channels: u32, + env_actor_classes: u32, + } + + struct WorkerSpec { + name: String, + /// The worker's inbound TCP listeners, for `Worker::Api::getInboundListeners()`. + inbound_listeners: Vec, + /// The `env` object: an encoded `Globals` message (compiled-bindings.capnp), in words. + /// Unused for dynamic workers, whose env is a `Frankenvalue`. + globals: Vec, + /// `accessBlobHeader`, when set: requests carrying this header have it parsed into the + /// request's access info. + access_blob_header: KjMaybe, + } + + /// A header the rewriter sets to `value`, or removes when it has none. `name` is a header + /// of the factory's table, as every header the config's `HttpOptions` name is. + struct HeaderEdit { + name: String, + value: KjMaybe, + } + + /// One datagram of a UDP flow; `ended` once the flow is over (an idle timeout). + struct UdpDatagram { + ended: bool, + data: Vec, + } + + /// Where the config has an actor class's `container` options: + /// `services[service_index].worker.durableObjectNamespaces[namespace_index]`. + #[derive(Clone, Debug, PartialEq, Eq)] + struct ContainerRef { + service_index: u32, + namespace_index: u32, + } + + /// A facet's id and class, as the actor that owns the facet resolved them. + struct FacetStartInfo { + id: KjOwn, + actor_class: Box, + } + + /// A config error, or else a warning, as the in-process server (in_process.rs) reports it. + struct ConfigReport { + error: bool, + message: String, + } + + /// Where an actor keeps its data. + struct ActorStorageSpec { + /// Durable: keep state across requests. Ephemeral: no storage at all. + durable: bool, + enable_sql: bool, + /// The key of the root actor whose storage this actor shares: its own key for a root + /// actor, its root's for a facet. + root_key: String, + /// A facet's number within its root actor's storage; none for a root actor. + facet_id: KjMaybe, + } + + /// What a channel token is for (`IoChannelFactory::ChannelTokenUsage`): restoring a stub over + /// RPC, or from Durable Object storage. The values are the C++ enum's. + #[repr(u32)] + enum TokenUsage { + RPC, + STORAGE, + } + + // ===================================================================================== + // The command line (bootstrap.h): the options the C++ driver applies, and the driver itself. + + /// The options of `serve` and `test` the C++ driver applies: to the process (perfetto), or + /// to the worker factory (experimental, Python). + #[namespace = "workerd::server::cli"] + struct ServeOrTestOptions { + perfetto_trace_path: KjMaybe, + perfetto_trace_categories: KjMaybe, + experimental: bool, + pyodide_package_disk_cache_dir: KjMaybe, + pyodide_bundle_disk_cache_dir: KjMaybe, + python_save_snapshot: bool, + python_save_baseline_snapshot: bool, + python_load_snapshot: KjMaybe, + python_snapshot_dir: KjMaybe, + } + + /// The options of `test` the C++ driver applies: logging, V8 modes, autogates, the + /// compatibility date every worker gets. + #[namespace = "workerd::server::cli"] + struct TestOptions { + no_verbose: bool, + predictable: bool, + gc_stress: bool, + all_autogates: bool, + compat_date: KjMaybe, + } + + #[namespace = "kj_rs_tokio"] + unsafe extern "C++" { + include!("kj-rs-tokio/tokio-event-port.h"); + + /// The KJ event loop's context (`kj_rs_tokio::Runtime::context`): the + /// `kj::Timer` and `kj::EventLoop` the process's C++ runs on. + type TokioAsyncIoContext = kj_rs_tokio::TokioAsyncIoContext; + } + + #[namespace = "workerd::server::cli"] + unsafe extern "C++" { + include!("workerd/server/factory/bootstrap.h"); + + /// Runs `command` (`run_pending_command`) with the process's logging set up on the C++ + /// stack around it: the crash handler, info logging if `verbose`, and the JSON logger if + /// `config` asks for structured logging (KJ requires a `kj::ExceptionCallback` to live + /// on the stack of the thread that installs it). The command gets `config` and whether + /// logging is structured. The command's result is the result. + fn with_process_context( + verbose: bool, + config: Vec, + command: Box, + ) -> Result; + + /// Sets up what the process needs around the Rust server -- perfetto, autogates, V8 + /// (platform and `jsg::V8System`), a tokio-backed `kj::Network` on the loop's timer -- + /// and returns the worker factory over the config, which owns it all: dropping the + /// factory tears the rest down after it, in reverse, flushing the perfetto trace. + /// `config` is an encoded message (segment table, then segments) in 8-byte words. + /// `test` is the `test` command's options, absent for `serve`. An error means the + /// process failed to start. + /// + /// The factory dies before the Runtime (the timer and the loop must outlive everything + /// on them). + fn bootstrap( + event_loop: Pin<&mut TokioAsyncIoContext>, + config: Vec, + options: &ServeOrTestOptions, + test: KjMaybe<&TestOptions>, + ) -> Result>; + + /// Ends the process with `code` at once, without running destructors (what + /// `kj::ProcessContext::exit()` does when `KJ_CLEAN_SHUTDOWN` is not set). Does not return. + fn cli_exit(code: i32); + + /// A log line for KJ's logger, at a `kj::LogSeverity` (`log.rs` maps tracing + /// levels onto it). + fn kj_log(severity: u8, file: &str, line: u32, message: &str); + + /// One of the command line's own messages under structured logging, at a + /// `kj::LogSeverity`: a line of the JSON logger's format on stderr, where the logger's + /// own lines go to stdout. What supervises the process reads the reason of a failed + /// start from stderr. + fn json_log_to_stderr(severity: u8, file: &str, line: u32, message: &str); + } + + // ===================================================================================== + // C++: the factory + + unsafe extern "C++" { + include!("workerd/server/factory/worker-factory.h"); + + /// Everything shared by the workers of one server: the V8 system, the HTTP header + /// table, the capnp factories, the channel token handler, the inspector. Created by the + /// C++ bootstrap (bootstrap.c++) for the lifetime of a run. + type WorkerFactory; + + /// The config, as the bytes of an encoded `Config` message the factory and the server + /// both read. Owned by the factory for the run; every reader into it stays valid. + fn factory_config<'a>(factory: &'a WorkerFactory) -> &'a [u8]; + fn factory_experimental(factory: &WorkerFactory) -> bool; + /// Registers the server as the resolver of channel tokens and debug-port requests. + /// Tokens cannot be decoded before this. + fn factory_set_server(factory: &WorkerFactory, server: Box); + /// Adds a background task to the factory's task set; it starts on the next turn of the + /// event loop. See `Factory::spawn`. + fn factory_spawn(factory: &WorkerFactory, task: Box); + /// Drops every background task now. The server's drop does this before it unlinks its + /// services, as a task may hold an actor and so its worker. + fn factory_clear_tasks(factory: &WorkerFactory); + /// Runs the tasks the server's drop spawned (and the tasks those spawn), then drops the + /// rest. See `Factory::settle_tasks`. + async fn factory_settle_tasks(factory: &WorkerFactory) -> Result<()>; + /// The reading of the factory's `kj::Timer`, in nanoseconds since the timer's origin. + fn factory_timer_now(factory: &WorkerFactory) -> u64; + /// Completes `nanos` from now on the factory's `kj::Timer`. + async fn factory_sleep(factory: &WorkerFactory, nanos: u64); + /// The header table every `kj::HttpHeaders` handed to a worker is built against. + fn factory_header_table<'a>(factory: &'a WorkerFactory) -> &'a HttpHeaderTable; + + /// Compiles a worker: its script, bindings and exports. Any config error is reported in + /// the returned info rather than thrown; a worker with errors still exists so that the + /// server can keep reporting the config's other errors. + /// + /// The code comes from the config's `services[config_service]` (a `worker` service) or, + /// for a dynamic worker, from `dynamic_source`, which is fetched here; the capabilities + /// in its `env` stay with the factory as the worker's own channel tables. + async fn factory_new_worker( + factory: &WorkerFactory, + spec: &WorkerSpec, + config_service: KjMaybe, + dynamic_source: KjMaybe>, + ) -> Result>; + + /// A compiled worker: the isolate, script and `Worker` plus what was learned compiling + /// them. + type CompiledWorker; + fn worker_info(worker: &CompiledWorker) -> WorkerInfo; + /// Compiles `ctx.exports` from an encoded `Globals` message. Called once, after the + /// server has numbered the worker's loopback channels. + fn worker_set_ctx_exports(worker: &CompiledWorker, globals: &[u64]) -> Result<()>; + /// Cancels the worker's requests still in flight (background work past the response: a + /// `waitUntil()`, a tail worker's event), each of which holds the worker and its I/O + /// channels; and drops a dynamic worker's env channel tables and tails, which may refer + /// back to the worker that loaded it. Called when the server unlinks the worker. + fn worker_unlink(worker: &CompiledWorker); + /// Starts a request on the worker. `channels` is the worker's I/O channel table for this + /// request; `tails` receive the request's trace once it completes (the server passes none + /// for a request that is itself a tail worker's, so that tails do not trace themselves, + /// and none for a dynamic worker, whose tails are its source's and the factory's own). + /// A static worker's entrypoint mints its own self token, which restores a stub to it. + fn worker_start_request( + worker: &CompiledWorker, + entrypoint: KjMaybe<&str>, + props: KjMaybe>, + actor: KjMaybe<&ActorHandle>, + channels: Box, + metadata: KjOwn, + tails: Box, + ) -> Result>; + /// `inner` with `keep` attached: dropped once `inner` has been. + fn worker_interface_attach( + inner: KjOwn, + keep: Box, + ) -> KjOwn; + /// Constructs an actor of `class_name` whose storage lives in `storage`. `hooks` is the + /// actor's way back to the server; `hibernation_manager` is the evicted predecessor's, + /// whose WebSockets the actor adopts. `container` gives the actor its container's Docker + /// client: the one a live container with the actor's id already has, else a new one. + fn worker_new_actor( + worker: &CompiledWorker, + class_name: &str, + props: KjMaybe>, + id: KjOwn, + storage: &ActorStorage, + spec: &ActorStorageSpec, + hooks: Box, + hibernation_manager: KjMaybe>, + container: KjMaybe<&ContainerRef>, + ) -> Result>; + + /// A live actor. + type ActorHandle; + /// An actor's `onBroken()`, which does not keep the actor alive: dropping the actor + /// closes its storage at once, whoever still waits for this. + type ActorBroken; + fn actor_on_broken(actor: &ActorHandle) -> KjOwn; + /// Resolves when the actor breaks (an uncaught error, an eviction request, a storage + /// failure); the error is the reason. + async fn actor_broken(broken: KjOwn) -> Result<()>; + /// Aborts the actor with `reason`; every in-flight request fails. Without a reason the + /// actor is shut down instead: its background work stops, and nothing is failed. + fn actor_abort(actor: &ActorHandle, reason: &AbortReason); + /// The isolate lock hibernating the actor's WebSockets takes, with no request to take + /// it on behalf of. + type ActorLock; + async fn actor_lock(actor: &ActorHandle) -> Result>; + /// Shuts the actor down for eviction: hibernates its WebSockets (given `lock`, if it + /// has a hibernation manager), then stops its background work with `reason` as the + /// disconnect reason. Returns false without touching the actor if something still holds + /// a strong reference to it (a request raced in); the caller retries later. + fn actor_shutdown( + actor: &ActorHandle, + reason: &str, + lock: KjMaybe>, + ) -> Result; + /// Resets the actor's SQLite database while its connection is open (`deleteAllActors`). + fn actor_reset_storage(actor: &ActorHandle); + /// The actor's hibernation manager, to hand to the actor that replaces it after eviction. + fn actor_hibernation_manager(actor: &ActorHandle) -> KjMaybe>; + type HibernationManager; + + /// A facet's start info, as the actor that owns the facet supplies it; resolving it + /// yields the facet's id and class. + type FacetStart; + async fn facet_start_resolve(start: KjOwn) -> Result; + + /// A Durable Object id: a 32-byte id for a durable namespace, or a name for an + /// ephemeral one. + type ActorIdHandle; + fn actor_id_clone(id: &ActorIdHandle) -> KjOwn; + /// The key the server files the actor under: the hex id, or the name. + fn actor_id_key(id: &ActorIdHandle) -> String; + fn actor_id_from_name(name: &str) -> KjOwn; + /// The id of a durable namespace, from its hex text (a debug-port request). + fn actor_id_from_hex(hex: &str) -> Result>; + + /// The storage of one actor namespace: its directory on disk (or none for in-memory + /// storage), the SQLite VFS over it, and its alarm scheduler. + type ActorStorage; + /// `path` is the namespace's directory; empty for in-memory storage. `actors` answers + /// the scheduler's requests for an actor to run an alarm on. + fn factory_new_actor_storage( + factory: &WorkerFactory, + path: &str, + unique_key: &str, + actors: Box, + ) -> Result>; + /// Deletes every actor's storage and every alarm. Callers abort the actors first. + fn actor_storage_delete_all(storage: &ActorStorage) -> Result<()>; + /// Deletes the storage of the facet `name` of the actor numbered `parent_facet_id` (none + /// for the root), and its descendants'. Nothing for a root actor that never had facets. + fn actor_storage_delete_facet( + storage: &ActorStorage, + root_key: &str, + parent_facet_id: KjMaybe, + name: &str, + ) -> Result<()>; + /// Replaces the storage of the facet `dst` (and its descendants') with a copy of the + /// facet `src`'s, both facets of the actor numbered `parent_facet_id`. + fn actor_storage_clone_facet( + storage: &ActorStorage, + root_key: &str, + parent_facet_id: KjMaybe, + src: &str, + dst: &str, + ) -> Result<()>; + /// The number of a named facet of the root actor `root_key`, allocating one if new. + /// None for in-memory storage (an `ActorCache` per actor), which numbers no facets. + fn actor_storage_facet_id( + storage: &ActorStorage, + root_key: &str, + parent_facet_id: KjMaybe, + name: &str, + ) -> Result>; + + /// Shuts down the Docker client of every actor's container and resolves once Docker has + /// removed the containers. No actor can start a container afterwards. + async fn factory_shutdown_containers(factory: &WorkerFactory) -> Result<()>; + + /// The source of a dynamic worker, as the worker that loads it supplies it; handed + /// straight to `factory_new_worker`. + type DynamicSource; + + /// Metadata of one request: the cf blob, the client address, span parents, the token + /// factory a restored stub should use. + type RequestMetadata; + fn new_request_metadata( + cf_blob_json: KjMaybe<&str>, + client_address: KjMaybe<&str>, + ) -> KjOwn; + /// The cf blob the request carries, as JSON. + fn request_metadata_cf_blob_json(metadata: &RequestMetadata) -> KjMaybe; + /// Marks the request as coming through a stub that was, or may be, stored durably. + fn request_metadata_set_from_persistent_stub( + metadata: Pin<&mut RequestMetadata>, + persistent: bool, + ); + /// Makes stubs the request creates for the actor itself restorable: they encode the + /// actor's namespace and id. + fn request_metadata_set_actor_self_token( + factory: &WorkerFactory, + metadata: Pin<&mut RequestMetadata>, + unique_key: &str, + id: &ActorIdHandle, + persistent: bool, + ); + + /// A `kj::Exception` the factory hands the server as an abort reason. `exception_throw` + /// throws it: the `Err` is how the server reads it, with its type, description, + /// location and details. + #[namespace = "kj"] + type Exception; + fn exception_throw(exception: &Exception) -> Result<()>; + /// The error `error` holds, the way KJ prints an exception (`kj::str`): `file:line: type: + /// description`, the location being the bridge's if the error was made in Rust. Without + /// a stack trace, which a `KjError` does not carry. + fn exception_text(error: &AbortReason) -> String; + + /// A JS value that can cross the RPC boundary: the props of an entrypoint, an actor + /// class or a dynamic worker's env. + #[namespace = "workerd"] + type Frankenvalue; + fn frankenvalue_from_json(json: &str) -> KjOwn; + fn frankenvalue_clone(value: &Frankenvalue) -> KjOwn; + fn frankenvalue_is_empty(value: &Frankenvalue) -> bool; + /// An empty object. + fn frankenvalue_new() -> KjOwn; + /// Sets the property `name` to a service stub (a `Fetcher` in JS) over `channel`. + fn frankenvalue_set_service_stub( + value: Pin<&mut Frankenvalue>, + name: &str, + channel: Box, + ); + + /// Serves a capnp-over-HTTP-CONNECT connection: the peer's `WorkerdBootstrap` dispatches + /// events to `target`. Resolves when the connection closes. + async fn factory_accept_bootstrap( + factory: &WorkerFactory, + stream: KjOwn, + target: Box, + ) -> Result<()>; + /// Serves a debug-port connection, resolving entrypoints and actors through the server + /// (`factory_set_server`). + async fn factory_accept_debug_port( + factory: &WorkerFactory, + stream: KjOwn, + ) -> Result<()>; + /// The client side of a capnp-over-HTTP-CONNECT connection to an external server. + type RpcClient; + fn new_rpc_client( + factory: &WorkerFactory, + stream: KjOwn, + ) -> KjOwn; + /// Sends a custom event (a trace, a JS RPC session, a tail stream, a UDP flow) to the + /// peer's `WorkerdBootstrap`. + async fn rpc_client_custom_event( + client: &RpcClient, + event: KjOwn, + cf_blob_json: KjMaybe<&str>, + ) -> Result; + async fn rpc_client_on_disconnect(client: &RpcClient); + + /// Starts the inspector on `address` on its own thread; every isolate the factory + /// creates afterwards registers with it. Returns the bound port. + fn factory_start_inspector(factory: &WorkerFactory, address: &str) -> Result; + + // The listeners' shims (worker-factory-listen.c++). + + /// A copy of `headers` with `edits` and then `injected` applied in order. + fn edit_headers( + table: &HttpHeaderTable, + headers: &HttpHeaders, + edits: &[HeaderEdit], + injected: &[HeaderEdit], + ) -> Result>; + /// A response that applies `edits` to the headers of every `send()` and + /// `acceptWebSocket()` before forwarding to `inner`, which it borrows for its life. + unsafe fn new_rewriting_response<'a>( + inner: Pin<&'a mut HttpServiceResponse>, + table: &HttpHeaderTable, + edits: &[HeaderEdit], + ) -> Result>; + /// The `ConnectResponse` of a raw TCP socket's `connect()`: accepting does nothing, + /// rejecting discards the body. + fn new_null_connect_response() -> KjOwn; + /// `JsgifyWebSocketErrors`: the WebSocket error handler whose exceptions reach JS as + /// `Error`s, for every server and client the Rust server runs. + fn new_jsgify_websocket_errors() -> KjOwn; + /// A `connect()` event delivering one UDP flow, addressed as `address`. + fn new_udp_connect_event(address: &str, flow: Box) -> KjOwn; + + type TokenUsage; + /// Channel tokens (channel-token.h): what restores a channel later, over RPC or from + /// Durable Object storage. Encoding a channel whose target is not `persistent` for + /// storage fails with a `DataCloneError`. + /// + /// A token is encoded at once; its bytes are ready at once too unless a channel in the + /// props has a token of its own that is not (see `PendingToken`). The props are borrowed + /// for the encoding only. + fn factory_encode_subrequest_token( + factory: &WorkerFactory, + service_name: &str, + entrypoint: KjMaybe<&str>, + props: KjMaybe<&Frankenvalue>, + persistent: bool, + usage: TokenUsage, + ) -> Result>; + fn factory_encode_actor_class_token( + factory: &WorkerFactory, + service_name: &str, + class_name: &str, + props: KjMaybe<&Frankenvalue>, + persistent: bool, + usage: TokenUsage, + ) -> Result>; + fn factory_encode_actor_token( + factory: &WorkerFactory, + unique_key: &str, + id: &ActorIdHandle, + persistent: bool, + usage: TokenUsage, + ) -> Result>; + /// A channel token as `ChannelTokenHandler` encodes it: bytes that are ready at once, + /// or a promise of them. `SubrequestChannel::getTokenMaybeSync()` hands the ready bytes + /// out synchronously, which is what lets a stub be stored inline in Durable Object + /// storage. + type PendingToken; + } + + // ===================================================================================== + // Rust: the server's channel objects + // + // An async function that borrows its arguments is declared `async unsafe fn f<'a>` with the + // one lifetime on every reference: the form the bridge requires for a future that is not + // `'static`. C++ keeps the arguments alive until the promise settles or is destroyed. + + extern "Rust" { + /// The Rust half of a command, run by `with_process_context` (entry.rs). + type PendingCommand; + fn run_pending_command( + command: Box, + config: Vec, + structured_logging: bool, + ) -> Result; + + /// The server run inside a test process (in_process.rs), on a factory the test built. + /// `run` is `workerd serve` until `drain`, with `--debug-port` if `debug_port` is not + /// empty; `test` is `workerd test`. `next_report` is the next config error or warning. + /// `connect` and `accept` are the two ends of a `loopback:` name. + type InProcessServer; + fn new_in_process_server(factory: KjOwn) -> Box; + async unsafe fn run<'a>(self: &'a InProcessServer, debug_port: &'a str) -> Result<()>; + async unsafe fn test<'a>( + self: &'a InProcessServer, + service_pattern: &'a str, + entrypoint_pattern: &'a str, + ) -> Result; + fn drain(self: &InProcessServer); + async unsafe fn next_report<'a>(self: &'a InProcessServer) -> ConfigReport; + fn connect(self: &InProcessServer, name: &str) -> Result>; + async unsafe fn accept<'a>( + self: &'a InProcessServer, + name: &'a str, + ) -> Result>; + /// Ends the server's tasks and drops the factory, logging an error if a service still + /// holds it. + async fn close_in_process_server(server: Box) -> Result<()>; + + /// A background task of the server, run by the factory's task set (`factory_spawn`); + /// `task_run` is the promise the task set holds. + type SpawnedTask; + async fn task_run(task: Box) -> Result<()>; + + /// The server, for the factory's callbacks: resolving channel tokens and debug-port + /// requests to channels. A weak handle: the server owns the factory, and every call + /// fails once the server is gone. + type ServerHandle; + fn server_clone(self: &ServerHandle) -> Box; + fn resolve_entrypoint( + self: &ServerHandle, + service_name: &str, + entrypoint: KjMaybe<&str>, + props: KjOwn, + persistent: bool, + ) -> Result>; + fn resolve_actor_class( + self: &ServerHandle, + service_name: &str, + class_name: KjMaybe<&str>, + props: KjOwn, + persistent: bool, + ) -> Result>; + fn resolve_actor( + self: &ServerHandle, + unique_key: &str, + id: KjOwn, + persistent: bool, + ) -> Result>; + /// The debug port's `getEntrypoint`: a worker's entrypoint (the default without + /// `entrypoint`), or a service that is not a worker, with `props` bound if given. + fn resolve_debug_entrypoint( + self: &ServerHandle, + service_name: &str, + entrypoint: KjMaybe<&str>, + props: KjMaybe>, + ) -> Result>; + /// The debug port's `getActor`: an actor of `class_name` in the service, by its id (hex + /// for a durable namespace, the name for an ephemeral one). + fn resolve_debug_actor( + self: &ServerHandle, + service_name: &str, + class_name: &str, + actor_id: &str, + ) -> Result>; + + /// A worker's I/O channel table for one request: what its bindings reach. + type ChannelFactory; + fn subrequest_channel( + self: &ChannelFactory, + channel: u32, + props: KjMaybe>, + persistent: bool, + ) -> Result>; + fn global_actor( + self: &ChannelFactory, + channel: u32, + id: KjOwn, + persistent: bool, + ) -> Result>; + fn colo_local_actor( + self: &ChannelFactory, + channel: u32, + id: &str, + ) -> Result>; + fn actor_class( + self: &ChannelFactory, + channel: u32, + props: KjMaybe>, + persistent: bool, + ) -> Result>; + /// The channel of the worker's cache API outbound, if configured. + fn cache_channel(self: &ChannelFactory) -> Result>; + /// The channel of the worker's Cloudflare Access identity binding, whose props come from + /// the request's access blob, if the worker has one. + fn access_binding_channel(self: &ChannelFactory) -> KjMaybe; + fn abort_all_actors(self: &ChannelFactory, reason: KjMaybe<&Exception>); + fn delete_all_actors(self: &ChannelFactory, reason: KjMaybe<&Exception>) -> Result<()>; + async unsafe fn evict_all_actors_for_test<'a>( + self: &'a ChannelFactory, + hibernate: bool, + ) -> Result<()>; + /// Aborts the isolate. A dynamic worker unloads; a static worker cannot be replaced, so + /// the error is what ends the process. + fn abort_isolate(self: &ChannelFactory, reason: &str) -> Result<()>; + /// Loads a dynamic worker, reachable as the returned stub's entrypoints and classes. + fn load_isolate( + self: &ChannelFactory, + loader_channel: u32, + name: KjMaybe<&str>, + source: KjOwn, + ) -> Result>; + /// Whether this worker has the workerd debug port binding. + fn has_debug_port(self: &ChannelFactory) -> bool; + + /// A loaded dynamic worker. + type WorkerStub; + fn entrypoint( + self: &WorkerStub, + name: KjMaybe<&str>, + props: KjOwn, + ) -> Box; + fn actor_class( + self: &WorkerStub, + name: KjMaybe<&str>, + props: KjOwn, + ) -> Box; + + /// Something that can start a request: a worker entrypoint, an actor, an external + /// server, the network, a directory. + type SubrequestChannel; + fn start_request( + self: &SubrequestChannel, + metadata: KjOwn, + ) -> Result>; + /// Whether the channel may be handed to another worker, as a stub. + fn require_allows_transfer(self: &SubrequestChannel) -> Result<()>; + /// The channel token that restores this channel. + fn token(self: &SubrequestChannel, usage: TokenUsage) -> Result>; + async unsafe fn evict_for_test<'a>( + self: &'a SubrequestChannel, + hibernate: bool, + ) -> Result<()>; + + /// A Durable Object class, from which actors are made. + type ActorClassChannel; + fn actor_class_channel_clone(self: &ActorClassChannel) -> Box; + fn require_allows_transfer(self: &ActorClassChannel) -> Result<()>; + fn token(self: &ActorClassChannel, usage: TokenUsage) -> Result>; + + /// A Durable Object namespace, for its alarm scheduler. + type ActorNamespaceHandle; + /// The actor to run an alarm on, started if needed. The id carries the name the actor was + /// created with, when the scheduler persisted one. + fn actor_for_alarm( + self: &ActorNamespaceHandle, + id: KjOwn, + ) -> Result>; + + /// An actor's way back to the server: the requests it raises for itself, its facets + /// (named child actors sharing its storage), and its transitions between idle and active. + type ActorHooks; + /// Starts a request the actor raises for itself: an alarm, a hibernated WebSocket's + /// event. + fn start_request( + self: &ActorHooks, + metadata: KjOwn, + ) -> Result>; + fn depth(self: &ActorHooks) -> u32; + /// The channel of the named facet, starting it if needed once `start` resolves. + fn facet( + self: &ActorHooks, + name: &str, + start: KjOwn, + ) -> Result>; + fn abort_facet(self: &ActorHooks, name: &str, reason: &Exception); + fn delete_facet(self: &ActorHooks, name: &str) -> Result<()>; + fn clone_facet(self: &ActorHooks, src: &str, dst: &str) -> Result<()>; + /// The actor's first request started. + fn active(self: &ActorHooks); + /// The actor's last request ended. + fn inactive(self: &ActorHooks); + + /// The reason of an `actor_abort`. `raise` returns it as the `Err`, which C++ catches as + /// the equivalent `kj::Exception`; `Ok` when there is none. + type AbortReason; + fn raise(self: &AbortReason) -> Result<()>; + + /// What a request's interface keeps alive (`worker_interface_attach`). + type KeepAlive; + + /// One UDP flow (every datagram to and from one peer, until idle), as the + /// `workerd::DatagramChannel` of a `UdpConnectCustomEvent`. + type UdpFlow; + /// The next datagram, or `ended`. One call at a time. + async unsafe fn receive<'a>(self: &'a UdpFlow) -> Result; + async unsafe fn send<'a>(self: &'a UdpFlow, datagram: &'a [u8]) -> Result<()>; + + /// The `WorkerInterface`s of a request's tail workers. + type WorkerInterfaceList; + fn len(self: &WorkerInterfaceList) -> usize; + fn is_streaming(self: &WorkerInterfaceList, index: usize) -> bool; + fn take(self: &mut WorkerInterfaceList, index: usize) -> Result>; + } +} + +// ===================================================================================== +/// A response whose headers are edited before they reach `inner`. Borrows `inner` for its life, +/// which the type carries. +pub struct RewritingResponse<'a> { + response: kj_rs::KjOwn, + _inner: std::marker::PhantomData<&'a mut ffi::HttpServiceResponse>, +} + +impl RewritingResponse<'_> { + pub fn as_mut(&mut self) -> std::pin::Pin<&mut ffi::HttpServiceResponse> { + self.response.as_mut() + } +} + +/// See `ffi::new_rewriting_response`. +pub fn rewriting_response<'a>( + inner: std::pin::Pin<&'a mut ffi::HttpServiceResponse>, + table: &ffi::HttpHeaderTable, + edits: &[ffi::HeaderEdit], +) -> crate::Result> { + // SAFETY: the returned wrapper carries `inner`'s lifetime, so it cannot outlive it. + let response = unsafe { ffi::new_rewriting_response(inner, table, edits) }?; + Ok(RewritingResponse { + response, + _inner: std::marker::PhantomData, + }) +} + +/// See `ffi::exception_throw`. +impl From<&ffi::Exception> for crate::Error { + fn from(exception: &ffi::Exception) -> Self { + match ffi::exception_throw(exception) { + Err(error) => error.into(), + Ok(()) => kj::failed!("exception_throw() returned"), + } + } +} diff --git a/src/workerd/server/server/channels.rs b/src/workerd/server/server/channels.rs new file mode 100644 index 00000000000..93f371abb65 --- /dev/null +++ b/src/workerd/server/server/channels.rs @@ -0,0 +1,280 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! The channel objects: what the runtime reaches into the server through. +//! +//! A worker's bindings are channel numbers. When a request uses one, the runtime asks the +//! worker's [`ChannelFactory`] for the channel behind the number and gets a [`SubrequestChannel`] +//! or an [`ActorClassChannel`]: a handle over an `Rc` of the object behind it, cheap to clone and +//! safe to hand to C++, which wraps it in the KJ interface the runtime expects +//! (worker-factory.h's `SubrequestChannelHandle`). A facet's class comes back from the runtime +//! and is unwrapped to the same `Rc` (`ActorClassChannelHandle::tryUnwrap`). +//! +//! Every type here is single-threaded (`Rc`): channels live and die on the loop thread, as the +//! runtime's do. + +use std::any::Any; +use std::rc::Rc; +use std::rc::Weak; + +use futures::future::LocalBoxFuture; +use kj_rs::KjOwn; + +use crate::Result; +use crate::actor::ActorContainer; +use crate::actor::ActorNamespace; +use crate::bridge::ffi; +use crate::config::Server; +use crate::worker::LinkedChannels; +use crate::worker::WorkerService; + +pub type WorkerInterface = ffi::WorkerInterface; +pub type Frankenvalue = ffi::Frankenvalue; +pub type RequestMetadata = ffi::RequestMetadata; +pub type ActorIdHandle = ffi::ActorIdHandle; +pub type DynamicSource = ffi::DynamicSource; +pub type FacetStart = ffi::FacetStart; +pub type PendingToken = ffi::PendingToken; +pub type TokenUsage = ffi::TokenUsage; + +/// Whether a stub for the channel may outlive the process (`Persistent` in the runtime). +pub type Persistent = bool; + +/// Something that starts requests: a worker entrypoint, an actor, an external server, the +/// network, a directory. The runtime's `IoChannelFactory::SubrequestChannel`. +pub trait Channel { + /// Starts a request; the returned interface handles exactly one event. + fn start_request(&self, metadata: KjOwn) -> Result>; + + /// Whether the channel may be handed to another worker as a stub. The default allows it. + fn require_allows_transfer(&self) -> Result<()> { + Ok(()) + } + + /// The token that restores this channel later, as the runtime encodes it: its bytes are + /// usually ready at once, which lets a stub be stored inline in Durable Object storage. + fn token(&self, usage: TokenUsage) -> Result>; + + /// This channel with `props` bound. Only channels that take props support it. + fn for_props( + &self, + props: KjOwn, + persistent: Persistent, + ) -> Result> { + let _ = (props, persistent); + Err(kj::failed!("this channel does not take props")) + } + + /// Test hook: evicts the actor behind the channel. + fn evict_for_test(&self, hibernate: bool) -> LocalBoxFuture<'_, Result<()>> { + let _ = hibernate; + Box::pin(async { + Err(kj::failed!( + "jsg.Error: evict() can only be used on a Durable Object stub." + )) + }) + } + + /// The worker entrypoint behind the channel, for a worker starting its tail workers: a + /// worker whose tail is one of its own entrypoints starts that tail as a tracer, so that the + /// tail's own request is not tailed in turn. `None` for a channel that is not a worker + /// entrypoint. + fn worker_entrypoint(&self) -> Option> { + None + } +} + +/// A worker entrypoint as a [`Channel`] reports it: enough to start the same request as a tracer. +pub struct WorkerEntrypoint<'a> { + pub worker: &'a WorkerService, + pub entrypoint: Option<&'a str>, + pub props: Option<&'a Frankenvalue>, +} + +/// A Durable Object class, from which actors are made. The runtime's +/// `IoChannelFactory::ActorClassChannel`. +pub trait ActorClass { + /// Resolves once the class can make actors (a dynamic worker's class waits for its worker to + /// start). + fn when_ready(&self) -> LocalBoxFuture<'_, Result<()>> { + Box::pin(async { Ok(()) }) + } + + /// Constructs an actor of this class. + fn new_actor(&self, request: NewActor<'_>) -> Result>; + + /// Starts a request on an actor of this class. + fn start_request( + &self, + metadata: KjOwn, + actor: &ffi::ActorHandle, + ) -> Result>; + + fn require_allows_transfer(&self) -> Result<()> { + Ok(()) + } + + /// As `Channel::token`. + fn token(&self, usage: TokenUsage) -> Result>; + + fn for_props( + &self, + props: KjOwn, + persistent: Persistent, + ) -> Result> { + let _ = (props, persistent); + Err(kj::failed!("this actor class does not take props")) + } +} + +/// Everything an actor needs to be constructed. +pub struct NewActor<'a> { + pub id: KjOwn, + /// The namespace's storage, shared by its actors. + pub storage: &'a ffi::ActorStorage, + pub spec: ffi::ActorStorageSpec, + pub hooks: Box, + /// Where the config has the class's `container` options, if it has any. + pub container: Option<&'a ffi::ContainerRef>, + /// The hibernation manager of the actor this one replaces after an eviction, with the + /// WebSockets it kept alive. + pub hibernation_manager: Option>, +} + +// ===================================================================================== +// The bridge types: the handles C++ holds. Each is a newtype so that cxx can name it; the +// methods the bridge declares on one are written beside the type it holds. + +pub struct SubrequestChannel(pub Rc); +pub struct ActorClassChannel(pub Rc); + +/// A worker's I/O channel table: what its bindings reach. The runtime's `IoChannelFactory`, one +/// per worker, handed to every request. +pub struct ChannelFactory(pub Rc); + +/// A loaded dynamic worker. The runtime's `WorkerStubChannel`. +pub struct WorkerStub(pub Rc); + +/// The server, as the factory's callbacks see it: it resolves channel tokens and debug-port +/// requests to channels. Weak: the server owns the factory that holds this, so it must not own +/// the server back. +pub struct ServerHandle(pub Weak); + +/// A Durable Object namespace, as its alarm scheduler sees it. Weak: the scheduler lives in the +/// namespace's storage, which the namespace owns. +pub struct ActorNamespaceHandle(pub Weak); + +/// An actor's way back to its container: the requests it raises for itself (alarms, hibernated +/// WebSocket events), its facets, and its transitions between idle and active. +/// +/// Weak: the actor owns this, the container the actor; and the actor can outlive its container, +/// while its last requests drain. +/// +/// A request is active while it holds a reference to the actor, which outlasts its response by +/// as much as the actor's pending `waitUntil()` work, and no longer: the interface the request +/// was started through may live on with an open WebSocket. +pub struct ActorHooks(pub Weak); + +/// The reason an actor is aborted for (`actor_abort`); none when it is only shut down. +/// +/// `raise` returns the reason as the `Err`, which the bridge throws as the equivalent +/// `kj::Exception`. +pub struct AbortReason(pub Option); + +impl AbortReason { + pub(crate) fn raise(&self) -> Result<()> { + self.0.clone().map_or(Ok(()), Err) + } +} + +/// What a request keeps alive until its interface is dropped. +pub struct KeepAlive { + _keep: Box, +} + +/// `interface`, with `keep` dropped after it. +pub fn attach(interface: KjOwn, keep: impl Any) -> KjOwn { + let keep = KeepAlive { + _keep: Box::new(keep), + }; + ffi::worker_interface_attach(interface, Box::new(keep)) +} + +/// A request's tail workers, started by the factory once it knows the request's tracer. +pub struct WorkerInterfaceList(pub Vec); + +pub struct Tail { + pub streaming: bool, + pub worker: Option>, +} + +impl SubrequestChannel { + #[must_use] + pub fn new(channel: Rc) -> Box { + Box::new(Self(channel)) + } + pub(crate) fn start_request( + &self, + metadata: KjOwn, + ) -> Result> { + self.0.start_request(metadata) + } + pub(crate) fn require_allows_transfer(&self) -> Result<()> { + self.0.require_allows_transfer() + } + pub(crate) fn token(&self, usage: TokenUsage) -> Result> { + self.0.token(usage) + } + pub(crate) async fn evict_for_test(&self, hibernate: bool) -> Result<()> { + self.0.evict_for_test(hibernate).await + } +} + +impl ActorClassChannel { + #[must_use] + pub fn new(class: Rc) -> Box { + Box::new(Self(class)) + } + #[expect(clippy::unnecessary_box_returns, reason = "cxx requires a Box")] + pub(crate) fn actor_class_channel_clone(&self) -> Box { + Box::new(Self(Rc::clone(&self.0))) + } + pub(crate) fn require_allows_transfer(&self) -> Result<()> { + self.0.require_allows_transfer() + } + pub(crate) fn token(&self, usage: TokenUsage) -> Result> { + self.0.token(usage) + } +} + +impl ServerHandle { + #[expect(clippy::unnecessary_box_returns, reason = "cxx requires a Box")] + pub(crate) fn server_clone(&self) -> Box { + Box::new(Self(Weak::clone(&self.0))) + } + pub(crate) fn server(&self) -> Result> { + self.0 + .upgrade() + .ok_or_else(|| kj::disconnected!("the server is shutting down")) + } +} + +impl WorkerInterfaceList { + #[must_use] + pub fn new(tails: Vec) -> Box { + Box::new(Self(tails)) + } + pub(crate) fn len(&self) -> usize { + self.0.len() + } + pub(crate) fn is_streaming(&self, index: usize) -> bool { + self.0.get(index).is_some_and(|tail| tail.streaming) + } + pub(crate) fn take(&mut self, index: usize) -> Result> { + self.0 + .get_mut(index) + .and_then(|tail| tail.worker.take()) + .ok_or_else(|| kj::failed!("tail worker {index} already taken")) + } +} diff --git a/src/workerd/server/server/config-test.rs b/src/workerd/server/server/config-test.rs new file mode 100644 index 00000000000..68256665735 --- /dev/null +++ b/src/workerd/server/server/config-test.rs @@ -0,0 +1,281 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +use std::cell::RefCell; + +use super::*; + +/// A reporter that collects into a shared list. +fn collecting_reporter() -> (Reporter, Rc>>) { + let errors = Rc::new(RefCell::new(Vec::new())); + let sink = Rc::clone(&errors); + let report = Reporter::new( + Box::new(move |error| sink.borrow_mut().push(error)), + Box::new(|_warning| {}), + true, + ); + (report, errors) +} + +/// The message a config `build` fills in, read back. +fn config_message( + build: impl FnOnce(config::Builder<'_>), +) -> capnp::message::Reader { + let mut message = capnp::message::Builder::new_default(); + build(message.init_root::()); + let words = capnp::serialize::write_message_to_words(&message); + capnp::serialize::read_message(words.as_slice(), ReaderOptions::new()).unwrap() +} + +fn actor_configs_of( + build: impl FnOnce(config::Builder<'_>), + experimental: bool, +) -> (Result, Vec) { + let message = config_message(build); + let config = message.get_root::().unwrap(); + let (report, errors) = collecting_reporter(); + let result = collect_actor_configs(config, experimental, &report).map_err(capnp_error); + let errors = errors.borrow().clone(); + (result, errors) +} + +#[test] +fn actor_configs_read_durable_and_ephemeral_namespaces() { + let (result, errors) = actor_configs_of( + |config| { + let mut service = config.init_services(1).get(0); + service.set_name("main"); + let mut worker = service.init_worker(); + worker + .reborrow() + .get_durable_object_storage() + .set_in_memory(()); + let mut namespaces = worker.init_durable_object_namespaces(2); + let mut durable = namespaces.reborrow().get(0); + durable.set_class_name("Counter"); + durable.set_unique_key("counter-key"); + durable.set_enable_sql(true); + let mut ephemeral = namespaces.get(1); + ephemeral.set_class_name("Cache"); + ephemeral.set_ephemeral_local(()); + ephemeral.set_prevent_eviction(true); + }, + true, + ); + assert!(errors.is_empty(), "{errors:?}"); + let configs = result.unwrap(); + let main = &configs["main"]; + assert_eq!( + main.get("Counter"), + Some(&ActorConfig::Durable { + unique_key: "counter-key".to_owned(), + evictable: true, + enable_sql: true, + workflow: None, + container: None, + }) + ); + assert_eq!( + main.get("Cache"), + Some(&ActorConfig::Ephemeral { + evictable: false, + enable_sql: false, + }) + ); + // Config order is kept. + let names: Vec<&String> = main.keys().collect(); + assert_eq!(names, ["Counter", "Cache"]); +} + +#[test] +fn ephemeral_namespaces_need_experimental() { + let (result, errors) = actor_configs_of( + |config| { + let mut service = config.init_services(1).get(0); + service.set_name("main"); + let worker = service.init_worker(); + let mut ns = worker.init_durable_object_namespaces(1).get(0); + ns.set_class_name("Cache"); + ns.set_ephemeral_local(()); + }, + false, + ); + result.unwrap(); + assert_eq!( + errors, + [ + "Ephemeral objects (Durable Object namespaces with type 'ephemeralLocal') are an \ + experimental feature which may change or go away in the future. You must run \ + workerd with `--experimental` to use this feature." + ] + ); +} + +#[test] +fn durable_classes_need_storage() { + let (result, errors) = actor_configs_of( + |config| { + let mut service = config.init_services(1).get(0); + service.set_name("main"); + let worker = service.init_worker(); + let mut ns = worker.init_durable_object_namespaces(1).get(0); + ns.set_class_name("Counter"); + ns.set_unique_key("k"); + }, + false, + ); + result.unwrap(); + assert_eq!( + errors, + [ + "Worker service \"main\" implements durable object classes but has \ + `durableObjectStorage` set to `none`." + ] + ); +} + +#[test] +fn duplicate_service_names_are_reported() { + let (result, errors) = actor_configs_of( + |config| { + let mut services = config.init_services(2); + services.reborrow().get(0).set_name("dup"); + services.reborrow().get(0).set_unspecified(()); + services.reborrow().get(1).set_name("dup"); + services.get(1).init_network(); + }, + false, + ); + let configs = result.unwrap(); + assert_eq!(configs.len(), 1); + assert_eq!(errors, ["Config defines multiple services named \"dup\"."]); +} + +#[test] +fn unique_key_modifier_is_unimplemented() { + let (result, _) = actor_configs_of( + |config| { + let mut service = config.init_services(1).get(0); + service.set_name("main"); + service + .init_worker() + .set_durable_object_unique_key_modifier("mod"); + }, + false, + ); + let error = result.err().unwrap(); + assert_eq!( + error.description(), + "durableObjectUniqueKeyModifier is not implemented yet" + ); +} + +struct NullChannel; + +impl Channel for NullChannel { + fn start_request(&self, _metadata: KjOwn) -> Result> { + Err(kj::failed!("null")) + } + + fn token(&self, _usage: TokenUsage) -> Result> { + Err(kj::failed!("null")) + } +} + +fn designator(service: &str, entrypoint: Option<&str>) -> Designator { + Designator { + service: service.to_owned(), + entrypoint: entrypoint.map(str::to_owned), + props_json: None, + error_context: "Worker \"w\"'s binding \"b\"".to_owned(), + } +} + +fn leaf_services() -> LinkedHashMap { + let mut services = LinkedHashMap::new(); + services.insert("net".to_owned(), Service::Leaf(Rc::new(NullChannel))); + services.insert( + "bad".to_owned(), + Service::Leaf(Rc::new(InvalidConfigChannel)), + ); + services +} + +#[test] +fn lookup_of_a_missing_service_is_reported() { + let services = leaf_services(); + let (report, errors) = collecting_reporter(); + let channel = lookup_service(&services, &report, &designator("nope", None)); + assert!(channel.start_request(dummy_metadata()).is_err()); + assert_eq!( + *errors.borrow(), + [ + "Worker \"w\"'s binding \"b\" refers to a service \"nope\", but no such service \ + is defined." + ] + ); + + let (report, errors) = collecting_reporter(); + lookup_actor_class(&services, &report, &designator("nope", None)); + assert_eq!( + *errors.borrow(), + [ + "Worker \"w\"'s binding \"b\" refers to a service \"nope\", but no such service \ + is defined." + ] + ); +} + +#[test] +fn lookup_of_a_leaf_with_an_entrypoint_is_reported() { + let services = leaf_services(); + let (report, errors) = collecting_reporter(); + lookup_service(&services, &report, &designator("net", Some("ep"))); + assert_eq!( + *errors.borrow(), + [ + "Worker \"w\"'s binding \"b\" refers to service \"net\" with a named entrypoint \ + \"ep\", but \"net\" is not a Worker, so does not have any named entrypoints." + ] + ); + + let (report, errors) = collecting_reporter(); + lookup_service(&services, &report, &designator("net", None)); + assert!(errors.borrow().is_empty()); +} + +#[test] +fn lookup_of_a_leaf_as_an_actor_class_is_reported() { + let services = leaf_services(); + let (report, errors) = collecting_reporter(); + lookup_actor_class(&services, &report, &designator("net", Some("C"))); + lookup_actor_class(&services, &report, &designator("net", None)); + assert_eq!( + *errors.borrow(), + [ + "Worker \"w\"'s binding \"b\" refers to service \"net\" with a named Durable \ + Object entrypoint \"C\", but \"net\" is not a Worker, so does not have any named \ + entrypoints.", + "Worker \"w\"'s binding \"b\" refers to service \"net\" as a Durable Object \ + class, but \"net\" is not a Worker, so cannot be used as a class.", + ] + ); +} + +#[test] +fn invalid_services_fail_requests_with_a_js_error() { + let services = leaf_services(); + let (report, errors) = collecting_reporter(); + let channel = lookup_service(&services, &report, &designator("bad", None)); + assert!(errors.borrow().is_empty()); + let error = channel.start_request(dummy_metadata()).err().unwrap(); + assert_eq!( + error.description(), + "jsg.Error: Service cannot handle requests because its config is invalid." + ); +} + +fn dummy_metadata() -> KjOwn { + ffi::new_request_metadata(kj_rs::KjMaybe::None, kj_rs::KjMaybe::None) +} diff --git a/src/workerd/server/server/config.rs b/src/workerd/server/server/config.rs new file mode 100644 index 00000000000..4aea4a5a24f --- /dev/null +++ b/src/workerd/server/server/config.rs @@ -0,0 +1,1466 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! The service graph, built from the config in three passes. +//! +//! The passes: the actor configs of every worker (needed before any worker compiles, so bindings +//! to another worker's classes can be checked), then every service in config order, then the +//! links between them. The `Server` owns the graph for the run and resolves channel tokens and +//! debug-port requests into it. + +use std::cell::Cell; +use std::cell::RefCell; +use std::collections::HashMap; +use std::collections::HashSet; +use std::rc::Rc; +use std::time::Duration; + +use capnp::message::ReaderOptions; +use capnp::serialize::BufferSegments; +use hashlink::LinkedHashMap; +use kj_rs::KjMaybe; +use kj_rs::KjOwn; +use workerd_capnp::config; +use workerd_capnp::service; +use workerd_capnp::service_designator; +use workerd_capnp::worker; +use workerd_capnp::workflows_engine; + +use crate::Result; +use crate::actor::ActorNamespace; +use crate::bindings::ActorConfig; +use crate::bindings::ActorConfigMap; +use crate::bindings::ActorConfigs; +use crate::bindings::Designator; +use crate::bindings::SPECIAL_SUBREQUEST_CHANNEL_COUNT; +use crate::bindings::WorkerLoaderDesignator; +use crate::bindings::Workflow; +use crate::bindings::len_u32; +use crate::bindings::workflow_namespace_key; +use crate::bridge::ffi; +use crate::channels::ActorClass; +use crate::channels::ActorClassChannel; +use crate::channels::ActorIdHandle; +use crate::channels::Channel; +use crate::channels::Frankenvalue; +use crate::channels::NewActor; +use crate::channels::PendingToken; +use crate::channels::RequestMetadata; +use crate::channels::ServerHandle; +use crate::channels::SubrequestChannel; +use crate::channels::TokenUsage; +use crate::channels::WorkerInterface; +use crate::listen::loopback::Loopback; +use crate::loader::WorkerLoaderNamespace; +use crate::services::DiskDirectoryService; +use crate::worker::LinkedChannels; +use crate::worker::WorkerService; + +/// The C++ worker factory: everything the workers of one run share, owned for the run. +pub struct Factory { + inner: KjOwn, + loopback: Loopback, +} + +impl Factory { + #[must_use] + pub fn new(inner: KjOwn) -> Self { + Self { + inner, + loopback: Loopback::default(), + } + } + + /// The `loopback:` namespace of this server's sockets and external services. + #[must_use] + pub fn loopback(&self) -> &Loopback { + &self.loopback + } + + /// The factory, for the bridge functions. + #[must_use] + pub fn raw(&self) -> &ffi::WorkerFactory { + &self.inner + } + + /// The config message; `get_root::()` gives the `Config`. Configs can + /// legitimately be very large and are not malicious: the traversal limit is off. + pub fn config(&self) -> Result>> { + let options = *ReaderOptions::new().traversal_limit_in_words(None); + capnp::serialize::read_message_from_flat_slice( + &mut ffi::factory_config(&self.inner), + options, + ) + .map_err(capnp_error) + } + + #[must_use] + pub fn experimental(&self) -> bool { + ffi::factory_experimental(&self.inner) + } + + /// The reading of the factory's `kj::Timer`, which the server's own delays run on, as the + /// time since the timer's origin. + #[must_use] + pub fn now(&self) -> Duration { + Duration::from_nanos(ffi::factory_timer_now(&self.inner)) + } + + /// Completes `delay` from now on the factory's `kj::Timer`. + pub async fn sleep(&self, delay: Duration) { + let nanos = u64::try_from(delay.as_nanos()).unwrap_or(u64::MAX); + // A timer's delay does not fail. + let _ = ffi::factory_sleep(&self.inner, nanos).await; + } +} + +/// A malformed config message; the CLI checked the message, so this is a bug. +pub fn capnp_error(error: capnp::Error) -> crate::Error { + let capnp::Error { extra, .. } = error; + kj::failed!("{extra}") +} + +/// A text field of the config, owned. +pub fn text(text: capnp::Result>) -> Result { + text.and_then(|text| text.to_string().map_err(capnp::Error::from)) + .map_err(capnp_error) +} + +/// An optional text field of the config: `has` is its `has_*` accessor's answer. +pub fn optional_text( + has: bool, + get: capnp::Result>, +) -> Result> { + if has { text(get).map(Some) } else { Ok(None) } +} + +/// Where config errors and warnings go. +/// +/// Every error is reported; once the services are built the server refuses to serve if there was +/// one, unless the reporter is `tolerant` (`--watch`, where someone is about to fix the config, +/// and the in-process server). +pub struct Reporter { + error: Box, + warning: Box, + tolerant: bool, + had_errors: Cell, +} + +impl Reporter { + #[must_use] + pub fn new(error: Box, warning: Box, tolerant: bool) -> Self { + Self { + error, + warning, + tolerant, + had_errors: Cell::new(false), + } + } + + pub fn error(&self, message: impl Into) { + self.had_errors.set(true); + (self.error)(message.into()); + } + + /// Whether an error was reported that the server must not serve with. + #[must_use] + pub fn refuses(&self) -> bool { + self.had_errors.get() && !self.tolerant + } + + pub fn warning(&self, message: impl Into) { + (self.warning)(message.into()); + } + + /// An error of one service: `service : `. + pub fn service_error(&self, service: &str, message: impl AsRef) { + self.error(format!("service {service}: {}", message.as_ref())); + } + + pub fn service_warning(&self, service: &str, message: impl AsRef) { + self.warning(format!("service {service}: {}", message.as_ref())); + } +} + +/// The CLI's substitutions for config values, by service or socket name. +#[derive(Default)] +pub struct Overrides { + /// `--directory-path`: a disk service's path. + pub directories: HashMap, + /// `--external-addr`: an external service's address. + pub externals: HashMap, +} + +/// One named service of the config. +pub enum Service { + Worker(Rc), + /// An external server or a network: a channel and nothing more. A service whose config was + /// rejected (the error was reported) is an [`InvalidConfigChannel`], which stands in so that + /// the rest of the config can be checked. + Leaf(Rc), + Disk(Rc), +} + +impl Service { + /// The service as a channel: a worker's default entrypoint, or the leaf itself. + #[must_use] + pub fn channel(&self) -> Rc { + match self { + Self::Worker(worker) => worker.default_entrypoint(), + Self::Leaf(channel) => Rc::clone(channel), + Self::Disk(disk) => Rc::clone(disk) as Rc, + } + } + + #[must_use] + pub fn as_worker(&self) -> Option<&Rc> { + match self { + Self::Worker(worker) => Some(worker), + _ => None, + } + } +} + +/// The channel of a service whose config is invalid. workerd refuses to serve a config with +/// errors, so only its `start_request` can be reached (by another service's link stage). +pub struct InvalidConfigChannel; + +impl Channel for InvalidConfigChannel { + fn start_request(&self, metadata: KjOwn) -> Result> { + let _ = metadata; + Err(kj::failed!( + "jsg.Error: Service cannot handle requests because its config is invalid." + )) + } + + fn token(&self, _usage: TokenUsage) -> Result> { + Err(kj::failed!( + "a service with an invalid config has no channel token" + )) + } +} + +/// The actor class of a designator the config got wrong. +pub struct InvalidConfigActorClass; + +impl ActorClass for InvalidConfigActorClass { + fn new_actor(&self, request: NewActor<'_>) -> Result> { + let _ = request; + Err(kj::failed!( + "jsg.Error: Cannot instantiate Durable Object class because its config is invalid." + )) + } + + fn start_request( + &self, + metadata: KjOwn, + actor: &ffi::ActorHandle, + ) -> Result> { + let _ = (metadata, actor); + Err(kj::failed!( + "an actor of an invalid config's class cannot exist" + )) + } + + fn require_allows_transfer(&self) -> Result<()> { + Err(kj::failed!( + "an invalid config's class cannot be transferred" + )) + } + + fn token(&self, _usage: TokenUsage) -> Result> { + Err(kj::failed!( + "an invalid config's class has no channel token" + )) + } +} + +/// The `workerLoader` namespaces: shared by id, or one per binding without an id. +#[derive(Default)] +struct WorkerLoaders { + by_id: HashMap>, + anonymous: Vec>, +} + +/// The services by name, in config order. +/// +/// A worker and its Durable Object namespaces refer to each other, a cycle only +/// `WorkerService::unlink` breaks, so dropping the services unlinks every worker: at teardown, +/// and when the graph is abandoned half built. +#[derive(Default)] +pub struct Services(LinkedHashMap); + +impl Drop for Services { + fn drop(&mut self) { + for worker in self.0.values().filter_map(Service::as_worker) { + worker.unlink(); + } + } +} + +/// The server: the service graph of one run. +pub struct Server { + report: Reporter, + services: Services, + /// Durable namespaces by `uniqueKey`, for actor tokens. + namespaces_by_unique_key: HashMap>, + worker_loaders: RefCell, + /// Declared last, so that it is dropped last: the services (and their isolates) refer to it. + factory: Rc, +} + +impl Server { + #[must_use] + pub fn factory(&self) -> &Rc { + &self.factory + } + + #[must_use] + pub fn report(&self) -> &Reporter { + &self.report + } + + /// What a `ServiceDesignator` resolves to; see [`lookup_service`]. + pub fn lookup(&self, designator: &Designator) -> Rc { + lookup_service(&self.services.0, &self.report, designator) + } + + /// Every service, in config order. + pub fn services(&self) -> impl Iterator { + self.services + .0 + .iter() + .map(|(name, service)| (name.as_str(), service)) + } + + /// Every worker service, in config order. + fn workers(&self) -> impl Iterator)> { + self.services + .0 + .iter() + .filter_map(|(name, service)| Some((name.as_str(), service.as_worker()?))) + } + + /// Aborts every evictable actor. `reason` becomes the error of the actors' in-flight requests. + pub fn abort_all_actors(&self, reason: Option<&crate::Error>) { + for (_, worker) in self.workers() { + for (_, namespace) in worker.namespaces() { + if namespace.is_evictable() { + namespace.abort_all(reason); + } + } + } + } + + /// Aborts every evictable actor and deletes its storage. + pub fn delete_all_actors(&self, reason: Option<&crate::Error>) -> Result<()> { + for (_, worker) in self.workers() { + for (_, namespace) in worker.namespaces() { + if namespace.is_evictable() { + namespace.delete_all(reason)?; + } + } + } + Ok(()) + } +} + +impl Drop for Server { + fn drop(&mut self) { + // Actors may have background work, which aborting them cancels. The run's background + // tasks (tasks.rs) go too: one may hold an actor, and so its class's worker. + self.abort_all_actors(Some(&kj::disconnected!("Server shutting down."))); + ffi::factory_clear_tasks(self.factory.raw()); + // The dynamic workers go before `services` unlinks the config's. + let loaders = std::mem::take(&mut *self.worker_loaders.borrow_mut()); + for loader in loaders.by_id.values().chain(&loaders.anonymous) { + loader.unlink(); + } + } +} + +/// The config's first pass: every worker's `durableObjectNamespaces`, checked. +/// +/// Every service gets an entry, so that a binding to a class of a service that is not a worker +/// is a lookup failure rather than a missing entry. +pub fn collect_actor_configs( + config: config::Reader<'_>, + experimental: bool, + report: &Reporter, +) -> capnp::Result { + // Every `uniqueKey` in the config, which no Workflow's namespace key may collide with. + let mut durable_namespace_keys = HashSet::new(); + for service_conf in config.get_services()? { + if let Ok(service::Which::Worker(worker_conf)) = service_conf.which() { + for ns in worker_conf?.get_durable_object_namespaces()? { + if let Ok(worker::durable_object_namespace::Which::UniqueKey(key)) = ns.which() { + durable_namespace_keys.insert(key?.to_string()?); + } + } + } + } + let mut workflow_namespace_keys = HashSet::new(); + + let mut actor_configs = ActorConfigs::new(); + for (service_index, service_conf) in (0..).zip(config.get_services()?) { + let name = service_conf.get_name()?.to_string()?; + let mut configs = ActorConfigMap::new(); + + if let Ok(service::Which::Worker(worker_conf)) = service_conf.which() { + let worker_conf = worker_conf?; + let mut had_durable = false; + for (namespace_index, ns) in (0..).zip(worker_conf.get_durable_object_namespaces()?) { + let class_name = ns.get_class_name()?.to_string()?; + match ns.which() { + Ok(worker::durable_object_namespace::Which::UniqueKey(unique_key)) => { + had_durable = true; + configs.insert( + class_name, + ActorConfig::Durable { + unique_key: unique_key?.to_string()?, + evictable: !ns.get_prevent_eviction(), + enable_sql: ns.get_enable_sql(), + workflow: None, + container: ns.has_container().then_some(ffi::ContainerRef { + service_index, + namespace_index, + }), + }, + ); + } + Ok(worker::durable_object_namespace::Which::EphemeralLocal(())) => { + if !experimental { + report.error( + "Ephemeral objects (Durable Object namespaces with type \ + 'ephemeralLocal') are an experimental feature which may change \ + or go away in the future. You must run workerd with \ + `--experimental` to use this feature.", + ); + } + configs.insert( + class_name, + ActorConfig::Ephemeral { + evictable: !ns.get_prevent_eviction(), + enable_sql: ns.get_enable_sql(), + }, + ); + } + Err(capnp::NotInSchema(_)) => report.error(format!( + "Encountered unknown DurableObjectNamespace type in service \"{name}\", \ + class \"{class_name}\". Was the config compiled with a newer version of \ + the schema?" + )), + } + } + + if worker_conf.has_workflows_engine() { + collect_workflow_configs( + &name, + worker_conf.get_workflows_engine()?, + &durable_namespace_keys, + &mut workflow_namespace_keys, + &mut configs, + report, + )?; + } + + match worker_conf.get_durable_object_storage().which() { + Ok(worker::durable_object_storage::Which::None(())) if had_durable => { + report.error(format!( + "Worker service \"{name}\" implements durable object classes but has \ + `durableObjectStorage` set to `none`." + )); + } + Ok(_) => {} + Err(capnp::NotInSchema(_)) => report.error(format!( + "Encountered unknown durableObjectStorage type in service \"{name}\". Was the \ + config compiled with a newer version of the schema?" + )), + } + + // Meant for parameterized workers; nothing implements it, so nothing may set it. + if worker_conf.has_durable_object_unique_key_modifier() { + return Err(capnp::Error::failed( + "durableObjectUniqueKeyModifier is not implemented yet".to_owned(), + )); + } + } + + if actor_configs.insert(name.clone(), configs).is_some() { + report.error(format!( + "Config defines multiple services named \"{name}\"." + )); + } + } + Ok(actor_configs) +} + +/// The first pass over one worker's `workflowsEngine`: checks it, and for every valid Workflow +/// synthesizes the `Durable` config of the namespace backing it, keyed by +/// [`workflow_namespace_key`]. What the Workflow names in other services (the engine's class, the +/// `bindingService`) is resolved by [`init_workflows`] once every service exists. +fn collect_workflow_configs( + name: &str, + engine: workflows_engine::Reader<'_>, + durable_namespace_keys: &HashSet, + workflow_namespace_keys: &mut HashSet, + configs: &mut ActorConfigMap, + report: &Reporter, +) -> capnp::Result<()> { + let has_actor_class = + engine.has_actor_class() && !engine.get_actor_class()?.get_name()?.is_empty(); + if !has_actor_class { + report.error(format!( + "Worker service \"{name}\"'s workflowsEngine is missing actorClass." + )); + } + let mut classes = HashSet::new(); + let mut names = HashSet::new(); + for workflow in engine.get_workflows()? { + let class_name = workflow.get_class_name()?.to_string()?; + let workflow_name = workflow.get_name()?.to_string()?; + let mut valid = has_actor_class; + + if class_name.is_empty() { + report.error(format!( + "Worker service \"{name}\" configures a Workflow without className." + )); + valid = false; + } else if !classes.insert(class_name.clone()) { + report.error(format!( + "Worker service \"{name}\" configures multiple Workflows for class \ + \"{class_name}\"." + )); + valid = false; + } + + if workflow_name.is_empty() { + report.error(format!( + "Worker service \"{name}\" configures a Workflow without name." + )); + valid = false; + } else if workflow_name.contains(['/', '\\']) { + report.error(format!( + "Worker service \"{name}\" configures Workflow name \"{workflow_name}\" \ + containing a path separator." + )); + valid = false; + } else if !names.insert(workflow_name.clone()) { + report.error(format!( + "Worker service \"{name}\" configures multiple Workflows named \ + \"{workflow_name}\"." + )); + valid = false; + } + + if !workflow.has_binding_service() || workflow.get_binding_service()?.get_name()?.is_empty() + { + report.error(format!( + "Worker service \"{name}\"'s Workflow \"{workflow_name}\" is missing \ + bindingService." + )); + valid = false; + } + + if !valid { + continue; + } + + let key = workflow_namespace_key(&workflow_name); + if configs.contains_key(&key) { + report.error(format!( + "Worker service \"{name}\"'s Workflow namespace conflicts with Durable Object \ + class \"{key}\"." + )); + continue; + } + if durable_namespace_keys.contains(&key) { + report.error(format!( + "Workflow ActorNamespace key \"{key}\" conflicts with a Durable Object namespace \ + unique key." + )); + continue; + } + if !workflow_namespace_keys.insert(key.clone()) { + report.error(format!( + "Workflow ActorNamespace key \"{key}\" is configured by more than one Worker." + )); + continue; + } + let mut errors = Vec::new(); + let binding_service = Designator::from_reader( + workflow.get_binding_service()?, + format!("Worker \"{name}\"'s Workflow \"{workflow_name}\"'s bindingService"), + &mut errors, + )?; + for error in errors { + report.service_error(name, error); + } + configs.insert( + key.clone(), + ActorConfig::Durable { + unique_key: key, + // Workflow actors must not be evicted mid-run, and their state is SQL-backed. + evictable: false, + enable_sql: true, + workflow: Some(Box::new(Workflow { + name: workflow_name, + class_name, + binding_service, + })), + container: None, + }, + ); + } + Ok(()) +} + +/// The pass after every service exists: for each Workflow the first pass found valid, resolves +/// the engine's actor class and the Workflow's `bindingService`, builds the props that tell the +/// engine which class it runs, and creates the namespace the first pass synthesized as one served +/// by the engine's class (`WorkerService::init_workflow_namespace`). +fn init_workflows( + config: config::Reader<'_>, + services: &LinkedHashMap, + actor_configs: &ActorConfigs, + report: &Reporter, + namespaces_by_unique_key: &mut HashMap>, +) -> Result<()> { + for service_conf in config.get_services().map_err(capnp_error)? { + let Ok(service::Which::Worker(worker_conf)) = service_conf.which() else { + continue; + }; + let worker_conf = worker_conf.map_err(capnp_error)?; + if !worker_conf.has_workflows_engine() { + continue; + } + let name = text(service_conf.get_name())?; + let Some(Service::Worker(app)) = services.get(&name) else { + report.service_error( + &name, + format!("Worker service \"{name}\" could not initialize its workflowsEngine."), + ); + continue; + }; + let conf = worker_conf.get_workflows_engine().map_err(capnp_error)?; + let Some(engine) = WorkflowEngine::resolve(&name, conf, app, services, report)? else { + continue; + }; + + for (key, config) in actor_configs.get(&name).into_iter().flatten() { + let ActorConfig::Durable { + workflow: Some(workflow), + .. + } = config + else { + continue; + }; + if let Some(namespace) = engine.init_workflow(workflow, key, config)? { + namespaces_by_unique_key.insert(key.clone(), namespace); + } + } + } + Ok(()) +} + +/// One worker's `workflowsEngine`, resolved: the app worker whose Workflows it runs, the engine +/// worker and the name of its class. +struct WorkflowEngine<'a> { + /// The app worker's service name, which the errors are reported under. + name: &'a str, + app: &'a Rc, + engine: &'a Rc, + engine_name: String, + engine_class: Option, + services: &'a LinkedHashMap, + report: &'a Reporter, +} + +impl<'a> WorkflowEngine<'a> { + /// Resolves `conf`'s `actorClass` to its worker, or none after reporting why it cannot be + /// (nothing, after the first pass reported the class missing). + fn resolve( + name: &'a str, + conf: workflows_engine::Reader<'_>, + app: &'a Rc, + services: &'a LinkedHashMap, + report: &'a Reporter, + ) -> Result> { + if !conf.has_actor_class() { + return Ok(None); + } + let designator = conf.get_actor_class().map_err(capnp_error)?; + let engine_name = text(designator.get_name())?; + if engine_name.is_empty() { + return Ok(None); + } + if !matches!( + designator.get_props().which(), + Ok(service_designator::props::Which::Empty(())) + ) { + report.service_error( + name, + "workflowsEngine.actorClass must not specify props; Workflow props are supplied \ + by the runtime.", + ); + return Ok(None); + } + let Some(engine_service) = services.get(&engine_name) else { + report.service_error( + name, + format!( + "workflowsEngine.actorClass refers to a service \"{engine_name}\", but no \ + such service is defined." + ), + ); + return Ok(None); + }; + let Service::Worker(engine) = engine_service else { + report.service_error( + name, + format!( + "workflowsEngine.actorClass refers to service \"{engine_name}\", but it is \ + not a Worker." + ), + ); + return Ok(None); + }; + let engine_class = optional_text(designator.has_entrypoint(), designator.get_entrypoint())?; + Ok(Some(Self { + name, + app, + engine, + engine_name, + engine_class, + services, + report, + })) + } + + /// Creates the namespace backing `workflow`, keyed `key`, on the app worker; or none after + /// reporting why it cannot. + fn init_workflow( + &self, + workflow: &Workflow, + key: &str, + config: &ActorConfig, + ) -> Result>> { + let (workflow_name, class_name) = (workflow.name.as_str(), workflow.class_name.as_str()); + if self.app.has_actor_class(key) { + self.report.service_error( + self.name, + format!( + "Workflow \"{workflow_name}\" namespace \"{key}\" conflicts with an exported \ + Durable Object class." + ), + ); + return Ok(None); + } + if !self.app.has_workflow_class(class_name) { + self.report.service_error( + self.name, + format!( + "Workflow \"{workflow_name}\" refers to class \"{class_name}\", but the \ + Worker exports no such WorkflowEntrypoint." + ), + ); + return Ok(None); + } + let Some(disk) = self.binding_storage(workflow_name, &workflow.binding_service) else { + return Ok(None); + }; + + // The one engine class serves every Workflow, so a Workflow's identity travels in its + // actors' props: `workflowClass` is a stub to the app worker's `WorkflowEntrypoint` (the + // user code the engine runs), `workflowClassName` and `workflowName` name it. That is + // why `workflowsEngine.actorClass` may carry no props of its own. + let names = serde_json::json!({ + "workflowClassName": class_name, + "workflowName": workflow_name, + }); + let mut props = ffi::frankenvalue_from_json(&names.to_string()); + ffi::frankenvalue_set_service_stub( + props.as_mut(), + "workflowClass", + SubrequestChannel::new(self.app.loopback_entrypoint(Some(class_name))?), + ); + + let engine_class = self.engine_class.as_deref(); + let Some(actor_class) = self.engine.actor_class(engine_class, Some(props), false) else { + self.report.service_error( + self.name, + format!( + "workflowsEngine.actorClass refers to service \"{}\" with Durable Object \ + entrypoint \"{}\", but no such class is exported.", + self.engine_name, + engine_class.unwrap_or("default") + ), + ); + return Ok(None); + }; + // The namespace's storage is the `bindingService` worker's directory. + let storage_path = match self.services.get(disk) { + Some(Service::Disk(disk)) => disk.writable_path(), + _ => None, + }; + if storage_path.is_none() { + self.report.service_error( + self.name, + format!( + "Workflow ActorNamespace \"{key}\" could not resolve its bindingService's \ + durableObjectStorage.localDisk." + ), + ); + } + // Restore-token persistence follows the engine's compatibility flags, not the app's. + self.app + .init_workflow_namespace( + key, + config.clone(), + actor_class, + storage_path, + self.engine.persistent_self_tokens(), + ) + .map(Some) + } + + /// The `localDisk` storage service of the worker a Workflow's `bindingService` names (which + /// the Workflow's namespace takes), once the worker is checked to export the stateless + /// entrypoint the designator names; or none after reporting why it cannot serve. + fn binding_storage(&self, workflow_name: &str, designator: &Designator) -> Option<&'a str> { + let binding_name = &designator.service; + let Some(binding_service) = self.services.get(binding_name) else { + self.report.service_error( + self.name, + format!( + "Workflow \"{workflow_name}\"'s bindingService refers to a service \ + \"{binding_name}\", but no such service is defined." + ), + ); + return None; + }; + let Service::Worker(binding) = binding_service else { + self.report.service_error( + self.name, + format!( + "Workflow \"{workflow_name}\"'s bindingService refers to service \ + \"{binding_name}\", but it is not a Worker." + ), + ); + return None; + }; + let entrypoint = designator.entrypoint.as_deref(); + if !binding.has_stateless_entrypoint(entrypoint) { + self.report.service_error( + self.name, + format!( + "Workflow \"{workflow_name}\"'s bindingService Worker does not export \ + WorkerEntrypoint \"{}\".", + entrypoint.unwrap_or("default") + ), + ); + return None; + } + let disk = binding.storage(); + if disk.is_none() { + self.report.service_error( + self.name, + format!( + "Workflow \"{workflow_name}\"'s bindingService Worker must configure \ + durableObjectStorage.localDisk; in-memory and absent storage are unsupported." + ), + ); + } + disk + } +} + +/// What a `ServiceDesignator` resolves to, or the invalid-config stand-in after the error was +/// reported. +pub fn lookup_service( + services: &LinkedHashMap, + report: &Reporter, + designator: &Designator, +) -> Rc { + let Designator { + service: target, + entrypoint, + props_json, + error_context: context, + } = designator; + let Some(service) = services.get(target) else { + report.error(format!( + "{context} refers to a service \"{target}\", but no such service is defined." + )); + return Rc::new(InvalidConfigChannel); + }; + let props = configured_props(props_json.as_deref()); + + let Service::Worker(worker) = service else { + if let Some(entrypoint) = entrypoint { + report.error(format!( + "{context} refers to service \"{target}\" with a named entrypoint \ + \"{entrypoint}\", but \"{target}\" is not a Worker, so does not have any \ + named entrypoints." + )); + } else if !ffi::frankenvalue_is_empty(&props) { + report.error(format!( + "{context} refers to service \"{target}\" and provides a `props` value, but \ + \"{target}\" is not a Worker, so cannot accept `props`" + )); + } + return service.channel(); + }; + if let Some(channel) = worker.entrypoint(entrypoint.as_deref(), Some(props), false) { + channel + } else if let Some(entrypoint) = entrypoint { + report.error(format!( + "{context} refers to service \"{target}\" with a named entrypoint \ + \"{entrypoint}\", but \"{target}\" has no such named entrypoint." + )); + Rc::new(InvalidConfigChannel) + } else { + report.error(format!( + "{context} refers to service \"{target}\", but does not specify an \ + entrypoint, and the service does not have a default entrypoint." + )); + Rc::new(InvalidConfigChannel) + } +} + +/// A configured designator's props, empty for `props.empty`: only a `ctx.exports` template goes +/// without props, and a configured binding is never one. +fn configured_props(props_json: Option<&str>) -> KjOwn { + props_json.map_or_else(ffi::frankenvalue_new, ffi::frankenvalue_from_json) +} + +/// What a `ServiceDesignator` naming a Durable Object class resolves to, or the invalid-config +/// stand-in after the error was reported. +pub fn lookup_actor_class( + services: &LinkedHashMap, + report: &Reporter, + designator: &Designator, +) -> Rc { + let Designator { + service: target, + entrypoint, + props_json, + error_context: context, + } = designator; + let Some(service) = services.get(target) else { + report.error(format!( + "{context} refers to a service \"{target}\", but no such service is defined." + )); + return Rc::new(InvalidConfigActorClass); + }; + let props = configured_props(props_json.as_deref()); + + let Service::Worker(worker) = service else { + if let Some(entrypoint) = entrypoint { + report.error(format!( + "{context} refers to service \"{target}\" with a named Durable Object \ + entrypoint \"{entrypoint}\", but \"{target}\" is not a Worker, so does not \ + have any named entrypoints." + )); + } else { + report.error(format!( + "{context} refers to service \"{target}\" as a Durable Object class, but \ + \"{target}\" is not a Worker, so cannot be used as a class." + )); + } + return Rc::new(InvalidConfigActorClass); + }; + if let Some(class) = worker.actor_class(entrypoint.as_deref(), Some(props), false) { + class + } else if let Some(entrypoint) = entrypoint { + report.error(format!( + "{context} refers to service \"{target}\" with a Durable Object entrypoint \ + \"{entrypoint}\", but \"{target}\" has no such exported entrypoint class." + )); + Rc::new(InvalidConfigActorClass) + } else { + report.error(format!( + "{context} refers to service \"{target}\", but does not specify an \ + entrypoint, and the service does export a Durable Object class as its \ + default entrypoint." + )); + Rc::new(InvalidConfigActorClass) + } +} + +impl Server { + /// Builds the service graph: the actor configs, then every service in config order, then + /// the links. Config errors go to `report`; the graph exists either way, so that the CLI can + /// report every error before it refuses to serve. + /// + /// `inbound_listeners` are the sockets bound ahead of the services, by the service they + /// serve, for `Worker::Api::getInboundListeners()`. + pub async fn start( + factory: Rc, + overrides: &Overrides, + report: Reporter, + mut inbound_listeners: HashMap>, + ) -> Result> { + let message = factory.config()?; + let config = message.get_root::().map_err(capnp_error)?; + + // First pass: the actor configs, which bindings to other workers' classes are checked + // against. + let actor_configs = + collect_actor_configs(config, factory.experimental(), &report).map_err(capnp_error)?; + + // Second pass: the services. + let mut services = Services::default(); + let mut namespaces_by_unique_key = HashMap::new(); + let service_confs = config.get_services().map_err(capnp_error)?; + for (index, service_conf) in (0..).zip(service_confs) { + let name = text(service_conf.get_name())?; + let service = make_service( + &factory, + &name, + index, + service_conf, + &actor_configs, + inbound_listeners.remove(&name).unwrap_or_default(), + overrides, + &report, + ) + .await?; + if let Service::Worker(worker) = &service { + for (_, namespace) in worker.namespaces() { + if let Some(unique_key) = namespace.unique_key() { + namespaces_by_unique_key.insert(unique_key.to_owned(), namespace); + } + } + } + // A service of the same name (the first pass reported it) is replaced. + if let Some(Service::Worker(replaced)) = services.0.insert(name, service) { + replaced.unlink(); + } + } + + if !services.0.contains_key("internet") { + let internet = crate::services::make_default_network_service(&factory)?; + services + .0 + .insert("internet".to_owned(), Service::Leaf(internet)); + } + + // Every service exists: wire each worker's Workflows to the engine that runs them. + init_workflows( + config, + &services.0, + &actor_configs, + &report, + &mut namespaces_by_unique_key, + )?; + + let server = Rc::new(Self { + report, + services, + namespaces_by_unique_key, + worker_loaders: RefCell::new(WorkerLoaders::default()), + factory, + }); + let handle = Box::new(ServerHandle(Rc::downgrade(&server))); + ffi::factory_set_server(server.factory.raw(), handle); + + // Third pass: the links. + for (name, worker) in server.workers() { + server.link_worker(name, worker)?; + } + Ok(server) + } + + /// Resolves everything `worker`'s config names into its channel table. + fn link_worker(self: &Rc, name: &str, worker: &Rc) -> Result<()> { + let Some(pending) = worker.take_pending_link() else { + return Err(kj::failed!("service \"{name}\" was already linked")); + }; + let report = &self.report; + let services = &self.services.0; + let lookup = |designator: &Designator| lookup_service(services, report, designator); + let lookup_class = + |designator: &Designator| lookup_actor_class(services, report, designator); + + // Both special channels ("next" and "null") reach the global outbound; the difference is + // a legacy artifact. + let global = lookup(&pending.global_outbound); + let mut subrequest: Vec> = Vec::new(); + for _ in 0..SPECIAL_SUBREQUEST_CHANNEL_COUNT { + subrequest.push(Rc::clone(&global)); + } + subrequest.extend(pending.bindings.subrequest.iter().map(lookup)); + // The loopback channels, in the order `ctx.exports` was numbered with. + for entrypoint in &pending.loopback.subrequest_entrypoints { + subrequest.push(worker.loopback_entrypoint(entrypoint.as_deref())?); + } + // The access binding's entrypoint is a template: its props are each request's identity. + let mut access_binding_channel = None; + if let Some(designator) = &pending.access_binding { + let target = &designator.service; + match services.get(target) { + Some(Service::Worker(target_worker)) => { + access_binding_channel = Some(len_u32(&subrequest)); + subrequest + .push(target_worker.loopback_entrypoint(designator.entrypoint.as_deref())?); + } + Some(_) => report.error(format!( + "Worker accessBindingService refers to service \"{target}\", but it is not a \ + Worker." + )), + None => report.error(format!( + "Worker accessBindingService refers to a service \"{target}\", but no such \ + service is defined." + )), + } + } + + let mut actor_class: Vec> = Vec::new(); + actor_class.extend(pending.bindings.actor_classes.iter().map(lookup_class)); + for class_name in &pending.loopback.actor_classes { + actor_class.push(worker.loopback_actor_class(class_name)?); + } + + // A namespace binding whose service or class does not exist was reported by the + // bindings compiler; its channel stays empty. + let mut actor: Vec>> = Vec::new(); + for designator in &pending.bindings.actors { + let target = match &designator.service { + Some(service) => services.get(service).and_then(Service::as_worker), + None => Some(worker), + }; + actor.push(target.and_then(|target| target.namespace(&designator.class_name))); + } + for class_name in &pending.loopback.actor_namespaces { + actor.push(worker.namespace(class_name)); + } + + let cache = pending.cache_api_outbound.as_ref().map(lookup); + + let storage_path = self.storage_path(name, pending.storage.as_deref()); + + let tails = pending.tails.iter().map(lookup).collect(); + let streaming_tails = pending.streaming_tails.iter().map(lookup).collect(); + + let worker_loaders = pending + .bindings + .worker_loaders + .iter() + .map(|designator| self.worker_loader(designator)) + .collect(); + + worker.link( + LinkedChannels { + first_subrequest: 0, + first_actor_class: 0, + subrequest, + actor, + actor_class, + cache, + tails, + streaming_tails, + worker_loaders, + access_binding_channel, + has_debug_port: pending.bindings.has_debug_port, + server: Rc::downgrade(self), + abort_isolate: None, + }, + storage_path.as_deref(), + ) + } + + /// The directory a worker's Durable Objects persist to: the writable path of its + /// `durableObjectStorage` disk service. `None` for in-memory storage, and after reporting a + /// disk service that is missing, read-only, or not a disk. + fn storage_path(&self, name: &str, disk_name: Option<&str>) -> Option { + let disk_name = disk_name?; + match self.services.0.get(disk_name) { + Some(Service::Disk(disk)) => { + let path = disk.writable_path(); + if path.is_none() { + self.report.service_error( + name, + format!( + "durableObjectStorage config refers to the disk service \ + \"{disk_name}\", but that service is defined read-only." + ), + ); + } + path.map(str::to_owned) + } + Some(_) => { + self.report.service_error( + name, + format!( + "durableObjectStorage config refers to the service \"{disk_name}\", \ + but that service is not a local disk service." + ), + ); + None + } + None => { + self.report.service_error( + name, + format!( + "durableObjectStorage config refers to a service \"{disk_name}\", but \ + no such service is defined." + ), + ); + None + } + } + } + + /// The loader namespace of one `workerLoader` binding: shared with every binding of the same + /// id, or the binding's own when it has none. + fn worker_loader( + self: &Rc, + designator: &WorkerLoaderDesignator, + ) -> Rc { + let mut loaders = self.worker_loaders.borrow_mut(); + if let Some(id) = &designator.id { + return Rc::clone(loaders.by_id.entry(id.clone()).or_insert_with(|| { + WorkerLoaderNamespace::new(Rc::downgrade(self), designator.name.clone()) + })); + } + let loader = WorkerLoaderNamespace::new(Rc::downgrade(self), designator.name.clone()); + loaders.anonymous.push(Rc::clone(&loader)); + loader + } +} + +/// A service whose construction failed is a config error: reported, and replaced by a service +/// that fails every request. +fn invalid_on_error(name: &str, report: &Reporter, service: Result) -> Service { + match service { + Ok(service) => service, + Err(error) => { + report.service_error(name, error.description()); + Service::Leaf(Rc::new(InvalidConfigChannel)) + } + } +} + +/// The second pass for one service: what its config says it serves. +#[expect( + clippy::too_many_arguments, + reason = "everything one service takes from the config pass" +)] +async fn make_service( + factory: &Rc, + name: &str, + index: u32, + conf: service::Reader<'_>, + actor_configs: &ActorConfigs, + inbound_listeners: Vec, + overrides: &Overrides, + report: &Reporter, +) -> Result { + match conf.which() { + Ok(service::Which::Unspecified(())) => { + report.error(format!( + "Service named \"{name}\" does not specify what to serve." + )); + Ok(Service::Leaf(Rc::new(InvalidConfigChannel))) + } + Ok(service::Which::External(external)) => { + let external = external.map_err(capnp_error)?; + let address = overrides.externals.get(name).map(String::as_str); + Ok(invalid_on_error( + name, + report, + crate::services::make_external_service(name, external, address, factory) + .map(Service::Leaf), + )) + } + Ok(service::Which::Network(network)) => { + let network = network.map_err(capnp_error)?; + Ok(invalid_on_error( + name, + report, + crate::services::make_network_service(network, factory).map(Service::Leaf), + )) + } + Ok(service::Which::Worker(worker)) => { + let worker = worker.map_err(capnp_error)?; + WorkerService::new( + Rc::clone(factory), + name, + index, + worker, + actor_configs, + inbound_listeners, + report, + ) + .await + .map(Service::Worker) + } + Ok(service::Which::Disk(disk)) => { + let disk = disk.map_err(capnp_error)?; + let path = overrides.directories.get(name).map(String::as_str); + Ok(invalid_on_error( + name, + report, + crate::services::make_disk_directory_service(name, disk, path, factory) + .map(Service::Disk), + )) + } + Err(capnp::NotInSchema(_)) => { + report.error(format!( + "Service named \"{name}\" has unrecognized type. Was the config compiled with a \ + newer version of the schema?" + )); + Ok(Service::Leaf(Rc::new(InvalidConfigChannel))) + } + } +} + +/// Channel tokens and debug-port requests, resolved into the graph. +impl ServerHandle { + pub(crate) fn resolve_entrypoint( + &self, + service_name: &str, + entrypoint: KjMaybe<&str>, + props: KjOwn, + persistent: bool, + ) -> Result> { + let entrypoint: Option<&str> = entrypoint.into(); + self.server()? + .stub_worker(service_name)? + .entrypoint(entrypoint, Some(props), persistent) + .map(SubrequestChannel::new) + .ok_or_else(|| { + kj::failed!( + "jsg.Error: Stub refers to a an entrypoint of the target service that doesn't \ + exist: {}", + entrypoint.unwrap_or("default") + ) + }) + } + + pub(crate) fn resolve_actor_class( + &self, + service_name: &str, + class_name: KjMaybe<&str>, + props: KjOwn, + persistent: bool, + ) -> Result> { + let class_name: Option<&str> = class_name.into(); + self.server()? + .stub_worker(service_name)? + .actor_class(class_name, Some(props), persistent) + .map(ActorClassChannel::new) + .ok_or_else(|| { + kj::failed!( + "jsg.Error: Stub refers to a an entrypoint of the target service that doesn't \ + exist: {}", + class_name.unwrap_or("default") + ) + }) + } + + pub(crate) fn resolve_actor( + &self, + unique_key: &str, + id: KjOwn, + persistent: bool, + ) -> Result> { + let server = self.server()?; + let namespace = server + .namespaces_by_unique_key + .get(unique_key) + .ok_or_else(|| { + kj::failed!( + "couldn't deserialize actor stub pointing at unknown namespace; namespaceKey = \ + {unique_key}" + ) + })?; + Ok(SubrequestChannel::new(namespace.channel(id, persistent))) + } + + /// The debug port's `getEntrypoint`: any service, a worker's by entrypoint. Its errors name + /// what the request asked for, where a channel token's name the stub. + pub(crate) fn resolve_debug_entrypoint( + &self, + service_name: &str, + entrypoint: KjMaybe<&str>, + props: KjMaybe>, + ) -> Result> { + let entrypoint: Option<&str> = entrypoint.into(); + let props: Option> = props.into(); + let server = self.server()?; + let service = server.debug_service(service_name)?; + let Some(worker) = service.as_worker() else { + if entrypoint.is_some() { + return Err(kj::failed!( + "jsg.Error: Worker does not support named entrypoints" + )); + } + return Ok(SubrequestChannel::new(match props { + Some(props) => service.channel().for_props(props, false)?, + None => service.channel(), + })); + }; + worker + .entrypoint( + entrypoint, + Some(props.unwrap_or_else(ffi::frankenvalue_new)), + false, + ) + .map(SubrequestChannel::new) + .ok_or_else(|| { + kj::failed!( + "jsg.Error: Worker does not export an entrypoint named \"{}\"", + entrypoint.unwrap_or("(default)") + ) + }) + } + + pub(crate) fn resolve_debug_actor( + &self, + service_name: &str, + class_name: &str, + actor_id: &str, + ) -> Result> { + let server = self.server()?; + let service = server.debug_service(service_name)?; + let worker = service + .as_worker() + .ok_or_else(|| kj::failed!("jsg.Error: Worker does not support Durable Objects"))?; + let namespace = worker.namespace(class_name).ok_or_else(|| { + kj::failed!( + "jsg.Error: Worker does not export a Durable Object class named \"{class_name}\"" + ) + })?; + Ok(SubrequestChannel::new(match namespace.unique_key() { + Some(_) => namespace.channel(ffi::actor_id_from_hex(actor_id)?, false), + None => namespace.channel_by_name(actor_id, false), + })) + } +} + +impl Server { + /// The service a debug-port request names. + fn debug_service(&self, service_name: &str) -> Result<&Service> { + self.services + .0 + .get(service_name) + .ok_or_else(|| kj::failed!("jsg.Error: Worker \"{service_name}\" not found")) + } + + /// The worker a channel token names. + fn stub_worker(&self, service_name: &str) -> Result<&Rc> { + let service = self.services.0.get(service_name).ok_or_else(|| { + kj::failed!("jsg.Error: Stub refers to a service that doesn't exist: {service_name}") + })?; + service.as_worker().ok_or_else(|| { + kj::failed!("jsg.Error: Stub refers to a service that is not a Worker: {service_name}") + }) + } +} + +#[cfg(test)] +#[path = "config-test.rs"] +mod tests; diff --git a/src/workerd/server/server/entry.rs b/src/workerd/server/server/entry.rs new file mode 100644 index 00000000000..2fad2e1e232 --- /dev/null +++ b/src/workerd/server/server/entry.rs @@ -0,0 +1,256 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! The `serve` and `test` commands: the command line's Rust half, and the process's schedule. +//! +//! The command line (cli/lib.rs) parses the arguments into [`RunOptions`] and calls +//! [`serve`] / [`test`]. Each creates the event loop -- a `kj_rs_tokio::Runtime`, the KJ loop +//! with tokio running whenever it sleeps -- has the C++ bootstrap +//! (bootstrap.c++) set up the process (logging, perfetto, V8) and build the worker factory on that +//! loop, then blocks on `server_serve` / `server_test`, which run the server +//! (`run.rs`). Every config error is printed, and the server then does not serve; unless +//! `--watch` is on, in which case it serves what it can, waits for the config to change and the +//! process re-executes itself. +//! +//! Exit: status 0 when the run completes, 1 if any error was reported or the run failed (the +//! failure is printed as `*** Uncaught exception ***`). However the run ended, the factory has +//! been dropped by then and, with it, V8 and the perfetto session. Unless `KJ_CLEAN_SHUTDOWN` is +//! set the process ends right there, without running the remaining destructors, like +//! `kj::ProcessContext::exit()`; with it, the Runtime is torn down and `main` returns the status. + +use std::convert::Infallible; +use std::future::Future; +use std::rc::Rc; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; + +use futures::future::Either; +use futures::future::LocalBoxFuture; +use kj_rs::KjOwn; + +use crate::Result; +use crate::bridge::ffi; +use crate::channels::AbortReason; +use crate::config::Factory; +use crate::config::Reporter; +use crate::log::Severity; +use crate::run::RunOptions; + +/// `--watch`: fails if watching does; a config change replaces the process with a fresh run of +/// the same command line instead. +pub type Watch = LocalBoxFuture<'static, Result>; + +/// Whether an error was reported (`error`): the exit status is then 1. +static HAD_ERRORS: AtomicBool = AtomicBool::new(false); + +/// Runs `workerd serve` (see the module doc); the exit status, if the process is to go on. +pub fn serve( + verbose: bool, + config: Vec, + options: ffi::ServeOrTestOptions, + run_options: RunOptions, + watch: Option, +) -> Result { + let command = move |factory, structured| server_serve(factory, run_options, watch, structured); + run(verbose, config, options, None, command) +} + +/// Runs `workerd test` (see the module doc); the exit status, if the process is to go on. +/// `patterns` are the `` and `` globs. +pub fn test( + verbose: bool, + config: Vec, + options: ffi::ServeOrTestOptions, + test: ffi::TestOptions, + run_options: RunOptions, + patterns: (String, String), + watch: Option, +) -> Result { + let command = + move |factory, structured| server_test(factory, run_options, patterns, watch, structured); + run(verbose, config, options, Some(test), command) +} + +/// A command's Rust half, for the C++ frame that sets up the process's logging around it +/// (`with_process_context`), which passes it the config and whether logging is structured. +pub struct PendingCommand(Box, bool) -> Result>); + +#[expect( + clippy::boxed_local, + reason = "the bridge hands an opaque Rust type to C++ and back as a Box" +)] +pub fn run_pending_command( + command: Box, + config: Vec, + structured_logging: bool, +) -> Result { + (command.0)(config, structured_logging) +} + +/// The process's schedule (see the module doc): the logging, the loop, the bootstrap, the +/// command, the exit. +fn run> + 'static>( + verbose: bool, + config: Vec, + options: ffi::ServeOrTestOptions, + test: Option, + command: impl FnOnce(KjOwn, bool) -> Fut + 'static, +) -> Result { + let command = move |config, structured| { + let mut runtime = kj_rs_tokio::Runtime::new()?; + let factory = ffi::bootstrap(runtime.context(), config, &options, test.as_ref().into())?; + // The command owns the factory and drops it when it completes (`shutdown`); if it does not + // complete, `block_on` drops it, and the factory with it, before returning. + let result = runtime.block_on(command(factory, structured)); + if let Err(failure) = result.and_then(|result| result) { + let failure = ffi::exception_text(&AbortReason(Some(failure))); + let message = format!("*** Uncaught exception ***\n{failure}"); + error(structured, &message); + } + let code = i32::from(HAD_ERRORS.load(Ordering::Relaxed)); + if std::env::var_os("KJ_CLEAN_SHUTDOWN").is_none() { + ffi::cli_exit(code); + } + drop(runtime); + Ok(code) + }; + let command = Box::new(PendingCommand(Box::new(command))); + Ok(ffi::with_process_context(verbose, config, command)?) +} + +/// Reports an error -- a config error, failed tests, the run's failure -- and makes the exit +/// status 1. +fn error(structured: bool, message: &str) { + HAD_ERRORS.store(true, Ordering::Relaxed); + report(structured, Severity::Error, message); +} + +/// Writes one of the command line's own messages to stderr: as it is, or, under structured +/// logging, as a line of the JSON logger's format. stderr is where what supervises the process +/// looks for the reason of a failed start; the logger's own lines go to stdout. +fn report(structured: bool, severity: Severity, message: &str) { + if structured { + ffi::json_log_to_stderr(severity as u8, file!(), line!(), message); + } else { + eprintln!("{message}"); + } +} + +/// Where config errors and warnings go (see `report`). With `--watch` the server serves despite +/// the errors, since this is a development server and someone is about to fix the config. +fn reporter(watching: bool, structured: bool) -> Reporter { + Reporter::new( + Box::new(move |message| error(structured, &message)), + Box::new(move |message| report(structured, Severity::Warning, &message)), + watching, + ) +} + +/// Runs `command` alongside the watcher, if any: a config change reloads the process instead. +async fn with_watch( + command: impl Future>, + watch: Option<&mut Watch>, +) -> Result { + let Some(watch) = watch else { + return command.await; + }; + let command = std::pin::pin!(command); + match futures::future::select(command, watch).await { + Either::Left((result, _)) => result, + Either::Right((failed, _)) => failed.map(|never| match never {}), + } +} + +/// Resolves on SIGTERM: the signal to drain gracefully. The handler is installed by the call, so +/// that a signal during startup drains the server once it is up. Never on Windows. +fn sigterm() -> impl Future { + #[cfg(unix)] + let signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()); + async move { + #[cfg(unix)] + match signal { + Ok(mut signal) => { + signal.recv().await; + } + Err(e) => { + tracing::error!("SIGTERM handler: {e}"); + std::future::pending::<()>().await; + } + } + #[cfg(not(unix))] + std::future::pending::<()>().await; + } +} + +/// `workerd serve`: serves `factory`'s config until SIGTERM drains it (or a `--watch` reload +/// replaces the process). A listener failure is the error. +async fn server_serve( + factory: KjOwn, + options: RunOptions, + mut watch: Option, + structured: bool, +) -> Result<()> { + crate::log::install(); + let report = reporter(watch.is_some(), structured); + let factory = Rc::new(Factory::new(factory)); + let result = with_watch( + crate::run::run(Rc::clone(&factory), options, report, sigterm()), + watch.as_mut(), + ) + .await; + let shutdown = shutdown(factory).await; + result?; + shutdown +} + +/// Tears the run down once the server is gone: ends the factory's background tasks, then drops +/// the factory and, with it, the last of the isolates, V8 and the perfetto session. A factory +/// still shared at this point is a leaked service. +pub(crate) async fn shutdown(factory: Rc) -> Result<()> { + factory.settle_tasks().await?; + if Rc::try_unwrap(factory).is_err() { + tracing::error!( + "the worker factory is still referenced after the server was dropped; a service leaked" + ); + } + Ok(()) +} + +/// `workerd test`: runs the tests of `factory`'s config. Under `--watch`, waits for a change +/// afterwards instead of returning. +async fn server_test( + factory: KjOwn, + options: RunOptions, + (service_pattern, entrypoint_pattern): (String, String), + mut watch: Option, + structured: bool, +) -> Result<()> { + crate::log::install(); + let report = reporter(watch.is_some(), structured); + let factory = Rc::new(Factory::new(factory)); + // Loopback sockets are for tests only. + factory.loopback().enable(); + let result = with_watch( + crate::run::test( + Rc::clone(&factory), + options, + report, + &service_pattern, + &entrypoint_pattern, + ), + watch.as_mut(), + ) + .await; + let shutdown = shutdown(factory).await; + let passed = result?; + shutdown?; + if passed == Some(false) { + error(structured, "Tests failed!"); + } + if let Some(watch) = watch { + // Under --watch the process stays for the next change rather than exiting. + watch.await?; + } + Ok(()) +} diff --git a/src/workerd/server/server/in_process.rs b/src/workerd/server/server/in_process.rs new file mode 100644 index 00000000000..16c44610930 --- /dev/null +++ b/src/workerd/server/server/in_process.rs @@ -0,0 +1,122 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! The server run inside a test process, for `server-test.c++`. +//! +//! The test builds the worker factory itself (its V8, timer, network and filesystem) and reaches +//! the server through the `loopback:` registry: the config's addresses are loopback names, the +//! test connects to the server's sockets by name and accepts, by name, the connections the +//! server makes, the `network` services' included (`Loopback::mock_internet`). Config errors and +//! warnings queue for the test to take as they are reported. + +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; + +use kj_rs::KjOwn; +use tokio::sync::Notify; +use tokio::sync::mpsc; + +use crate::Result; +use crate::bridge::ffi; +use crate::config::Factory; +use crate::config::Reporter; +use crate::listen::loopback::LoopbackListener; +use crate::run::RunOptions; + +pub struct InProcessServer { + factory: Rc, + reports: mpsc::UnboundedSender, + reported: RefCell>, + drain: Notify, + /// The names the test accepts connections for. + listeners: RefCell>>, +} + +pub fn new_in_process_server(factory: KjOwn) -> Box { + crate::log::install(); + let factory = Rc::new(Factory::new(factory)); + factory.loopback().enable(); + factory.loopback().mock_internet(); + let (reports, reported) = mpsc::unbounded_channel(); + Box::new(InProcessServer { + factory, + reports, + reported: RefCell::new(reported), + drain: Notify::new(), + listeners: RefCell::default(), + }) +} + +/// Ends the server's background tasks and drops the factory; a factory something still holds +/// is logged as an error. +pub async fn close_in_process_server(server: Box) -> Result<()> { + let InProcessServer { factory, .. } = *server; + crate::entry::shutdown(factory).await +} + +impl InProcessServer { + fn reporter(&self) -> Reporter { + let report = |error| { + let reports = self.reports.clone(); + // The receiver lives as long as the sender: both are the server's. + move |message| drop(reports.send(ffi::ConfigReport { error, message })) + }; + Reporter::new(Box::new(report(true)), Box::new(report(false)), true) + } + + /// The next config error or warning the server reports. + pub async fn next_report(&self) -> ffi::ConfigReport { + // The borrow lasts one poll. The sender is this server's, so the channel never closes. + match std::future::poll_fn(|cx| self.reported.borrow_mut().poll_recv(cx)).await { + Some(report) => report, + None => std::future::pending().await, + } + } + + /// `workerd serve`, until [`Self::drain`]; `debug_port` is `--debug-port`, if not empty. + pub async fn run(&self, debug_port: &str) -> Result<()> { + let options = RunOptions { + debug_port: (!debug_port.is_empty()).then(|| debug_port.to_owned()), + ..RunOptions::default() + }; + let factory = Rc::clone(&self.factory); + crate::run::run(factory, options, self.reporter(), self.drain.notified()).await + } + + /// `workerd test`. + pub async fn test(&self, service_pattern: &str, entrypoint_pattern: &str) -> Result { + let passed = crate::run::test( + Rc::clone(&self.factory), + RunOptions::default(), + self.reporter(), + service_pattern, + entrypoint_pattern, + ) + .await?; + Ok(passed == Some(true)) + } + + pub fn drain(&self) { + self.drain.notify_one(); + } + + /// A connection to the loopback name `name`, queued for whoever listens on it. + pub fn connect(&self, name: &str) -> Result> { + let stream = self.factory.loopback().connect(name)?; + Ok(kj_hyper::into_kj_stream(stream)) + } + + /// The next connection made to the loopback name `name`. + pub async fn accept(&self, name: &str) -> Result> { + let listener = match self.listeners.borrow_mut().entry(name.to_owned()) { + std::collections::hash_map::Entry::Occupied(entry) => Rc::clone(entry.get()), + std::collections::hash_map::Entry::Vacant(entry) => { + let listener = self.factory.loopback().listen(name)?; + Rc::clone(entry.insert(Rc::new(listener))) + } + }; + Ok(kj_hyper::into_kj_stream(listener.accept().await?)) + } +} diff --git a/src/workerd/server/server/lib.rs b/src/workerd/server/server/lib.rs new file mode 100644 index 00000000000..a8dac9f9dc7 --- /dev/null +++ b/src/workerd/server/server/lib.rs @@ -0,0 +1,29 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! workerd's server: the single-tenant Workers runtime. +//! +//! The server reads the config (workerd.capnp), builds the service graph, binds the sockets, and +//! runs the event loop. Everything that touches the isolate is delegated to the C++ worker +//! factory (worker-factory.h) through the bridge in `bridge`; the server owns the rest: +//! config interpretation, bindings and channel numbering, services, listeners, actors, dynamic +//! workers, drain and the test runner. + +pub mod actor; +pub mod bindings; +pub mod bridge; +pub mod channels; +pub mod config; +pub mod entry; +pub mod in_process; +pub mod listen; +pub mod loader; +pub mod log; +pub mod run; +pub mod services; +pub mod tasks; +pub mod worker; + +pub type Error = cxx::KjError; +pub type Result = std::result::Result; diff --git a/src/workerd/server/server/listen/loopback-test.rs b/src/workerd/server/server/listen/loopback-test.rs new file mode 100644 index 00000000000..aaf50b6361f --- /dev/null +++ b/src/workerd/server/server/listen/loopback-test.rs @@ -0,0 +1,85 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +use tokio::io::AsyncReadExt; +use tokio::io::AsyncWriteExt; + +use super::*; + +fn runtime() -> tokio::runtime::Runtime { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() +} + +async fn exchange(mut a: LoopbackStream, mut b: LoopbackStream) { + a.write_all(b"ping").await.unwrap(); + let mut buf = [0; 4]; + b.read_exact(&mut buf).await.unwrap(); + assert_eq!(&buf, b"ping"); + b.write_all(b"pong").await.unwrap(); + a.read_exact(&mut buf).await.unwrap(); + assert_eq!(&buf, b"pong"); +} + +#[test] +fn loopback_is_off_until_enabled() { + let loopback = Loopback::default(); + assert!(!loopback.is_enabled()); + let err = loopback.listen("svc").unwrap_err(); + assert!(err.description().contains("loopback:svc")); + assert!(loopback.connect("svc").is_err()); + loopback.enable(); + assert!(loopback.is_enabled()); + assert!(loopback.listen("svc").is_ok()); +} + +#[test] +fn a_name_is_listened_on_once() { + let loopback = Loopback::default(); + loopback.enable(); + let _listener = loopback.listen("svc").unwrap(); + assert!( + loopback + .listen("svc") + .unwrap_err() + .description() + .contains("already listened on") + ); + assert!(loopback.listen("other").is_ok()); +} + +#[test] +fn a_connection_made_before_listening_waits_in_the_queue() { + let loopback = Loopback::default(); + loopback.enable(); + runtime().block_on(async { + let client = loopback.connect("svc").unwrap(); + let listener = loopback.clone().listen("svc").unwrap(); + let server = listener.accept().await.unwrap(); + exchange(client, server).await; + }); +} + +#[test] +fn a_listener_receives_later_connections() { + let loopback = Loopback::default(); + loopback.enable(); + runtime().block_on(async { + let listener = loopback.listen("svc").unwrap(); + let accepting = listener.accept(); + let client = loopback.connect("svc").unwrap(); + let server = accepting.await.unwrap(); + exchange(server, client).await; + }); +} + +#[test] +fn connecting_fails_once_the_listener_is_gone() { + let loopback = Loopback::default(); + loopback.enable(); + drop(loopback.listen("svc").unwrap()); + assert!(loopback.connect("svc").is_err()); +} diff --git a/src/workerd/server/server/listen/loopback.rs b/src/workerd/server/server/listen/loopback.rs new file mode 100644 index 00000000000..5513c62e486 --- /dev/null +++ b/src/workerd/server/server/listen/loopback.rs @@ -0,0 +1,211 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! `loopback:` addresses: connections serviced within the process, for `workerd test`. +//! +//! A loopback address names a queue. `connect()` makes an in-memory pipe (`tokio::io::duplex`), +//! queues one end and returns the other; the name's listener takes from the queue, so a +//! connection made before anyone listens waits there. Loopback is off by default: the addresses +//! are refused until the registry is enabled, which `workerd test` does -- in production, direct +//! service bindings do the same job with less machinery. +//! +//! Dialers run inside kj-hyper's client, which asks for `Send + Sync`: the registry is behind a +//! mutex, and the dialer of one peer holds a clone of it. + +use std::cell::RefCell; +use std::collections::HashMap; +use std::pin::Pin; +use std::sync::Arc; +use std::sync::Mutex; +use std::sync::PoisonError; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::task::Context; +use std::task::Poll; + +use kj_hyper::Hangup; +use kj_hyper::client::Dialed; +use tokio::io::AsyncRead; +use tokio::io::AsyncWrite; +use tokio::io::DuplexStream; +use tokio::io::ReadBuf; +use tokio::sync::mpsc; +use tokio::sync::watch; + +use crate::Result; + +/// Each direction of a loopback connection buffers this much before the writer waits. +const BUFFER_SIZE: usize = 65536; + +/// One end of a loopback connection. Dropping it resolves the other end's `hangup()`, as +/// dropping an end of kj's in-memory pipe resolves the other's `whenWriteDisconnected()`. +pub struct LoopbackStream { + stream: DuplexStream, + _alive: watch::Sender<()>, + peer: watch::Receiver<()>, +} + +impl LoopbackStream { + fn pair() -> (Self, Self) { + let (a, b) = tokio::io::duplex(BUFFER_SIZE); + let (a_alive, a_gone) = watch::channel(()); + let (b_alive, b_gone) = watch::channel(()); + let end = |stream, alive, peer| Self { + stream, + _alive: alive, + peer, + }; + (end(a, a_alive, b_gone), end(b, b_alive, a_gone)) + } + + /// Resolves once the other end is dropped. + pub fn hangup(&self) -> Hangup { + let mut peer = self.peer.clone(); + Box::pin(async move { + // Fails once the sender, which the other end holds, is gone. + let _ = peer.changed().await; + Ok(()) + }) + } +} + +impl From for Dialed { + fn from(stream: LoopbackStream) -> Self { + let hangup = stream.hangup(); + Self::with_hangup(stream, hangup) + } +} + +impl AsyncRead for LoopbackStream { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + Pin::new(&mut self.stream).poll_read(cx, buf) + } +} + +impl AsyncWrite for LoopbackStream { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Pin::new(&mut self.stream).poll_write(cx, buf) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.stream).poll_flush(cx) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.stream).poll_shutdown(cx) + } +} + +/// The server's loopback namespace: a handle to state shared by its clones. +#[derive(Clone, Default)] +pub struct Loopback { + shared: Arc, +} + +#[derive(Default)] +struct Shared { + enabled: AtomicBool, + internet: AtomicBool, + queues: Mutex>, +} + +/// The connections made to one name and not yet accepted. The receiver is taken by the name's +/// listener. +struct Queue { + sender: mpsc::UnboundedSender, + receiver: Option>, +} + +/// The listening side of one name: there is at most one. +#[derive(Debug)] +pub struct LoopbackListener { + name: String, + receiver: RefCell>, +} + +impl Loopback { + /// Makes `loopback:` addresses usable from now on. Cannot be reversed. + pub fn enable(&self) { + self.shared.enabled.store(true, Ordering::Relaxed); + } + + #[must_use] + pub fn is_enabled(&self) -> bool { + self.shared.enabled.load(Ordering::Relaxed) + } + + /// Makes the `network` services connect within this registry instead: the name is the host, + /// with `:port` unless the port is 80. For the server run inside a test process + /// (`in_process.rs`), whose harness stands in for every host. Cannot be reversed. + pub fn mock_internet(&self) { + self.shared.internet.store(true, Ordering::Relaxed); + } + + #[must_use] + pub fn mocks_internet(&self) -> bool { + self.shared.internet.load(Ordering::Relaxed) + } + + /// `f` on the queue for `name`, created on first use. + fn with_queue(&self, name: &str, f: impl FnOnce(&mut Queue) -> T) -> Result { + if !self.is_enabled() { + return Err(kj::failed!( + "loopback: addresses are only available under `workerd test`: loopback:{name}" + )); + } + let queues = &self.shared.queues; + let mut queues = queues.lock().unwrap_or_else(PoisonError::into_inner); + Ok(f(queues.entry(name.to_owned()).or_insert_with(|| { + let (sender, receiver) = mpsc::unbounded_channel(); + Queue { + sender, + receiver: Some(receiver), + } + }))) + } + + /// The listener for `name`; a name is listened on once. + pub fn listen(&self, name: &str) -> Result { + let receiver = self.with_queue(name, |queue| queue.receiver.take())?; + receiver + .map(|receiver| LoopbackListener { + name: name.to_owned(), + receiver: RefCell::new(receiver), + }) + .ok_or_else(|| kj::failed!("loopback:{name} is already listened on")) + } + + /// A connection to `name`: one end, the other queued for the name's listener. Fails once + /// the listener is gone. + pub fn connect(&self, name: &str) -> Result { + let (ours, theirs) = LoopbackStream::pair(); + self.with_queue(name, |queue| queue.sender.send(theirs))? + .map_err(|_| { + kj::disconnected!("loopback:{name}: the listener has stopped accepting connections") + })?; + Ok(ours) + } +} + +impl LoopbackListener { + /// The next connection, waiting for one if none is queued. + pub async fn accept(&self) -> Result { + // The borrow lasts one poll, never across a wait, so concurrent accepts cannot collide. + std::future::poll_fn(|cx| self.receiver.borrow_mut().poll_recv(cx)) + .await + .ok_or_else(|| kj::disconnected!("loopback:{}: the registry is gone", self.name)) + } +} + +#[cfg(test)] +#[path = "loopback-test.rs"] +mod tests; diff --git a/src/workerd/server/server/listen/mod-test.rs b/src/workerd/server/server/listen/mod-test.rs new file mode 100644 index 00000000000..fea5fb16927 --- /dev/null +++ b/src/workerd/server/server/listen/mod-test.rs @@ -0,0 +1,65 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +use super::*; + +fn runtime() -> tokio::runtime::Runtime { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() +} + +#[test] +fn host_of_address_strips_the_port_only() { + assert_eq!(host_of_address("127.0.0.1:8080"), "127.0.0.1"); + assert_eq!(host_of_address("*:8080"), "*"); + assert_eq!(host_of_address("[::1]:8080"), "[::1]"); + assert_eq!(host_of_address("::1"), "::1"); + assert_eq!(host_of_address("localhost"), "localhost"); + assert_eq!(host_of_address("unix:/tmp/sock"), "unix:/tmp/sock"); +} + +#[test] +fn the_peer_cf_blob_names_the_client() { + runtime().block_on(async { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let client = tokio::net::TcpStream::connect(listener.local_addr().unwrap()) + .await + .unwrap(); + let (stream, peer) = listener.accept().await.unwrap(); + let accepted = Accepted::Socket(Socket::Tcp(stream), Some(peer)); + let blob: serde_json::Value = + serde_json::from_str(&peer_cf_blob(&accepted).unwrap()).unwrap(); + assert_eq!(blob["clientIp"], peer.to_string()); + drop(client); + }); +} + +#[cfg(unix)] +#[test] +fn a_unix_peer_cf_blob_carries_credentials() { + runtime().block_on(async { + let (a, _b) = tokio::net::UnixStream::pair().unwrap(); + let uid = a.peer_cred().unwrap().uid(); + let accepted = Accepted::Socket(Socket::Unix(a), None); + let blob: serde_json::Value = + serde_json::from_str(&peer_cf_blob(&accepted).unwrap()).unwrap(); + assert_eq!(blob["clientUid"], u64::from(uid)); + }); +} + +#[test] +fn a_loopback_socket_listens_in_process_with_no_peer_identity() { + let loopback = Loopback::default(); + loopback.enable(); + runtime().block_on(async { + let listener = listen("loopback:svc", 80, &loopback).await.unwrap(); + assert_eq!(listener.port().unwrap(), 0); + let _client = loopback.connect("svc").unwrap(); + let accepted = listener.accept().await.unwrap(); + assert!(matches!(accepted, Accepted::Loopback(_))); + assert_eq!(peer_cf_blob(&accepted), None); + }); +} diff --git a/src/workerd/server/server/listen/mod.rs b/src/workerd/server/server/listen/mod.rs new file mode 100644 index 00000000000..a4f75209d21 --- /dev/null +++ b/src/workerd/server/server/listen/mod.rs @@ -0,0 +1,661 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! The sockets of the config and the listeners over them: HTTP and HTTPS (hyper, through +//! kj-hyper), TCP and UDP (`connect()` events), and the debug port. +//! +//! Sockets are kj-rs-io's (`TokioAddress`: KJ's address grammar, resolver, socket options and +//! accept retries over tokio) but for a `loopback:name` address, which names a queue of this +//! server's registry (`listen::loopback`). They are bound before the services start so that a +//! worker can learn its own address. A listener is one future: its accept loop and every +//! connection it accepted, so that `run()` can wait for the connections to finish once draining +//! has stopped the accept loop. A failure of the accept loop itself is the listener's failure, +//! and fatal to the server; a failure of one connection is logged and ends that connection only. + +pub mod loopback; +pub mod udp; + +use std::collections::HashMap; +use std::future::Future; +use std::net::SocketAddr; +use std::pin::Pin; +use std::rc::Rc; +use std::sync::Arc; + +use cxx::KjExceptionType; +use futures::FutureExt; +use futures::StreamExt; +use futures::TryFutureExt; +use futures::future::Either; +use futures::stream::FuturesUnordered; +use kj_hyper::Hangup; +use kj_hyper::server::Connect; +use kj_hyper::server::Handler; +use kj_hyper::server::ServerSettings; +use kj_hyper::server::Shutdown; +use kj_rs::KjMaybe; +use kj_rs_io::Socket; +use kj_rs_io::TokioAddress; +use tokio::net::UdpSocket; +use tokio::sync::watch; +use worker::ConnectResponse; +use worker::ConnectSettings; +use worker::CxxWorkerInterface; +use worker::Headers; +use worker::HeadersRef; +use worker::Method; +use worker::Service; +use worker::ServiceResponse; +use workerd_capnp::config; +use workerd_capnp::socket; + +use crate::Result; +use crate::bridge; +use crate::bridge::ffi; +use crate::channels::AbortReason; +use crate::channels::Channel; +use crate::channels::SubrequestChannel; +use crate::config::Factory; +use crate::config::Reporter; +use crate::config::capnp_error; +use crate::config::text; +use crate::listen::loopback::Loopback; +use crate::listen::loopback::LoopbackListener; +use crate::listen::loopback::LoopbackStream; +use crate::services::header_table; +use crate::services::rewriter::HttpRewriter; +use crate::services::send_error; + +// ===================================================================================== +// Addresses + +/// The host of a listen address, without its port, for a worker's inbound listener info and the +/// authority of a TCP socket's `connect()`. Unix addresses are returned whole. +#[must_use] +pub fn host_of_address(address: &str) -> String { + if address.starts_with("unix:") { + return address.to_owned(); + } + if let Some(colon) = address.rfind(':') + && (address.starts_with('[') || address.find(':') == Some(colon)) + { + // A bare IPv6 literal without brackets contains colons but no port. + return address[..colon].to_owned(); + } + address.to_owned() +} + +fn default_port_for(sock: socket::Reader<'_>) -> u16 { + match sock.which() { + Ok(socket::Which::Http(_)) => 80, + Ok(socket::Which::Https(_)) => 443, + _ => 0, + } +} + +// ===================================================================================== +// Sockets + +/// A connection-oriented listening socket: kj-rs-io's over the sockets the address names, or +/// the queue of a `loopback:` name. +pub enum StreamListener { + Socket(Box), + Loopback(LoopbackListener), +} + +/// An accepted connection and, of a TCP one, who it came from. +pub enum Accepted { + Socket(Socket, Option), + Loopback(LoopbackStream), +} + +impl StreamListener { + /// The port of the first socket; 0 for a Unix or loopback socket, as KJ reports it. + pub fn port(&self) -> Result { + match self { + Self::Socket(listener) => Ok(listener.port()?), + Self::Loopback(_) => Ok(0), + } + } + + /// The next connection. Dropping the future accepts nothing. + pub async fn accept(&self) -> Result { + match self { + Self::Socket(listener) => { + let (socket, peer) = listener.accept().await?; + Ok(Accepted::Socket(socket, peer)) + } + Self::Loopback(listener) => Ok(Accepted::Loopback(listener.accept().await?)), + } + } +} + +/// A socket of the config, bound. +pub enum BoundSocket { + Stream(StreamListener), + Datagram(UdpSocket), +} + +/// Listens on `address`; a loopback address takes its name's listener from `loopback`. +pub async fn listen( + address: &str, + default_port: u16, + loopback: &Loopback, +) -> Result { + if let Some(name) = address.strip_prefix("loopback:") { + return loopback.listen(name).map(StreamListener::Loopback); + } + let address = TokioAddress::parse_str(address, default_port).await?; + Ok(StreamListener::Socket(address.listen()?)) +} + +/// Binds the datagram socket `address` names. +async fn bind_udp(address: &str, default_port: u16) -> Result { + let address = TokioAddress::parse_str(address, default_port).await?; + Ok(address.bind_udp()?) +} + +/// A listening socket inherited from the CLI (`--socket-fd`) as a listener; its family decides +/// what it accepts. +fn wrap_inherited(socket: socket2::Socket) -> Result { + socket + .set_nonblocking(true) + .map_err(|e| kj::failed!("--socket-fd: {e}"))?; + Ok(StreamListener::Socket(kj_rs_io::wrap_listener(socket)?)) +} + +// ===================================================================================== +// Binding + +/// The config's sockets, bound. `sockets[i]` is `None` for a socket that could not be bound: the +/// error was reported. +pub struct BoundSockets { + pub sockets: Vec>, + /// The TCP sockets by the service they serve, for `Worker::Api::getInboundListeners()`. + pub inbound: HashMap>, +} + +/// Binds every socket of the config, taking the CLI's overrides for it out of `addresses` and +/// `inherited` (what is left in them matched no socket). Bind failures are config errors. +#[expect( + clippy::implicit_hasher, + reason = "the maps are the command line's, with the default hasher" +)] +pub async fn bind_sockets( + config: config::Reader<'_>, + addresses: &mut HashMap, + inherited: &mut HashMap, + loopback: &Loopback, + report: &Reporter, +) -> Result { + let mut bound = BoundSockets { + sockets: Vec::new(), + inbound: HashMap::new(), + }; + for sock in config.get_sockets().map_err(capnp_error)? { + let name = text(sock.get_name())?; + let is_udp = matches!(sock.which(), Ok(socket::Which::Udp(_))); + let inherited_listener = inherited.remove(&name); + let address = if let Some(address) = addresses.remove(&name) { + address + } else if inherited_listener.is_some() { + String::new() + } else if sock.has_address() { + text(sock.get_address())? + } else { + report.error(format!( + "Socket \"{name}\" has no address in the config, so must be specified on the \ + command line with `--socket-addr`." + )); + bound.sockets.push(None); + continue; + }; + + if is_udp { + if inherited_listener.is_some() { + report.error(format!( + "Socket \"{name}\" is a UDP socket; --socket-fd overrides (which pass a \ + listening connection-oriented socket) are not supported for it." + )); + bound.sockets.push(None); + continue; + } + let socket = match bind_udp(&address, default_port_for(sock)).await { + Ok(socket) => socket, + Err(e) => { + report.error(format!("Socket \"{name}\": {}", e.description())); + bound.sockets.push(None); + continue; + } + }; + bound + .sockets + .push(Some((BoundSocket::Datagram(socket), address))); + continue; + } + + let listener = match inherited_listener { + Some(socket) => wrap_inherited(socket), + None => listen(&address, default_port_for(sock), loopback).await, + }; + let listener = match listener { + Ok(listener) => listener, + Err(e) => { + report.error(format!("Socket \"{name}\": {}", e.description())); + bound.sockets.push(None); + continue; + } + }; + + // A loopback socket is not a listener another process could reach. + let designator = sock.get_service().map_err(capnp_error)?; + if matches!(sock.which(), Ok(socket::Which::Tcp(_))) + && !matches!(listener, StreamListener::Loopback(_)) + && designator.has_name() + { + let service = text(designator.get_name())?; + bound + .inbound + .entry(service) + .or_default() + .push(ffi::InboundListener { + protocol: "tcp".to_owned(), + address: host_of_address(&address), + port: listener.port()?, + }); + } + bound + .sockets + .push(Some((BoundSocket::Stream(listener), address))); + } + Ok(bound) +} + +/// A bound socket's port, for the control channel's `listen` event. +pub fn bound_port(socket: &BoundSocket) -> Result { + match socket { + BoundSocket::Stream(listener) => listener.port(), + BoundSocket::Datagram(socket) => socket + .local_addr() + .map(|addr| addr.port()) + .map_err(|e| kj::failed!("getsockname: {e}")), + } +} + +// ===================================================================================== +// Connections + +/// Any tokio byte stream hyper can serve. +trait Io: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send {} +impl Io for T {} + +/// A connection's transport once TLS (if any) is done: the tokio stream the listener accepted, +/// or the TLS stream over it. +type Transport = Pin>; + +/// The cf blob describing a peer that sent none itself: its address for a network peer, its +/// process credentials for a local one. A loopback peer has no identity to describe. +fn peer_cf_blob(accepted: &Accepted) -> Option { + match accepted { + Accepted::Socket(_, Some(peer)) => { + Some(serde_json::json!({ "clientIp": peer.to_string() }).to_string()) + } + #[cfg(unix)] + Accepted::Socket(Socket::Unix(stream), None) => { + let mut blob = serde_json::Map::new(); + if let Ok(cred) = stream.peer_cred() { + if let Some(pid) = cred.pid() { + blob.insert("clientPid".to_owned(), pid.into()); + } + blob.insert("clientUid".to_owned(), cred.uid().into()); + } + Some(serde_json::Value::Object(blob).to_string()) + } + Accepted::Socket(..) | Accepted::Loopback(_) => None, + } +} + +/// Wraps the connection in TLS when the socket has it, and boxes the transport. Draining drops +/// a connection that is still shaking hands, as it drops one not yet accepted. +async fn transport( + accepted: Accepted, + tls: Option<&Arc>, + context: &ListenContext, +) -> Result { + let io: Transport = match accepted { + Accepted::Socket(Socket::Tcp(stream), _) => Box::pin(stream), + #[cfg(unix)] + Accepted::Socket(Socket::Unix(stream), _) => Box::pin(stream), + Accepted::Loopback(stream) => Box::pin(stream), + }; + let Some(config) = tls else { + return Ok(io); + }; + let handshake = std::pin::pin!(kj_hyper::tls::accept(io, Arc::clone(config))); + match futures::future::select(handshake, std::pin::pin!(context.drained())).await { + Either::Left((stream, _)) => Ok(Box::pin(stream?)), + Either::Right(_) => Err(kj::disconnected!("the server is draining")), + } +} + +/// Resolves once the peer of `accepted` is gone, as `whenWriteDisconnected()` of kj's own stream +/// over it would: a socket hung up or failed (not a peer that shut down its side), a loopback +/// connection's other end dropped. A socket's costs a descriptor for as long as the +/// connection is served (kj-rs-io/stream.rs, "whenWriteDisconnected costs a descriptor"). +fn hangup(accepted: &Accepted) -> Hangup { + match accepted { + Accepted::Socket(Socket::Tcp(stream), _) => { + kj_rs_io::when_write_disconnected(stream).err_into().boxed() + } + #[cfg(unix)] + Accepted::Socket(Socket::Unix(stream), _) => { + kj_rs_io::when_write_disconnected(stream).err_into().boxed() + } + Accepted::Loopback(stream) => stream.hangup(), + } +} + +/// Logs a connection's failure the way workerd's `handleApplicationError` does: a peer that went +/// away is not news. +fn log_connection_error(what: &str, error: &crate::Error) { + if error.exception_type() != KjExceptionType::Disconnected { + tracing::error!("{what}: {}", error.description()); + } +} + +/// Logs a failed HTTP call the way workerd's `handleApplicationError` does, the error as KJ +/// prints an exception; the client gets kj-hyper's bare 500. +fn log_uncaught(error: &crate::Error) { + if error.exception_type() != KjExceptionType::Disconnected { + let error = ffi::exception_text(&AbortReason(Some(error.clone()))); + tracing::error!("Uncaught exception: {error}"); + } +} + +/// What every listener shares: the factory (for the header table and the RPC bootstrap), and +/// the drain signal. `draining` flips once, when the server stops accepting connections. +pub struct ListenContext { + pub factory: Rc, + pub draining: watch::Receiver, +} + +impl ListenContext { + /// Resolves once draining begins. + async fn drained(&self) { + let mut draining = self.draining.clone(); + let _ = draining.wait_for(|draining| *draining).await; + } +} + +/// Runs a listener: accepts until draining begins, then waits for the connections it accepted. +/// `accept` yields one connection per call and accepts nothing when dropped unfinished; its +/// failure is the listener's. A connection's failure is logged as `what`. +async fn accept_loop( + context: &ListenContext, + what: &str, + mut accept: impl FnMut() -> A, + on_drain: impl FnOnce(), +) -> Result<()> +where + A: Future>, + C: Future> + 'static, +{ + let log = |result: Result<()>| { + if let Err(e) = result { + log_connection_error(what, &e); + } + }; + let mut connections = FuturesUnordered::new(); + let mut drained = std::pin::pin!(context.drained().fuse()); + loop { + let mut accepting = std::pin::pin!(accept().fuse()); + futures::select_biased! { + () = drained => break, + result = connections.select_next_some() => log(result), + connection = accepting => connections.push(connection?.boxed_local()), + } + } + on_drain(); + // Every connection finishes on its own once told to shut down. + while let Some(result) = connections.next().await { + log(result); + } + Ok(()) +} + +// ===================================================================================== +// HTTP + +/// One socket's HTTP settings, shared by its connections. +pub struct HttpSocket { + pub channel: Rc, + pub rewriter: Rc, + pub physical_protocol: &'static str, + pub tls: Option>, +} + +/// Serves HTTP on `listener` until the server drains. +pub async fn listen_http( + context: Rc, + listener: StreamListener, + socket: Rc, +) -> Result<()> { + // The application negotiates WebSocket compression itself. + let settings = Rc::new(ServerSettings { + websocket_errors: Some(ffi::new_jsgify_websocket_errors()), + websocket_compression: kj_hyper::WebSocketCompression::MANUAL, + ..ServerSettings::default() + }); + let shutdown = Rc::new(Shutdown::new()); + let serve = |accepted: Accepted| { + let context = Rc::clone(&context); + let socket = Rc::clone(&socket); + let settings = Rc::clone(&settings); + let shutdown = Rc::clone(&shutdown); + async move { + let cf_blob = (!socket.rewriter.has_cf_blob_header()) + .then(|| peer_cf_blob(&accepted)) + .flatten(); + let hangup = hangup(&accepted); + let io = transport(accepted, socket.tls.as_ref(), &context).await?; + let handler: Rc = Rc::new(HttpConnection { + context: Rc::clone(&context), + socket, + cf_blob, + }); + let table = header_table(&context.factory); + kj_hyper::server::serve_connection(io, hangup, table, settings, handler, &shutdown) + .await + } + }; + accept_loop( + &context, + "HTTP connection failed", + || listener.accept().map_ok(&serve), + || shutdown.shutdown(), + ) + .await +} + +/// One HTTP connection's handler: the socket's service, with the socket's rewriting and the +/// peer's cf blob applied to each request. +struct HttpConnection { + context: Rc, + socket: Rc, + /// Built from the peer identity when the socket's options name no cf blob header. + cf_blob: Option, +} + +impl HttpConnection { + fn start_request(&self, cf_blob: Option<&str>) -> Result { + let metadata = ffi::new_request_metadata(cf_blob.into(), KjMaybe::None); + Ok(CxxWorkerInterface::new( + self.socket.channel.start_request(metadata)?, + )) + } +} + +#[async_trait::async_trait(?Send)] +impl Handler for HttpConnection { + async fn request<'a>( + &'a self, + method: Method, + url: &'a [u8], + headers: HeadersRef<'a>, + body: Pin<&'a mut worker::AsyncInputStream>, + response: ServiceResponse<'a>, + ) -> Result<()> { + async { + let table = header_table(&self.context.factory); + let rewriter = &self.socket.rewriter; + let url = + std::str::from_utf8(url).map_err(|_| kj::failed!("request URL is not UTF-8"))?; + let Some((rewritten, cf_blob)) = rewriter.rewrite_incoming_request( + table, + url, + self.socket.physical_protocol, + headers, + )? + else { + return send_error(response, 400, "Bad Request", &Headers::new(table)).await; + }; + let cf_blob = cf_blob.or_else(|| self.cf_blob.clone()); + let mut worker = self.start_request(cf_blob.as_deref())?; + let headers = rewritten + .headers + .as_deref() + .map_or(headers, HeadersRef::from); + let mut response = + bridge::rewriting_response(response.into_ffi(), table, rewriter.response_edits())?; + worker + .request( + method, + rewritten.url.as_bytes(), + headers, + body, + ServiceResponse::from(response.as_mut()), + ) + .await + } + .await + .inspect_err(log_uncaught) + } + + async fn connect<'a>( + &'a self, + host: &'a [u8], + headers: HeadersRef<'a>, + connect: Connect, + ) -> Result<()> { + async { + let table = header_table(&self.context.factory); + if let Some(capnp_host) = self.socket.rewriter.capnp_connect_host() + && capnp_host.as_bytes() == host + { + // The client is opening a capnp session. + let empty = Headers::new(table); + let tunnel = connect.accept(200, "OK", HeadersRef::from(&empty))?; + let stream = tunnel.into_kj(); + let target = SubrequestChannel::new(Rc::clone(&self.socket.channel)); + let factory = self.context.factory.raw(); + return Ok(ffi::factory_accept_bootstrap(factory, stream, target).await?); + } + let mut worker = self.start_request(self.cf_blob.as_deref())?; + let (mut tunnel, mut response) = connect.into_kj(); + worker + .connect( + host, + headers, + tunnel.as_mut(), + ConnectResponse::from(response.as_mut()), + ConnectSettings { + use_tls: false, + tls_starter: KjMaybe::None, + }, + ) + .await + } + .await + .inspect_err(log_uncaught) + } +} + +// ===================================================================================== +// TCP + +/// Serves raw TCP on `listener`: each connection is a `connect()` event on the socket's service, +/// addressed to `authority` (the socket as bound), until the server drains. +pub async fn listen_tcp( + context: Rc, + listener: StreamListener, + channel: Rc, + tls: Option>, + authority: String, +) -> Result<()> { + let authority = Rc::new(authority); + let serve = |accepted: Accepted| { + let context = Rc::clone(&context); + let channel = Rc::clone(&channel); + let tls = tls.clone(); + let authority = Rc::clone(&authority); + async move { + let peer = match &accepted { + Accepted::Socket(_, peer) => peer.map(|peer| peer.to_string()), + Accepted::Loopback(_) => None, + }; + let metadata = ffi::new_request_metadata(KjMaybe::None, peer.as_deref().into()); + let mut worker = CxxWorkerInterface::new(channel.start_request(metadata)?); + let hangup = hangup(&accepted); + let io = transport(accepted, tls.as_ref(), &context).await?; + let mut stream = kj_hyper::into_kj_stream_with(io, Some(hangup.shared())); + let mut response = ffi::new_null_connect_response(); + let headers = Headers::new(header_table(&context.factory)); + worker + .connect( + authority.as_bytes(), + HeadersRef::from(&headers), + stream.as_mut(), + ConnectResponse::from(response.as_mut()), + ConnectSettings { + use_tls: false, + tls_starter: KjMaybe::None, + }, + ) + .await + } + }; + accept_loop( + &context, + "TCP connect() handler threw", + || listener.accept().map_ok(&serve), + || {}, + ) + .await +} + +// ===================================================================================== +// Debug port + +/// Serves the workerd debug port on `listener`; the factory resolves its requests through the +/// server. A connection lasts for as long as its client keeps it, so draining drops them all. +pub async fn listen_debug_port(context: Rc, listener: StreamListener) -> Result<()> { + let serve = |accepted: Accepted| { + let context = Rc::clone(&context); + async move { + let stream = kj_hyper::into_kj_stream(transport(accepted, None, &context).await?); + Ok(ffi::factory_accept_debug_port(context.factory.raw(), stream).await?) + } + }; + let what = "debug port connection failed"; + let serving = accept_loop(&context, what, || listener.accept().map_ok(&serve), || {}); + let drained = std::pin::pin!(context.drained()); + match futures::future::select(std::pin::pin!(serving), drained).await { + Either::Left((result, _)) => result, + Either::Right(_) => Ok(()), + } +} + +#[cfg(test)] +#[path = "mod-test.rs"] +mod tests; diff --git a/src/workerd/server/server/listen/udp-test.rs b/src/workerd/server/server/listen/udp-test.rs new file mode 100644 index 00000000000..30a24bb80d6 --- /dev/null +++ b/src/workerd/server/server/listen/udp-test.rs @@ -0,0 +1,37 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +use super::*; + +#[test] +fn a_datagram_that_fills_the_buffer_is_dropped_as_truncated() { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + runtime.block_on(async { + let receiver = UdpSocket::bind("127.0.0.1:0").await.unwrap(); + let sender = UdpSocket::bind("127.0.0.1:0").await.unwrap(); + let to = receiver.local_addr().unwrap(); + sender.send_to(b"truncated", to).await.unwrap(); + sender.send_to(b"full", to).await.unwrap(); + sender.send_to(b"fit", to).await.unwrap(); + + let mut buffer = [0u8; 4]; + assert_eq!( + receive_datagram(&receiver, &mut buffer).await.unwrap(), + None + ); + assert_eq!( + receive_datagram(&receiver, &mut buffer).await.unwrap(), + None + ); + let (len, peer) = receive_datagram(&receiver, &mut buffer) + .await + .unwrap() + .unwrap(); + assert_eq!(&buffer[..len], b"fit"); + assert_eq!(peer, sender.local_addr().unwrap()); + }); +} diff --git a/src/workerd/server/server/listen/udp.rs b/src/workerd/server/server/listen/udp.rs new file mode 100644 index 00000000000..6d1ffeeed29 --- /dev/null +++ b/src/workerd/server/server/listen/udp.rs @@ -0,0 +1,243 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! UDP sockets: the listener that serves each peer's datagrams as one flow, a `connect()` event +//! whose `workerd::DatagramChannel` is a [`UdpFlow`]. + +use std::cell::Cell; +use std::cell::RefCell; +use std::collections::HashMap; +use std::net::SocketAddr; +use std::rc::Rc; +use std::time::Duration; + +use futures::future::Either; +use kj_rs::KjMaybe; +use tokio::net::UdpSocket; +use worker::CxxWorkerInterface; +use worker::Interface; + +use super::ListenContext; +use super::accept_loop; +use crate::Result; +use crate::bridge::ffi; +use crate::channels::Channel; +use crate::config::Factory; + +/// The largest datagram the listener serves: comfortably above the largest UDP payload a peer +/// could ever send (65507 bytes plus headers). A larger one is truncated by the kernel with no way +/// to recover the tail, so it is dropped. +const MAX_DATAGRAM_SIZE: usize = 65535; + +/// Receives one datagram into `buffer`, one byte longer than the largest datagram served: none for +/// a datagram that filled it, which the kernel cut short (Windows reports that as `WSAEMSGSIZE`). +async fn receive_datagram( + socket: &UdpSocket, + buffer: &mut [u8], +) -> std::io::Result> { + match socket.recv_from(buffer).await { + Ok((len, _)) if len == buffer.len() => Ok(None), + Ok(received) => Ok(Some(received)), + #[cfg(windows)] + Err(e) + if e.raw_os_error() == Some(windows_sys::Win32::Networking::WinSock::WSAEMSGSIZE) => + { + Ok(None) + } + Err(e) => Err(e), + } +} + +/// The listener's side of a flow: where its datagrams go. +struct FlowInbox { + id: u64, + sender: tokio::sync::mpsc::UnboundedSender>, + pending_bytes: Rc>, + last_seen: Rc>, +} + +/// The flows in progress, by peer, for routing a later datagram to the flow already dispatched +/// for it. +type Flows = Rc>>; + +/// What a queued datagram counts against the flow's `maxPendingBytes`: its payload plus the +/// handle that holds it, so that a burst of tiny datagrams cannot queue without bound. +fn queued_size(datagram: &[u8]) -> usize { + datagram.len() + std::mem::size_of::>() +} + +/// One UDP flow: every datagram to and from one peer, until none has arrived for the idle +/// timeout. The `workerd::DatagramChannel` of the `connect()` event dispatched for the flow. +pub struct UdpFlow { + id: u64, + peer: SocketAddr, + socket: Rc, + receiver: RefCell>>, + pending_bytes: Rc>, + /// When the peer's last datagram arrived, on the factory's timer. + last_seen: Rc>, + idle_timeout: Duration, + factory: Rc, + ended: Cell, + flows: Flows, +} + +impl UdpFlow { + /// The next datagram, or the end of the flow once it has been idle for the timeout. + #[expect( + clippy::await_holding_refcell_ref, + reason = "the borrow is the one-call-at-a-time contract of DatagramChannel::receive()" + )] + pub async fn receive(&self) -> Result { + let ended = || ffi::UdpDatagram { + ended: true, + data: Vec::new(), + }; + if self.ended.get() { + return Ok(ended()); + } + let mut receiver = self + .receiver + .try_borrow_mut() + .map_err(|_| kj::failed!("DatagramChannel::receive() already has a pending call"))?; + loop { + let deadline = self.last_seen.get() + self.idle_timeout; + let idle = deadline.saturating_sub(self.factory.now()); + let idle = std::pin::pin!(self.factory.sleep(idle)); + match futures::future::select(std::pin::pin!(receiver.recv()), idle).await { + Either::Left((Some(data), _)) => { + self.pending_bytes + .set(self.pending_bytes.get().saturating_sub(queued_size(&data))); + return Ok(ffi::UdpDatagram { ended: false, data }); + } + // A datagram that arrived meanwhile moved the deadline. + Either::Right(_) + if self.factory.now() < self.last_seen.get() + self.idle_timeout => {} + // The listener is gone, or the flow idled out. + Either::Left((None, _)) | Either::Right(_) => break, + } + } + self.ended.set(true); + self.unregister(); + Ok(ended()) + } + + pub async fn send(&self, datagram: &[u8]) -> Result<()> { + self.socket + .send_to(datagram, self.peer) + .await + .map(drop) + .map_err(|e| kj::failed!("UDP send failed: {e}")) + } + + /// Stops routing the peer's datagrams here (unless a newer flow for the peer took over). + fn unregister(&self) { + let mut flows = self.flows.borrow_mut(); + if flows + .get(&self.peer) + .is_some_and(|inbox| inbox.id == self.id) + { + flows.remove(&self.peer); + } + } +} + +impl Drop for UdpFlow { + fn drop(&mut self) { + self.unregister(); + } +} + +/// Serves UDP on `socket`, each peer's datagrams as one flow. +/// +/// A new peer's first datagram starts a flow, dispatched as one `connect()` event on the +/// service; later datagrams from the peer join it until it has been idle for `idle_timeout`. A +/// flow with `max_pending_bytes` queued drops what arrives next rather than holding up other +/// peers. +pub async fn listen_udp( + context: Rc, + socket: UdpSocket, + channel: Rc, + address: String, + idle_timeout: Duration, + max_pending_bytes: usize, +) -> Result<()> { + let socket = Rc::new(socket); + let flows: Flows = Rc::default(); + let next_id = Cell::new(0u64); + let address = Rc::new(address); + let accept = || { + let socket = Rc::clone(&socket); + let flows = Rc::clone(&flows); + let channel = Rc::clone(&channel); + let address = Rc::clone(&address); + let next_id = &next_id; + let factory = &context.factory; + async move { + // One receive buffer per new flow; datagrams of known flows reuse it in the loop. + let mut buffer = vec![0u8; MAX_DATAGRAM_SIZE + 1]; + loop { + let Some((len, peer)) = receive_datagram(&socket, &mut buffer) + .await + .map_err(|e| kj::failed!("UDP receive failed: {e}"))? + else { + continue; + }; + let data = buffer[..len].to_vec(); + if let Some(inbox) = flows.borrow().get(&peer) { + inbox.last_seen.set(factory.now()); + let queued = queued_size(&data); + // An empty queue takes any datagram, so one always reaches a waiting + // `receive()` however small `max_pending_bytes` is. + let pending = inbox.pending_bytes.get(); + if (pending == 0 || pending + queued <= max_pending_bytes) + && inbox.sender.send(data).is_ok() + { + inbox.pending_bytes.set(pending + queued); + } + continue; + } + // A new peer: a new flow, dispatched as one connect() event. + let id = next_id.get(); + next_id.set(id + 1); + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + let pending_bytes = Rc::new(Cell::new(queued_size(&data))); + let last_seen = Rc::new(Cell::new(factory.now())); + let _ = sender.send(data); + flows.borrow_mut().insert( + peer, + FlowInbox { + id, + sender, + pending_bytes: Rc::clone(&pending_bytes), + last_seen: Rc::clone(&last_seen), + }, + ); + let flow = Box::new(UdpFlow { + id, + peer, + socket: Rc::clone(&socket), + receiver: RefCell::new(receiver), + pending_bytes, + last_seen, + idle_timeout, + factory: Rc::clone(factory), + ended: Cell::new(false), + flows: Rc::clone(&flows), + }); + return Ok(async move { + let metadata = ffi::new_request_metadata(KjMaybe::None, KjMaybe::None); + let mut worker = CxxWorkerInterface::new(channel.start_request(metadata)?); + let event = ffi::new_udp_connect_event(&address, flow); + worker.custom_event(event).await.map(drop) + }); + } + } + }; + accept_loop(&context, "UDP connect() handler threw", accept, || {}).await +} + +#[cfg(test)] +#[path = "udp-test.rs"] +mod tests; diff --git a/src/workerd/server/server/loader.rs b/src/workerd/server/server/loader.rs new file mode 100644 index 00000000000..0fa3f3310c7 --- /dev/null +++ b/src/workerd/server/server/loader.rs @@ -0,0 +1,338 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! Dynamic workers: the `workerLoader` binding's namespaces, each a set of workers loaded at +//! runtime from code a worker supplies. +//! +//! A loaded worker is a [`WorkerStub`]. Its startup (fetching the source, compiling, linking) +//! runs as one shared future that begins as soon as the worker is loaded; the entrypoints and +//! classes handed out before it resolves wait on it, and a startup failure is the error every one +//! of them fails with. + +use std::cell::Cell; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; +use std::rc::Weak; + +use futures::FutureExt; +use futures::future::LocalBoxFuture; +use futures::future::Shared; +use kj_rs::KjMaybe; +use kj_rs::KjOwn; +use worker::Interface; + +use crate::Result; +use crate::bridge::ffi; +use crate::channels::ActorClass; +use crate::channels::ActorClassChannel; +use crate::channels::Channel; +use crate::channels::DynamicSource; +use crate::channels::Frankenvalue; +use crate::channels::NewActor; +use crate::channels::PendingToken; +use crate::channels::RequestMetadata; +use crate::channels::SubrequestChannel; +use crate::channels::TokenUsage; +use crate::channels::WorkerInterface; +use crate::channels::attach; +use crate::config::Factory; +use crate::config::Server; +use crate::worker::WorkerService; + +/// One `workerLoader` namespace. Bindings with the same `id` share a namespace, so a worker +/// loaded under a name by one binding is the one another finds under that name. +pub struct WorkerLoaderNamespace { + server: Weak, + name: String, + /// The named workers. A worker that aborts its isolate leaves the map, so the next load under + /// its name compiles afresh. Unnamed workers are owned by their JS handles alone. + isolates: RefCell>>, +} + +impl WorkerLoaderNamespace { + /// `name` is the binding's name, for error logging. + #[must_use] + pub fn new(server: Weak, name: String) -> Rc { + Rc::new(Self { + server, + name, + isolates: RefCell::new(HashMap::new()), + }) + } + + /// Loads (or, under a `name` already loaded, finds) a dynamic worker. + pub fn load( + self: &Rc, + name: Option<&str>, + source: KjOwn, + ) -> Result> { + let server = self + .server + .upgrade() + .ok_or_else(|| kj::failed!("the server is shutting down"))?; + let factory = server.factory(); + // An isolate's name is never keyed on nor shown to the app; it is for error logs. + let stub = match name { + Some(name) => { + if let Some(stub) = self.isolates.borrow().get(name) { + return Ok(Rc::clone(stub)); + } + let namespace = Rc::downgrade(self); + let key = name.to_owned(); + let on_abort: Box = Box::new(move || { + if let Some(namespace) = namespace.upgrade() { + namespace.isolates.borrow_mut().remove(&key); + } + }); + let stub = WorkerStub::new( + Rc::clone(factory), + Weak::clone(&self.server), + format!("{}:{name}", self.name), + source, + Some(on_abort), + ); + self.isolates + .borrow_mut() + .insert(name.to_owned(), Rc::clone(&stub)); + stub + } + None => WorkerStub::new( + Rc::clone(factory), + Weak::clone(&self.server), + format!("{}:dynamic:{:032x}", self.name, rand::random::()), + source, + None, + ), + }; + + // Startup runs whether or not anything calls the worker, and the task holds the stub + // until it is done: an unnamed stub's only other owner is its JS handle, which GC may drop + // while the source is still being fetched (the fetch re-enters the loading worker), and + // an aborted named stub has left the map. A stub dropped before its service exists would + // leave that service never unlinked. + let keep = Rc::clone(&stub); + factory.spawn_detached(async move { + // A startup failure reaches the callers that await the stub. + let _ = keep.startup().await; + }); + Ok(stub) + } + + /// Drops every loaded worker's links, as the server does its own at teardown. + pub fn unlink(&self) { + for stub in self.isolates.borrow().values() { + stub.unlink(); + } + } +} + +/// A loaded dynamic worker. The runtime's `WorkerStubChannel`. +pub struct WorkerStub { + /// Resolves with the started worker, or the startup error. Only clones are polled, so the + /// result stays readable here (`Shared::peek`). + startup: Shared>>>, + unlinked: Cell, +} + +impl WorkerStub { + /// `on_abort` runs (once) when the worker aborts its isolate; a named stub uses it to leave + /// its namespace. The stub's startup compiles the worker from its source (which the factory + /// fetches) and links it. + fn new( + factory: Rc, + server: Weak, + isolate_name: String, + source: KjOwn, + on_abort: Option>, + ) -> Rc { + let fired = Cell::new(false); + let abort_isolate: Box = Box::new(move || { + if fired.replace(true) { + return; + } + if let Some(on_abort) = &on_abort { + on_abort(); + } + }); + let startup = + WorkerService::new_dynamic(factory, isolate_name, source, server, abort_isolate) + .boxed_local() + .shared(); + Rc::new(Self { + startup, + unlinked: Cell::new(false), + }) + } + + /// Resolves once the worker has started; fails with the startup error. + pub fn startup(&self) -> Shared>>> { + self.startup.clone() + } + + /// The started worker, once startup has resolved. + fn service(&self) -> Result> { + match self.startup.peek() { + Some(started) => started.clone(), + None => Err(kj::failed!("the dynamic worker has not started")), + } + } + + /// Drops the worker's links now. After this, dropping the stub does nothing more. + pub fn unlink(&self) { + if let Ok(service) = self.service() { + service.unlink(); + } + self.unlinked.set(true); + } +} + +impl Drop for WorkerStub { + /// Unlinks the worker on the next turn of the event loop: a stub is typically dropped while + /// another isolate is current (or by a request inside the dynamic isolate itself, through + /// `ctx.restore()`), so its isolate cannot be entered now. A stub already unlinked has + /// nothing left to do. + fn drop(&mut self) { + if self.unlinked.get() { + return; + } + let Ok(service) = self.service() else { + return; + }; + let factory = Rc::clone(service.factory()); + factory.spawn_detached(async move { service.unlink() }); + } +} + +#[expect(clippy::unnecessary_box_returns, reason = "cxx requires a Box")] +impl crate::channels::WorkerStub { + pub(crate) fn entrypoint( + &self, + name: KjMaybe<&str>, + props: KjOwn, + ) -> Box { + SubrequestChannel::new(Rc::new(DynamicEntrypoint { + stub: Rc::clone(&self.0), + name: Option::<&str>::from(name).map(str::to_owned), + props, + })) + } + + pub(crate) fn actor_class( + &self, + name: KjMaybe<&str>, + props: KjOwn, + ) -> Box { + ActorClassChannel::new(Rc::new(DynamicActorClass { + stub: Rc::clone(&self.0), + name: Option::<&str>::from(name).map(str::to_owned), + props, + })) + } +} + +pub(crate) fn dynamic_transfer_error() -> crate::Error { + kj::failed!( + "jsg.DOMException(DataCloneError): Entrypoints to dynamically-loaded workers cannot be \ + transferred to other Workers, because the system does not know how to reload this \ + Worker from scratch. Instead, have the parent Worker expose an entrypoint which \ + constructs the dynamic worker and forwards to it." + ) +} + +/// An entrypoint of a dynamic worker. Requests made before the worker has started wait for it. +struct DynamicEntrypoint { + stub: Rc, + /// `None` is the default entrypoint. + name: Option, + props: KjOwn, +} + +impl Channel for DynamicEntrypoint { + fn start_request(&self, metadata: KjOwn) -> Result> { + let (stub, name) = (Rc::clone(&self.stub), self.name.clone()); + let props = ffi::frankenvalue_clone(&self.props); + // Starts the request on the started worker. The request keeps the stub alive until it + // is done: the stub's drop unlinks the worker, which a request in flight must not see. + let start = move || -> Result> { + let channel = stub + .service()? + .entrypoint(name.as_deref(), Some(props), false) + .ok_or_else(|| match &name { + Some(name) => kj::failed!("jsg.Error: Worker has no such entrypoint: {name}"), + None => kj::failed!("jsg.Error: Worker has no default entrypoint."), + })?; + Ok(attach(channel.start_request(metadata)?, stub)) + }; + if self.stub.service().is_ok() { + return start(); + } + let startup = self.stub.startup(); + Ok(worker::PromisedInterface::new(async move { + startup.await?; + start() + }) + .into_kj()) + } + + fn require_allows_transfer(&self) -> Result<()> { + Err(dynamic_transfer_error()) + } + + fn token(&self, _usage: TokenUsage) -> Result> { + Err(dynamic_transfer_error()) + } +} + +/// A Durable Object class of a dynamic worker. `when_ready` waits for the worker to start. +struct DynamicActorClass { + stub: Rc, + name: Option, + props: KjOwn, +} + +impl DynamicActorClass { + /// The started worker's class; `when_ready` must have resolved. + fn inner(&self) -> Result> { + let service = self.stub.service().map_err(|_| { + kj::failed!("ActorClassChannel is not ready yet; should have awaited whenReady()") + })?; + let props = ffi::frankenvalue_clone(&self.props); + service + .actor_class(self.name.as_deref(), Some(props), false) + .ok_or_else(|| match &self.name { + Some(name) => kj::failed!("jsg.Error: Worker has no such actor class: {name}"), + None => kj::failed!("jsg.Error: Worker has no default actor class."), + }) + } +} + +impl ActorClass for DynamicActorClass { + fn when_ready(&self) -> LocalBoxFuture<'_, Result<()>> { + Box::pin(async move { + self.stub.startup().await?; + self.inner().map(drop) + }) + } + + fn new_actor(&self, request: NewActor<'_>) -> Result> { + self.inner()?.new_actor(request) + } + + fn start_request( + &self, + metadata: KjOwn, + actor: &ffi::ActorHandle, + ) -> Result> { + self.inner()?.start_request(metadata, actor) + } + + fn require_allows_transfer(&self) -> Result<()> { + Err(dynamic_transfer_error()) + } + + fn token(&self, _usage: TokenUsage) -> Result> { + Err(dynamic_transfer_error()) + } +} diff --git a/src/workerd/server/server/log.rs b/src/workerd/server/server/log.rs new file mode 100644 index 00000000000..18846928b71 --- /dev/null +++ b/src/workerd/server/server/log.rs @@ -0,0 +1,129 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! The server's `tracing` events as KJ log lines, so that Rust and C++ log through one logger +//! with one set of levels and one structured-logging format. + +use std::fmt::Write as _; +use std::sync::Once; + +use tracing::Level; +use tracing::field::Field; +use tracing::field::Visit; +use tracing::span; + +use crate::bridge::ffi; + +/// `kj::LogSeverity`, without `FATAL`: an event is never fatal. +#[derive(Clone, Copy)] +pub(crate) enum Severity { + Info = 0, + Warning = 1, + Error = 2, + Dbg = 4, +} + +impl Severity { + fn of(level: Level) -> Self { + match level { + Level::ERROR => Self::Error, + Level::WARN => Self::Warning, + Level::INFO => Self::Info, + Level::DEBUG | Level::TRACE => Self::Dbg, + } + } +} + +/// Renders an event's fields the way `KJ_LOG` renders its arguments: the message first, then +/// `name = value` for every other field, separated by `; `. +#[derive(Default)] +struct Line { + message: String, + fields: String, +} + +impl Visit for Line { + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { + if field.name() == "message" { + let _ = write!(self.message, "{value:?}"); + } else { + if !self.fields.is_empty() { + self.fields.push_str("; "); + } + let _ = write!(self.fields, "{} = {value:?}", field.name()); + } + } + + fn record_str(&mut self, field: &Field, value: &str) { + if field.name() == "message" { + self.message.push_str(value); + } else { + self.record_debug(field, &value); + } + } +} + +struct KjLogger; + +impl tracing::Subscriber for KjLogger { + /// KJ always prints its debug severity, which does not suit the chatter of the crates the + /// server uses (hyper-util narrates every pooled connection): events are heard from INFO up. + fn enabled(&self, metadata: &tracing::Metadata<'_>) -> bool { + *metadata.level() <= Level::INFO + } + + fn new_span(&self, _span: &span::Attributes<'_>) -> span::Id { + span::Id::from_u64(1) + } + + fn record(&self, _span: &span::Id, _values: &span::Record<'_>) {} + + fn record_follows_from(&self, _span: &span::Id, _follows: &span::Id) {} + + fn event(&self, event: &tracing::Event<'_>) { + let mut line = Line::default(); + event.record(&mut line); + if !line.fields.is_empty() { + if !line.message.is_empty() { + line.message.push_str("; "); + } + line.message.push_str(&line.fields); + } + let metadata = event.metadata(); + ffi::kj_log( + Severity::of(*metadata.level()) as u8, + metadata.file().unwrap_or(""), + metadata.line().unwrap_or(0), + &line.message, + ); + } + + fn enter(&self, _span: &span::Id) {} + + fn exit(&self, _span: &span::Id) {} +} + +/// A line at KJ's debug severity, as `KJ_LOG(DBG, ...)` writes it, for lines that are an +/// interface (the test runner's results, which test harnesses match). Not a `tracing` event: a +/// build that caps `tracing`'s level (`release_max_level_info`) compiles `debug!` out. +#[track_caller] +pub(crate) fn debug(message: &str) { + let location = std::panic::Location::caller(); + ffi::kj_log( + Severity::Dbg as u8, + location.file(), + location.line(), + message, + ); +} + +/// Routes every `tracing` event of the process to KJ's logger. Idempotent. +pub fn install() { + static INSTALLED: Once = Once::new(); + INSTALLED.call_once(|| { + // A failure means another subscriber is already in place, which is what a test harness + // embedding the server would want. + let _ = tracing::subscriber::set_global_default(KjLogger); + }); +} diff --git a/src/workerd/server/server/run.rs b/src/workerd/server/server/run.rs new file mode 100644 index 00000000000..ad809a49f66 --- /dev/null +++ b/src/workerd/server/server/run.rs @@ -0,0 +1,466 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! `workerd serve` and `workerd test`: bind the sockets, build the service graph, listen, and +//! either wait for the drain signal or run the tests. + +use std::collections::HashMap; +use std::future::Future; +use std::io::Write; +use std::rc::Rc; +use std::time::Duration; + +use futures::StreamExt; +use futures::future::Either; +use futures::future::LocalBoxFuture; +use futures::stream::FuturesUnordered; +use kj_rs::KjMaybe; +use tokio::sync::watch; +use worker::CxxWorkerInterface; +use worker::Interface; +use workerd_capnp::config; +use workerd_capnp::service; +use workerd_capnp::socket; + +use crate::Result; +use crate::bindings::Designator; +use crate::bridge::ffi; +use crate::channels::Channel; +use crate::config::Factory; +use crate::config::Overrides; +use crate::config::Reporter; +use crate::config::Server; +use crate::config::capnp_error; +use crate::config::text; +use crate::listen; +use crate::listen::BoundSocket; +use crate::listen::HttpSocket; +use crate::listen::ListenContext; +use crate::listen::StreamListener; +use crate::log; +use crate::services::network::tls_options; +use crate::services::rewriter::HttpRewriter; + +/// The command line's choices for a run. +#[derive(Default)] +pub struct RunOptions { + /// `--directory-path` and `--external-addr`; `--socket-addr` goes in `socket_addresses`. + pub overrides: Overrides, + /// `--socket-addr`, by socket name. + pub socket_addresses: HashMap, + /// `--socket-fd`, by socket name: the listening sockets, as the descriptors duplicated for + /// the server to own. + pub socket_fds: HashMap, + /// `--inspector-addr`. + pub inspector: Option, + /// `--control-fd`: where the `listen` events go, one JSON object per line. + pub control: Option, + /// `--debug-port`. + pub debug_port: Option, +} + +/// The control channel (`--control-fd`), written line by line; a failed write is logged. The +/// events are formatted by hand to keep their fields in a fixed order, `port` last: consumers +/// include shell scripts that cut the port off the end of the line. +struct Control(Option); + +impl Control { + fn write(&mut self, line: &str) { + if let Some(file) = &mut self.0 + && let Err(e) = file.write_all(line.as_bytes()) + { + tracing::error!("--control-fd: {e}"); + } + } + + fn listen(&mut self, socket: &str, port: u16) { + let socket = serde_json::Value::from(socket); + self.write(&format!( + "{{\"event\":\"listen\",\"socket\":{socket},\"port\":{port}}}\n" + )); + } + + fn listen_inspector(&mut self, port: u16) { + self.write(&format!( + "{{\"event\":\"listen-inspector\",\"port\":{port}}}\n" + )); + } +} + +/// Everything set up before either command diverges: the graph, the listeners, the drain +/// signal. +struct Running { + server: Rc, + /// The listener futures; each ends when the server has drained, or fails fatally. + listeners: FuturesUnordered>>, + drain: watch::Sender, +} + +/// Binds the sockets, starts the inspector, builds the service graph, and starts the listeners. +/// Config errors go to `report`; there is nothing to run (`None`) if it refuses to serve with +/// them. `for_test` exempts the `TEST_TMPDIR` directory override from the unmatched-override +/// check. +async fn start( + factory: Rc, + mut options: RunOptions, + report: Reporter, + for_test: bool, +) -> Result> { + let message = factory.config()?; + let config = message.get_root::().map_err(capnp_error)?; + let mut control = Control(options.control.take()); + + let bound = listen::bind_sockets( + config, + &mut options.socket_addresses, + &mut options.socket_fds, + factory.loopback(), + &report, + ) + .await?; + for name in options + .socket_addresses + .keys() + .chain(options.socket_fds.keys()) + { + report.error(format!( + "Config did not define any socket named \"{name}\" to match the override provided \ + on the command line." + )); + } + + // The inspector starts before any isolate exists, so that every isolate registers with it. + if let Some(address) = &options.inspector { + let port = ffi::factory_start_inspector(factory.raw(), address)?; + control.listen_inspector(port); + } + + let server = Server::start( + Rc::clone(&factory), + &options.overrides, + report, + bound.inbound, + ) + .await?; + report_unmatched_overrides(&server, &options.overrides, config, for_test)?; + + let (drain, draining) = watch::channel(false); + let context = Rc::new(ListenContext { + factory: Rc::clone(&factory), + draining, + }); + let listeners = FuturesUnordered::new(); + let mut ports = Vec::new(); + let sockets = config.get_sockets().map_err(capnp_error)?; + for (sock, bound) in sockets.iter().zip(bound.sockets) { + // A socket that failed to bind already reported its error. + let Some((bound, address)) = bound else { + continue; + }; + let name = text(sock.get_name())?; + let mut errors = Vec::new(); + let designator = Designator::from_reader( + sock.get_service().map_err(capnp_error)?, + format!("Socket \"{name}\""), + &mut errors, + ) + .map_err(capnp_error)?; + for error in errors { + server.report().error(error); + } + let channel = server.lookup(&designator); + ports.push((name, listen::bound_port(&bound)?)); + if let Some(listener) = + listener(Rc::clone(&context), sock, bound, &address, channel, &server)? + { + listeners.push(listener); + } + } + // Every config error is reported by now; a config that is refused gets no `listen` event. + if server.report().refuses() { + return Ok(None); + } + for (name, port) in ports { + control.listen(&name, port); + } + + if let Some(address) = &options.debug_port { + let listener = listen::listen(address, 0, factory.loopback()).await?; + control.listen("debug-port", listener.port()?); + listeners.push(Box::pin(listen::listen_debug_port( + Rc::clone(&context), + listener, + ))); + } + + Ok(Some(Running { + server, + listeners, + drain, + })) +} + +/// The listener for one bound socket, or none for a socket whose type is unknown (reported). +fn listener( + context: Rc, + sock: socket::Reader<'_>, + bound: BoundSocket, + address: &str, + channel: Rc, + server: &Server, +) -> Result>>> { + let name = text(sock.get_name())?; + let tls = |options: workerd_capnp::tls_options::Reader<'_>| { + kj_hyper::tls::server_config(&tls_options(options)?) + }; + let http = |listener: StreamListener, + options: workerd_capnp::http_options::Reader<'_>, + physical_protocol: &'static str, + tls: Option>, + context: Rc, + channel: Rc| { + let socket = Rc::new(HttpSocket { + channel, + rewriter: Rc::new(HttpRewriter::new(options)?), + physical_protocol, + tls, + }); + let listener: LocalBoxFuture<'static, Result<()>> = + Box::pin(listen::listen_http(context, listener, socket)); + Ok::<_, crate::Error>(listener) + }; + Ok(Some(match (sock.which(), bound) { + (Ok(socket::Which::Http(options)), BoundSocket::Stream(listener)) => http( + listener, + options.map_err(capnp_error)?, + "http", + None, + context, + channel, + )?, + (Ok(socket::Which::Https(https)), BoundSocket::Stream(listener)) => { + let tls = tls(https.get_tls_options().map_err(capnp_error)?)?; + http( + listener, + https.get_options().map_err(capnp_error)?, + "https", + Some(tls), + context, + channel, + )? + } + (Ok(socket::Which::Tcp(tcp)), BoundSocket::Stream(listener)) => { + let tls = if tcp.has_tls_options() { + Some(tls(tcp.get_tls_options().map_err(capnp_error)?)?) + } else { + None + }; + // The authority handed to the connect() handler is the endpoint as bound, so it is + // truthful for a configured port of 0. + let authority = format!("{}:{}", listen::host_of_address(address), listener.port()?); + Box::pin(listen::listen_tcp( + context, listener, channel, tls, authority, + )) + } + (Ok(socket::Which::Udp(udp)), BoundSocket::Datagram(socket)) => { + Box::pin(listen::udp::listen_udp( + context, + socket, + channel, + address.to_owned(), + Duration::from_millis(udp.get_idle_timeout_ms().into()), + udp.get_max_pending_bytes() as usize, + )) + } + (Err(capnp::NotInSchema(_)), _) => { + server.report().error(format!( + "Encountered unknown socket type in \"{name}\". Was the config compiled with a \ + newer version of the schema?" + )); + return Ok(None); + } + // bind_sockets binds a datagram socket for UDP and a stream listener otherwise. + _ => return Err(kj::failed!("socket \"{name}\" was bound as the wrong kind")), + })) +} + +/// Reports the `--external-addr` and `--directory-path` overrides that name no service of their +/// kind. +fn report_unmatched_overrides( + server: &Server, + overrides: &Overrides, + config: config::Reader<'_>, + for_test: bool, +) -> Result<()> { + let mut externals: Vec<&str> = overrides.externals.keys().map(String::as_str).collect(); + let mut directories: Vec<&str> = overrides.directories.keys().map(String::as_str).collect(); + for conf in config.get_services().map_err(capnp_error)? { + let name = text(conf.get_name())?; + match conf.which() { + Ok(service::Which::External(_)) => externals.retain(|n| *n != name), + Ok(service::Which::Disk(_)) => directories.retain(|n| *n != name), + _ => {} + } + } + for name in externals { + server.report().error(format!( + "Config did not define any external service named \"{name}\" to match the override \ + provided on the command line." + )); + } + for name in directories { + // Due to a historical bug, `workerd test` didn't check for the existence of unmatched + // overrides, and our own tests became dependent on the ability to override TEST_TMPDIR + // even if it was not used in the config. + if for_test && name == "TEST_TMPDIR" { + continue; + } + server.report().error(format!( + "Config did not define any disk service named \"{name}\" to match the override \ + provided on the command line." + )); + } + Ok(()) +} + +/// Drives the listeners until they all end (the server drained) or one fails. +async fn serve_listeners( + listeners: &mut FuturesUnordered>>, +) -> Result<()> { + while let Some(result) = listeners.next().await { + result?; + } + Ok(()) +} + +/// `workerd serve`: serves until `drain` resolves, then stops accepting connections, lets the +/// open ones finish, and stops the actors' containers. A listener failing is fatal and is the +/// error returned. +pub async fn run( + factory: Rc, + options: RunOptions, + report: Reporter, + drain: impl Future, +) -> Result<()> { + let Some(Running { + server, + mut listeners, + drain: draining, + }) = start(factory, options, report, false).await? + else { + return Ok(()); + }; + + let drain = std::pin::pin!(drain); + let serving = std::pin::pin!(serve_listeners(&mut listeners)); + // Draining stops the accept loops; the listeners then end once their connections have. + match futures::future::select(serving, drain).await { + Either::Left((result, _)) => result?, + Either::Right(((), serving)) => { + draining.send_replace(true); + serving.await?; + } + } + + // All incoming requests have drained. Stop container-enabled actors so they cannot race + // their terminal Docker cleanup, then wait for it while their namespaces remain available. + let workers = server + .services() + .filter_map(|(_, service)| service.as_worker()); + for (_, namespace) in workers.flat_map(|worker| worker.namespaces()) { + namespace.begin_container_cleanup(); + } + Ok(ffi::factory_shutdown_containers(server.factory().raw()).await?) +} + +/// `workerd test`: runs the `test()` handler of every entrypoint matching the patterns (globs +/// as the `glob` crate reads them, matched against the whole name). +/// +/// The sockets listen meanwhile (tests can configure them). True if every test passed and there +/// was at least one; `None` for a config `report` refuses, whose tests do not run. +pub async fn test( + factory: Rc, + options: RunOptions, + report: Reporter, + service_pattern: &str, + entrypoint_pattern: &str, +) -> Result> { + let glob = |pattern: &str| { + glob::Pattern::new(pattern).map_err(|e| kj::failed!("test filter \"{pattern}\": {e}")) + }; + let (services, entrypoints) = (glob(service_pattern)?, glob(entrypoint_pattern)?); + let Some(Running { + server, + mut listeners, + drain: _drain, + }) = start(factory, options, report, true).await? + else { + return Ok(None); + }; + + // Test harnesses match these lines; they are debug-level because info logging is + // optional and a warning or error would confuse people. + let run_one = |channel: Rc, name: String| async move { + log::debug(&format!("[ TEST ] {name}")); + let metadata = ffi::new_request_metadata(KjMaybe::None, KjMaybe::None); + let mut worker = CxxWorkerInterface::new(channel.start_request(metadata)?); + let start = std::time::Instant::now(); + let result = worker.test().await?; + let duration = start.elapsed(); + let verdict = if result { "PASS" } else { "FAIL" }; + log::debug(&format!("[ {verdict} ] {name} ({duration:?})")); + Ok::(result) + }; + + let mut cases = Vec::new(); + for (name, service) in server.services() { + if !services.matches(name) { + continue; + } + let Some(worker) = service.as_worker() else { + continue; + }; + if worker.has_handler(None, "test") && entrypoints.matches("default") { + cases.push((service.channel(), name.to_owned())); + } + for entrypoint in worker.entrypoint_names() { + if entrypoints.matches(entrypoint) + && worker.has_handler(Some(entrypoint), "test") + && let Some(channel) = worker.entrypoint(Some(entrypoint), None, false) + { + cases.push((channel, format!("{name}:{entrypoint}"))); + } + } + } + // The tests run as a task of the KJ loop: a test starts in the turn after the one before it + // returned, ahead of the events that one left queued (its tail workers' deliveries), where + // this future would resume only once the loop is idle. The task holds the channels, not the + // server, whose drop clears the tasks. + let tests = server.factory().spawn(async move { + let (mut passed, mut failed) = (0u32, 0u32); + for (channel, name) in cases { + if run_one(channel, name).await? { + passed += 1; + } else { + failed += 1; + } + } + Ok::<_, crate::Error>((passed, failed)) + }); + + // A listener failing while the tests run is fatal, as in `run()`. + let serving = std::pin::pin!(serve_listeners(&mut listeners)); + let (passed, failed) = match futures::future::select(tests, serving).await { + Either::Left((result, _)) => result?, + Either::Right((result, tests)) => { + result?; + tests.await? + } + }; + + if passed + failed == 0 { + tracing::error!("No tests found!"); + } + Ok(Some(passed > 0 && failed == 0)) +} diff --git a/src/workerd/server/server/services/disk-test.rs b/src/workerd/server/server/services/disk-test.rs new file mode 100644 index 00000000000..e1a584e4724 --- /dev/null +++ b/src/workerd/server/server/services/disk-test.rs @@ -0,0 +1,76 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +use super::*; + +/// `path_segments(url)`, comparable with the expected segments written as string literals. +#[derive(Debug)] +struct Segments(Option>); + +impl PartialEq>> for Segments { + fn eq(&self, expected: &Option>) -> bool { + let actual = self + .0 + .as_ref() + .map(|segments| segments.iter().map(String::as_str).collect::>()); + actual == *expected + } +} + +fn segments(url: &str) -> Segments { + Segments(path_segments(url)) +} + +#[test] +fn paths_are_split_decoded_and_checked() { + assert_eq!(segments("http://h/"), Some(vec![])); + assert_eq!(segments("http://h"), Some(vec![])); + assert_eq!(segments("http://h/a/b.txt?x=1"), Some(vec!["a", "b.txt"])); + assert_eq!(segments("http://h/dir/"), Some(vec!["dir"])); + assert_eq!(segments("http://h/a%20b/c%2Fd"), None); + assert_eq!(segments("http://h/sp%20ace"), Some(vec!["sp ace"])); + assert_eq!(segments("http://h/../etc"), Some(vec!["etc"])); + assert_eq!(segments("http://h/a/../b"), Some(vec!["b"])); + assert_eq!(segments("http://h/a/%2e%2e/b"), Some(vec!["b"])); + assert_eq!(segments("http://h/a//b"), None); + assert_eq!(segments("http://h/./a"), Some(vec!["a"])); + assert_eq!(segments("http://h/a%00"), None); + assert_eq!(segments("http://h/.hidden"), Some(vec![".hidden"])); +} + +#[test] +fn a_segment_is_one_entry_name() { + assert!(is_entry_name("b.txt")); + for segment in ["", ".", "..", "/", "a/b", "a\0"] { + assert!(!is_entry_name(segment), "{segment:?}"); + } + // What Windows reads as separators, drives and streams are ordinary characters elsewhere. + for segment in [ + "x\\..\\..\\secret", + "..\\secret", + "C:\\Windows\\win.ini", + "C:secret", + "file:stream", + ] { + assert_eq!(is_entry_name(segment), cfg!(not(windows)), "{segment:?}"); + } +} + +#[test] +fn one_range_is_served_and_several_get_everything() { + assert_eq!(parse_range("bytes=0-99", 1000), Range::Bytes(0, 99)); + assert_eq!(parse_range("bytes=500-", 1000), Range::Bytes(500, 999)); + assert_eq!(parse_range("bytes=-100", 1000), Range::Bytes(900, 999)); + assert_eq!(parse_range("BYTES = 5-6", 1000), Range::Bytes(5, 6)); + assert_eq!(parse_range("bytes=-5000", 1000), Range::Everything); + assert_eq!(parse_range("bytes=0-", 1000), Range::Everything); + assert_eq!(parse_range("bytes=0-2000", 1000), Range::Everything); + assert_eq!(parse_range("bytes=0-1, 5-6", 1000), Range::Everything); + assert_eq!(parse_range("bytes=1000-", 1000), Range::Unsatisfiable); + assert_eq!(parse_range("bytes=5-2", 1000), Range::Unsatisfiable); + assert_eq!(parse_range("bytes=-", 1000), Range::Unsatisfiable); + assert_eq!(parse_range("bytes=a-b", 1000), Range::Unsatisfiable); + assert_eq!(parse_range("items=0-1", 1000), Range::Unsatisfiable); + assert_eq!(parse_range("bytes=0-1", 0), Range::Unsatisfiable); +} diff --git a/src/workerd/server/server/services/disk.rs b/src/workerd/server/server/services/disk.rs new file mode 100644 index 00000000000..6639e53507e --- /dev/null +++ b/src/workerd/server/server/services/disk.rs @@ -0,0 +1,533 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! The `disk` service: a directory served over HTTP. +//! +//! GET and HEAD read files (with one byte range) and list directories as JSON; PUT and DELETE, +//! on a writable directory, replace and remove entries. The service is also the directory a +//! worker's `durableObjectStorage.localDisk` names. +//! +//! Disk I/O is synchronous, as it is in `kj::Directory`: the files are local, and the loop +//! thread reads them in 64 KiB steps between writes to the response. + +use std::io::Read; +use std::io::Seek; +use std::io::Write; +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; +use std::pin::Pin; +use std::rc::Rc; +use std::sync::atomic::AtomicU64; +use std::sync::atomic::Ordering; +use std::time::SystemTime; + +use httpdate::fmt_http_date; +use kj::http::ConnectResponse; +use kj::http::ConnectSettings; +use kj::http::HeaderId; +use kj::http::HeaderTable; +use kj::http::Headers; +use kj::http::HeadersRef; +use kj::http::Method; +use kj::http::Service; +use kj::http::ServiceResponse; +use kj::io::AsyncInputStream; +use kj::io::AsyncIoStream; +use kj_hyper::io_kj_error; +use kj_rs::KjOwn; +use percent_encoding::percent_decode_str; +use worker::AlarmResult; +use worker::Interface; +use worker::ScheduledResult; +use workerd_capnp::disk_directory; + +use crate::Result; +use crate::channels::Channel; +use crate::channels::PendingToken; +use crate::channels::RequestMetadata; +use crate::channels::TokenUsage; +use crate::channels::WorkerInterface; +use crate::config::Factory; +use crate::config::text; +use crate::services::header_table; +use crate::services::not_transferable; +use crate::services::send_error; +use crate::services::unsupported; + +// ======================================================================================= +// Pure parts + +/// Whether `segment` names one entry of a directory: a single normal path component, so nothing +/// a separator would split, no `.`, `..`, root or drive, and (on Windows) no stream of a file. +fn is_entry_name(segment: &str) -> bool { + let mut components = Path::new(segment).components(); + matches!(components.next(), Some(Component::Normal(name)) if name == segment) + && components.next().is_none() + && !segment.contains('\0') + && !(cfg!(windows) && segment.contains(':')) +} + +/// The path a request URL names, as segments below the directory. +/// +/// Dot segments, literal or percent-encoded, are resolved as the URL standard resolves them, +/// never above the root. `None` for a path with a segment that is not an entry's name, which +/// the service must not serve. +pub fn path_segments(url: &str) -> Option> { + let url = url::Url::parse(url).ok()?; + let path = url.path(); + let path = path.strip_prefix('/').unwrap_or(path); + let path = path.strip_suffix('/').unwrap_or(path); + if path.is_empty() { + return Some(Vec::new()); + } + path.split('/') + .map(|segment| { + let segment = percent_decode_str(segment).decode_utf8().ok()?; + is_entry_name(&segment).then(|| segment.into_owned()) + }) + .collect() +} + +/// What a `Range` header asks for, against a body of a known length. +#[derive(Debug, PartialEq, Eq)] +pub enum Range { + /// The header is malformed, or its range is beyond the body: 416. + Unsatisfiable, + /// The whole body, or several ranges, which get the whole body: send it as usual. + Everything, + /// One inclusive byte range within the body. + Bytes(u64, u64), +} + +pub fn parse_range(header: &str, length: u64) -> Range { + let Some((unit, spec)) = header.split_once('=') else { + return Range::Unsatisfiable; + }; + if !unit.trim().eq_ignore_ascii_case("bytes") { + return Range::Unsatisfiable; + } + if spec.contains(',') { + return Range::Everything; + } + let bound = |text: &str| match text.trim() { + "" => Ok(None), + text => text.parse::().map(Some), + }; + let Some((Ok(first), Ok(last))) = spec.split_once('-').map(|(a, b)| (bound(a), bound(b))) + else { + return Range::Unsatisfiable; + }; + let end = length.wrapping_sub(1); + let (start, end) = match (first, last) { + (None, None) => return Range::Unsatisfiable, + // A suffix range: the last `suffix` bytes, or everything when it asks for more. + (None, Some(suffix)) => (length.saturating_sub(suffix), end), + (Some(first), last) => (first, last.map_or(end, |last| last.min(end))), + }; + if length == 0 || start > end { + Range::Unsatisfiable + } else if start == 0 && end == length - 1 { + Range::Everything + } else { + Range::Bytes(start, end) + } +} + +/// A directory entry's type, as the listing names it. +fn entry_type(file_type: std::fs::FileType) -> &'static str { + #[cfg(unix)] + { + use std::os::unix::fs::FileTypeExt; + if file_type.is_block_device() { + return "blockDevice"; + } + if file_type.is_char_device() { + return "characterDevice"; + } + if file_type.is_fifo() { + return "namedPipe"; + } + if file_type.is_socket() { + return "socket"; + } + } + if file_type.is_symlink() { + "symlink" + } else if file_type.is_dir() { + "directory" + } else if file_type.is_file() { + "file" + } else { + "other" + } +} + +// ======================================================================================= +// The service + +struct Disk { + factory: Rc, + root: PathBuf, + writable: bool, + allow_dotfiles: bool, +} + +impl Disk { + /// The path below the root the segments name; the segments passed validation. + fn join(&self, segments: &[String]) -> PathBuf { + segments + .iter() + .fold(self.root.clone(), |path, segment| path.join(segment)) + } +} + +/// A `disk` service: a directory served over HTTP, and the directory a worker's +/// `durableObjectStorage.localDisk` names. +pub struct DiskDirectoryService(Rc); + +impl DiskDirectoryService { + /// The directory's path when the service is writable, which Durable Object storage needs. + #[must_use] + pub fn writable_path(&self) -> Option<&str> { + self.0.writable.then(|| self.0.root.to_str()).flatten() + } +} + +impl Channel for DiskDirectoryService { + fn start_request(&self, _metadata: KjOwn) -> Result> { + Ok(DiskRequest(Rc::clone(&self.0)).into_kj()) + } + + fn token(&self, _usage: TokenUsage) -> Result> { + Err(not_transferable("DiskDirectoryService")) + } +} + +/// `path_override` is the CLI's `--directory-path` for this service. +pub fn make_disk_directory_service( + name: &str, + conf: disk_directory::Reader<'_>, + path_override: Option<&str>, + factory: &Rc, +) -> Result> { + let path = match path_override { + Some(path) => path.to_owned(), + None if conf.has_path() => text(conf.get_path())?, + None => { + return Err(kj::failed!( + "Directory \"{name}\" has no path in the config, so must be specified on the \ + command line with `--directory-path`." + )); + } + }; + let root = std::env::current_dir() + .map_err(|e| io_kj_error(&e))? + .join(&path); + if !root.is_dir() { + return Err(kj::failed!("Directory named \"{name}\" not found: {path}")); + } + Ok(Rc::new(DiskDirectoryService(Rc::new(Disk { + factory: Rc::clone(factory), + root, + writable: conf.get_writable(), + allow_dotfiles: conf.get_allow_dotfiles(), + })))) +} + +// ======================================================================================= +// Requests + +fn kj_headers<'t>( + table: &'t HeaderTable, + map: &http::HeaderMap, +) -> Result> { + kj_hyper::HeaderBlock::new(map, &http::Extensions::new()).to_kj(table) +} + +fn header_value(value: impl AsRef) -> Result { + http::HeaderValue::from_str(value.as_ref()) + .map_err(|_| kj::failed!("invalid header value: {}", value.as_ref())) +} + +fn len_u64(len: usize) -> u64 { + u64::try_from(len).unwrap_or(u64::MAX) +} + +/// Copies `length` bytes of the file from `start` into `out`. +async fn copy_file( + path: &Path, + start: u64, + length: u64, + out: &mut kj::io::AsyncOutputStream<'_>, +) -> Result<()> { + let mut file = std::fs::File::open(path).map_err(|e| io_kj_error(&e))?; + if start > 0 { + file.seek(std::io::SeekFrom::Start(start)) + .map_err(|e| io_kj_error(&e))?; + } + let mut buffer = vec![0; 64 * 1024]; + let mut remaining = length; + while remaining > 0 { + let want = usize::try_from(remaining).map_or(buffer.len(), |r| r.min(buffer.len())); + let read = file + .read(&mut buffer[..want]) + .map_err(|e| io_kj_error(&e))?; + if read == 0 { + return Err(kj::disconnected!( + "the file ended before its declared length" + )); + } + out.write(&buffer[..read]).await?; + remaining -= len_u64(read); + } + Ok(()) +} + +/// A file that is removed when dropped, unless it was renamed away first. +struct Temporary(PathBuf); + +impl Drop for Temporary { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.0); + } +} + +/// Writes the body to a temporary file beside `path`, then renames it into place, so that a +/// reader never sees a partial file. A write that fails or is abandoned leaves nothing behind. +async fn write_replacing(path: &Path, mut body: Pin<&mut AsyncInputStream>) -> Result<()> { + static NONCE: AtomicU64 = AtomicU64::new(0); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| io_kj_error(&e))?; + } + let name = path.file_name().map(|n| n.to_string_lossy().into_owned()); + let temp = Temporary(path.with_file_name(format!( + ".{}.{}.{}.tmp", + name.unwrap_or_default(), + std::process::id(), + NONCE.fetch_add(1, Ordering::Relaxed) + ))); + let mut file = std::fs::File::create(&temp.0).map_err(|e| io_kj_error(&e))?; + let mut buffer = vec![0; 64 * 1024]; + loop { + let read = body.as_mut().try_read(&mut buffer, 1).await?; + if read == 0 { + break; + } + file.write_all(&buffer[..read]) + .map_err(|e| io_kj_error(&e))?; + } + drop(file); + std::fs::rename(&temp.0, path).map_err(|e| io_kj_error(&e)) +} + +/// Removes a file, or a directory and everything in it; false when there was nothing there. +fn remove(path: &Path) -> Result { + let metadata = match std::fs::symlink_metadata(path) { + Ok(metadata) => metadata, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(e) => return Err(io_kj_error(&e)), + }; + if metadata.is_dir() { + std::fs::remove_dir_all(path) + } else { + std::fs::remove_file(path) + } + .map_err(|e| io_kj_error(&e))?; + Ok(true) +} + +struct DiskRequest(Rc); + +impl DiskRequest { + async fn file( + &self, + method: Method, + path: &Path, + metadata: &std::fs::Metadata, + request_headers: HeadersRef<'_>, + response: ServiceResponse<'_>, + ) -> Result<()> { + let table = header_table(&self.0.factory); + let size = metadata.len(); + let mut headers = http::HeaderMap::new(); + headers.insert( + http::header::CONTENT_TYPE, + header_value("application/octet-stream")?, + ); + if let Ok(modified) = metadata.modified() { + headers.insert( + http::header::LAST_MODIFIED, + header_value(fmt_http_date(modified))?, + ); + } + // Partial content for a single satisfiable range; several ranges get everything. + let mut range = None; + if method == Method::GET + && let Some(header) = request_headers.get(HeaderId::RANGE) + { + match parse_range(&String::from_utf8_lossy(header), size) { + Range::Unsatisfiable => { + let mut headers = http::HeaderMap::new(); + headers.insert( + http::header::CONTENT_RANGE, + header_value(format!("bytes */{size}"))?, + ); + let headers = kj_headers(table, &headers)?; + return send_error(response, 416, "Range Not Satisfiable", &*headers).await; + } + Range::Bytes(start, end) => range = Some((start, end)), + Range::Everything => {} + } + } + // The header is set explicitly so that a worker calling this service in-process, with + // no HTTP connection in between, sees a `Content-Length` too. + let (status, text, start, length) = match range { + Some((start, end)) => { + let length = end - start + 1; + headers.insert( + http::header::CONTENT_RANGE, + header_value(format!("bytes {start}-{end}/{size}"))?, + ); + (206, "Partial Content", start, length) + } + None => (200, "OK", 0, size), + }; + headers.insert( + http::header::CONTENT_LENGTH, + header_value(length.to_string())?, + ); + let headers = kj_headers(table, &headers)?; + let mut out = response.send(status, text, HeadersRef::from(&*headers), Some(length))?; + if method == Method::HEAD { + return Ok(()); + } + copy_file(path, start, length, &mut out).await + } + + async fn directory( + &self, + method: Method, + path: &Path, + metadata: &std::fs::Metadata, + response: ServiceResponse<'_>, + ) -> Result<()> { + let table = header_table(&self.0.factory); + let mut headers = http::HeaderMap::new(); + headers.insert( + http::header::CONTENT_TYPE, + header_value("application/json")?, + ); + if let Ok(modified) = metadata.modified() { + headers.insert( + http::header::LAST_MODIFIED, + header_value(fmt_http_date(modified))?, + ); + } + let headers = kj_headers(table, &headers)?; + // No size: the listing may become a stream some day. + let mut out = response.send(200, "OK", HeadersRef::from(&*headers), None)?; + if method == Method::HEAD { + return Ok(()); + } + let mut entries = Vec::new(); + for entry in std::fs::read_dir(path).map_err(|e| io_kj_error(&e))? { + let entry = entry.map_err(|e| io_kj_error(&e))?; + let name = entry.file_name().to_string_lossy().into_owned(); + if !self.0.allow_dotfiles && name.starts_with('.') { + continue; + } + let kind = entry.file_type().map_or("other", entry_type); + entries.push((name, kind)); + } + entries.sort(); + let listing = entries + .iter() + .map(|(name, kind)| serde_json::json!({"name": name, "type": kind})) + .collect::>(); + let json = serde_json::to_string(&listing).map_err(|e| kj::failed!("{e}"))?; + out.write(json.as_bytes()).await + } +} + +#[async_trait::async_trait(?Send)] +impl Service for DiskRequest { + async fn request<'a>( + &'a mut self, + method: Method, + url: &'a [u8], + headers: HeadersRef<'a>, + request_body: Pin<&'a mut AsyncInputStream>, + response: ServiceResponse<'a>, + ) -> Result<()> { + let disk = &self.0; + let table = header_table(&disk.factory); + let none = Headers::new(table); + let url = std::str::from_utf8(url).map_err(|_| kj::failed!("request URL is not UTF-8"))?; + let segments = path_segments(url).filter(|segments| { + disk.allow_dotfiles || !segments.iter().any(|segment| segment.starts_with('.')) + }); + match method { + Method::GET | Method::HEAD => { + let Some(segments) = segments else { + return send_error(response, 404, "Not Found", &none).await; + }; + let path = disk.join(&segments); + let Ok(metadata) = std::fs::metadata(&path) else { + return send_error(response, 404, "Not Found", &none).await; + }; + if metadata.is_file() { + self.file(method, &path, &metadata, headers, response).await + } else if metadata.is_dir() { + self.directory(method, &path, &metadata, response).await + } else { + send_error(response, 406, "Not Acceptable", &none).await + } + } + Method::PUT | Method::DELETE => { + if !disk.writable { + return send_error(response, 405, "Method Not Allowed", &none).await; + } + let Some(segments) = segments.filter(|segments| !segments.is_empty()) else { + return send_error(response, 403, "Unauthorized", &none).await; + }; + let path = disk.join(&segments); + if method == Method::PUT { + write_replacing(&path, request_body).await?; + } else if !remove(&path)? { + return send_error(response, 404, "Not Found", &none).await; + } + response.send(204, "No Content", &none, None)?; + Ok(()) + } + _ => send_error(response, 501, "Not Implemented", &none).await, + } + } + + async fn connect<'a>( + &'a mut self, + _host: &'a [u8], + _headers: HeadersRef<'a>, + _connection: Pin<&'a mut AsyncIoStream>, + _response: ConnectResponse<'a>, + _settings: ConnectSettings<'a>, + ) -> Result<()> { + Err(unsupported("Disk directory services")) + } +} + +#[async_trait::async_trait(?Send)] +impl Interface for DiskRequest { + async fn run_scheduled(&mut self, _time: &SystemTime, _cron: &str) -> Result { + Err(unsupported("Disk directory services")) + } + + async fn run_alarm(&mut self, _time: &SystemTime, _retry_count: u32) -> Result { + Err(unsupported("Disk directory services")) + } +} + +#[cfg(test)] +#[path = "disk-test.rs"] +mod tests; diff --git a/src/workerd/server/server/services/mod.rs b/src/workerd/server/server/services/mod.rs new file mode 100644 index 00000000000..085214fc734 --- /dev/null +++ b/src/workerd/server/server/services/mod.rs @@ -0,0 +1,640 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! The external and network services: leaf channels of the service graph whose requests leave +//! the process over HTTP or TCP. +//! +//! Each is an `Rc` of shared state; `start_request` wraps it in a fresh `worker::Interface` for +//! one event. Outbound HTTP goes through kj-hyper's pooled client, which borrows the factory's +//! header table, so a client is built per event over the service's dialer or peer filter; the +//! TLS configuration, the expensive part, is built once per service. + +pub mod disk; +pub mod network; +pub mod rewriter; + +use std::cell::RefCell; +use std::pin::Pin; +use std::rc::Rc; +use std::rc::Weak; +use std::sync::Arc; +use std::time::SystemTime; + +use kj::http::ConnectResponse; +use kj::http::ConnectSettings; +use kj::http::HeaderTable; +use kj::http::Headers; +use kj::http::HeadersRef; +use kj::http::Method; +use kj::http::Service; +use kj::http::ServiceResponse; +use kj::io::AsyncInputStream; +use kj::io::AsyncIoStream; +use kj_hyper::WebSocketCompression; +use kj_hyper::client::Client; +use kj_hyper::client::ClientSettings; +use kj_hyper::client::Dialed; +use kj_hyper::client::Peer; +use kj_hyper::client::connect_allowed; +use kj_hyper::io_kj_error; +use kj_rs::KjOwn; +use kj_rs_io::Socket; +use kj_rs_io::TokioAddress; +use worker::AlarmResult; +use worker::CustomEvent; +use worker::CustomEventResult; +use worker::Interface; +use worker::ScheduledResult; +use workerd_capnp::external_server; + +use crate::Error; +use crate::Result; +use crate::bridge; +use crate::bridge::ffi; +use crate::channels::Channel; +use crate::channels::PendingToken; +use crate::channels::RequestMetadata; +use crate::channels::TokenUsage; +use crate::channels::WorkerInterface; +use crate::config::Factory; +use crate::config::capnp_error; +use crate::config::optional_text; +use crate::config::text; +use crate::listen::host_of_address; +use crate::listen::loopback::Loopback; +pub use crate::services::disk::DiskDirectoryService; +pub use crate::services::disk::make_disk_directory_service; +use crate::services::network::PeerFilter; +use crate::services::network::tls_options; +use crate::services::rewriter::HttpRewriter; +use crate::services::rewriter::Style; + +// ======================================================================================= +// Shared pieces + +/// The header table every `kj::HttpHeaders` a worker sees is built against. +pub fn header_table(factory: &Factory) -> &HeaderTable { + ffi::factory_header_table(factory.raw()) +} + +/// A leaf service cannot be handed to another worker as a stub (`DataCloneError`). +pub fn not_transferable(what: &str) -> Error { + kj::failed!("jsg.DOMException(DataCloneError): {what} can't be passed over RPC.") +} + +/// The error a leaf service answers events it has no handler for with. +pub fn unsupported(what: &str) -> Error { + kj::failed!("jsg.Error: {what} don't support this event type.") +} + +/// `kj::HttpService::Response::sendError`: the status text as the body. +pub async fn send_error<'h>( + response: ServiceResponse<'_>, + status: u32, + text: &str, + headers: impl Into>, +) -> Result<()> { + let mut body = response.send(status, text, headers, Some(text.len() as u64))?; + body.write(text.as_bytes()).await +} + +/// The request's cf blob, as JSON, when the runtime attached one. +fn cf_blob_json(metadata: &RequestMetadata) -> Option { + ffi::request_metadata_cf_blob_json(metadata).into() +} + +/// kj's client settings for these services: the application negotiates WebSocket compression +/// itself. +fn client_settings() -> ClientSettings { + ClientSettings { + websocket_compression: WebSocketCompression::MANUAL, + websocket_errors: Some(ffi::new_jsgify_websocket_errors()), + ..ClientSettings::default() + } +} + +// ======================================================================================= +// Dialing one peer + +/// TLS to one peer: the configuration and the name the certificate must carry. +struct Tls { + config: Arc, + server_name: String, +} + +/// Connects to one configured peer: its address in kj-rs-io's grammar, resolved each time, or +/// a `loopback:` name, a socket of this server (`listen::loopback`). +struct Dialer { + address: String, + default_port: u16, + loopback: Loopback, + tls: Option, +} + +/// A loopback connection nobody listens for, as the refusal a socket would report. +#[expect(clippy::needless_pass_by_value, reason = "a `map_err` adapter")] +fn refused(error: Error) -> std::io::Error { + std::io::Error::new( + std::io::ErrorKind::ConnectionRefused, + error.description().to_owned(), + ) +} + +impl Dialer { + /// `certificate_host` is the name TLS verifies, else the address's host. + fn new( + address: &str, + default_port: u16, + tls: Option<(Arc, Option)>, + loopback: &Loopback, + ) -> Self { + let tls = tls.map(|(config, certificate_host)| Tls { + config, + server_name: certificate_host + .unwrap_or_else(|| host_of_address(address).trim_matches(['[', ']']).to_owned()), + }); + Self { + address: address.to_owned(), + default_port, + loopback: loopback.clone(), + tls, + } + } + + /// The connection, with its hang-up signal taken before any TLS. + async fn dial(&self) -> std::io::Result { + let stream: Dialed = if let Some(name) = self.address.strip_prefix("loopback:") { + self.loopback.connect(name).map_err(refused)?.into() + } else { + let address = TokioAddress::parse_str(&self.address, self.default_port).await?; + match address.connect_first().await? { + Socket::Tcp(stream) => stream.into(), + #[cfg(unix)] + Socket::Unix(stream) => stream.into(), + } + }; + match &self.tls { + Some(tls) => stream.tls(Arc::clone(&tls.config), &tls.server_name).await, + None => Ok(stream), + } + } +} + +/// `settings` reborrowed for a shorter lifetime, for a client that lives only for the call: the +/// bridge struct is invariant in its lifetime, so it cannot shrink on its own. +fn reborrow_settings<'s>( + use_tls: bool, + tls_starter: &'s mut Option>, +) -> ConnectSettings<'s> { + ConnectSettings { + use_tls, + tls_starter: tls_starter.as_mut().map(Pin::as_mut).into(), + } +} + +/// A kj-hyper client of the dialer's peer, asked for URLs in `style`. +fn fixed_client<'t>(table: &'t HeaderTable, dialer: &Arc, style: Style) -> Client<'t> { + let dialer = Arc::clone(dialer); + let dial = move || { + let dialer = Arc::clone(&dialer); + async move { dialer.dial().await } + }; + let peer = match style { + Style::Host => Peer::Origin, + Style::Proxy => Peer::Proxy, + }; + Client::new(table, client_settings(), peer, dial) +} + +// ======================================================================================= +// External HTTP + +/// A capnp connection to the external server, over an HTTP CONNECT tunnel. +struct RpcConnection(KjOwn); + +struct ExternalHttp { + factory: Rc, + dialer: Arc, + rewriter: HttpRewriter, + /// The capnp connection custom events go over, made when the first one needs it and dropped + /// when it is lost. + rpc: RefCell>>, +} + +impl ExternalHttp { + /// The capnp connection, opened if there is none. A connection that is lost is forgotten, + /// so the next event opens a new one. + async fn rpc(self: &Rc) -> Result> { + if let Some(connection) = &*self.rpc.borrow() { + return Ok(Rc::clone(connection)); + } + let host = self + .rewriter + .capnp_connect_host() + .ok_or_else(|| kj::failed!("jsg.Error: This ExternalServer not configured for RPC."))?; + // The tunnel is an HTTP CONNECT to the external server. + let table = header_table(&self.factory); + let client = fixed_client(table, &self.dialer, self.rewriter.style()); + let tunnel = client.tunnel(host).await?; + let connection = Rc::new(RpcConnection(ffi::new_rpc_client( + self.factory.raw(), + tunnel, + ))); + *self.rpc.borrow_mut() = Some(Rc::clone(&connection)); + let service: Weak = Rc::downgrade(self); + let watched = Rc::clone(&connection); + self.factory.spawn_detached(async move { + // A failure of the wait itself is a lost connection too. + let _ = ffi::rpc_client_on_disconnect(&watched.0).await; + if let Some(service) = service.upgrade() { + let mut current = service.rpc.borrow_mut(); + if current.as_ref().is_some_and(|c| Rc::ptr_eq(c, &watched)) { + *current = None; + } + } + }); + Ok(connection) + } +} + +/// An `external` service speaking HTTP or HTTPS. +pub struct ExternalHttpService(Rc); + +impl Channel for ExternalHttpService { + fn start_request(&self, metadata: KjOwn) -> Result> { + Ok(ExternalHttpRequest { + service: Rc::clone(&self.0), + metadata, + } + .into_kj()) + } + + fn token(&self, _usage: TokenUsage) -> Result> { + Err(not_transferable("ExternalService")) + } +} + +/// One event on an [`ExternalHttpService`]. +struct ExternalHttpRequest { + service: Rc, + metadata: KjOwn, +} + +#[async_trait::async_trait(?Send)] +impl Service for ExternalHttpRequest { + async fn request<'a>( + &'a mut self, + method: Method, + url: &'a [u8], + headers: HeadersRef<'a>, + request_body: Pin<&'a mut AsyncInputStream>, + response: ServiceResponse<'a>, + ) -> Result<()> { + let table = header_table(&self.service.factory); + let rewriter = &self.service.rewriter; + let url = std::str::from_utf8(url).map_err(|_| kj::failed!("request URL is not UTF-8"))?; + let rewritten = rewriter.rewrite_outgoing_request( + table, + url, + headers, + cf_blob_json(&self.metadata).as_deref(), + )?; + let headers = rewritten + .headers + .as_deref() + .map_or(headers, HeadersRef::from); + let mut client = fixed_client(table, &self.service.dialer, rewriter.style()); + let mut response = + bridge::rewriting_response(response.into_ffi(), table, rewriter.response_edits())?; + client + .request( + method, + rewritten.url.as_bytes(), + headers, + request_body, + ServiceResponse::from(response.as_mut()), + ) + .await + } + + async fn connect<'a>( + &'a mut self, + host: &'a [u8], + headers: HeadersRef<'a>, + connection: Pin<&'a mut AsyncIoStream>, + response: ConnectResponse<'a>, + settings: ConnectSettings<'a>, + ) -> Result<()> { + let table = header_table(&self.service.factory); + let style = self.service.rewriter.style(); + let mut client = fixed_client(table, &self.service.dialer, style); + let mut tls_starter = settings.tls_starter.into(); + let settings = reborrow_settings(settings.use_tls, &mut tls_starter); + client + .connect(host, headers, connection, response, settings) + .await + } +} + +#[async_trait::async_trait(?Send)] +impl Interface for ExternalHttpRequest { + async fn run_scheduled(&mut self, _time: &SystemTime, _cron: &str) -> Result { + Err(unsupported("External HTTP servers")) + } + + async fn run_alarm(&mut self, _time: &SystemTime, _retry_count: u32) -> Result { + Err(unsupported("External HTTP servers")) + } + + /// Custom events go over capnp RPC to the peer's `WorkerdBootstrap`. + async fn custom_event(&mut self, event: KjOwn) -> Result { + let connection = self.service.rpc().await?; + let result = ffi::rpc_client_custom_event( + &connection.0, + event, + cf_blob_json(&self.metadata).as_deref().into(), + ) + .await?; + Ok(result.into()) + } +} + +// ======================================================================================= +// External TCP + +struct ExternalTcp { + factory: Rc, + dialer: Arc, +} + +/// An `external` service speaking raw TCP: `connect` is its one event. +pub struct ExternalTcpService(Rc); + +impl Channel for ExternalTcpService { + fn start_request(&self, _metadata: KjOwn) -> Result> { + Ok(ExternalTcpRequest(Rc::clone(&self.0)).into_kj()) + } + + fn token(&self, _usage: TokenUsage) -> Result> { + Err(not_transferable("ExternalService")) + } +} + +struct ExternalTcpRequest(Rc); + +#[async_trait::async_trait(?Send)] +impl Service for ExternalTcpRequest { + async fn request<'a>( + &'a mut self, + _method: Method, + _url: &'a [u8], + _headers: HeadersRef<'a>, + _request_body: Pin<&'a mut AsyncInputStream>, + _response: ServiceResponse<'a>, + ) -> Result<()> { + Err(unsupported("External TCP servers")) + } + + /// The tunnel is the connection to the server itself, pumped both ways until either side + /// ends. + async fn connect<'a>( + &'a mut self, + _host: &'a [u8], + _headers: HeadersRef<'a>, + connection: Pin<&'a mut AsyncIoStream>, + response: ConnectResponse<'a>, + _settings: ConnectSettings<'a>, + ) -> Result<()> { + let stream = self.0.dialer.dial().await.map_err(|e| io_kj_error(&e))?; + response.accept(200, "OK", &Headers::new(header_table(&self.0.factory)))?; + Ok(kj_hyper::ffi::pump_tunnel(connection, stream.into_kj()).await?) + } +} + +#[async_trait::async_trait(?Send)] +impl Interface for ExternalTcpRequest { + async fn run_scheduled(&mut self, _time: &SystemTime, _cron: &str) -> Result { + Err(unsupported("External TCP servers")) + } + + async fn run_alarm(&mut self, _time: &SystemTime, _retry_count: u32) -> Result { + Err(unsupported("External TCP servers")) + } +} + +// ======================================================================================= +// Network + +struct Network { + factory: Rc, + filter: Arc, + /// None when the config has no `tlsOptions`: `https` URLs then fail. + tls: Option>, +} + +/// A `network` service: connections to whatever host a request names, within the filter. +pub struct NetworkService(Rc); + +impl Channel for NetworkService { + fn start_request(&self, _metadata: KjOwn) -> Result> { + Ok(NetworkRequest(Rc::clone(&self.0)).into_kj()) + } + + fn token(&self, _usage: TokenUsage) -> Result> { + Err(not_transferable("NetworkService")) + } +} + +struct NetworkRequest(Rc); + +impl NetworkRequest { + fn client(&self) -> Client<'_> { + let filter = Arc::clone(&self.0.filter); + let loopback = self.0.factory.loopback().clone(); + let connect = move |host: String, port| { + let filter = Arc::clone(&filter); + let loopback = loopback.clone(); + async move { + if loopback.mocks_internet() { + let name = if port == 80 { + host + } else { + format!("{host}:{port}") + }; + return Ok(loopback.connect(&name).map_err(refused)?.into()); + } + let stream = connect_allowed(&host, port, |peer| filter.allows(peer.ip())).await?; + Ok(Dialed::from(stream)) + } + }; + Client::internet( + header_table(&self.0.factory), + client_settings(), + self.0.tls.clone(), + connect, + ) + } +} + +#[async_trait::async_trait(?Send)] +impl Service for NetworkRequest { + async fn request<'a>( + &'a mut self, + method: Method, + url: &'a [u8], + headers: HeadersRef<'a>, + request_body: Pin<&'a mut AsyncInputStream>, + response: ServiceResponse<'a>, + ) -> Result<()> { + let mut client = self.client(); + client + .request(method, url, headers, request_body, response) + .await + } + + async fn connect<'a>( + &'a mut self, + host: &'a [u8], + headers: HeadersRef<'a>, + connection: Pin<&'a mut AsyncIoStream>, + response: ConnectResponse<'a>, + settings: ConnectSettings<'a>, + ) -> Result<()> { + let mut client = self.client(); + let mut tls_starter = settings.tls_starter.into(); + let settings = reborrow_settings(settings.use_tls, &mut tls_starter); + client + .connect(host, headers, connection, response, settings) + .await + } +} + +#[async_trait::async_trait(?Send)] +impl Interface for NetworkRequest { + async fn run_scheduled(&mut self, _time: &SystemTime, _cron: &str) -> Result { + Err(unsupported("External HTTP servers")) + } + + async fn run_alarm(&mut self, _time: &SystemTime, _retry_count: u32) -> Result { + Err(unsupported("External HTTP servers")) + } +} + +// ======================================================================================= +// Construction + +fn tls_config(conf: workerd_capnp::tls_options::Reader<'_>) -> Result> { + kj_hyper::tls::client_config(&tls_options(conf)?) +} + +/// An `external` service: an HTTP or TCP server elsewhere. `address_override` is the CLI's +/// `--external-addr` for this service. +pub fn make_external_service( + name: &str, + conf: external_server::Reader<'_>, + address_override: Option<&str>, + factory: &Rc, +) -> Result> { + let address = match address_override { + Some(address) => address.to_owned(), + None if conf.has_address() => text(conf.get_address())?, + None => { + return Err(kj::failed!( + "External service \"{name}\" has no address in the config, so must be specified \ + on the command line with `--external-addr`." + )); + } + }; + let http = |rewriter: HttpRewriter, dialer: Dialer| -> Rc { + Rc::new(ExternalHttpService(Rc::new(ExternalHttp { + factory: Rc::clone(factory), + dialer: Arc::new(dialer), + rewriter, + rpc: RefCell::new(None), + }))) + }; + match conf.which() { + Ok(external_server::Which::Http(options)) => { + let rewriter = HttpRewriter::new(options.map_err(capnp_error)?)?; + Ok(http( + rewriter, + Dialer::new(&address, 80, None, factory.loopback()), + )) + } + Ok(external_server::Which::Https(https)) => { + let rewriter = HttpRewriter::new(https.get_options().map_err(capnp_error)?)?; + let tls = tls_config(https.get_tls_options().map_err(capnp_error)?)?; + let host = optional_text(https.has_certificate_host(), https.get_certificate_host())?; + Ok(http( + rewriter, + Dialer::new(&address, 443, Some((tls, host)), factory.loopback()), + )) + } + Ok(external_server::Which::Tcp(tcp)) => { + let tls = if tcp.has_tls_options() { + let host = optional_text(tcp.has_certificate_host(), tcp.get_certificate_host())?; + Some(( + tls_config(tcp.get_tls_options().map_err(capnp_error)?)?, + host, + )) + } else { + None + }; + Ok(Rc::new(ExternalTcpService(Rc::new(ExternalTcp { + factory: Rc::clone(factory), + dialer: Arc::new(Dialer::new(&address, 80, tls, factory.loopback())), + })))) + } + Err(capnp::NotInSchema(_)) => Err(kj::failed!( + "External service named \"{name}\" has unrecognized protocol. Was the config compiled \ + with a newer version of the schema?" + )), + } +} + +fn network_service( + factory: &Rc, + filter: PeerFilter, + tls: Option>, +) -> Rc { + Rc::new(NetworkService(Rc::new(Network { + factory: Rc::clone(factory), + filter: Arc::new(filter), + tls, + }))) +} + +fn texts(list: capnp::Result>) -> Result> { + list.map_err(capnp_error)?.iter().map(text).collect() +} + +/// A `network` service: outbound connections to the addresses `conf` allows. +pub fn make_network_service( + conf: workerd_capnp::network::Reader<'_>, + factory: &Rc, +) -> Result> { + let allow = texts(conf.get_allow())?; + let deny = texts(conf.get_deny())?; + let filter = PeerFilter::new( + allow.iter().map(String::as_str), + deny.iter().map(String::as_str), + )?; + let tls = if conf.has_tls_options() { + Some(tls_config(conf.get_tls_options().map_err(capnp_error)?)?) + } else { + None + }; + Ok(network_service(factory, filter, tls)) +} + +/// The service the config gets when it defines none named "internet": the public network +/// (`allow = ["public"]`) with TLS through the system trust store. +pub fn make_default_network_service(factory: &Rc) -> Result> { + let filter = PeerFilter::new(["public"], [])?; + let tls = kj_hyper::tls::client_config(&kj_hyper::tls::TlsOptions { + trust_browser_cas: true, + ..kj_hyper::tls::TlsOptions::default() + })?; + Ok(network_service(factory, filter, Some(tls))) +} diff --git a/src/workerd/server/server/services/network-test.rs b/src/workerd/server/server/services/network-test.rs new file mode 100644 index 00000000000..11e4798d83e --- /dev/null +++ b/src/workerd/server/server/services/network-test.rs @@ -0,0 +1,91 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +use capnp::message::Builder; + +use super::*; + +fn ip(text: &str) -> IpAddr { + text.parse().unwrap() +} + +#[test] +fn tls_options_map_onto_kj_hyper() { + let mut message = Builder::new_default(); + let mut conf = message.init_root::>(); + conf.set_trust_browser_cas(true); + conf.set_require_client_certs(true); + conf.set_min_version(tls_options::Version::Tls1Dot3); + conf.reborrow().init_trusted_certificates(1).set(0, "cert"); + let mut keypair = conf.reborrow().init_keypair(); + keypair.set_private_key("key"); + keypair.set_certificate_chain("chain"); + let options = tls_options(message.get_root_as_reader().unwrap()).unwrap(); + assert!(options.trust_browser_cas); + assert!(options.require_client_certs); + assert_eq!(options.min_version, MinVersion::Tls13); + assert_eq!(options.trusted_certificates, ["cert"]); + let keypair = options.keypair.unwrap(); + assert_eq!( + ( + keypair.private_key.as_str(), + keypair.certificate_chain.as_str() + ), + ("key", "chain") + ); + // rustls speaks no TLS below 1.2. + let mut conf = message.get_root::>().unwrap(); + conf.set_min_version(tls_options::Version::Tls1Dot0); + let options = tls_options(message.get_root_as_reader().unwrap()).unwrap(); + assert_eq!(options.min_version, MinVersion::Tls12); +} + +#[test] +fn public_excludes_private_local_and_reserved() { + let filter = PeerFilter::new(["public"], []).unwrap(); + assert!(filter.allows(ip("1.1.1.1"))); + assert!(filter.allows(ip("2606:4700::1111"))); + assert!(!filter.allows(ip("10.0.0.1"))); + assert!(!filter.allows(ip("127.0.0.1"))); + assert!(!filter.allows(ip("0.0.0.0"))); + assert!(!filter.allows(ip("224.0.0.1"))); + assert!(!filter.allows(ip("fe80::1"))); + assert!(!filter.allows(ip("::ffff:192.168.1.1"))); +} + +#[test] +fn private_includes_local_and_network_excludes_it() { + let private = PeerFilter::new(["private"], []).unwrap(); + assert!(private.allows(ip("10.0.0.1"))); + assert!(private.allows(ip("127.0.0.1"))); + assert!(!private.allows(ip("1.1.1.1"))); + let network = PeerFilter::new(["network"], []).unwrap(); + assert!(network.allows(ip("10.0.0.1"))); + assert!(network.allows(ip("1.1.1.1"))); + assert!(!network.allows(ip("127.0.0.1"))); + assert!(!network.allows(ip("255.255.255.255"))); +} + +#[test] +fn a_deny_wins_unless_a_more_specific_allow_covers_the_address() { + let filter = PeerFilter::new(["private", "10.1.0.0/16"], ["10.0.0.0/8", "local"]).unwrap(); + assert!(!filter.allows(ip("10.2.3.4"))); + assert!(filter.allows(ip("10.1.3.4"))); + assert!(filter.allows(ip("192.168.0.1"))); + assert!(!filter.allows(ip("127.0.0.1"))); + let public = PeerFilter::new(["public"], ["1.1.1.0/24"]).unwrap(); + assert!(!public.allows(ip("1.1.1.1"))); + assert!(public.allows(ip("1.0.0.1"))); +} + +#[test] +fn denying_network_or_public_is_refused_and_unix_rules_are_ignored() { + assert!(PeerFilter::new(["public"], ["network"]).is_err()); + assert!(PeerFilter::new(["network"], ["public"]).is_err()); + assert!(PeerFilter::new(["10.0.0.0/33"], []).is_err()); + assert!(PeerFilter::new([], ["10.0.0.0"]).is_err()); + let filter = PeerFilter::new(["unix", "local"], ["unix-abstract"]).unwrap(); + assert!(filter.allows(ip("127.0.0.1"))); + assert!(!filter.allows(ip("1.1.1.1"))); +} diff --git a/src/workerd/server/server/services/network.rs b/src/workerd/server/server/services/network.rs new file mode 100644 index 00000000000..20811a59c1f --- /dev/null +++ b/src/workerd/server/server/services/network.rs @@ -0,0 +1,190 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! The pure parts of the outbound services: the config's `TlsOptions` mapped onto kj-hyper's, +//! and the peer filter a `network` service's `allow` and `deny` lists describe. + +use std::net::IpAddr; + +use ipnet::IpNet; +use kj_hyper::tls::Keypair; +use kj_hyper::tls::MinVersion; +use kj_hyper::tls::TlsOptions; +use workerd_capnp::tls_options; + +use crate::Result; +use crate::config::capnp_error; +use crate::config::text; + +/// kj-hyper's TLS options from the config's. +pub fn tls_options(conf: tls_options::Reader<'_>) -> Result { + let keypair = if conf.has_keypair() { + let keypair = conf.get_keypair().map_err(capnp_error)?; + Some(Keypair { + certificate_chain: text(keypair.get_certificate_chain())?, + private_key: text(keypair.get_private_key())?, + }) + } else { + None + }; + let trusted_certificates = conf + .get_trusted_certificates() + .map_err(capnp_error)? + .iter() + .map(text) + .collect::>>()?; + // rustls speaks TLS 1.2 and 1.3; every floor the config can name below 1.2 is 1.2. + let min_version = match conf.get_min_version() { + Ok(tls_options::Version::Tls1Dot3) => MinVersion::Tls13, + Ok(_) => MinVersion::Tls12, + Err(capnp::NotInSchema(_)) => { + return Err(kj::failed!( + "Encountered unknown TlsOptions::minVersion setting. Was the config compiled with \ + a newer version of the schema?" + )); + } + }; + Ok(TlsOptions { + keypair, + trusted_certificates, + require_client_certs: conf.get_require_client_certs(), + trust_browser_cas: conf.get_trust_browser_cas(), + min_version, + }) +} + +// ======================================================================================= +// Peer filter + +/// An address range, `address/bits`. +fn cidr(pattern: &str) -> Result { + pattern + .parse() + .map_err(|_| kj::failed!("invalid CIDR: {pattern}")) +} + +fn cidrs(patterns: &[&str]) -> Result> { + patterns.iter().map(|pattern| cidr(pattern)).collect() +} + +/// Whether `range` covers `address`. An IPv4 range also covers the IPv4-mapped IPv6 addresses +/// of its members, as `kj::CidrRange` does. +fn covers(range: &IpNet, address: IpAddr) -> bool { + range.contains(&address) || range.contains(&address.to_canonical()) +} + +/// localhost, and 0.0.0.0 / ::, which connect to localhost on many systems. +const LOCAL: &[&str] = &["127.0.0.0/8", "::1/128", "0.0.0.0/32", "::/128"]; + +/// RFC1918 and RFC4193 private networks, RFC6598 shared address space, link-local ranges. +const PRIVATE: &[&str] = &[ + "10.0.0.0/8", + "100.64.0.0/10", + "169.254.0.0/16", + "172.16.0.0/12", + "192.168.0.0/16", + "fc00::/7", + "fe80::/10", +]; + +/// Ranges reserved for other protocols: part of neither "public", "private", "network" nor +/// "local", though a config may allow them by CIDR. +const RESERVED: &[&str] = &[ + "192.0.0.0/24", + "224.0.0.0/4", + "240.0.0.0/4", + "255.255.255.255/32", + "2001::/23", + "ff00::/8", +]; + +/// Which peers a `network` service may connect to: `kj::_::NetworkFilter`'s rules over the +/// config's `allow` and `deny` lists. +/// +/// An address is allowed when an allow rule covers it and no deny rule at least as specific +/// does; "public" and "network" count as the least specific rules. "unix" and "unix-abstract" +/// are accepted and have no effect: the network client dials IP addresses only. +#[derive(Debug)] +pub struct PeerFilter { + allow_public: bool, + allow_network: bool, + allow: Vec, + deny: Vec, + local: Vec, + private: Vec, + reserved: Vec, +} + +impl PeerFilter { + pub fn new<'a>( + allow: impl IntoIterator, + deny: impl IntoIterator, + ) -> Result { + let mut filter = Self { + allow_public: false, + allow_network: false, + allow: Vec::new(), + deny: Vec::new(), + local: cidrs(LOCAL)?, + private: cidrs(PRIVATE)?, + reserved: cidrs(RESERVED)?, + }; + for rule in allow { + match rule { + "local" => filter.allow.extend(&filter.local), + "network" => filter.allow_network = true, + "private" => { + filter.allow.extend(&filter.private); + filter.allow.extend(&filter.local); + } + "public" => filter.allow_public = true, + "unix" | "unix-abstract" => {} + rule => filter.allow.push(cidr(rule)?), + } + } + for rule in deny { + match rule { + "local" => filter.deny.extend(&filter.local), + "network" => { + return Err(kj::failed!("don't deny 'network', allow 'local' instead")); + } + "private" => filter.deny.extend(&filter.private), + "public" => { + return Err(kj::failed!("don't deny 'public', allow 'private' instead")); + } + "unix" | "unix-abstract" => {} + rule => filter.deny.push(cidr(rule)?), + } + } + Ok(filter) + } + + #[must_use] + pub fn allows(&self, address: IpAddr) -> bool { + let any = |ranges: &[IpNet]| ranges.iter().any(|range| covers(range, address)); + let mut allowed = false; + let mut allow_specificity = 0; + if self.allow_public && !any(&self.private) && !any(&self.local) && !any(&self.reserved) { + allowed = true; + } + if self.allow_network && !any(&self.local) && !any(&self.reserved) { + allowed = true; + } + for range in &self.allow { + if covers(range, address) { + allow_specificity = allow_specificity.max(range.prefix_len()); + allowed = true; + } + } + allowed + && !self + .deny + .iter() + .any(|range| covers(range, address) && range.prefix_len() >= allow_specificity) + } +} + +#[cfg(test)] +#[path = "network-test.rs"] +mod tests; diff --git a/src/workerd/server/server/services/rewriter-test.rs b/src/workerd/server/server/services/rewriter-test.rs new file mode 100644 index 00000000000..656a6e965ae --- /dev/null +++ b/src/workerd/server/server/services/rewriter-test.rs @@ -0,0 +1,106 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +use capnp::message::Builder; +use kj::http::Headers; + +use super::*; + +fn options(build: impl FnOnce(http_options::Builder<'_>)) -> HttpRewriter { + let mut message = Builder::new_default(); + build(message.init_root::>()); + HttpRewriter::new( + message + .get_root_as_reader::>() + .unwrap(), + ) + .unwrap() +} + +fn header(headers: &ffi::HttpHeaders, id: HeaderId) -> Option> { + HeadersRef::from(headers).get(id).map(<[u8]>::to_vec) +} + +#[test] +fn host_style_outgoing_moves_the_host_into_its_header() { + let table = HeaderTable::builtin(); + let rewriter = options(|mut o| o.set_style(http_options::Style::Host)); + let mut headers = Headers::new(&table); + headers.set(HeaderId::CONTENT_TYPE, "text/plain"); + let rewritten = rewriter + .rewrite_outgoing_request( + &table, + "https://user:secret@example.com:8443/a/b?c=d", + headers.as_ref(), + None, + ) + .unwrap(); + assert_eq!(rewritten.url, "/a/b?c=d"); + let edited = rewritten.headers.unwrap(); + assert_eq!( + header(&edited, HeaderId::HOST).as_deref(), + Some(&b"example.com:8443"[..]) + ); + assert_eq!( + header(&edited, HeaderId::CONTENT_TYPE).as_deref(), + Some(&b"text/plain"[..]) + ); +} + +#[test] +fn host_style_incoming_rebuilds_the_absolute_url() { + let table = HeaderTable::builtin(); + let rewriter = options(|mut o| o.set_style(http_options::Style::Host)); + let mut headers = Headers::new(&table); + headers.set(HeaderId::HOST, "foo.example"); + let (rewritten, cf_blob) = rewriter + .rewrite_incoming_request(&table, "/x?y=1", "https", headers.as_ref()) + .unwrap() + .unwrap(); + assert_eq!(rewritten.url, "https://foo.example/x?y=1"); + // Nothing to change in the headers, so they are the request's own. + assert!(rewritten.headers.is_none()); + assert_eq!(cf_blob, None); +} + +#[test] +fn host_style_incoming_without_a_host_is_a_bad_request() { + let table = HeaderTable::builtin(); + let rewriter = options(|mut o| o.set_style(http_options::Style::Host)); + let headers = Headers::new(&table); + assert!( + rewriter + .rewrite_incoming_request(&table, "/", "http", headers.as_ref()) + .unwrap() + .is_none() + ); +} + +#[test] +fn proxy_style_leaves_the_request_alone() { + let table = HeaderTable::builtin(); + let rewriter = options(|mut o| o.set_style(http_options::Style::Proxy)); + let headers = Headers::new(&table); + let rewritten = rewriter + .rewrite_outgoing_request(&table, "http://h/p", headers.as_ref(), None) + .unwrap(); + assert_eq!(rewritten.url, "http://h/p"); + assert!(rewritten.headers.is_none()); +} + +#[test] +fn a_header_the_table_lacks_is_refused() { + // The factory's table has every header the config names; this one has only kj's own. + let table = HeaderTable::builtin(); + let rewriter = options(|mut o| { + o.set_style(http_options::Style::Proxy); + o.init_inject_request_headers(1).get(0).set_name("X-Add"); + }); + let headers = Headers::new(&table); + assert!( + rewriter + .rewrite_outgoing_request(&table, "http://h/", headers.as_ref(), None) + .is_err() + ); +} diff --git a/src/workerd/server/server/services/rewriter.rs b/src/workerd/server/server/services/rewriter.rs new file mode 100644 index 00000000000..1a76bb891fa --- /dev/null +++ b/src/workerd/server/server/services/rewriter.rs @@ -0,0 +1,240 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! The `HttpOptions` of a socket or an external server applied to the traffic through it. +//! +//! The options are the URL style (proxy-form URLs with the host inside, or origin-form URLs with +//! a `Host` header), the cf blob header, injected headers, and the host that carries capnp RPC +//! over HTTP CONNECT. +//! +//! Headers stay `kj::HttpHeaders` throughout. Every header the options name is in the factory's +//! header table, so the rewriter says what to set or remove by name (`HeaderEdit`) and the +//! factory applies it by table id to a copy; kj then writes those headers where it writes the +//! table's, spelled as the config spells them. + +use http::Uri; +use kj::http::HeaderId; +use kj::http::HeaderTable; +use kj::http::HeadersRef; +use kj_rs::KjOwn; +use workerd_capnp::http_options; + +use crate::Result; +use crate::bridge::ffi; +use crate::config::capnp_error; +use crate::config::optional_text; +use crate::config::text; + +/// How URLs travel: whole, as a proxy is asked for them, or as a path with the host in the +/// `Host` header. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Style { + Host, + Proxy, +} + +pub struct HttpRewriter { + style: Style, + forwarded_proto_header: Option, + cf_blob_header: Option, + capnp_connect_host: Option, + request_headers: Vec, + response_headers: Vec, +} + +/// A request as the options leave it. +pub struct Rewritten { + pub url: String, + /// The headers to use in place of the request's, when the options change any. + pub headers: Option>, +} + +fn injected( + headers: capnp::struct_list::Reader<'_, http_options::header::Owned>, +) -> Result> { + headers + .iter() + .map(|header| { + Ok(ffi::HeaderEdit { + name: text(header.get_name())?, + value: optional_text(header.has_value(), header.get_value())?.into(), + }) + }) + .collect() +} + +fn edit(name: &str, value: Option<&str>) -> ffi::HeaderEdit { + ffi::HeaderEdit { + name: name.to_owned(), + value: value.map(str::to_owned).into(), + } +} + +impl HttpRewriter { + pub fn new(options: http_options::Reader<'_>) -> Result { + Ok(Self { + style: match options.get_style() { + Ok(http_options::Style::Host) => Style::Host, + Ok(http_options::Style::Proxy) => Style::Proxy, + Err(capnp::NotInSchema(_)) => { + return Err(kj::failed!( + "Encountered unknown HttpOptions::style setting. Was the config compiled \ + with a newer version of the schema?" + )); + } + }, + forwarded_proto_header: optional_text( + options.has_forwarded_proto_header(), + options.get_forwarded_proto_header(), + )?, + cf_blob_header: optional_text( + options.has_cf_blob_header(), + options.get_cf_blob_header(), + )?, + capnp_connect_host: optional_text( + options.has_capnp_connect_host(), + options.get_capnp_connect_host(), + )?, + request_headers: injected(options.get_inject_request_headers().map_err(capnp_error)?)?, + response_headers: injected( + options.get_inject_response_headers().map_err(capnp_error)?, + )?, + }) + } + + #[must_use] + pub const fn style(&self) -> Style { + self.style + } + + #[must_use] + pub const fn has_cf_blob_header(&self) -> bool { + self.cf_blob_header.is_some() + } + + #[must_use] + pub fn capnp_connect_host(&self) -> Option<&str> { + self.capnp_connect_host.as_deref() + } + + /// The edits `injectResponseHeaders` makes to a response's headers. + #[must_use] + pub fn response_edits(&self) -> &[ffi::HeaderEdit] { + &self.response_headers + } + + /// `headers` with `edits` and then the injected request headers applied; none when that + /// changes nothing. + fn edited( + &self, + table: &HeaderTable, + headers: HeadersRef<'_>, + edits: &[ffi::HeaderEdit], + ) -> Result>> { + let injected = &self.request_headers; + if edits.is_empty() && injected.is_empty() { + return Ok(None); + } + let edited = ffi::edit_headers(table, headers.as_ffi(), edits, injected)?; + Ok(Some(edited)) + } + + /// Rewrites a request leaving for an external server. `url` is what the worker asked for: a + /// proxy-form URL. + pub fn rewrite_outgoing_request( + &self, + table: &HeaderTable, + url: &str, + headers: HeadersRef<'_>, + cf_blob_json: Option<&str>, + ) -> Result { + let mut url = url.to_owned(); + let mut edits = Vec::new(); + if self.style == Style::Host { + let parsed: Uri = url + .parse() + .map_err(|_| kj::failed!("invalid outgoing request URL: {url}"))?; + let authority = parsed + .authority() + .ok_or_else(|| kj::failed!("outgoing request URL has no host: {url}"))? + .as_str(); + // The host and port, as `kj::Url`'s host: credentials stay out of the header. + let host = authority + .rsplit_once('@') + .map_or(authority, |(_, host)| host); + edits.push(edit("Host", Some(host))); + if let Some(forwarded_proto) = &self.forwarded_proto_header { + let scheme = parsed.scheme_str().unwrap_or("http"); + edits.push(edit(forwarded_proto, Some(scheme))); + } + parsed + .path_and_query() + .map_or("/", http::uri::PathAndQuery::as_str) + .clone_into(&mut url); + } + if let Some(cf_blob) = &self.cf_blob_header { + edits.push(edit(cf_blob, cf_blob_json)); + } + let headers = self.edited(table, headers, &edits)?; + Ok(Rewritten { url, headers }) + } + + /// Rewrites a request arriving on a socket. `url` is what the client sent; `physical_protocol` + /// is the socket's ("http" or "https"). Returns the request to give the worker and the cf + /// blob it carried, if the options name a header for it; none for a request the options can + /// make no sense of, to be answered with 400. + pub fn rewrite_incoming_request( + &self, + table: &HeaderTable, + url: &str, + physical_protocol: &str, + headers: HeadersRef<'_>, + ) -> Result)>> { + let mut url = url.to_owned(); + let mut edits = Vec::new(); + if self.style == Style::Host { + let Ok(parsed) = url.parse::() else { + return Ok(None); + }; + let Some(host) = headers + .get(HeaderId::HOST) + .and_then(|host| std::str::from_utf8(host).ok()) + else { + return Ok(None); + }; + let mut scheme = physical_protocol; + if let Some(header) = &self.forwarded_proto_header + && let Some(forwarded) = headers.get_by_name(header) + { + let Ok(forwarded) = std::str::from_utf8(forwarded) else { + return Ok(None); + }; + scheme = forwarded; + edits.push(edit(header, None)); + } + let path = parsed + .path_and_query() + .map_or("/", http::uri::PathAndQuery::as_str); + url = format!("{scheme}://{host}{path}"); + } + + let mut cf_blob_json = None; + if let Some(header) = &self.cf_blob_header + && let Some(blob) = headers.get_by_name(header) + { + let Ok(blob) = std::str::from_utf8(blob) else { + return Ok(None); + }; + cf_blob_json = Some(blob.to_owned()); + edits.push(edit(header, None)); + } + + let headers = self.edited(table, headers, &edits)?; + Ok(Some((Rewritten { url, headers }, cf_blob_json))) + } +} + +#[cfg(test)] +#[path = "rewriter-test.rs"] +mod tests; diff --git a/src/workerd/server/server/tasks.rs b/src/workerd/server/server/tasks.rs new file mode 100644 index 00000000000..9b2b9063729 --- /dev/null +++ b/src/workerd/server/server/tasks.rs @@ -0,0 +1,59 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! The server's background tasks: futures no request owns (an actor's on-broken monitor, its +//! idle shutdown, a namespace's container cleanup, a dropped stub's deferred unlink, the +//! `workerd test` loop). +//! +//! They run in the worker factory's `kj::TaskSet`, polled by the KJ event loop like every other +//! future the server hands C++, so they are ordered with the events they react to: a monitor +//! sees its actor break before the failed request's reply reaches the caller. They also end +//! with the server (its drop clears them; `Factory::settle_tasks` ends the ones the drop +//! spawned), before V8 goes away. Neither holds for tasks on the tokio `LocalSet` +//! (`kj_rs_tokio::spawn`), which run only once the KJ loop idles and live until the event port is +//! torn down. + +use std::future::Future; + +use futures::FutureExt; +use futures::future::LocalBoxFuture; +use futures::future::RemoteHandle; + +use crate::Result; +use crate::bridge::ffi; +use crate::config::Factory; + +/// A spawned task's owner. Dropping the handle cancels the task: its future is dropped at the +/// task's next poll, never mid-poll, so a task may drop its own handle. +pub type TaskHandle = RemoteHandle<()>; + +/// The future `ffi::factory_spawn` hands to the C++ task set, polled there by `task_run`. +pub struct SpawnedTask(LocalBoxFuture<'static, ()>); + +/// Runs a spawned task to its end; the C++ side awaits this as the task's promise. +pub async fn task_run(task: Box) -> Result<()> { + task.0.await; + Ok(()) +} + +impl Factory { + /// Spawns `future` as a background task of the run; it starts on the next turn of the event + /// loop. The returned handle resolves to its output, and cancels it when dropped. + pub fn spawn(&self, future: F) -> RemoteHandle { + let (task, handle) = future.remote_handle(); + self.spawn_detached(task); + handle + } + + /// Spawns `future` to run until it ends or the run does. + pub fn spawn_detached(&self, future: impl Future + 'static) { + ffi::factory_spawn(self.raw(), Box::new(SpawnedTask(future.boxed_local()))); + } + + /// Ends the background tasks the server's drop spawned (a dropped stub unlinks its worker + /// on the next turn): they run, and whatever remains is dropped. + pub async fn settle_tasks(&self) -> Result<()> { + Ok(ffi::factory_settle_tasks(self.raw()).await?) + } +} diff --git a/src/workerd/server/server/worker.rs b/src/workerd/server/server/worker.rs new file mode 100644 index 00000000000..2d526ac5d1f --- /dev/null +++ b/src/workerd/server/server/worker.rs @@ -0,0 +1,1093 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +//! A worker service: a compiled worker, its entrypoints and actor classes, and the I/O channel +//! table its requests see. +//! +//! The service is built in two steps. `WorkerService::new` compiles the worker through the +//! factory and keeps, as a [`PendingLink`], every designator its bindings name; once every +//! service exists the server resolves those into a [`LinkedChannels`] table and calls `link`. +//! Until then the service starts no requests. + +use std::cell::RefCell; +use std::collections::HashSet; +use std::ptr; +use std::rc::Rc; +use std::rc::Weak; + +use hashlink::LinkedHashMap; +use kj_rs::KjMaybe; +use kj_rs::KjOwn; +use workerd_capnp::service_designator; +use workerd_capnp::worker; + +use crate::Result; +use crate::actor::ActorNamespace; +use crate::bindings::ActorConfig; +use crate::bindings::ActorConfigMap; +use crate::bindings::ActorConfigs; +use crate::bindings::CompiledBindings; +use crate::bindings::Designator; +use crate::bindings::Exports; +use crate::bindings::LoopbackGlobals; +use crate::bindings::SPECIAL_SUBREQUEST_CHANNEL_COUNT; +use crate::bindings::compile_bindings; +use crate::bindings::len_u32; +use crate::bindings::loopback_globals; +use crate::bridge::ffi; +use crate::channels::ActorClass; +use crate::channels::ActorClassChannel; +use crate::channels::ActorIdHandle; +use crate::channels::Channel; +use crate::channels::ChannelFactory; +use crate::channels::DynamicSource; +use crate::channels::Frankenvalue; +use crate::channels::NewActor; +use crate::channels::PendingToken; +use crate::channels::Persistent; +use crate::channels::RequestMetadata; +use crate::channels::SubrequestChannel; +use crate::channels::Tail; +use crate::channels::TokenUsage; +use crate::channels::WorkerEntrypoint; +use crate::channels::WorkerInterface; +use crate::channels::WorkerInterfaceList; +use crate::channels::WorkerStub; +use crate::config::Factory; +use crate::config::Reporter; +use crate::config::Server; +use crate::config::capnp_error; +use crate::config::text; +use crate::loader::WorkerLoaderNamespace; +use crate::loader::dynamic_transfer_error; + +/// What the link stage resolves for a worker: every designator its config names, and the loopback +/// channels `ctx.exports` was numbered with. +pub struct PendingLink { + pub global_outbound: Designator, + pub cache_api_outbound: Option, + pub bindings: CompiledBindings, + pub loopback: LoopbackGlobals, + pub tails: Vec, + pub streaming_tails: Vec, + /// `durableObjectStorage.localDisk`: the disk service whose directory holds the storage. + pub storage: Option, + /// `accessBindingService`: the worker whose entrypoint receives the request's Access identity. + pub access_binding: Option, +} + +/// The I/O channel table of a linked worker: what each channel number its bindings hold reaches. +/// One per worker, shared by every request. The runtime's `IoChannelFactory`. +/// +/// A dynamic worker's `env` channels are the factory's own (worker-factory-impl.h); its table +/// here holds only the channels numbered after them, its `ctx.exports`, and `first_subrequest` +/// / `first_actor_class` say where it starts. A config worker's table starts at zero. +pub struct LinkedChannels { + pub first_subrequest: u32, + /// Indexed by channel number less `first_subrequest`: the two global-outbound slots, the + /// bindings' channels, then the loopback entrypoints (and the access binding's entrypoint, + /// if any). + pub subrequest: Vec>, + /// Indexed by actor channel; `None` when the binding's config was invalid. + pub actor: Vec>>, + pub first_actor_class: u32, + /// Indexed by actor class channel less `first_actor_class`. + pub actor_class: Vec>, + pub cache: Option>, + pub tails: Vec>, + pub streaming_tails: Vec>, + pub worker_loaders: Vec>, + /// The subrequest channel whose props come from the request's Access blob. + pub access_binding_channel: Option, + pub has_debug_port: bool, + pub server: Weak, + /// How a dynamic worker unloads itself; a static worker aborting its isolate ends the + /// process. + pub abort_isolate: Option>, +} + +/// A compiled worker and what the service graph hangs off it. +pub struct WorkerService { + factory: Rc, + /// The service's name in the config; none for a dynamic worker, which no token can name. + service_name: Option, + worker: KjOwn, + /// The default export's handlers, if the worker has a default export. + default_handlers: Option>, + /// Named exports with their handlers, in export order (loopback channels follow it). + /// Workflow classes are among them. + named_entrypoints: LinkedHashMap>, + /// Exported Durable Object classes, in export order. + actor_classes: Vec, + /// Exported `WorkflowEntrypoint` classes, which `named_entrypoints` also lists (a Workflow + /// class is a stateless entrypoint at runtime). + workflow_classes: Vec, + /// `durableObjectStorage.localDisk`'s disk service, which a Workflow's namespace takes its + /// storage from. + storage: Option, + /// The `allow_irrevocable_stub_storage` flag: whether tokens to this worker may be stored. + persistent_self_tokens: bool, + pending: RefCell>, + channels: RefCell>>, + /// The namespaces of the classes the config gives storage to, by class name, in config + /// order. Cleared by `unlink`: each namespace's class refers back to this service. + namespaces: RefCell>>, +} + +impl WorkerService { + /// Compiles the worker `conf` (the config's service `service_index`, named `name`) and + /// prepares its link. Config errors are reported, not returned: a worker with errors still + /// exists so that the rest of the config's errors can be found. + pub async fn new( + factory: Rc, + name: &str, + service_index: u32, + conf: worker::Reader<'_>, + actor_configs: &ActorConfigs, + inbound_listeners: Vec, + report: &Reporter, + ) -> Result> { + let experimental = factory.experimental(); + let empty = ActorConfigMap::new(); + let local_actor_configs = actor_configs.get(name).unwrap_or(&empty); + + let mut bindings = + compile_bindings(name, conf, actor_configs, local_actor_configs, experimental) + .map_err(capnp_error)?; + for error in std::mem::take(&mut bindings.errors) { + report.service_error(name, error); + } + + let access_blob_header = if !conf.has_access_blob_header() { + None + } else if experimental { + Some(text(conf.get_access_blob_header())?) + } else { + report.service_error( + name, + format!( + "Worker \"{name}\" has accessBlobHeader configured but this is an experimental \ + feature. You must run workerd with `--experimental` to use this feature." + ), + ); + None + }; + + let spec = ffi::WorkerSpec { + name: name.to_owned(), + inbound_listeners, + globals: std::mem::take(&mut bindings.globals), + access_blob_header: access_blob_header.into(), + }; + let compiled = ffi::factory_new_worker( + factory.raw(), + &spec, + KjMaybe::Some(service_index), + KjMaybe::None, + ) + .await?; + + let info = ffi::worker_info(&compiled); + for error in info.errors { + report.service_error(name, error); + } + for warning in info.warnings { + report.service_warning(name, warning); + } + + let (default_handlers, named_entrypoints) = split_entrypoints(info.entrypoints); + let named_names: Vec = named_entrypoints.keys().cloned().collect(); + let exports = Exports { + has_default_entrypoint: default_handlers.is_some(), + named_entrypoints: &named_names, + workflow_classes: &info.workflow_classes, + actor_classes: &info.actor_classes, + }; + let loopback = loopback_globals( + &exports, + local_actor_configs, + |class| bindings.workflow_binding_channels.get(class).copied(), + len_u32(&bindings.subrequest), + len_u32(&bindings.actors), + len_u32(&bindings.actor_classes), + ) + .map_err(capnp_error)?; + ffi::worker_set_ctx_exports(&compiled, &loopback.globals)?; + + let pending = pending_link(name, conf, bindings, loopback, report).map_err(capnp_error)?; + + let this = Rc::new(Self { + factory, + service_name: Some(name.to_owned()), + worker: compiled, + default_handlers, + named_entrypoints, + actor_classes: info.actor_classes, + workflow_classes: info.workflow_classes, + storage: pending.storage.clone(), + persistent_self_tokens: info.persistent_self_tokens, + pending: RefCell::new(Some(pending)), + channels: RefCell::new(None), + namespaces: RefCell::new(LinkedHashMap::new()), + }); + this.init_namespaces(local_actor_configs); + Ok(this) + } +} + +/// The compiled worker's entrypoints, split into the default export's handlers and the named +/// exports' (in export order). +type SplitEntrypoints = ( + Option>, + LinkedHashMap>, +); + +fn split_entrypoints(entrypoints: Vec) -> SplitEntrypoints { + let mut default_handlers = None; + let mut named_entrypoints = LinkedHashMap::new(); + for entrypoint in entrypoints { + let handlers: HashSet = entrypoint.handlers.into_iter().collect(); + if entrypoint.is_default { + default_handlers = Some(handlers); + } else { + named_entrypoints.insert(entrypoint.name, handlers); + } + } + (default_handlers, named_entrypoints) +} + +impl WorkerService { + /// Compiles a dynamic worker from `source` and links it at once. The factory keeps the + /// channels the source's `env`, global outbound and tails supply, and serves their numbers + /// itself; the table here is the worker's own `ctx.exports`, numbered after them. A dynamic + /// worker has no config, so no namespaces, no pending link and no name a token could carry; + /// `name` is for error logs. Compilation errors fail the load rather than being reported. + pub async fn new_dynamic( + factory: Rc, + name: String, + source: KjOwn, + server: Weak, + abort_isolate: Box, + ) -> Result> { + let spec = ffi::WorkerSpec { + name, + inbound_listeners: Vec::new(), + globals: Vec::new(), + access_blob_header: KjMaybe::None, + }; + let compiled = + ffi::factory_new_worker(factory.raw(), &spec, KjMaybe::None, KjMaybe::Some(source)) + .await?; + + let info = ffi::worker_info(&compiled); + if !info.errors.is_empty() { + return Err(kj::failed!( + "jsg.Error: Failed to start Worker:\n{}", + info.errors.join("\n") + )); + } + for warning in &info.warnings { + tracing::warn!("{warning}"); + } + + let (default_handlers, named_entrypoints) = split_entrypoints(info.entrypoints); + let named_names: Vec = named_entrypoints.keys().cloned().collect(); + let exports = Exports { + has_default_entrypoint: default_handlers.is_some(), + named_entrypoints: &named_names, + workflow_classes: &info.workflow_classes, + actor_classes: &info.actor_classes, + }; + // `ctx.exports` is numbered after the env's channels, which the factory serves. + let loopback = loopback_globals( + &exports, + &ActorConfigMap::new(), + |_| None, + info.env_subrequest_channels, + 0, + info.env_actor_classes, + ) + .map_err(capnp_error)?; + ffi::worker_set_ctx_exports(&compiled, &loopback.globals)?; + + let this = Rc::new(Self { + factory, + service_name: None, + worker: compiled, + default_handlers, + named_entrypoints, + actor_classes: info.actor_classes, + workflow_classes: info.workflow_classes, + storage: None, + persistent_self_tokens: info.persistent_self_tokens, + pending: RefCell::new(None), + channels: RefCell::new(None), + namespaces: RefCell::new(LinkedHashMap::new()), + }); + + let subrequest = loopback + .subrequest_entrypoints + .iter() + .map(|entrypoint| this.loopback_entrypoint(entrypoint.as_deref())) + .collect::>>()?; + let actor_class = loopback + .actor_classes + .iter() + .map(|class_name| this.loopback_actor_class(class_name)) + .collect::>>()?; + this.link( + LinkedChannels { + first_subrequest: info.env_subrequest_channels + SPECIAL_SUBREQUEST_CHANNEL_COUNT, + subrequest, + actor: Vec::new(), + first_actor_class: info.env_actor_classes, + actor_class, + cache: None, + tails: Vec::new(), + streaming_tails: Vec::new(), + worker_loaders: Vec::new(), + access_binding_channel: None, + has_debug_port: false, + server, + abort_isolate: Some(abort_isolate), + }, + None, + )?; + Ok(this) + } +} + +fn designators( + list: capnp::struct_list::Reader<'_, service_designator::Owned>, + context: &str, + errors: &mut Vec, +) -> capnp::Result> { + list.iter() + .map(|reader| Designator::from_reader(reader, context.to_owned(), errors)) + .collect() +} + +/// Everything the link stage needs from `conf`, beyond the bindings. +fn pending_link( + name: &str, + conf: worker::Reader<'_>, + bindings: CompiledBindings, + loopback: LoopbackGlobals, + report: &Reporter, +) -> capnp::Result { + let mut errors = Vec::new(); + let global_outbound = Designator::from_reader( + conf.get_global_outbound()?, + format!("Worker \"{name}\"'s globalOutbound"), + &mut errors, + )?; + let cache_api_outbound = if conf.has_cache_api_outbound() { + Some(Designator::from_reader( + conf.get_cache_api_outbound()?, + format!("Worker \"{name}\"'s cacheApiOutbound"), + &mut errors, + )?) + } else { + None + }; + let tails = designators( + conf.get_tails()?, + &format!("Worker \"{name}\"'s tails"), + &mut errors, + )?; + let streaming_tails = designators( + conf.get_streaming_tails()?, + &format!("Worker \"{name}\"'s streaming tails"), + &mut errors, + )?; + // In-memory storage has no disk; an unknown kind was reported by the config's first pass. + let storage = match conf.get_durable_object_storage().which() { + Ok(worker::durable_object_storage::Which::LocalDisk(disk)) => Some(disk?.to_string()?), + _ => None, + }; + let access_binding = if conf.has_access_binding_service() { + Some(Designator::from_reader( + conf.get_access_binding_service()?, + "Worker accessBindingService".to_owned(), + &mut errors, + )?) + } else { + None + }; + for error in errors { + report.service_error(name, error); + } + Ok(PendingLink { + global_outbound, + cache_api_outbound, + bindings, + loopback, + tails, + streaming_tails, + storage, + access_binding, + }) +} + +impl WorkerService { + /// One namespace per class the config gives storage to. A class without a matching export + /// gets a namespace anyway: calls to it fail at runtime, not at startup. A Workflow's + /// namespace is another worker's class over another worker's storage, created once every + /// service exists (`init_workflow_namespace`). + fn init_namespaces(self: &Rc, configs: &ActorConfigMap) { + let mut namespaces = self.namespaces.borrow_mut(); + for (class_name, config) in configs { + if let ActorConfig::Durable { + workflow: Some(_), .. + } = config + { + continue; + } + if !self.actor_classes.contains(class_name) { + tracing::warn!( + "A DurableObjectNamespace in the config referenced the class \"{class_name}\", \ + but no such Durable Object class is exported from the worker. Please make \ + sure the class name matches, it is exported, and the class extends \ + 'DurableObject'. Attempts to call to this Durable Object class will fail at \ + runtime, but historically this was not a startup-time error. Future versions \ + of workerd may make this a startup-time error." + ); + } + let actor_class: Rc = Rc::new(ActorClassImpl { + worker: Rc::clone(self), + class_name: class_name.clone(), + props: None, + persistent: false, + }); + let namespace = ActorNamespace::new( + Rc::clone(&self.factory), + class_name.clone(), + config.clone(), + actor_class, + self.persistent_self_tokens, + ); + namespaces.insert(class_name.clone(), namespace); + } + } + + /// Creates the namespace backing one configured Workflow, keyed by `key`, its unique key. + /// Its actors are `actor_class`, the engine's class with the Workflow's props bound, and its + /// storage is in `storage_path`, the directory of the Workflow's `bindingService` worker. + /// `persistent_self_tokens` is the engine worker's. + pub fn init_workflow_namespace( + &self, + key: &str, + config: ActorConfig, + actor_class: Rc, + storage_path: Option<&str>, + persistent_self_tokens: Persistent, + ) -> Result> { + let namespace = ActorNamespace::new( + Rc::clone(&self.factory), + key.to_owned(), + config, + actor_class, + persistent_self_tokens, + ); + namespace.link(storage_path)?; + self.namespaces + .borrow_mut() + .insert(key.to_owned(), Rc::clone(&namespace)); + Ok(namespace) + } + + /// The factory the worker was compiled by, for its namespaces' storage, containers and + /// tokens. + #[must_use] + pub fn factory(&self) -> &Rc { + &self.factory + } + + /// The service's config name, which restores a stub to the worker. A dynamic worker has + /// none, so it cannot be reached from another worker as a stub: nothing could reload it from + /// a token. + pub fn transferable_name(&self) -> Result<&str> { + self.service_name + .as_deref() + .ok_or_else(dynamic_transfer_error) + } + + /// Whether channel tokens to this worker may be stored (`allow_irrevocable_stub_storage`). + #[must_use] + pub fn persistent_self_tokens(&self) -> Persistent { + self.persistent_self_tokens + } + + /// Whether the worker exports the Durable Object class `name`. + #[must_use] + pub fn has_actor_class(&self, name: &str) -> bool { + self.actor_classes.iter().any(|class| class == name) + } + + /// Whether the worker exports the `WorkflowEntrypoint` class `name`. + #[must_use] + pub fn has_workflow_class(&self, name: &str) -> bool { + self.workflow_classes.iter().any(|class| class == name) + } + + /// Whether the worker exports a plain stateless entrypoint: the `WorkerEntrypoint` named + /// `name`, or the default export when `name` is none. A Workflow class is not one: a + /// `WorkflowEntrypoint` cannot be, for one, another Workflow's `bindingService`. + #[must_use] + pub fn has_stateless_entrypoint(&self, name: Option<&str>) -> bool { + match name { + Some(name) => { + self.named_entrypoints.contains_key(name) && !self.has_workflow_class(name) + } + None => self.has_default_entrypoint(), + } + } + + /// The disk service of the worker's `localDisk` Durable Object storage, which a Workflow's + /// `bindingService` worker must have to provide the Workflow's storage. + #[must_use] + pub fn storage(&self) -> Option<&str> { + self.storage.as_deref() + } + + #[must_use] + pub fn has_default_entrypoint(&self) -> bool { + self.default_handlers.is_some() + } + + /// Whether `entrypoint` (the default when `None`) exports `handler`, e.g. `fetch` or `test`. + #[must_use] + pub fn has_handler(&self, entrypoint: Option<&str>, handler: &str) -> bool { + let handlers = match entrypoint { + Some(name) => self.named_entrypoints.get(name), + None => self.default_handlers.as_ref(), + }; + handlers.is_some_and(|handlers| handlers.contains(handler)) + } + + /// The named entrypoints, in export order. + pub fn entrypoint_names(&self) -> impl Iterator { + self.named_entrypoints.keys().map(String::as_str) + } + + /// The namespace of `class_name`, if the config gives the class storage. + #[must_use] + pub fn namespace(&self, class_name: &str) -> Option> { + self.namespaces.borrow().get(class_name).cloned() + } + + /// Every namespace, in config order. + #[must_use] + pub fn namespaces(&self) -> Vec<(String, Rc)> { + self.namespaces + .borrow() + .iter() + .map(|(name, namespace)| (name.clone(), Rc::clone(namespace))) + .collect() + } + + /// The channel of an entrypoint (the default when `name` is `None`), with `props` bound. + /// `None` when there is no such named entrypoint. A worker without a default export still + /// yields a channel for the default entrypoint, whose requests fail: it is not a config + /// error, and configs depend on it. + #[must_use] + pub fn entrypoint( + self: &Rc, + name: Option<&str>, + props: Option>, + persistent: Persistent, + ) -> Option> { + let entrypoint = if let Some(name) = name { + // A Durable Object class named as an entrypoint is accepted, with no handlers: + // calls to it fail at runtime. + if !self.named_entrypoints.contains_key(name) + && !self.actor_classes.iter().any(|class| class == name) + { + return None; + } + Some(name.to_owned()) + } else { + None + }; + Some(Rc::new(EntrypointChannel { + worker: Rc::clone(self), + entrypoint, + props, + persistent, + })) + } + + /// The default entrypoint's channel, with no props: the worker as a service. + #[must_use] + pub fn default_entrypoint(self: &Rc) -> Rc { + Rc::new(EntrypointChannel { + worker: Rc::clone(self), + entrypoint: None, + props: None, + persistent: false, + }) + } + + /// The `ctx.exports` channel of an entrypoint: the template its props specialize. + pub fn loopback_entrypoint(self: &Rc, name: Option<&str>) -> Result> { + let exists = match name { + Some(name) => self.named_entrypoints.contains_key(name), + None => self.default_handlers.is_some(), + }; + if !exists { + return Err(kj::failed!( + "getLoopbackEntrypoint() called for entrypoint that doesn't exist" + )); + } + Ok(Rc::new(EntrypointChannel { + worker: Rc::clone(self), + entrypoint: name.map(str::to_owned), + props: None, + persistent: false, + })) + } + + /// The Durable Object class `name` with `props` bound; `None` when the worker exports no + /// such class (or `name` is `None`: a default export is never a class). + #[must_use] + pub fn actor_class( + self: &Rc, + name: Option<&str>, + props: Option>, + persistent: Persistent, + ) -> Option> { + let class_name = self + .actor_classes + .iter() + .find(|class| Some(class.as_str()) == name)?; + Some(Rc::new(ActorClassImpl { + worker: Rc::clone(self), + class_name: class_name.clone(), + props, + persistent, + })) + } + + /// The `ctx.exports` class of `name`: the template its props specialize. + pub fn loopback_actor_class(self: &Rc, name: &str) -> Result> { + self.actor_class(Some(name), None, false).ok_or_else(|| { + kj::failed!("getLoopbackActorClass() called for actor class that doesn't exist") + }) + } +} + +impl WorkerService { + /// The designators awaiting the link stage. Taken once, by the server's third pass. + pub fn take_pending_link(&self) -> Option { + self.pending.borrow_mut().take() + } + + /// Installs the channel table and opens the namespaces' storage. `storage_path` is the + /// directory of the worker's `localDisk` storage service, if any. A Workflow's namespace + /// opened its own when it was created (`init_workflow_namespace`). + pub fn link(&self, channels: LinkedChannels, storage_path: Option<&str>) -> Result<()> { + if self.channels.borrow().is_some() { + return Err(kj::failed!("already called link()")); + } + *self.channels.borrow_mut() = Some(Rc::new(channels)); + for namespace in self.namespaces.borrow().values() { + if !namespace.is_linked() { + namespace.link(storage_path)?; + } + } + Ok(()) + } + + /// Cancels the worker's background work, then drops the namespaces and the channel table + /// (the factory's part of it included), so that the graph's reference cycles are gone before + /// the services are. The background work goes first: it holds the channels and the actors. + /// Each is taken out of its cell before it is dropped, so that nothing a destructor reaches + /// finds the cell borrowed. + pub fn unlink(&self) { + ffi::worker_unlink(&self.worker); + let namespaces = std::mem::take(&mut *self.namespaces.borrow_mut()); + let channels = self.channels.borrow_mut().take(); + drop((namespaces, channels)); + } + + fn channels(&self) -> Result> { + self.channels + .borrow() + .clone() + .ok_or_else(|| kj::failed!("link() has not been called")) + } + + /// Starts a request on `entrypoint` (the default when `None`), on `actor` if the entrypoint + /// is a Durable Object class. `is_tracer` marks a request that is itself a tail worker's, + /// which gets no tail workers of its own. + pub fn start_request( + &self, + entrypoint: Option<&str>, + props: Option>, + actor: Option<&ffi::ActorHandle>, + metadata: KjOwn, + is_tracer: bool, + ) -> Result> { + let channels = self.channels()?; + + // The test event is not traced; a test of span tracing can still tail a request the + // test makes. + let mut tails = Vec::new(); + if entrypoint != Some("test") { + for tail in &channels.tails { + if let Some(worker) = self.start_tail(tail, is_tracer)? { + tails.push(Tail { + streaming: false, + worker: Some(worker), + }); + } + } + for tail in &channels.streaming_tails { + if let Some(worker) = self.start_tail(tail, is_tracer)? { + tails.push(Tail { + streaming: true, + worker: Some(worker), + }); + } + } + } + + ffi::worker_start_request( + &self.worker, + entrypoint.into(), + props.into(), + actor.into(), + Box::new(ChannelFactory(channels)), + metadata, + WorkerInterfaceList::new(tails), + ) + .map_err(Into::into) + } + + /// Starts a tail worker for one of this worker's requests. A tail that is an entrypoint of + /// this same worker is started as a tracer, and not at all when the request being tailed is + /// already a tracer's: a worker tailing itself must not recurse. Only the direct + /// self-reference is caught; a cycle through another worker is not. + fn start_tail( + &self, + tail: &Rc, + is_tracer: bool, + ) -> Result>> { + let metadata = ffi::new_request_metadata(KjMaybe::None, KjMaybe::None); + match tail.worker_entrypoint() { + Some(target) if ptr::eq(target.worker, self) => { + if is_tracer { + return Ok(None); + } + let props = target.props.map(ffi::frankenvalue_clone); + self.start_request(target.entrypoint, props, None, metadata, true) + .map(Some) + } + _ => tail.start_request(metadata).map(Some), + } + } + + /// The token of `entrypoint` with `props` bound, as the runtime encodes it. + fn token( + &self, + entrypoint: Option<&str>, + props: Option<&Frankenvalue>, + persistent: Persistent, + usage: TokenUsage, + ) -> Result> { + Ok(ffi::factory_encode_subrequest_token( + self.factory.raw(), + self.transferable_name()?, + entrypoint.into(), + props.into(), + persistent, + usage, + )?) + } +} + +/// One entrypoint of a worker, with props bound or, for a `ctx.exports` template, not yet. +/// +/// `persistent` is set only for a channel that came from `ctx.exports` in a worker with +/// `allow_irrevocable_stub_storage`, or was restored from a token that recorded that. +struct EntrypointChannel { + worker: Rc, + /// `None` is the default entrypoint. + entrypoint: Option, + props: Option>, + persistent: Persistent, +} + +impl Channel for EntrypointChannel { + fn start_request( + &self, + mut metadata: KjOwn, + ) -> Result> { + // A restored persistent stub tells the target so that it re-verifies that it still allows + // persistent stubs; a bit already set by an outer hop is kept. + ffi::request_metadata_set_from_persistent_stub(metadata.as_mut(), self.persistent); + // A template called without props runs with empty props. + let props = self.props.as_deref().map(ffi::frankenvalue_clone); + self.worker + .start_request(self.entrypoint.as_deref(), props, None, metadata, false) + } + + fn require_allows_transfer(&self) -> Result<()> { + self.worker.transferable_name().map(drop) + } + + fn token(&self, usage: TokenUsage) -> Result> { + self.worker.token( + self.entrypoint.as_deref(), + self.props.as_deref(), + self.persistent, + usage, + ) + } + + /// Specializes a `ctx.exports` template; a channel that already has props cannot be. + fn for_props( + &self, + props: KjOwn, + persistent: Persistent, + ) -> Result> { + if self.props.is_some() { + return Err(kj::failed!("can't override props for this service")); + } + Ok(Rc::new(Self { + worker: Rc::clone(&self.worker), + entrypoint: self.entrypoint.clone(), + props: Some(props), + persistent, + })) + } + + fn worker_entrypoint(&self) -> Option> { + Some(WorkerEntrypoint { + worker: &self.worker, + entrypoint: self.entrypoint.as_deref(), + props: self.props.as_deref(), + }) + } +} + +/// One Durable Object class of a worker, with props bound or, for a `ctx.exports` template, not +/// yet. +struct ActorClassImpl { + worker: Rc, + class_name: String, + props: Option>, + persistent: Persistent, +} + +impl ActorClass for ActorClassImpl { + fn new_actor(&self, request: NewActor<'_>) -> Result> { + // A template used without props constructs the actor with empty props. + let props = self.props.as_deref().map(ffi::frankenvalue_clone); + ffi::worker_new_actor( + &self.worker.worker, + &self.class_name, + props.into(), + request.id, + request.storage, + &request.spec, + request.hooks, + request.hibernation_manager.into(), + request.container.into(), + ) + .map_err(Into::into) + } + + /// Props are not passed per request: the actor was constructed with them. + fn start_request( + &self, + metadata: KjOwn, + actor: &ffi::ActorHandle, + ) -> Result> { + self.worker + .start_request(Some(&self.class_name), None, Some(actor), metadata, false) + } + + fn require_allows_transfer(&self) -> Result<()> { + self.worker.transferable_name().map(drop) + } + + /// A template (no props) is not serializable: `ctx.exports` classes must be specialized + /// before they can be sent anywhere. + fn token(&self, usage: TokenUsage) -> Result> { + let service_name = self.worker.transferable_name()?; + let props = self + .props + .as_deref() + .ok_or_else(|| kj::failed!("an unspecialized loopback actor class has no token"))?; + Ok(ffi::factory_encode_actor_class_token( + self.worker.factory.raw(), + service_name, + &self.class_name, + Some(props).into(), + self.persistent, + usage, + )?) + } + + fn for_props( + &self, + props: KjOwn, + persistent: Persistent, + ) -> Result> { + if self.props.is_some() { + return Err(kj::failed!("can't override props for this actor class")); + } + Ok(Rc::new(Self { + worker: Rc::clone(&self.worker), + class_name: self.class_name.clone(), + props: Some(props), + persistent, + })) + } +} + +impl LinkedChannels { + fn subrequest(&self, channel: u32) -> Result<&Rc> { + channel + .checked_sub(self.first_subrequest) + .and_then(|index| self.subrequest.get(index as usize)) + .ok_or_else(|| kj::failed!("invalid subrequest channel number")) + } + + fn namespace(&self, channel: u32) -> Result<&Rc> { + self.actor + .get(channel as usize) + .ok_or_else(|| kj::failed!("invalid actor channel number"))? + .as_ref() + .ok_or_else(|| kj::failed!("jsg.Error: Actor namespace configuration was invalid.")) + } +} + +impl ChannelFactory { + /// With props, the channel must be a `ctx.exports` template to specialize. + pub(crate) fn subrequest_channel( + &self, + channel: u32, + props: KjMaybe>, + persistent: bool, + ) -> Result> { + let target = self.0.subrequest(channel)?; + Ok(SubrequestChannel::new(match Option::from(props) { + Some(props) => target.for_props(props, persistent)?, + None => Rc::clone(target), + })) + } + + pub(crate) fn global_actor( + &self, + channel: u32, + id: KjOwn, + persistent: bool, + ) -> Result> { + let namespace = self.0.namespace(channel)?; + // The bindings compiler only makes durable bindings to durable namespaces. + if namespace.unique_key().is_none() { + return Err(kj::failed!( + "expected a durable namespace on this actor channel" + )); + } + Ok(SubrequestChannel::new(namespace.channel(id, persistent))) + } + + pub(crate) fn colo_local_actor( + &self, + channel: u32, + id: &str, + ) -> Result> { + let namespace = self.0.namespace(channel)?; + if namespace.unique_key().is_some() { + return Err(kj::failed!( + "expected an ephemeral namespace on this actor channel" + )); + } + Ok(SubrequestChannel::new(namespace.channel_by_name(id, false))) + } + + pub(crate) fn actor_class( + &self, + channel: u32, + props: KjMaybe>, + persistent: bool, + ) -> Result> { + let class = channel + .checked_sub(self.0.first_actor_class) + .and_then(|index| self.0.actor_class.get(index as usize)) + .ok_or_else(|| kj::failed!("invalid actor class channel number"))?; + Ok(ActorClassChannel::new(match Option::from(props) { + Some(props) => class.for_props(props, persistent)?, + None => Rc::clone(class), + })) + } + + pub(crate) fn cache_channel(&self) -> Result> { + let cache = self.0.cache.clone(); + Ok(SubrequestChannel::new(cache.ok_or_else(|| { + kj::failed!("jsg.Error: No Cache was configured") + })?)) + } + + pub(crate) fn access_binding_channel(&self) -> KjMaybe { + self.0.access_binding_channel.into() + } + + pub(crate) fn abort_all_actors(&self, reason: KjMaybe<&ffi::Exception>) { + let reason = Option::<&ffi::Exception>::from(reason).map(crate::Error::from); + if let Some(server) = self.0.server.upgrade() { + server.abort_all_actors(reason.as_ref()); + } + } + + pub(crate) fn delete_all_actors(&self, reason: KjMaybe<&ffi::Exception>) -> Result<()> { + let reason = Option::<&ffi::Exception>::from(reason).map(crate::Error::from); + match self.0.server.upgrade() { + Some(server) => server.delete_all_actors(reason.as_ref()), + None => Ok(()), + } + } + + pub(crate) async fn evict_all_actors_for_test(&self, hibernate: bool) -> Result<()> { + let namespaces = self.0.actor.iter().flatten(); + let evictions = namespaces.map(|namespace| namespace.evict_all_for_test(hibernate)); + futures::future::try_join_all(evictions).await?; + Ok(()) + } + + /// A dynamic worker unloads. A static worker cannot be replaced, so the call fails; the + /// runtime treats that failure as fatal and the process ends. + pub(crate) fn abort_isolate(&self, reason: &str) -> Result<()> { + match &self.0.abort_isolate { + Some(abort) => { + abort(); + Ok(()) + } + None => Err(kj::failed!( + "abortIsolate() called, terminating process; reason = {reason}" + )), + } + } + + pub(crate) fn load_isolate( + &self, + loader_channel: u32, + name: KjMaybe<&str>, + source: KjOwn, + ) -> Result> { + let loader = self + .0 + .worker_loaders + .get(loader_channel as usize) + .ok_or_else(|| kj::failed!("invalid worker loader channel number"))?; + Ok(Box::new(WorkerStub(loader.load(name.into(), source)?))) + } + + pub(crate) fn has_debug_port(&self) -> bool { + self.0.has_debug_port + } +} diff --git a/src/workerd/server/tests/server-harness.mjs b/src/workerd/server/tests/server-harness.mjs index 65ebad55fa0..be4fefa1b95 100644 --- a/src/workerd/server/tests/server-harness.mjs +++ b/src/workerd/server/tests/server-harness.mjs @@ -63,20 +63,30 @@ export class WorkerdServerHarness { console.log('[HARNESS] Starting workerd with args:', args); this.#child = spawn(this.#workerdBinary, args, options); - // Create a promise for every named listen port we were told in our constructor to expect. Parse - // messages from our control FD and resolve the promises as we see ports come online. - // - // TODO(perf): Registering a separate callback for every named port isn't very efficient -- - // we'll parse JSON N times -- but we typically don't have many named ports, and I don't want to - // spend forever on this code. + // Create a promise for every named listen port we were told in our constructor to expect, and + // resolve them as the control FD reports ports coming online. The control FD carries one JSON + // object per line; a read may return several lines at once or a partial one, so the stream is + // split on newlines before parsing. + const listeners = new Set(); + let buffered = ''; + this.#child.stdio[CONTROL_FD].on('data', (data) => { + buffered += data; + const lines = buffered.split('\n'); + buffered = lines.pop(); + for (const line of lines) { + if (line.length === 0) continue; + const parsed = JSON.parse(line); + console.log('[HARNESS] Control message:', parsed); + for (const listener of listeners) listener(parsed); + } + }); + this.#listenPorts = new Map(); for (const listenPort of this.#listenPortNames) { this.#listenPorts.set( listenPort, new Promise((resolve, reject) => { - this.#child.stdio[CONTROL_FD].on('data', (data) => { - const parsed = JSON.parse(data); - console.log('[HARNESS] Control message:', parsed); + listeners.add((parsed) => { if (parsed.event === 'listen' && parsed.socket === listenPort) { resolve(parsed.port); } @@ -88,9 +98,7 @@ export class WorkerdServerHarness { // Do the same as the above for the inspector port. this.#listenInspectorPort = new Promise((resolve, reject) => { - this.#child.stdio[CONTROL_FD].on('data', (data) => { - const parsed = JSON.parse(data); - console.log('[HARNESS] Inspector message:', parsed); + listeners.add((parsed) => { if (parsed.event === 'listen-inspector') { resolve(parsed.port); } diff --git a/src/workerd/server/tests/udp-connect-ts-streams/config.capnp b/src/workerd/server/tests/udp-connect-ts-streams/config.capnp index adb0f722a48..69958863632 100644 --- a/src/workerd/server/tests/udp-connect-ts-streams/config.capnp +++ b/src/workerd/server/tests/udp-connect-ts-streams/config.capnp @@ -6,7 +6,7 @@ const config :Workerd.Config = ( ( name = "main", worker = .worker ), ], sockets = [ - ( name = "udp", address = "*:0", udp = (idleTimeoutMs = 1000), service = "main" ), + ( name = "udp", address = "127.0.0.1:0", udp = (idleTimeoutMs = 1000), service = "main" ), ], autogates = [ "workerd-autogate-per-isolate-javascript-bootstrap", diff --git a/src/workerd/server/tests/udp-connect/config.capnp b/src/workerd/server/tests/udp-connect/config.capnp index 8b131c531af..76f866a6d43 100644 --- a/src/workerd/server/tests/udp-connect/config.capnp +++ b/src/workerd/server/tests/udp-connect/config.capnp @@ -6,7 +6,7 @@ const config :Workerd.Config = ( ( name = "main", worker = .worker ), ], sockets = [ - ( name = "udp", address = "*:0", udp = (idleTimeoutMs = 200), service = "main" ), + ( name = "udp", address = "127.0.0.1:0", udp = (idleTimeoutMs = 200), service = "main" ), ] ); diff --git a/src/workerd/server/tests/udp-datagram-drop/config.capnp b/src/workerd/server/tests/udp-datagram-drop/config.capnp index da404a8b36a..04c257855da 100644 --- a/src/workerd/server/tests/udp-datagram-drop/config.capnp +++ b/src/workerd/server/tests/udp-datagram-drop/config.capnp @@ -6,7 +6,7 @@ const config :Workerd.Config = ( ( name = "main", worker = .worker ), ], sockets = [ - ( name = "udp", address = "*:0", + ( name = "udp", address = "127.0.0.1:0", udp = (idleTimeoutMs = 1000, maxPendingBytes = 95), service = "main" ), ] ); diff --git a/src/workerd/server/workerd-api.c++ b/src/workerd/server/workerd-api.c++ index e61a28c3aea..3ab5e2e519b 100644 --- a/src/workerd/server/workerd-api.c++ +++ b/src/workerd/server/workerd-api.c++ @@ -569,21 +569,16 @@ void WorkerdApi::compileModules(jsg::Lock& lockParam, WD_STRONG_BOOL(IsInternalBinding); static v8::Local createBindingValue(JsgWorkerdIsolate::Lock& lock, - const WorkerdApi::Global& global, + Global::Reader global, CompatibilityFlags::Reader featureFlags, - uint32_t ownerId, api::MemoryCacheProvider& memoryCacheProvider, IsInternalBinding isInternal) { TRACE_EVENT("workerd", "WorkerdApi::createBindingValue()"); - using Global = WorkerdApi::Global; auto context = lock.v8Context(); - v8::Local value; - // When new binding types are created. If their value resolves to be a string // or a JSON stringified/stringifiable value, then it should be added to - // process.env here as well, just like with Global::Json and kj::String - // entries. + // process.env here as well, just like with `json` and `text` entries. // // It is important to understand the process.env is fundamentally different // from the existing bag of bindings. The keys and values on process.env are @@ -594,191 +589,200 @@ static v8::Local createBindingValue(JsgWorkerdIsolate::Lock& lock, // as long as the observable behavior remains the same we can do so without // Yet Another Compat Flag. - KJ_SWITCH_ONEOF(global.value) { - KJ_CASE_ONEOF(json, Global::Json) { - value = jsg::check(v8::JSON::Parse(context, lock.str(json.text))); - } + switch (global.which()) { + case Global::JSON: + return jsg::check(v8::JSON::Parse(context, lock.str(global.getJson()))); - KJ_CASE_ONEOF(pipeline, Global::Fetcher) { - value = lock.wrap(context, - lock.alloc(pipeline.channel, - pipeline.requiresHost ? api::Fetcher::RequiresHostAndProtocol::YES - : api::Fetcher::RequiresHostAndProtocol::NO, - pipeline.isInHouse, api::RpcCompatGateBypassed(isInternal.toBool()))); - } + case Global::FETCHER: + return lock.wrap(context, + lock.alloc(global.getFetcher(), api::Fetcher::RequiresHostAndProtocol::YES, + /*isInHouse=*/false, api::RpcCompatGateBypassed(isInternal.toBool()))); - KJ_CASE_ONEOF(loopback, Global::LoopbackServiceStub) { - value = lock.wrap(context, lock.alloc(loopback.channel)); - } + case Global::LOOPBACK_SERVICE_STUB: + return lock.wrap( + context, lock.alloc(global.getLoopbackServiceStub())); - KJ_CASE_ONEOF(ns, Global::KvNamespace) { - value = lock.wrap(context, - lock.alloc(kj::str(ns.bindingName), - kj::Array{}, ns.subrequestChannel)); - } + case Global::KV_NAMESPACE: + return lock.wrap(context, + lock.alloc(kj::str(global.getName()), + kj::Array{}, global.getKvNamespace())); - KJ_CASE_ONEOF(r2, Global::R2Bucket) { - value = lock.wrap(context, + case Global::R2_BUCKET: { + auto r2 = global.getR2Bucket(); + return lock.wrap(context, lock.alloc( - featureFlags, r2.subrequestChannel, kj::str(r2.bucket), kj::str(r2.bindingName))); + featureFlags, r2.getChannel(), kj::str(r2.getBucket()), kj::str(global.getName()))); } - KJ_CASE_ONEOF(ns, Global::QueueBinding) { - value = lock.wrap(context, lock.alloc(ns.subrequestChannel)); - } + case Global::QUEUE: + return lock.wrap(context, lock.alloc(global.getQueue())); - KJ_CASE_ONEOF(key, Global::CryptoKey) { + case Global::CRYPTO_KEY: { + auto key = global.getCryptoKey(); api::SubtleCrypto::ImportKeyData keyData; - KJ_SWITCH_ONEOF(key.keyData) { - KJ_CASE_ONEOF(data, kj::Array) { - auto u8 = jsg::JsBufferSource(jsg::JsUint8Array::create(lock, data)); - keyData = u8.addRef(lock); - } - KJ_CASE_ONEOF(json, Global::Json) { - v8::Local str = lock.wrap(context, kj::mv(json.text)); - v8::Local obj = jsg::check(v8::JSON::Parse(context, str)); - keyData = lock.unwrap(context, obj); - } + auto data = key.getKeyData(); + if (data.isJson()) { + v8::Local str = lock.wrap(context, kj::str(data.getJson())); + v8::Local obj = jsg::check(v8::JSON::Parse(context, str)); + keyData = lock.unwrap(context, obj); + } else { + auto u8 = jsg::JsBufferSource(jsg::JsUint8Array::create(lock, data.getBytes())); + keyData = u8.addRef(lock); } - v8::Local algoStr = lock.wrap(context, kj::mv(key.algorithm.text)); + v8::Local algoStr = lock.wrap(context, kj::str(key.getAlgorithm())); v8::Local algo = jsg::check(v8::JSON::Parse(context, algoStr)); auto importKeyAlgo = lock.unwrap>(context, algo); + auto usages = KJ_MAP(usage, key.getUsages()) { return kj::str(usage); }; - jsg::Ref importedKey = - api::SubtleCrypto::importKeySync(lock, key.format, kj::mv(keyData), - api::interpretAlgorithmParam(kj::mv(importKeyAlgo)), key.extractable, key.usages); - - value = lock.wrap(context, kj::mv(importedKey)); + return lock.wrap(context, + api::SubtleCrypto::importKeySync(lock, key.getFormat(), kj::mv(keyData), + api::interpretAlgorithmParam(kj::mv(importKeyAlgo)), key.getExtractable(), usages)); } - KJ_CASE_ONEOF(cache, Global::MemoryCache) { - value = lock.wrap(context, - lock.alloc(memoryCacheProvider.getUse(cache.cacheId, + case Global::MEMORY_CACHE: { + auto cache = global.getMemoryCache(); + // A cache without an id is not shared. + kj::Maybe cacheId; + if (cache.getCacheId().size() > 0) cacheId = cache.getCacheId(); + return lock.wrap(context, + lock.alloc(memoryCacheProvider.getUse(cacheId, { - .maxKeys = cache.maxKeys, - .maxValueSize = cache.maxValueSize, - .maxTotalValueSize = cache.maxTotalValueSize, + .maxKeys = cache.getMaxKeys(), + .maxValueSize = cache.getMaxValueSize(), + .maxTotalValueSize = cache.getMaxTotalValueSize(), }))); } - KJ_CASE_ONEOF(ns, Global::EphemeralActorNamespace) { - value = lock.wrap(context, lock.alloc(ns.actorChannel)); - } - KJ_CASE_ONEOF(ns, Global::LoopbackEphemeralActorNamespace) { - value = lock.wrap(context, - lock.alloc( - ns.actorChannel, lock.alloc(ns.classChannel))); + case Global::EPHEMERAL_ACTOR_NAMESPACE: + return lock.wrap( + context, lock.alloc(global.getEphemeralActorNamespace())); + + case Global::LOOPBACK_EPHEMERAL_ACTOR_NAMESPACE: { + auto ns = global.getLoopbackEphemeralActorNamespace(); + return lock.wrap(context, + lock.alloc(ns.getActorChannel(), + lock.alloc(ns.getClassChannel()))); } - KJ_CASE_ONEOF(ns, Global::DurableActorNamespace) { - value = lock.wrap(context, - lock.alloc(ns.actorChannel, - kj::heap(ns.uniqueKey), api::ActorCallRetriesAllowed::YES, - Persistent::NO, ns.userDefinedRetryPolicy)); + case Global::DURABLE_ACTOR_NAMESPACE: { + auto ns = global.getDurableActorNamespace(); + kj::Maybe retryPolicy; + if (ns.hasRetryPolicy()) { + auto policy = ns.getRetryPolicy(); + retryPolicy = api::UserDefinedRetryPolicy{ + .maxAttempts = policy.getMaxAttempts(), + .timeout = policy.getTimeoutMs() * kj::MILLISECONDS, + }; + } + return lock.wrap(context, + lock.alloc(ns.getActorChannel(), + kj::heap(ns.getUniqueKey()), api::ActorCallRetriesAllowed::YES, + Persistent::NO, retryPolicy)); } - KJ_CASE_ONEOF(ns, Global::LoopbackDurableActorNamespace) { - value = lock.wrap(context, - lock.alloc(ns.actorChannel, - kj::heap(ns.uniqueKey), api::ActorCallRetriesAllowed::YES, - lock.alloc(ns.classChannel), featureFlags, + + case Global::LOOPBACK_DURABLE_ACTOR_NAMESPACE: { + auto ns = global.getLoopbackDurableActorNamespace(); + return lock.wrap(context, + lock.alloc(ns.getActorChannel(), + kj::heap(ns.getUniqueKey()), api::ActorCallRetriesAllowed::YES, + lock.alloc(ns.getClassChannel()), featureFlags, /*userDefinedRetryPolicy=*/kj::none)); } - KJ_CASE_ONEOF(ae, Global::AnalyticsEngine) { + case Global::ANALYTICS_ENGINE: { + auto ae = global.getAnalyticsEngine(); // Use subrequestChannel as logfwdrChannel - value = lock.wrap(context, + return lock.wrap(context, lock.alloc( - ae.subrequestChannel, kj::str(ae.dataset), ae.version, ownerId)); + ae.getChannel(), kj::str(ae.getDataset()), /*version=*/0, /*ownerId=*/1)); } - KJ_CASE_ONEOF(text, kj::String) { - value = lock.wrap(context, kj::mv(text)); - } + case Global::TEXT: + return lock.wrap(context, kj::str(global.getText())); - KJ_CASE_ONEOF(data, kj::Array) { - value = lock.wrap(context, kj::heapArray(data.asPtr())); - } + case Global::DATA: + return lock.wrap(context, kj::heapArray(global.getData())); - KJ_CASE_ONEOF(wrapped, Global::Wrapped) { + case Global::WRAPPED: { + auto wrapped = global.getWrapped(); + auto innerBindings = wrapped.getInnerBindings(); // wrapped bindings can be produced by internal modules only - KJ_IF_SOME(moduleNs, lock.resolveInternalModule(wrapped.moduleName)) { + KJ_IF_SOME(moduleNs, lock.resolveInternalModule(wrapped.getModuleName())) { // build env object with inner bindings auto env = v8::Object::New(lock.v8Isolate); - for (const auto& innerBinding: wrapped.innerBindings) { - lock.v8Set(env, innerBinding.name, - createBindingValue(lock, innerBinding, featureFlags, ownerId, memoryCacheProvider, - IsInternalBinding::YES)); + for (auto innerBinding: innerBindings) { + lock.v8Set(env, innerBinding.getName(), + createBindingValue( + lock, innerBinding, featureFlags, memoryCacheProvider, IsInternalBinding::YES)); } // obtain exported function to call - auto fn = lock.v8Get(moduleNs, wrapped.entrypoint); - KJ_ASSERT(fn->IsFunction(), "Entrypoint is not a function", wrapped.entrypoint); + auto fn = lock.v8Get(moduleNs, wrapped.getEntrypoint()); + KJ_ASSERT(fn->IsFunction(), "Entrypoint is not a function", wrapped.getEntrypoint()); // invoke the function, its result will be binding value v8::Local arg = env.As(); - value = jsg::check(v8::Function::Cast(*fn)->Call(context, context->Global(), 1, &arg)); - if (wrapped.entrypoint == "default"_kj && wrapped.innerBindings.size() == 1 && - wrapped.innerBindings[0].name == api::WRAPPED_BINDING_INNER_NAME && - wrapped.innerBindings[0].value.is()) { + auto value = jsg::check(v8::Function::Cast(*fn)->Call(context, context->Global(), 1, &arg)); + if (wrapped.getEntrypoint() == "default"_kj && innerBindings.size() == 1 && + innerBindings[0].getName() == api::WRAPPED_BINDING_INNER_NAME && + innerBindings[0].isFetcher()) { KJ_IF_SOME(binding, lock.getTypeHandler>().tryUnwrap(lock, value)) { - binding->setWrapperModule(kj::str(wrapped.moduleName)); + binding->setWrapperModule(kj::str(wrapped.getModuleName())); } } + return value; } else { - KJ_FAIL_REQUIRE( - "wrapped binding module can't be resolved (internal modules only)", wrapped.moduleName); + KJ_FAIL_REQUIRE("wrapped binding module can't be resolved (internal modules only)", + wrapped.getModuleName()); } } - KJ_CASE_ONEOF(hyperdrive, Global::Hyperdrive) { - value = lock.wrap(context, - lock.alloc(hyperdrive.subrequestChannel, kj::str(hyperdrive.database), - kj::str(hyperdrive.user), kj::str(hyperdrive.password), kj::str(hyperdrive.scheme))); - } - KJ_CASE_ONEOF(unsafe, Global::UnsafeEval) { - value = lock.wrap(context, lock.alloc()); - } - KJ_CASE_ONEOF(actorClass, Global::ActorClass) { - value = lock.wrap(context, lock.alloc(actorClass.channel)); + case Global::HYPERDRIVE: { + auto hyperdrive = global.getHyperdrive(); + return lock.wrap(context, + lock.alloc(hyperdrive.getChannel(), kj::str(hyperdrive.getDatabase()), + kj::str(hyperdrive.getUser()), kj::str(hyperdrive.getPassword()), + kj::str(hyperdrive.getScheme()))); } - KJ_CASE_ONEOF(actorClass, Global::LoopbackActorClass) { - value = lock.wrap(context, lock.alloc(actorClass.channel)); - } + case Global::UNSAFE_EVAL: + return lock.wrap(context, lock.alloc()); + + case Global::ACTOR_CLASS: + return lock.wrap(context, lock.alloc(global.getActorClass())); - KJ_CASE_ONEOF(workerLoader, Global::WorkerLoader) { - value = lock.wrap(context, + case Global::LOOPBACK_ACTOR_CLASS: + return lock.wrap( + context, lock.alloc(global.getLoopbackActorClass())); + + case Global::WORKER_LOADER: + return lock.wrap(context, lock.alloc( - workerLoader.channel, CompatibilityDateValidation::CODE_VERSION)); - } + global.getWorkerLoader(), CompatibilityDateValidation::CODE_VERSION)); - KJ_CASE_ONEOF(_, Global::WorkerdDebugPort) { - value = lock.wrap(context, lock.alloc()); - } + case Global::WORKERD_DEBUG_PORT: + return lock.wrap(context, lock.alloc()); } - - return value; + KJ_FAIL_REQUIRE("unknown Global type", global.which()); } -void WorkerdApi::compileGlobals(jsg::Lock& lockParam, - kj::ArrayPtr globals, - v8::Local target, - uint32_t ownerId) const { +void WorkerdApi::compileGlobals( + jsg::Lock& lockParam, capnp::List::Reader globals, v8::Local target) const { TRACE_EVENT("workerd", "WorkerdApi::compileGlobals()"); auto& lock = kj::downcast(lockParam); lockParam.withinHandleScope([&] { auto& featureFlags = *impl->features; - for (auto& global: globals) { + for (auto global: globals) { lockParam.withinHandleScope([&] { // Don't use String's usual TypeHandler here because we want to intern the string. auto value = createBindingValue( - lock, global, featureFlags, ownerId, impl->memoryCacheProvider, IsInternalBinding::NO); + lock, global, featureFlags, impl->memoryCacheProvider, IsInternalBinding::NO); KJ_ASSERT(!value.IsEmpty(), "global did not produce v8::Value"); - lockParam.v8Set(target, global.name, value); + lockParam.v8Set(target, global.getName(), value); }); } }); @@ -791,83 +795,6 @@ void WorkerdApi::setModuleFallbackCallback(kj::Function& // ======================================================================================= -WorkerdApi::Global WorkerdApi::Global::clone() const { - Global result; - result.name = kj::str(name); - - KJ_SWITCH_ONEOF(value) { - KJ_CASE_ONEOF(json, Global::Json) { - result.value = json.clone(); - } - KJ_CASE_ONEOF(fetcher, Global::Fetcher) { - result.value = fetcher.clone(); - } - KJ_CASE_ONEOF(loopback, Global::LoopbackServiceStub) { - result.value = loopback.clone(); - } - KJ_CASE_ONEOF(kvNamespace, Global::KvNamespace) { - result.value = kvNamespace.clone(); - } - KJ_CASE_ONEOF(r2Bucket, Global::R2Bucket) { - result.value = r2Bucket.clone(); - } - KJ_CASE_ONEOF(queueBinding, Global::QueueBinding) { - result.value = queueBinding.clone(); - } - KJ_CASE_ONEOF(key, Global::CryptoKey) { - result.value = key.clone(); - } - KJ_CASE_ONEOF(cache, Global::MemoryCache) { - result.value = cache.clone(); - } - KJ_CASE_ONEOF(ns, Global::EphemeralActorNamespace) { - result.value = ns.clone(); - } - KJ_CASE_ONEOF(ns, Global::LoopbackEphemeralActorNamespace) { - result.value = ns.clone(); - } - KJ_CASE_ONEOF(ns, Global::DurableActorNamespace) { - result.value = ns.clone(); - } - KJ_CASE_ONEOF(ns, Global::LoopbackDurableActorNamespace) { - result.value = ns.clone(); - } - KJ_CASE_ONEOF(ae, Global::AnalyticsEngine) { - result.value = ae.clone(); - } - KJ_CASE_ONEOF(text, kj::String) { - result.value = kj::str(text); - } - KJ_CASE_ONEOF(data, kj::Array) { - result.value = kj::heapArray(data.asPtr()); - } - KJ_CASE_ONEOF(wrapped, Global::Wrapped) { - result.value = wrapped.clone(); - } - KJ_CASE_ONEOF(hyperdrive, Global::Hyperdrive) { - result.value = hyperdrive.clone(); - } - KJ_CASE_ONEOF(unsafe, Global::UnsafeEval) { - result.value = Global::UnsafeEval{}; - } - - KJ_CASE_ONEOF(actorClass, Global::ActorClass) { - result.value = actorClass.clone(); - } - KJ_CASE_ONEOF(actorClass, Global::LoopbackActorClass) { - result.value = actorClass.clone(); - } - KJ_CASE_ONEOF(workerLoader, Global::WorkerLoader) { - result.value = workerLoader.clone(); - } - KJ_CASE_ONEOF(workerdDebugPort, Global::WorkerdDebugPort) { - result.value = workerdDebugPort.clone(); - } - } - - return result; -} - const WorkerdApi& WorkerdApi::from(const Worker::Api& api) { return kj::downcast(api); } diff --git a/src/workerd/server/workerd-api.h b/src/workerd/server/workerd-api.h index 27516ecb7a5..04614dce24b 100644 --- a/src/workerd/server/workerd-api.h +++ b/src/workerd/server/workerd-api.h @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace workerd { @@ -85,241 +86,9 @@ class WorkerdApi final: public Worker::Api { const Worker::Script::ScriptSource& source, const Worker::Isolate& isolate) const override; - // A pipeline-level binding. - struct Global { - // TODO(cleanup): Get rid of this and just load from config.Worker.bindings capnp structure - // directly. - - struct Json { - kj::String text; - - Json clone() const { - return Json{.text = kj::str(text)}; - } - }; - struct Fetcher { - uint channel; - bool requiresHost; - bool isInHouse; - - Fetcher clone() const { - return *this; - } - }; - struct LoopbackServiceStub { - uint channel; - - LoopbackServiceStub clone() const { - return *this; - } - }; - struct KvNamespace { - uint subrequestChannel; - kj::String bindingName; - - KvNamespace clone() const { - return KvNamespace{ - .subrequestChannel = subrequestChannel, .bindingName = kj::str(bindingName)}; - } - }; - struct R2Bucket { - uint subrequestChannel; - kj::String bucket; - kj::String bindingName; - - R2Bucket clone() const { - return R2Bucket{ - .subrequestChannel = subrequestChannel, - .bucket = kj::str(bucket), - .bindingName = kj::str(bindingName), - }; - } - }; - struct QueueBinding { - uint subrequestChannel; - - QueueBinding clone() const { - return *this; - } - }; - struct CryptoKey { - kj::String format; - kj::OneOf, Json> keyData; - Json algorithm; - bool extractable; - kj::Array usages; - - CryptoKey clone() const { - decltype(keyData) clonedKeyData; - KJ_SWITCH_ONEOF(keyData) { - KJ_CASE_ONEOF(bytes, kj::Array) { - clonedKeyData = kj::heapArray(bytes.asPtr()); - } - KJ_CASE_ONEOF(json, Json) { - clonedKeyData = json.clone(); - } - } - return CryptoKey{ - .format = kj::str(format), - .keyData = kj::mv(clonedKeyData), - .algorithm = algorithm.clone(), - .extractable = extractable, - .usages = KJ_MAP(s, usages) { return kj::str(s); }, - }; - } - }; - - struct MemoryCache { - kj::Maybe cacheId = kj::none; - uint32_t maxKeys; - uint32_t maxValueSize; - uint64_t maxTotalValueSize; - - MemoryCache clone() const { - return MemoryCache{ - .cacheId = cacheId.map([](auto& id) { return kj::str(id); }), - .maxKeys = maxKeys, - .maxValueSize = maxValueSize, - .maxTotalValueSize = maxTotalValueSize, - }; - } - }; - - struct EphemeralActorNamespace { - uint actorChannel; - - EphemeralActorNamespace clone() const { - return *this; - } - }; - struct LoopbackEphemeralActorNamespace { - uint actorChannel; - uint classChannel; - - LoopbackEphemeralActorNamespace clone() const { - return *this; - } - }; - struct DurableActorNamespace { - uint actorChannel; - kj::StringPtr uniqueKey; - kj::Maybe userDefinedRetryPolicy; - - DurableActorNamespace clone() const { - return *this; - } - }; - struct LoopbackDurableActorNamespace { - uint actorChannel; - kj::StringPtr uniqueKey; - uint classChannel; - - LoopbackDurableActorNamespace clone() const { - return *this; - } - }; - struct Wrapped { - // data carrier for configured WrappedBinding - kj::String moduleName; - kj::String entrypoint; - kj::Array innerBindings; - - Wrapped clone() const { - return Wrapped{.moduleName = kj::str(moduleName), - .entrypoint = kj::str(entrypoint), - .innerBindings = KJ_MAP(b, innerBindings) { return b.clone(); }}; - } - }; - struct AnalyticsEngine { - uint subrequestChannel; - kj::String dataset; - int64_t version; - AnalyticsEngine clone() const { - return AnalyticsEngine{ - .subrequestChannel = subrequestChannel, .dataset = kj::str(dataset), .version = version}; - } - }; - struct Hyperdrive { - uint subrequestChannel; - kj::String database; - kj::String user; - kj::String password; - kj::String scheme; - - Hyperdrive clone() const { - return Hyperdrive{ - .subrequestChannel = subrequestChannel, - .database = kj::str(database), - .user = kj::str(user), - .password = kj::str(password), - .scheme = kj::str(scheme), - }; - } - }; - struct UnsafeEval {}; - - struct ActorClass { - uint channel; - - ActorClass clone() const { - return *this; - } - }; - - struct LoopbackActorClass { - uint channel; - - LoopbackActorClass clone() const { - return *this; - } - }; - - struct WorkerLoader { - uint channel; - - WorkerLoader clone() const { - return *this; - } - }; - - struct WorkerdDebugPort { - WorkerdDebugPort clone() const { - return *this; - } - }; - - kj::String name; - kj::OneOf, - Wrapped, - AnalyticsEngine, - Hyperdrive, - UnsafeEval, - MemoryCache, - ActorClass, - LoopbackActorClass, - WorkerLoader, - WorkerdDebugPort> - value; - - Global clone() const; - }; - - void compileGlobals(jsg::Lock& lock, - kj::ArrayPtr globals, - v8::Local target, - uint32_t ownerId) const; + // Sets each of `globals`, a worker's bindings as the server interpreted them, on `target`. + void compileGlobals( + jsg::Lock& lock, capnp::List::Reader globals, v8::Local target) const; // Part of the original module registry API. static kj::Maybe tryCompileModule(jsg::Lock& js, diff --git a/src/workerd/util/setup-async-io-test.c++ b/src/workerd/util/setup-async-io-test.c++ index 4234e406f00..8e5552234d8 100644 --- a/src/workerd/util/setup-async-io-test.c++ +++ b/src/workerd/util/setup-async-io-test.c++ @@ -2,8 +2,9 @@ // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 -// Verifies that kj::setupAsyncIo() -- the public kj entry point, called exactly as cli-main.c++ -// calls it -- resolves to the tokio-backed implementation from //src/workerd/util:setup-async-io. +// Verifies that kj::setupAsyncIo() -- the public kj entry point, called exactly as workerd's +// inspector and fallback-service threads call it -- resolves to the tokio-backed implementation +// from //src/workerd/util:setup-async-io. // If kj's own definition were linked instead (kj-async-os leaking in through a dependency edge), // the process would run on kj::UnixEventPort while tests that build their own event loop stayed // green. This test observes which one is actually bound. diff --git a/src/workerd/util/setup-async-io.c++ b/src/workerd/util/setup-async-io.c++ index c1651540428..9b2dd9b1f3b 100644 --- a/src/workerd/util/setup-async-io.c++ +++ b/src/workerd/util/setup-async-io.c++ @@ -15,8 +15,8 @@ // subclassing; its method *definitions* live in async-unix.c++, which is absent from this link. // So we define an inert kj::UnixEventPort here (its ctor/dtor + the EventPort/SleepHooks virtuals) // with no ODR competitor, construct one, and bind the reference to it. It is never driven -- the -// event loop runs on kj_rs_tokio::TokioEventPort; signals use kj_rs_io::onSignal() -- and nothing -// in workerd reads AsyncIoContext::unixEventPort. Its methods KJ_UNIMPLEMENTED as a backstop: if +// event loop runs on kj_rs_tokio::TokioEventPort -- and nothing in workerd reads +// AsyncIoContext::unixEventPort. Its methods KJ_UNIMPLEMENTED as a backstop: if // anything ever does drive it, the link/run fails loudly rather than silently. // // If kj-async-os is ever accidentally linked in, this TU's definitions collide with the native