Dan Lapid
90d · built 2026-11-10
Performance
What Dan Lapid shipped in the selected window, measured in ETV, and how it compares with the 90 days before it.
Effective capacity
+52.9engineers
delivers like 53.9 (53.9x pre-AI)
Output (ETV)
42.0ETV
+1492.4% vs 2.6 prior
Features share
62.2%
−23.4 pp vs prior window
Fixes share
5.1%
+0.6 pp vs prior window
Work mix
62.2% Features6.6% Maintenance21.3% Tests4.8% Docs5.1% Fixes
116 commits over 90 days, ending 2026-11-10.
Daily performance
Daily ETV, stacked by Features, Maintenance, Tests, Docs and Fixes.
Repository spread
Where this developer's commits land. Concentrated work (top1 > 80%) vs polymath spread (top1 < 30%).
| Repo | Commits | ETV |
|---|---|---|
| workerd | 110 | 41.4 |
| workers-rs | 1 | 0.4 |
| workers-sdk | 1 | 0.2 |
Most impactful commits
Top 10 by ETV in the last 90 days.
- 11.4ETVserver: add the C++ worker factory and the Rust server crate The new server is added beside the C++ one and is not yet part of the `workerd` binary; the next commit switches the binary over and deletes `server.c++`. Here it builds, and the crate's unit tests run. The `workerd-server` crate (`server/`) drives a C++ `WorkerFactory` (`factory/`) through one cxx bridge (`server/bridge.rs`), the one module of the crate that allows `unsafe`. The two depend on each other through that bridge, so they land together. The bridge's async functions that borrow their arguments are declared `async unsafe fn f<'a>`, the form the in-tree cxx requires of a future that is not `'static`. The factory keeps everything that touches the isolate: compiling a worker, starting a request on it, constructing actors with their storage, alarms and containers, the capnp RPC servers, channel-token encoding, the inspector, and the single-tenant policy objects. The server owns the rest: config interpretation, bindings and channel numbering, services, listeners, actor lifecycle and eviction, dynamic workers, drain and the test runner. A worker's bindings cross as one message: the server interprets `Worker.bindings` and `ctx.exports` into `Globals` (`compiled-bindings.capnp`), every capability a channel number, and a new `WorkerdApi::compileGlobals` overload reads that message. The overload taking `WorkerdApi::Global` stays for `server.c++` until the switch. `server/entry.rs` owns the process's schedule: `factory/bootstrap.c++` sets up logging around the command, a `kj_rs_tokio::Runtime` builds the KJ loop with its tokio runtime, the bootstrap sets up perfetto, autogates and V8 on it and builds the factory, and the command blocks on the server as a task on that loop. Sockets are bound and external servers dialed through kj-rs-io's Rust API, and HTTP in both directions is hyper through kj-hyper. Alongside: - `build/wd_rust_crate.bzl` gains `cxx_bridge_visibility`, so that the factory's C++, in another package, can include the server crate's bridge header. - `deps/rust/Cargo.toml` adds `httpdate`, `percent-encoding` and `url` (the disk service), `data-encoding` (CryptoKey bindings), `glob` (test filters) and `ipnet` (`allow` / `deny` ranges). - `clippy.toml` adds `SQLite` and `WebSockets` to `doc-valid-idents`, for the new doc comments. - `json-logger.h` declares `buildJsonLogMessage`, which the server's command-line log lines use. - `:channel-token`, `:v8-platform-impl` and `:workerd_capnp` are visible to the new subpackages. Size, by `wc -l` without the `-test.rs` files: the server crate is 9,779 lines, the factory 3,821 and `compiled-bindings.capnp` 107, together 13,707 against the 9,193 they replace (`server.c++`, `server.h`, `cli-main.{h,c++}`): 1.49x. The crate's unit tests are another 1,412 lines in eight `-test.rs` files. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>github.com-cloudflare-workerd · e697d639 · 2026-10-12
- 6.7ETVkj-hyper: hyper, rustls and kj's WebSockets behind kj-typed seams kj-hyper is the HTTP/1.1, TLS and WebSocket layer for a Rust server whose handlers are kj HTTP interfaces. `server::serve_connection` serves one accepted tokio connection with hyper and dispatches each request to a `Handler` that takes exactly what `kj::HttpService::request` takes (kj headers, a `kj::AsyncInputStream` body, a `kj::HttpService::Response`); CONNECT reaches `Handler::connect` as a `server::Connect` that is answered in Rust or handed to C++ as a `kj::HttpService::ConnectResponse`. `client::Client` implements the kj `Service` trait over hyper-util's pooled legacy client: `Client::new(table, settings, peer, dial)` to one peer through a dialer, asked for paths (`Peer::Origin`) or, of an HTTP proxy, for whole URLs (`Peer::Proxy`), with `tunnel(host)` an HTTP CONNECT through it; `Client::internet(table, settings, tls, connect)` to whatever authority a request's URL names over the caller's `connect(host, port)` (`client::connect_allowed` resolves and connects to the first address a filter admits that accepts), with startTls upgrade through kj's `TlsStarterCallback`. `tls` builds rustls client and server configs from workerd's `TlsOptions` (keypair, trusted certificates, the platform's browser CAs, client-certificate requirement, minimum version). Its cryptography is ring's, except the ECDSA signatures ring cannot verify and BoringSSL can (a P-521 key, or SHA-512 with a P-256 or P-384 key), which the BoringSSL workerd already links verifies (`ecdsa_verify` in kj-hyper.c++), so a chain through a P-521 CA verifies as under kj-http. WebSockets are kj's: kj-hyper does the RFC 6455 handshake and the permessage-deflate agreement (kj's own parser, in kj-hyper.c++) and hands the upgraded transport to `kj::newWebSocket`. A handshake with an unsupported `Sec-WebSocket-Version` is refused with `426` naming version 13 (kj: `400`), and one by POST with `400`. Tokio streams cross to C++ as `kj::AsyncIoStream` (`into_kj_stream`, `into_kj_stream_with`). kj objects that keep a reference into Rust-owned state -- `kj::HttpHeaders` into their header table, a server response or WebSocket into the `WebSocketErrorHandler` in the settings -- cross the bridge as `ffi::Borrowing<'a, T>`, a `KjOwn` bounded by that borrow; the raw bridge functions that build them are `unsafe` and called only from its wrappers. A transport comes with its `Hangup`, a future that resolves when kj's own stream over it would resolve `whenWriteDisconnected()` (a socket hung up or failed, `kj_rs_io::when_write_disconnected`; not a peer that shut down its side; never on Windows, as under kj). `serve_connection` takes it as an argument, and a dialer returns a socket, whose signal `client::Dialed::from` takes, or a `Dialed` made with one. The kj streams made of the connection resolve `whenWriteDisconnected()` from it, and so a `kj::WebSocket` its `whenAborted()`; a stream from plain `into_kj_stream` never does. A socket's signal holds a dup of its descriptor until it is dropped. As kj's `HttpServer` does, `serve_connection` cancels the handler calls of a connection whose transport hangs up (or fails) while it is served; otherwise a call runs to completion, past its response. Messages go out as kj writes them (`body::Head`): the header table's headers first, in the table's order and spelling, then the rest as added, with the connection-level headers, the framing and `Connection: close` set by the protocol at kj's position, so hyper adds none of its own for those. Everything else is hyper's, request parsing included, and differs from kj-http in places: a header's repeated values are written together; a response to HEAD never says `Transfer-Encoding`; a GET or HEAD request body of unknown length is not sent (kj chunks one whose headers say `Transfer-Encoding`); the connection closes after a request that says `Connection: close` (hyper then says so itself, after the other headers) or is HTTP/1.0 without keep-alive; a request with both `Content-Length` and `Transfer-Encoding` is read as chunked and closes the connection; and a request hyper cannot parse never reaches the `Handler` but is answered `400`, `414` or `431` with hyper's description of the error as the body. A call that fails before responding is answered with a bare `500 Internal Server Error` that closes the connection: an exception's text, which kj's default error handler sends, never reaches a client. DISCONNECTED gets no answer, and a failure after the response head went out drops the connection. The prerequisites it needs from the neighbouring crates come along: kj-rs-io's `when_write_disconnected(&socket)` and a public `exception_type` (the type kj gives an I/O error, which `io_kj_error` applies), and in the `kj` crate `ServiceResponse::accept_websocket` with an opaque `kj::WebSocket`, `HeaderTable::builtin()`, and `AsyncInputStream::try_read` / `try_get_length`. The contract tests in kj-hyper/tests are ported from kj's HTTP and TLS tests: a C++ `kj::HttpService` served by kj-hyper and kj-hyper's client behind `kj::newHttpClient`, checked on the wire over loopback TCP and in-process pipes (plain and TLS), driven by a `kj_test` on the tokio-backed `kj::setupAsyncIo()`. They also check header order on the wire, a proxy's whole URLs, and a WebSocket's `whenAborted()` on a reset. TLS policy and unit tests live beside the modules. Nothing in the workerd binary uses the crate yet; workerd's Rust server, in a following change, does. The bridge declarations use the `async unsafe fn f<'a>` form the in-tree cxx requires for borrowed async arguments; they move to the safe form once the bridge supports it. Crate patches (patches/rust/crates/<crate>/, applied through rust.MODULE.bazel): `http` and `httparse` accept the header-value bytes kj accepts (only NUL, CR and LF are rejected), so values round-trip byte for byte; hyper's `HeaderCaseMap` is public so header names keep the application's spelling; hyper's graceful shutdown keeps a connection with a partially received request open until it is answered, as kj's `drain()` does; hyper's client keeps a connection out of the pool until its response body is consumed, as kj's client does; and the response hyper's server generates for a request it cannot parse explains the error in a text/plain body, as kj's server does and RFC 9110 section 15.5 asks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>github.com-cloudflare-workerd · 5f4f3d3e · 2026-09-26
- 5.1ETVkj-rs-io: KJ async I/O interfaces backed by tokio Final piece of the Rust I/O backend, built on the kj-rs bridge already on main: implements the abstract kj::AsyncIoStream / kj::ConnectionReceiver / kj::Network / kj::AsyncIoProvider / kj::LowLevelAsyncIoProvider interfaces over tokio, so KJ async I/O runs on the tokio-backed kj::EventPort from kj-rs-tokio instead of kj's OS event loop. - Streams, networking (one listening socket per resolved address, KJ's aggregate receiver), socket pairs for the provider's newOneWayPipe/newTwoWayPipe (real sockets, so a write into an empty pipe completes without a reader and workerd's loopback transport gets the sockets it asks for), the --watch file watcher (Rust over the notify crate), signal delivery, and SIGPIPE handling. - Addresses are typed on the bridge. A SocketAddress shared struct (family tag plus that family's fields) is the only form an address takes between Rust and C++: Rust converts it to and from std's SocketAddr / std::os::unix::net::SocketAddr (safe code; the crate views or builds no struct sockaddr bytes at all), and async-io.c++ is the only place a raw sockaddr is decoded (getSockaddr) or encoded (getsockname/getpeername, KJ's NetworkFilter), field by field, at the KJ interfaces that speak them. A caller's struct with garbage past its family's fields, an oversized addrlen, a pathname filling sun_path with no NUL, or a zero-filled sockaddr_un all decode to what KJ makes of them; short or unknown-family structs throw. ffi.rs's hand-written unsafe is fd ownership and the raw read-buffer view alone. - The C++ half is interface adaptation with KJ's own structure where KJ has one. The connect fall-through loop and the accept loop live in the adapter, as in kj/async-io-unix.c++, and apply restrictPeers() there: to each target before connect() tries it and to each accepted peer, through PeerFilter, a wrapper over KJ's own kj::_::NetworkFilter behind a kj::Arc chain (atomic because KJ allows a kj::NetworkAddress to be cloned on another thread and clone() takes a share; a kj::Rc raced under TSAN). Rust returns the peer's typed address with each accepted or connected stream and lists the targets in order; no filter object and no C++ callback crosses the bridge. Both loops own their shares (listener handle, filter), so a receiver or address destroyed with an operation pending does not dangle. KJ's parse-time rejection of a filtered literal (and getSockaddr's eager check) is not reproduced: connect() rejects the same address with the same text. - Two port checks, both two thread-local reads. ensure_loop_thread() before every registration (connect, listen, wrap, resolve, signals, the hangup watch): a call on a thread without a TokioEventPort, 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). ensure_owner_loop() -- each stream and listener records the runtime it was registered with -- at the point an operation is about to wait for readiness, never on the fast path: a stream or listener carried to another loop thread fails at its first wait instead of parking on its creator's idle driver forever. Otherwise kj-rs-io is ordinary tokio code: a TokioEventPort thread is a tokio runtime thread for its whole life (kj-rs-tokio), so tokio's own constructors register with the loop's driver as they are; no wrapper runtime type, no lint. - Every adapter method starts its promise inside the call, as KJ's native streams start their operation (coroutine bodies run to their first co_await; eagerlyEvaluate -> EagerPromiseNode -> kj-rs FutureAwaiter::onReady polls on the caller's stack), so a promise that is kept but never awaited completes as the loop turns. When the syscall itself happens is deliberately tokio's semantics, not KJ's: reads and writes are tokio's try_read_buf / try_write / try_write_vectored plus readiness waits, with no direct socket2 / nix / libc syscalls, and vectored writes rely on std clamping the iovec count to IOV_MAX. KJ's AsyncStreamFd issues write(2) synchronously, so a KJ caller may drop a write's promise on the spot and the bytes still go out; under tokio that write is not sent if it is the first operation on a descriptor the driver has not yet seen ready. The long-term direction is to move workerd's I/O onto tokio, so the crate is written as the tokio program it will be part of; workerd's full test suite under the Rust backend has no fire-and-forget write of that kind. - whenWriteDisconnected() costs one dup(2)'d descriptor per stream, created on first use: tokio has one readiness registration per socket, and waiting on it for a hangup would park a concurrent writer. kj-http observes every served connection, so a workerd process holding N connections holds about 2N descriptors under this backend. That is a decision, stated in stream.rs: KJ permits a never-resolving promise (the Windows arm returns one), but early client-disconnect detection is what lets workerd stop work for clients that went away. - Ownership at the FFI boundary: raw fds/SOCKETs become owned typed handles in one `unsafe` block per (unsafe fn) bridge entry point (ffi.rs, the crate's only module allowed to write unsafe), with a bad handle reported as a kj::Exception rather than a panic; KJ read buffers, which callers may leave uninitialized, cross as pointer + length and are handled as MaybeUninit storage rather than `&mut [u8]`. Bridge declarations borrow only where the future really borrows (buffers); operations whose futures own their state are declared safe and lifetime-free, so the compiler enforces that independence. - Operations own their state: every Rust object behind a C++ wrapper is a handle to Arc-shared state and every bridged operation owns a share, so a wrapper destroyed with a read pending does not dangle -- the socket lives until the operation settles or is cancelled (the caller's buffer remains KJ's contract, as under KJ). Every handle is Send + Sync by type (Arc, atomics, Mutex; tokio's own resources are Send + Sync), asserted at compile time in lib.rs, so a rust::Box that C++ carries to another thread is never a memory-safety question. - KJ interface parity for what workerd uses: the address grammar is KJ's SocketAddress::parse for everything workerd.capnp documents for Socket.address / ExternalServer.address -- IP literals, wildcards, decimal ports, service names, hostnames (getaddrinfo with KJ's hints -- AF_UNSPEC, AI_V4MAPPED | AI_ADDRCONFIG -- so IPv6 scope IDs resolve and a host without IPv6 gets no AAAA results; the dns-lookup crate, chosen over tokio::net::lookup_host for exactly those hints and service names), unix: paths and, on Linux, unix-abstract: names (std's from_abstract_name behind tokio's bind_addr / connect_addr); abortRead() ends a pending read with EOF on every platform (the stream records the abort and wakes a parked read itself, since Windows' AFD poller reports no event for a local shutdown(SD_RECEIVE)) and performs KJ's shutdown(SHUT_RD); tryRead waits for readability on EAGAIN whatever minBytes is; address text and watched paths cross the bridge as bytes; connectAuthenticated() and acceptAuthenticated() build the peer identity from the typed peer address, with the network's or receiver's own filter chain threaded into the identity's NetworkAddress as KJ does; accept() retries KJ's set of transient per-connection failures and tolerates TCP_NODELAY failing on an already reset socket; setupTokioAsyncIo() ignores SIGPIPE once per process like kj::UnixEventPort. - Scope: this is workerd's provider, not a drop-in for every KJ program, and lib.rs ("Scope: workerd's provider") says so with the rule behind the list: no consumer in workerd's production code or its configuration surface (workerd.capnp's documented grammar counts), and hand-written libc / sockaddr / fd code to keep. Left out, documented at each site: kj's unix pipe-fd tier (wrapInputFd / wrapOutputFd take sockets only, kj's win32 definition, on every platform; newOneWayPipe is a socket pair), wrapConnectingSocketFd (UNIMPLEMENTED, like getsockopt/setsockopt and newPipeThread), wrapListenSocketFd with a caller-owned NetworkFilter (the two-argument allow-all overload workerd uses works; anything else is UNIMPLEMENTED rather than borrowed for the receiver's lifetime) -- both stubs close a TAKE_OWNERSHIP handle before throwing, since KJ's owning overloads have already released it -- KJ's strtoul(..., 0) port grammar, KJ's std::set re-sort of resolver results (getaddrinfo's RFC 6724 order is kept), the parse-time filter check, and content hashing / ctime tracking in the file watcher (metadata stamps only; the residue -- a same-length rewrite of the same inode within one kernel timestamp tick -- is documented). - The file watcher watches each file's directory (and a symlink target's directory: resolved through dangling links too, so a link whose target is created later fires, re-resolved while the target is missing, and re-registered when a retarget is reported) and judges changes by re-stamping the watched files (inode, size, mtime) whenever the backend reports anything -- an event, an overflow, an error. No event kinds or paths, no content hash, no ctime; a chmod or a replayed pre-watch event moves no stamp and does not fire. No event is stored (the producer only wakes the consumer) and no per-file watch or per-entry descriptor exists. The hand-off from notify's thread is a tokio Notify whose stored permit cannot lose a wake-up; onChange() rejects a second concurrent waiter. It has no C++ wrapper of its own: workerd's TokioFileWatcher (the io_backend change) holds the Rust watcher directly through the three bridged calls. Depends on the kj-rs same-thread waker cells change (#7349, the #7010 subset) for use in workerd: without it, kj-rs's cross-thread waker path races (FuturePollEvent::enterPollScope reading an unfulfilled waker promise, which --config=tsan reports intermittently). This backend must not be enabled by default before that change lands; until then the tokio-backed I/O is opt-in (nothing on main uses it). Dependencies: declares the socket2 (IPV6_V6ONLY, shutdown(2), and the family of a wrapped descriptor), dns-lookup (a safe getaddrinfo wrapper, called with KJ's hints on every platform), notify (file watching, default backends: FSEvents on macOS, which keeps no per-entry descriptor), nix (`fs` for fcntl, `signal` for SIGPIPE) and windows-sys (the Win32 / winsock error codes of KJ's exception-type table) crates and enables tokio's signal and io-util (try_read_buf into uninitialized buffers) features; Cargo.lock repinned accordingly. The CoreServices framework is linked on macOS for FSEvents. Tests: tokio-backed streams and networks (including SIGPIPE survival in a child process with the default disposition, a multi-address hostname listener accepting on every family, vectored writes past IOV_MAX worth of empty pieces and of non-empty pieces, unawaited (kept) writes still going out, abortRead ending a pending read, AF_UNIX pathname and -- on Linux -- abstract sockets listening, connecting, printing and identifying, socket-pair provider semantics (the Windows loopback pair accepting only its own client), decimal ports and service names with the octal/hex grammar gone, the intentional UNIMPLEMENTED / sockets-only stubs), file watching (including a symlinked file whose target lives elsewhere, a retargeted symlink whose new target directory is watched from then on, and a symlink whose target does not exist yet), connectAuthenticated identities over TCP and unix sockets (with the network's filter kept), sockaddrs with garbage past the family's fields or in their padding decoding to the same address, a sun_path-filling unterminated pathname printed whole, short or unknown-family sockaddrs rejected, restrictPeers applied at connect() and accept() (not at parse), every bridged operation refused with a kj::Exception on a thread without a TokioEventPort or under a foreign entered runtime (accept() included), reads and accepts on a different port refused at their first wait, transferred handles closed by the UNIMPLEMENTED stubs, addresses cloned concurrently on two threads (TSAN-clean, kj::Arc), HTTP over tokio-backed streams, a zero-initialized sockaddr_un through getSockaddr, an address with no socket addresses failing connect() and listen(), a zero-minimum read waiting for data, a cancelled backpressured write leaving the socket usable, Cap'n Proto RPC over tokio streams, and PeerFilter's chain ownership and atomic sharing. The C++ tests link statically on every platform: Bazel links cc_test binaries dynamically by default, and under the Linux tsan config each shared library then carries its own unwinder, so an exception thrown in libkj-rs-io-lib.so that unwinds through a frame in libkj-async-io.so (kj's inline owning wrap*Fd overloads) aborted in _Unwind_SetGR. Clean under --config=asan, --config=tsan-macos and the Linux tsan lane. Co-Authored-By: Harris Hancock <harris@cloudflare.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>github.com-cloudflare-workerd · 216ed990 · 2026-09-12
- 2.2ETVMake workerd's entry point Rust workerd now starts from a Rust main(). Rust owns the process: the command line, where the config comes from, --watch and the reload it triggers, and the compiled-binary format. C++ keeps the two things only C++ can do -- compiling a schema-file config with capnp::SchemaParser and running V8 and the Server -- behind a small bridge, and both halves live together in src/workerd/server. The C++ that remains is the Server's own surface; going lower means changing the Server. Compiled binaries embed the config as a standard encoded message rather than a flat single-segment one; a compiled binary always carries the workerd that reads it, so no deployed binary is affected. Two command-line corners change: the accidental `--p` spelling of --perfetto-trace is gone, and naming one socket in both --socket-addr and --socket-fd is an error instead of whichever came last winning. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>github.com-cloudflare-workerd · e03943a6 · 2026-09-21
- 1.5ETVPublish stream state before settling pending requests Keep queues and pending requests independently owned through notifications, and publish terminal or released state before settlement. Preserve replacement readers and writers. Keep BYOB progress, wrapper lifetime, and buffered bytes in sync across fan-out. Share draining between reads and close, preserving minimum-fill behavior, typed-view boundaries, and follow-up pulls.github.com-cloudflare-workerd · bc7398b2 · 2026-10-17
- 1.3ETVkj-rs-tokio: a kj::EventPort driven by a per-thread tokio runtime Piece of the Rust I/O backend split, built on the kj-rs bridge already on main: TokioEventPort implements kj::EventPort over a per-thread tokio current_thread runtime, so a KJ event loop sleeps by parking inside tokio's scheduler -- tokio tasks (and, in the next piece, tokio I/O) run whenever KJ would otherwise block. - wait()/poll() park in LocalSet::block_on on the runtime, woken by cross-thread wake(), by KJ itself when a tokio task queues KJ work, or by the next KJ timer deadline (tokio's timer wheel; ~1 ms, the same granularity as KJ's epoll-based port). - A tokio task running inside the park may use KJ freely -- fulfill a PromiseFulfiller, arm a timer, add to a TaskSet, wake a bridged future -- and the port learns of it through KJ's own hooks rather than a convention: kj::EventLoop reports setRunnable(false) right before it calls wait() (capnproto/capnproto#2814), so the first arm during the park is a setRunnable(true) edge the port turns into a notify; and the port installs itself as the kj::TimerImpl's SleepHooks for the duration of the park, so a sooner timer armed mid-park ends the park and kj::Timer::now() reads the live clock. The one thing a task must not do is re-enter promise.wait() / waitScope.poll() on the loop thread (tokio rejects the nested block_on; the panic surfaces as a kj::Exception). - The port owns its kj::EventLoop, declared last so it is destroyed first, and cancels the LocalSet's spawned tasks in its own destructor before any member goes. Teardown order is member order rather than something a context has to remember: spawned tasks routinely own KJ promises (a bridged PromiseFuture holds an OwnPromiseNode and an armed awaiter Event; a KJ timer promise holds a TimerImpl entry), and dropping them after the loop died was a use-after-free (reproduced: ~TimerPromiseAdapter dereferencing the destroyed TimerImpl). A bare port with no TokioAsyncIoContext is safe too. One port per thread is KJ_REQUIREd on the C++ side and asserted on the Rust side. - kj_rs_tokio::spawn() enqueues !Send futures onto the loop's LocalSet, pinned to the loop thread. - The loop thread stays inside the runtime's tokio context for the port's whole life (EnteredRuntime, the crate's one hand-written unsafe: tokio's EnterGuard<'a> carries its lifetime only as PhantomData, so it is extended to 'static and dropped before the runtime by a manual Drop). Bridged futures are polled outside block_on, and everything they create -- tokio sockets, AsyncFds, signal streams, timers -- must find this runtime's drivers; entering once replaces re-entering around every poll of every I/O future. TokioPort is therefore !Send by type (the context is left on the thread that entered it), which is the contract it always had. - setupTokioAsyncIo() yields the port (loop) + wait scope only; the I/O providers over this port arrive in the kj-rs-io piece. - This port relies on capnproto/capnproto#2814 (lock-free Executor::isCurrent(); setRunnable(false) before EventPort::wait()), now merged into v2. This adds tokio's `sync` feature (Notify) to the workspace; no new crates. Also folds in the #7011 review fixes: link the test bridge through link_deps; avoid tokio TLS access during late thread teardown; bound port shutdown so it does not wait on spawn_blocking tasks; keep task cancellation and thread-affine port operations on the receiver's physical owner thread (rejecting foreign-thread calls); reject tokio setup inside an already-current KJ loop before installing runtime TLS; make the async I/O context immovable; and measure timer deadlines from the precise clock. - Tests: a spawned task fulfilling a kj::PromiseFulfiller during a timed park and during a wait-forever park; a KJ timer armed by a task during both kinds of park, honored at its own (live-clock) deadline; a cross-thread bridged wake arriving while the loop is busy-polling rather than parked; spawned tasks holding a KJ timer / a bridged KJ promise across context teardown and across bare-port teardown (ASAN); one port per thread; a bridged future woken from a plain std::thread while the loop is parked; poll() budget under a yield-forever task; nested promise.wait() from a task surfacing as a kj::Exception; timer cancellation; already-due timers; a panicking spawned task surfacing as a kj::Exception and a detached task still completing; two tokio-ported loops executeAsync-ing into each other concurrently (TSAN); and the review-fix reproducers (port outliving tokio thread-locals, drop not waiting on blocking tasks, foreign-thread cancellation/operations, already-current KJ loop rejection). port.rs units: wake-latch semantics, a concurrent wake() storm from multiple threads (TSAN), cross-thread Handle::spawn, poll() advancing a ready LocalSet task, !Send spawned futures, and the bare-TokioPort Drop fallback. Clean under --config=asan and --config=tsan-macos. - Object-relationship overview in tokio-event-port.h. Co-Authored-By: Harris Hancock <harris@cloudflare.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>github.com-cloudflare-workerd · 10c06735 · 2026-08-14
- 0.8ETVkj-rs-io: "loopback:" addresses, used by `workerd test` The "loopback:" addresses `workerd test` uses to exercise the network stack inside one process move from the CLI's own kj::Network wrapper into kj-rs-io, alongside the rest of the tokio I/O layer. A loopback address names a queue: connect() makes a socket pair, queues one end and returns the other, and a receiver's accept() takes from the queue. Every TokioNetwork carries a LoopbackRegistry, shared down its restrictPeers() chain and off unless enableLoopback() is called, so the production address grammar is unchanged; a name belongs to the loop that first parsed it. `workerd test` enables it on the provider's network, and the wrapper goes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>github.com-cloudflare-workerd · 28a0e039 · 2026-09-22
- 0.7ETVserver: run workerd on the Rust server `server.c++`, `server.h` and `cli-main.{h,c++}` are deleted, with the `WorkerdApi::Global` struct and the `compileGlobals` overload taking it. The `workerd` binary is the Rust command line (`cli/`, `workerd-cli`) over the `workerd-server` crate and the C++ `WorkerFactory` of the previous commit: the command line runs each command through the crate's `entry` module in place of `cli-main.c++`, and hands inherited `--socket-fd` sockets to the server. This is the first use of kj-hyper in the workerd binary. src/workerd/server/AGENTS.md has the design in detail. Parity is shown by `server-test.c++`, which is kept and runs the Rust server inside the test process. Each `TestServer` builds a `WorkerFactory` on the test's V8, mock timer, network and a temp directory and hands it to `InProcessServer` (`server/in_process.rs`). The config's socket and external addresses become `loopback:` names the test connects to and accepts on, so no connection but UDP's leaves the process. An unexpected error log, config error or warning fails a case, as does a factory still referenced once the server is closed. main has 109 cases and this tree 111 (3 are Linux-only in both): 1 moved, 3 added, 9 changed, and every other body identical. - Moved `UDP listener drops truncated datagrams` to a unit test in `server/listen/udp-test.rs`: only main's mock port could deliver a truncated datagram to a whole listener, and a real socket cannot overfill its 65,535-byte buffer, so the test receives into a 4-byte buffer. - Added `a durableObjectClass binding without props can be sent over RPC`: a configured binding without props is not a `ctx.exports` template. - Added `Workflow engine configuration is checked field by field`: new coverage of the `workflowsEngine` config errors. - Added `a service that fails to start does not leak the services before it`: the factory is unreferenced after a failed start. - `Durable Objects (on disk)`: shared storage is a real temp directory symlinked into the test root (`newSharedDirectory()`), since the server opens real files; it holds 9 files, not 6 (`-shm`). - `Durable Object alarm persistence (on disk)`, `Durable Object evictions when callback scheduled`, `Durable Object facets` and `Durable Object facet cloning` use the same shared directory. - `Durable Objects websocket constructor blockConcurrencyWhile throws after send` reads the `pending` frame before EOF, because the in-memory connection is buffered. - `network outbound with allow/deny`: the lists are CIDRs, because the server parses the filter. - `disk service` sets modification times with `test.setModified` where main assigned `test.fakeDate`, because real files take the real time. - `disk service allow dotfiles` sets `.dot`'s time the same way, and expects 204, not 403, for `PUT /%2e%2e/secret` (disk service URLs, below). - Harness: `receiveSubrequest` no longer asserts a `network` service's peer filter, since the service dials the loopback registry first; four unit tests in `server/services/network-test.rs` cover the filter. In the end-to-end tests, `tests/server-harness.mjs` splits the `--control-fd` stream on newlines before parsing, since one read may carry several events or part of one, and the three UDP configs bind `127.0.0.1:0` in place of `*:0`, which their `udp4` clients reach inside Bazel's macOS sandbox. Behaviour changes against main's C++ server follow. Process and command line: - Every config error is reported and every worker compiled, then exit 1; main exited at the first error. - A refused config writes no `listen` event to `--control-fd`; main wrote the earlier sockets'. - A socket that cannot be bound is a config error (`Socket "main": bind(): ...`); main died with `*** Fatal uncaught kj::Exception`. - Structured logging keeps its two streams: worker consoles and KJ logs are JSON lines on stdout, and the command line's own messages (config errors and warnings, `Tests failed!`) are JSON lines on stderr. What differs: the run failure (`*** Uncaught exception ***`) is a JSON line on stderr too (main: plain text), and the stderr lines carry `source` `src/workerd/server/server/entry.rs:133` (raw `file!()`, with a `src/` prefix no other log line has) where main's named `json-logger.c++`. - An error that passed through Rust prints as `file:line: type: description` without `stack:`; one made in Rust has the bridge's `file:line`. - A failure before the server runs (a bad V8 flag) prints its description only: no `file:line`, type or stack. - V8 is torn down on every exit (main: only with `KJ_CLEAN_SHUTDOWN`), and the perfetto session on the config-error exit too. - `workerd test` filters are `glob` crate patterns over the whole name: `[...]` and `**` are special, `*` crosses `/`, a suffix after `/` no longer matches, and a bad pattern is a run failure. - `[ PASS ]` / `[ FAIL ]` durations are Rust's `Duration` debug form (`3.520417ms`, the micro sign as U+00B5); main printed `5.708ms` and U+03BC. - `analyticsEngine` and `unsafeEval` bindings without `--experimental` are left out of `env` with the shared wording; main kept the `analyticsEngine` binding. - `--perfetto-trace`: of main's 33 server-layer `TRACE_EVENT`s three remain, in the factory (`Bootstrap()`, `worker_start_request()`, `factory_new_worker()`); the other 30 (startup and drain phases, bindings, listeners, services, requests) are gone. - Windows: stdio is not put in binary mode. HTTP on the config's sockets and to `external` and `network` services is hyper in place of kj-http; the inspector, the fallback service, the container client and the Pyodide bundle download (`pyodide.c++`, with kj's TLS) keep kj-http. The rule applied: hyper's behaviour is accepted where hyper conforms to the RFC, where both conform and where both deviate; hyper is patched where only kj conformed, and the explanation body on a parse-error response (in kj-hyper) is the one such patch. - The connection closes after a request with `Connection: close` or HTTP/1.0 without keep-alive. - An HTTP/1.0 request gets an HTTP/1.0 status line and an unchunked body ended by close. - `Content-Length` with `Transfer-Encoding` is read as chunked, with `Content-Length` hidden, and the connection then closes. - `400` for an invalid or differing `Content-Length` and a `Transfer-Encoding` not ending in `chunked` (main: 500), for `Transfer-Encoding` on HTTP/1.0 and an unknown version (main: served), and for a garbage request line (main: 501). - Accepted where main answered 500 or 501: identical duplicate `Content-Length`, `Transfer-Encoding: gzip, chunked`, absolute-form targets, `OPTIONS *`, a leading empty line. - HEAD of a streamed response omits `Transfer-Encoding`; a header's repeated values are written adjacent. - An unsupported or missing `Sec-WebSocket-Version` gets `426` with `Sec-WebSocket-Version: 13` (main: 400); a WebSocket handshake by POST gets `400` (main: 500). - Unparsable requests get `400`, `414` or `431` with hyper's description as the body; a target over 65,534 bytes is `414` (main served 70,000). - Head limit: main refused at 128 KiB; hyper's is nominally 417,792 bytes (500,000 accepted, 520,000 refused), and more than 16,384 headers is `431`. - An idle keep-alive connection closes after 15 s (main: 5 s). - A GET or HEAD request body of unknown length is not forwarded upstream (main sent it chunked). Outbound, TLS, disk, config: - `external` and `network` services open a connection per subrequest (a service builds its kj-hyper client per event); main reused connections. - An external server's address is parsed and resolved at each dial; main resolved once at startup. - A `network` service resolves with tokio's `lookup_host` (no `AI_ADDRCONFIG` / `AI_V4MAPPED`) and dials IP addresses only. - `unix` / `unix-abstract` in a network service's `allow` or `deny` do nothing; on main `connect({hostname: "unix:sock", port: 1})` reached a unix socket named `sock:1` (names with `/` are refused on both). - TLS is rustls: `cipherList` is ignored (main failed startup on an unusable list), and a `minVersion` below TLS 1.2 means 1.2. - `requireClientCerts` with `trustBrowserCas` is refused at startup, as a run failure; main accepted it. - Disk service URLs go through the `url` crate: `.` and `..` resolve even percent-encoded (`PUT /%2e%2e/secret` is 204 inside the directory; main: 403), `\` separates segments, and `GET /sub//f.txt` is 404 (main: 200). - CryptoKey `pkcs8` / `spki` PEM is read by rustls' parser, not main's `decodePem`. - A `fromEnvironment` binding whose value is not UTF-8 is converted lossily; main passed the bytes through. Outside src/workerd/server: - Comments and docs that named the deleted files name their replacements: `src/workerd/api/restore.c++`, `src/workerd/util/setup-async-io-test.c++`, `docs/jsg.md`, `docs/reference/detail/new-module-registry.md`, the AGENTS.md files (kj-rs-io's entry follows in the next commit), bonk's rust-first paths, and `just test-compile-flags`, which now checks `factory/worker-factory.c++`. Known gaps: - Outbound connections are not reused (above): three fetches through an `external` service open three connections where main opened one. - The config inputs that are accepted and ignored, above (`cipherList`, a low `minVersion`, `unix` / `unix-abstract` filter entries), produce no config error or warning. - `StructuredLoggingProcessContext` (`json-logger.h`) has no user left but its three cases in `json-logger-test.c++`. - Three comments still name the deleted server: `src/workerd/server/log-schema.capnp`, `src/workerd/api/sockets.h` and `src/workerd/api/tests/worker-loader-test.js`. What has run, on the series as one change: `bazel test --test_size_filters= //src/...` on macOS arm64 only, with 2,132 passing, 8 skipped and 8 failing. Six are `container-client` (`testPidNamespace` and `testSetEgressHttp`, against a hand-built substitute image) and `worker-loader-test`'s four Python cases (a certificate error fetching the Pyodide bundle, under main's server too), three variants each. The other two (`http-nodejs-test` and `http-client-cpp`, one variant each) failed on timing while the machine was loaded and pass in 5 reruns on their own. Not run: anything on Linux or Windows, which leaves the three `#if __linux__` cases, `unix-abstract:` addresses, the `--socket-fd` listening check and fuzzilli to CI; clang-tidy on `factory/*`; `@gc-stress`, ASAN, TSAN and coverage, including the `KJ_CLEAN_SHUTDOWN` exit path; CryptoKey PEM on malformed input; and miniflare against this binary. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>github.com-cloudflare-workerd · c7094b7f · 2026-10-12
- 0.7ETVworker: PromisedInterface, into_kj and notSupported for Rust interfaces Adds to the `worker` crate the pieces a Rust caller needs to stand in for, and hand off to, a C++ `workerd::WorkerInterface`: - `PromisedInterface`: an `Interface` whose target is still starting. It holds the future that produces the target's `WorkerInterface`; the first event drives that future and every event, first or later, waits for the target and then runs on it. A start failure is kept as the complete `KjError` (type, description, throw site and detail records, which is how CPU, memory, wall-time and kill-switch failures are classified) and every later event fails with a clone of it. C++ constructs one through `new_promised_interface` / `newPromisedInterface(WorkerPromise)`, the counterpart of `newPromisedWorkerInterface`. - `Interface::abandon_alarm`, with the same no-op default as `WorkerInterface::abandonAlarm`, bridged in both directions (`RustWorkerInterface::abandonAlarm` -> `Wrapper::abandon_alarm`, and `CxxWorkerInterface::abandon_alarm` -> `worker_abandon_alarm`). The stored alarm time crosses as i64 nanoseconds like the other dates. `PromisedInterface` delegates it to its target once started, as `PromisedWorkerInterface` does, so an actor reached through a Rust interface still clears its alarm state when retries are exhausted. - `Interface::into_kj()`: converts a Rust `Interface` into the `kj::Own<WorkerInterface>` that C++ expects, through the `wrapper_into_kj` bridge function (`kj::from<kj_rs::Rust>` over the `Wrapper` box in ffi.c++). - `not_supported(event)` and `Interface::custom_event`'s default body: answer a custom event the way a C++ `WorkerInterface` that returns `event->notSupported()` does, so implementations that do not handle custom events no longer have to answer for the event themselves. The `ok` test worker drops its `custom_event` override accordingly, and a KJ_TEST checks the default defers to the event's `notSupported()`. - `custom_event_failed(event, error)`: delivers a start failure to a custom event that will never run (`event->failed(...)`), so a JS RPC session resolves its client to the original error rather than reporting the event destroyed before completion. The error crosses as a boxed Rust `Error` whose `raise()` returns it as `Err`; the bridge's own `Result` path throws that as the equivalent `kj::Exception`, so the event receives the full exception (location and detail records included, which `JsRpcSessionCustomEvent::failed()` relies on) rather than one rebuilt from type and text. `PromisedInterface` uses it when its target fails to start. `toRustOutcome` moves from ffi.c++ into bridge.h alongside its inverse `fromImpl`, `:bridge` becomes publicly visible, and the crate gains the `futures` dependency for `LocalBoxFuture`. The `custom_event_failed` shim that delivers that failure is declared fallible (`-> Result<()>`): `CustomEvent::failed()` is overridable and may throw, and an `extern "C++"` shim that can throw must return `Result` so the exception reaches Rust as an error rather than a panic at the bridge. `PromisedInterface::custom_event` propagates it, so a throwing `failed()` rejects the event with that exception. The other reverse shims are all `async -> Result<..>` already, and `wrapper_into_kj` only heap-allocates. KJ_TESTs cover the startup failure surfacing in full on repeated later events and through `customEvent`'s `failed()`, a `failed()` that throws rejecting the `customEvent` promise with its exception, and `abandonAlarm` delegation through `PromisedInterface` both before and after the target starts. These are used by the Rust server in a following PR. Co-Authored-By: Claude Code <noreply@anthropic.com>github.com-cloudflare-workerd · ea5812da · 2026-09-26
- 0.7ETVkj-rs: drop a KjOwn<T> through kj::Own<T>'s own destructor Rust dropped every KjOwn<T> as a kj::Own<void>. kj::Own<T>::~Own() hands the disposer the address of the complete object (dynamic_cast<void*> for a polymorphic T); kj::Own<void> passes the T subobject's address unchanged. When T is a base at a nonzero offset of the object it points into (multiple inheritance: RequestObserverWithTracer's WorkerInterface base, kj::NullStream's AsyncOutputStream base), the object was deleted at the wrong address. The cxx bridge now generates a per-type drop, as it does for UniquePtr<T>: a C++ function running kj::Own<T>::~Own() and a Rust OwnTarget implementation calling it, for every T the bridge declares and holds in a KjOwn. A bridge that only aliases a type asks its declaring bridge for the implementation with `impl KjOwn<T> {}`. The primitive types a KjOwn may hold are implemented in kj-rs itself. The new own-test reproduces the miscomputed address with a two-base class handed to Rust as a kj::Own of its second base. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>github.com-cloudflare-workerd · 1481f440 · 2026-09-23