Deforkify event loop - #2
Merged
Merged
Conversation
Drop the wasm-streams checkout and cdylib patch in favor of guybedford/wasm-streams (MattiasBuelens/wasm-streams PR pending). The socket2 git rev was exactly the v0.6.5 release.
One perpetual `#[wasm_bindgen(jspi)]` export builds a current-thread runtime and `block_on`s the whole server lifetime; every park suspends the Wasm stack on `epoll_wait`, so the hosted runtime adapter is gone. Pumpkin binds its stock `TcpListener` on 25565 inside the Durable Object's port table, and the object routes each inbound socket to it with `handleAsNodeConnection`, replacing the injected-stream entry point, wasm-streams, and the workers-rs dependency. `stop` cancels the server and the run promise settling is the checkpoint signal. `-sREENTRANT_JSPI` gives each activation its own shadow stack, so other entries into the module while the server is suspended cannot clobber its frames. Toolchain: Rust beta; emscripten main plus the JSPI hooks, reentrant JSPI and epoll listener PRs, with the paired emscripten-releases LLVM and the jspi-hooks Binaryen branch built by setup; wasm-bindgen 0.2.128 CLI via `-sWASM_BINDGEN`; exnref exception handling throughout; tokio `emscripten-epoll`, mio tokio-rs/mio#1969, libc `libc-0.2`. rustc needs a larger compile-thread stack for pumpkin-data. The wasm-bindgen and workers-rs patches and the CLI lockfile are gone; the Pumpkin patch drops the injected-stream entry point. Requires a workerd with per-Durable-Object port tables and `net.Server` inbound routing (`MINIFLARE_WORKERD_PATH`). CI moves to Linux.
The whole Worker is now Rust linked directly by cargo and emcc, with no JSPI and no JS-side driver. The Durable Object owns an EventLoopRuntime (tokio-rs/tokio#8479): Tokio's current-thread scheduler and drivers wait on the host event loop through Emscripten's epoll listeners (emscripten-core/emscripten#27547). Exports return promises; the server lifetime is a scheduled root that completes by callback after the final save and checkpoint. Connections arriving while the server starts or stops wait for the next phase change and retry. worker/index.mjs re-exports the generated module and derives the object class from DurableObject for RPC. The worker and worker-build dependencies, the TypeScript worker, and the filesystem fixtures are removed. The Pumpkin patch makes the stop token replaceable behind a StopSignal and adds reset_stop so a server can start again in the same instance. Under NODERAWFS the host enforces permissions; src/workerd.js forces FS.ignorePermissions since workerd reports directories without execute bits, and keeps stdio on the console callbacks. Documentation describes the event-loop model and the workerd node:fs requirements (positional buffer I/O, O_TRUNC, O_CREAT, rename over an existing path).
guybedford
force-pushed
the
deforkify-event-loop
branch
4 times, most recently
from
September 24, 2026 06:39
b48bd74 to
bae8d13
Compare
guybedford
marked this pull request as ready for review
September 24, 2026 06:49
guybedford
force-pushed
the
deforkify-event-loop
branch
7 times, most recently
from
September 25, 2026 21:25
dfe2fbe to
16a7895
Compare
The Worker is now written against the `worker` crate with its `experimental_tokio` feature, built by worker-build's Emscripten mode: worker-build owns the codegen and link settings, wraps the exports into the entrypoint and derives the Durable Object class for RPC, so the hand-written JavaScript entrypoint goes away. `connect` is a `#[wasm_bindgen(experimental_tokio)]` export on Tokio's host-driven event loop. The toolchain follows the sealed releases: worker-build (cloudflare/workers-rs #1061, installed by setup from the revision Cargo.toml pins the `worker` crate to) provisions Emscripten 6.0.10 with the epoll-listener and async-DNS patches (tag 6.0.10-cf.emscripten) and the wasm-bindgen 0.2.129 CLI itself; Tokio 1.53.1 with the event loop (tag 1.53.1-cf.emscripten) and mio 1.2.3 with the Emscripten selector (tag 1.2.3-cf.emscripten) are git dependencies, and wasm-bindgen and wasm-streams 0.7 come from crates.io. Setup keeps only the patched Pumpkin checkout under .work; Wrangler's build command runs scripts/build.sh.
The world is again a live SQLite-backed filesystem instead of a tree copied to and from storage at start and stop: the object constructor mounts durable-object-fs at /data through worker-fs-mount, and src/workerd.js rebinds Emscripten's NODERAWFS to that node:fs implementation, so every Pumpkin write is durable as it happens. Relative paths are resolved against Emscripten's own working directory, which is the object's rather than the isolate's. The restore/checkpoint code and the files table go away; status reports the last save time. The stdio override is removed: workerd's node:fs supports the process's stdout and stderr descriptors, and Wrangler relays them as plain stdout and stderr lines rather than console errors. Wrangler's build command runs scripts/build.sh so it resolves the provisioned worker-build regardless of PATH.
workerd 1.20260918.1 ships the net.Server Durable Object routing and the node:fs fixes this server needs, so Wrangler 4.135.0's bundled workerd is used as is; the MINIFLARE_WORKERD_PATH instructions for running a workerd main build go away. The version is also pinned directly in package.json.
Ports Dan Lapid's dlapid/moreMemoryOpts (ec02ab2, cd83251) onto the event-loop runtime, keeping what applies and dropping what the runtime change superseded. Pumpkin patches: packed chunk palettes with copy-on-write light arrays, on-demand carving masks, bounded density pools, pruned structure caches and lazily grown pathfinder heaps (pumpkin-memory.patch); byte-bounded outgoing chunk batches with backpressure, idle generation-graph release, and feature-gated imports so headless builds compile without warnings (pumpkin-emscripten.patch). The injected-stream connection entry point, the checkpoint barrier through the chunk writer, and the inline Rayon shim are not taken: the stock accept loop, the mounted filesystem and Tokio-spawned jobs cover them. Worker: operator settings (view and simulation distance, player limit, compression, MOTD, seed, idle timeout) are Worker variables, validated in the object's constructor and reported in status. The `#[event(connect)]` handler reads the Minecraft handshake over the `worker::Socket`: server-list pings are answered from the object's metadata without starting a server, and login connections are forwarded with `Stub::connect`, the consumed bytes written ahead and both directions copied with Tokio. The object's `connect` hands each socket to Pumpkin's listener with `Socket::handle_as_node_connection`. After the last player leaves the server saves the world and stays up for the reconnect window before stopping; status gains runtime_starts, idle_deadline and settings. Packet compression is on by default. worker-fs-mount rejected the S_IFREG | perms mode Emscripten's FS.open passes to its backend, so a player's data file could not be created on disconnect; npm install now applies patches/worker-fs-mount-open-mode.patch (masking numeric modes to their permission bits, as Node does) until danlapid/worker-fs-mount#10 ships. Tests: compression-aware protocol client, unit tests for framing, an experience probe for measuring configurations, and an integration flow covering pings that must not start the runtime, the player limit, a reconnect during the idle window and restart without compression. Docs: configuration guide with the measurement results.
guybedford
force-pushed
the
deforkify-event-loop
branch
from
September 25, 2026 22:48
16a7895 to
a244fb5
Compare
The deployed Worker was last uploaded with the declarative `exports` configuration, which the API does not allow reverting to `durable_objects` bindings and `migrations`, so wrangler.jsonc declares `MinecraftWorld` as a SQLite durable-object export and the Worker reaches its namespace through `cloudflare:workers`' `exports` rather than an environment binding. Wrangler 4.141.0 with workerd 1.20260925.1, and setup runs `npm ci` so one command provisions everything.
wrangler.jsonc had its build hook renamed away by a probe run, so `wrangler dev` and `deploy` were not rebuilding Rust; the hook is back. worker-build (cloudflare/workers-rs#1061) now runs wasm-opt single-threaded on macOS itself, so the build script no longer does.
worker-fs-mount's mount table is per isolate, and in production an isolate outlives an object instance: constructing the object again (after eviction, or another world landing in the same isolate) threw "Path already mounted". The constructor now unmounts first; one wasm instance serves one object at a time, so the current object owns the mount.
cloudflare/workers-rs#1061 has landed and shipped: the `worker` crate comes from crates.io and setup installs the matching worker-build release. The Worker's CPU limit is raised to 3,000,000 ms for world generation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #1: the server moves from JSPI onto Tokio's host-driven event loop, and onto the released tooling around it. The Worker is written against the
workercrate and built byworker-build --emscripten, runs on stock Wrangler/workerd, and the world is a mounted SQLite filesystem again.Changed vs #1:
connectis a#[wasm_bindgen(experimental_tokio)]export on Tokio'sLocalEventLoop(rt: support hosted event loops on wasm32-unknown-emscripten tokio-rs/tokio#8484); the host event loop is the runtime's wait, exports return promises and nothing blocks or suspends. No-sREENTRANT_JSPI, no shadow-stack switchingworkercrate (git dependency on worker-build --emscripten cloudflare/workers-rs#1061,experimental_tokiofeature) andworker-build --emscripten, installed by setup from the same revision; worker-build provisions Emscripten (emsdk 6.0.10 + its bundled epoll-listener/async-DNS patches) and the wasm-bindgen CLI itself, so.work/holds only the patched Pumpkin checkout. Replaces the hand-written JS entrypoint and the direct emcc/wasm-bindgen invocationexports(the deployed Worker's existing configuration) and reached throughcloudflare:workers'exports. node:net: add net.Server and cloudflare:node connectHandler cloudflare/workerd#7306 and #7313 (net.Server routing,handleAsNodeConnection) and the node:fs fixes #7368, #7369, #7393, #7394 have all shipped, soMINIFLARE_WORKERD_PATHis gone/datathroughworker-fs-mountwith thedurable-object-fsbackend, so every Pumpkin write is durable as it happens; the tree-to-SQLite checkpoint/restore (persist.rs, the files table) is removed and status reportssaved_atStopSignal/reset_stoplet an object start another server after one stopsstdout:/stderr:linesAlso brings in dlapid/moreMemoryOpts (ec02ab2, cd83251), ported onto this runtime so everything lands together:
VIEW_DISTANCE,SIMULATION_DISTANCE,MAX_PLAYERS,COMPRESSION_THRESHOLD/LEVEL,MOTD,WORLD_SEED,IDLE_TIMEOUT_SECONDS), validated in the object and reported in status; packet compression on by default. See docs/configuration.md#[event(connect)]handler reads the Minecraft handshake over theworker::Socket: server-list pings are answered from object metadata without starting a server, and login connections are forwarded withStub::connectand copied with Tokio; the object is a#[durable_object(connect)]whose handler hands each socket to Pumpkin's listener withSocket::handle_as_node_connectionand awaits it (the mount usesstate.storage().as_raw(), the final saveStorage::sync)idle_deadline,runtime_startsin status)worker-fs-mount0.2.0 rejects theS_IFREG | permsmode Emscripten'sFS.openpasses to its backend (Node masks numeric modes to0o7777); the fix is danlapid/worker-fs-mount#10, and until it shipsnpm installapplies it to the installed package aspatches/worker-fs-mount-open-mode.patch.PR branches referenced directly, now as tags
6.0.10-cf.emscripten: the 6.0.10 release plus Add emscripten_epoll_add_listener for epoll readiness callbacks emscripten-core/emscripten#27547 (epoll listeners), Add emscripten_dns_lookup_async / emscripten_dns_lookup_result emscripten-core/emscripten#27742 (async DNS), Add JSPI lifecycle hooks (<emscripten/jspi.h>) emscripten-core/emscripten#27698 (JSPI lifecycle hooks) and Add REENTRANT_JSPI: a shadow stack per JSPI activation emscripten-core/emscripten#27699 (REENTRANT_JSPI). The JSPI hooks are re-included for the wasm-bindgen jspi +experimental_tokiopath; this server does not use them. LLVM, Binaryen and Node come from the paired emsdk 6.0.10 releaseversion_132_jspi_hooks_1: Add jspi-hooks pass WebAssembly/binaryen#9102 (the JSPI-hooks path only)1.53.1-cf.emscripten: wasm: support JSPI for Emscripten target tokio-rs/tokio#8285, wasm: support the net feature on Emscripten tokio-rs/tokio#8438, rt: addLocalEventLoopdriven by a host event loop tokio-rs/tokio#8479, rt: support hosted event loops on wasm32-unknown-emscripten tokio-rs/tokio#8484, plus the emscripten-jspi-hooks layer1.2.3-cf.emscripten: Add wasm32-unknown-emscripten target support tokio-rs/mio#1969experimental_tokioattribute (Add #[wasm_bindgen(experimental_tokio)] for exports driven on Tokio's event-loop runtime wasm-bindgen/wasm-bindgen#5334) and the rlib-only fix (Build only the rlib crate type MattiasBuelens/wasm-streams#40) are released, so no wasm-bindgen or wasm-streams fork remainsgbedford/worker-build-emscripten: worker-build --emscripten cloudflare/workers-rs#1061 (theworkercrate'sexperimental_tokiofeature,#[event(connect)],Stub::connect,Socket::handle_as_node_connectionandworker-build --emscripten)libc-0.2branch: emscripten: Add epoll support rust-lang/libc#5427 merged, unreleasedemscripten: Enablegetrandomsupport for Emscripten briansmith/ring#2877To run - same instructions as the README, on stock Wrangler; no workerd, Emscripten or wasm-bindgen build needed.
bash scripts/setup.shprovisions everything (Pumpkin checkout, worker-build,npm ci);npm testpasses end to end from a clean checkout, printingPUMPKIN-DO-SQLITE-RESTART-OKafter verifying player and chunk restoration across a restart.