From 02714c296f74b9c4b0bed81972e4e27a2f4117f7 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Mon, 10 Aug 2026 14:11:32 +0200 Subject: [PATCH] docs: update plan done logs (Phases 1-4) - 01: crate scaffolding - 02: event loop epoll - 03: hand-rolled HTTP - 04: tokio/axum cutover --- docs/plan/done/01-scafolding-crates.md | 6 +++--- docs/plan/done/02-event-loop-epoll.md | 18 ++++++++--------- docs/plan/done/03-hand-rolled-http.md | 20 +++++++++---------- .../plan/done/04-cutover-remove-tokio-axum.md | 10 +++++----- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/plan/done/01-scafolding-crates.md b/docs/plan/done/01-scafolding-crates.md index 2b86312..20b587e 100644 --- a/docs/plan/done/01-scafolding-crates.md +++ b/docs/plan/done/01-scafolding-crates.md @@ -15,8 +15,8 @@ Lay out the full `crates/` directory tree that the 7-phase `.wo` runtime design 1. **Scope: all 7 phases.** 14 new empty library crates covering Phases 2 → 6 land in one pass. `rt` (already shipping Stage 2) is the 15th. 2. **`rt` stays monolithic.** Today's Stage 2 code — lexer / parser / AST / compile / engine / server — stays inside `crates/rt/` and continues to satisfy the 14 existing unit tests. Code migrates into the new crates as each phase activates, not in this pass. 3. **Contents: `Cargo.toml` + `src/lib.rs` doc-comment only.** Each `lib.rs` is one module-level `//!` doc block pointing at the phase doc, naming the responsibilities, and flagging which modules in `rt` migrate here later. No placeholder types, no stub traits. -4. **No `wo-` prefix.** New runtime crates are `ql`, `value`, `engine`, etc. — not `wo-ql`, `wo-value`. The prefix is redundant inside the project's own `wo` namespace and noisy in imports (`use ql::Parser` beats `use wo_ql::Parser`). The v1 crates in `reference/crates/` keep their `wo-` prefix — the distinct prefix makes the v1/v2 split visible at a glance. -5. **Workspace membership: root `Cargo.toml` lists every new crate as a member.** `reference/crates` stays `exclude`-d (nested workspace, separate v1 code). +4. **No `wo-` prefix.** New runtime crates are `ql`, `value`, `engine`, etc. — not `wo-ql`, `wo-value`. The prefix is redundant inside the project's own `wo` namespace and noisy in imports (`use ql::Parser` beats `use wo_ql::Parser`). The v1 crates in `.dev/reference/crates/` keep their `wo-` prefix — the distinct prefix makes the v1/v2 split visible at a glance. +5. **Workspace membership: root `Cargo.toml` lists every new crate as a member.** `.dev/reference/crates` stays `exclude`-d (nested workspace, separate v1 code). Rationale and alternatives considered: see [`../../CLAUDE.md`](../../CLAUDE.md) "What's in `rt` today vs. what the empty crates promise" and the recorded `AskUserQuestion` answers that preceded this plan. @@ -53,7 +53,7 @@ All names are stable — documented in [`../runtime/database/07-wo-seg-migration | `crates/README.md` with the phase-mapped inventory | ✅ | | `cargo build` at root (compiles 15 crates) | ✅ | | `cargo test --lib` at root (14 existing `rt` tests) | ✅ | -| `cd reference/crates && cargo build && cargo test` (v1 still green) | ✅ | +| `cd .dev/reference/crates && cargo build && cargo test` (v1 still green) | ✅ | | `cargo run --bin wo -- run docs/examples/blog` (Stage 2 still serves) | ✅ | ## Non-scope diff --git a/docs/plan/done/02-event-loop-epoll.md b/docs/plan/done/02-event-loop-epoll.md index 237e004..3c9410d 100644 --- a/docs/plan/done/02-event-loop-epoll.md +++ b/docs/plan/done/02-event-loop-epoll.md @@ -21,17 +21,17 @@ Nothing is removed in this phase. The module sits alongside the tokio-backed axu | File | Responsibility | Port source | | --- | --- | --- | -| `mod.rs` | Re-exports `EventLoop`, `Event`, `Interest`, `Token`, `EventFd`, `TimerFd`, `SignalFd` | [`reference/crates/wo-event/src/lib.rs`](../../reference/crates/wo-event/src/lib.rs) (9 LOC) | -| `netpoll_epoll.rs` | `EventLoop { fd, events }` — `new()`, `register(raw_fd, interest, token)`, `wait_once(timeout) -> &[Event]`, `deregister(raw_fd)` | [`reference/crates/wo-event/src/epoll.rs`](../../reference/crates/wo-event/src/epoll.rs) (183 LOC); [`reference/go/src/runtime/netpoll_epoll.go`](../../reference/go/src/runtime/netpoll_epoll.go) for idiom | -| `eventfd.rs` | `EventFd { fd }` — counter semaphore for cross-fd wake-up (subscription dispatch, shutdown signal) | [`reference/crates/wo-event/src/eventfd.rs`](../../reference/crates/wo-event/src/eventfd.rs) (66 LOC) | -| `timerfd.rs` | `TimerFd { fd }` — oneshot + periodic timers as fds for the loop | [`reference/crates/wo-event/src/timerfd.rs`](../../reference/crates/wo-event/src/timerfd.rs) (91 LOC) | -| `signalfd.rs` | `SignalFd { fd }` — SIGINT / SIGTERM / SIGHUP delivered as fd reads for graceful shutdown without a tokio signal handler | [`reference/crates/wo-event/src/signalfd.rs`](../../reference/crates/wo-event/src/signalfd.rs) (62 LOC) | +| `mod.rs` | Re-exports `EventLoop`, `Event`, `Interest`, `Token`, `EventFd`, `TimerFd`, `SignalFd` | [`.dev/reference/crates/wo-event/src/lib.rs`](../../.dev/reference/crates/wo-event/src/lib.rs) (9 LOC) | +| `netpoll_epoll.rs` | `EventLoop { fd, events }` — `new()`, `register(raw_fd, interest, token)`, `wait_once(timeout) -> &[Event]`, `deregister(raw_fd)` | [`.dev/reference/crates/wo-event/src/epoll.rs`](../../.dev/reference/crates/wo-event/src/epoll.rs) (183 LOC); [`.dev/reference/go/src/runtime/netpoll_epoll.go`](../../.dev/reference/go/src/runtime/netpoll_epoll.go) for idiom | +| `eventfd.rs` | `EventFd { fd }` — counter semaphore for cross-fd wake-up (subscription dispatch, shutdown signal) | [`.dev/reference/crates/wo-event/src/eventfd.rs`](../../.dev/reference/crates/wo-event/src/eventfd.rs) (66 LOC) | +| `timerfd.rs` | `TimerFd { fd }` — oneshot + periodic timers as fds for the loop | [`.dev/reference/crates/wo-event/src/timerfd.rs`](../../.dev/reference/crates/wo-event/src/timerfd.rs) (91 LOC) | +| `signalfd.rs` | `SignalFd { fd }` — SIGINT / SIGTERM / SIGHUP delivered as fd reads for graceful shutdown without a tokio signal handler | [`.dev/reference/crates/wo-event/src/signalfd.rs`](../../.dev/reference/crates/wo-event/src/signalfd.rs) (62 LOC) | -Total: ~410 LOC lifted and adapted. The v1 code already compiles standalone in `reference/crates/wo-event/` and has unit tests; the port is near-verbatim plus namespace cleanups. +Total: ~410 LOC lifted and adapted. The v1 code already compiles standalone in `.dev/reference/crates/wo-event/` and has unit tests; the port is near-verbatim plus namespace cleanups. ### Why `runtime/` not `event/` -Go's equivalent code lives at [`reference/go/src/runtime/netpoll_epoll.go`](../../reference/go/src/runtime/netpoll_epoll.go) alongside siblings like `netpoll_kqueue.go` (macOS/BSD), `netpoll_io_uring.go` (if/when Go adds it), and the shared `netpoll.go` interface. The directory name "runtime" signals that this is the layer beneath user code — scheduler / netpoll / syscall shims — and the filename prefix `netpoll_` makes each implementation alternative visible at a glance. Adopting the same convention in writeonce makes porting ideas bidirectional: a reader who knows Go's layout can find the writeonce equivalent by trimming the `.go` extension and swapping it for `.rs`. When Phase 3's io_uring arrives it'll land as `netpoll_io_uring.rs` next to the epoll one; a cross-platform stub would be `netpoll.rs`. Module boundary and naming both match. See [`docs/plan/assembly/00-overview.md`](./assembly/00-overview.md) for why we stop short of mirroring Go's assembly conventions. +Go's equivalent code lives at [`.dev/reference/go/src/runtime/netpoll_epoll.go`](../../.dev/reference/go/src/runtime/netpoll_epoll.go) alongside siblings like `netpoll_kqueue.go` (macOS/BSD), `netpoll_io_uring.go` (if/when Go adds it), and the shared `netpoll.go` interface. The directory name "runtime" signals that this is the layer beneath user code — scheduler / netpoll / syscall shims — and the filename prefix `netpoll_` makes each implementation alternative visible at a glance. Adopting the same convention in writeonce makes porting ideas bidirectional: a reader who knows Go's layout can find the writeonce equivalent by trimming the `.go` extension and swapping it for `.rs`. When Phase 3's io_uring arrives it'll land as `netpoll_io_uring.rs` next to the epoll one; a cross-platform stub would be `netpoll.rs`. Module boundary and naming both match. See [`docs/plan/assembly/00-overview.md`](./assembly/00-overview.md) for why we stop short of mirroring Go's assembly conventions. ### `Cargo.toml` change @@ -74,7 +74,7 @@ for event in loop_.wait_once(Some(Duration::from_millis(100)))? { - `read()` on the eventfd returns `1`. 3. A second unit test validates `TimerFd::oneshot(100ms)` fires within a `wait_once(500ms)` window. 4. All 14 existing `rt` tests still pass. `cargo run --bin wo -- run docs/examples/blog` still serves (tokio path unchanged). -5. `cd reference/crates && cargo build && cargo test` still green (nothing touched). +5. `cd .dev/reference/crates && cargo build && cargo test` still green (nothing touched). ## Non-scope @@ -91,7 +91,7 @@ cargo build cargo test --lib runtime # new tests in crates/rt/src/runtime/ cargo test --lib # all 14 existing + new epoll/eventfd/timerfd tests green cargo run --bin wo -- run docs/examples/blog # axum path unchanged, still serves -cd reference/crates && cargo build && cargo test # v1 untouched +cd .dev/reference/crates && cargo build && cargo test # v1 untouched ``` ## After this phase diff --git a/docs/plan/done/03-hand-rolled-http.md b/docs/plan/done/03-hand-rolled-http.md index 6482378..19ae5d2 100644 --- a/docs/plan/done/03-hand-rolled-http.md +++ b/docs/plan/done/03-hand-rolled-http.md @@ -9,10 +9,10 @@ A non-blocking HTTP/1.1 server module that accepts connections, parses requests, ## Design decisions (locked) 1. **HTTP/1.1 only, keep-alive supported.** HTTP/2 and HTTP/3 are not on the roadmap for Stage 2 — they need ALPN / TLS support we don't have a plan for yet. HTTP/1.1 covers every endpoint the blog + ecommerce samples exercise. -2. **Per-connection state machine.** Each accepted socket fd is registered on the event loop with its own `Connection { state: Reading | Writing | Idle, parser, pending_response }`. Edge-triggered `EPOLLIN`/`EPOLLOUT` drive state transitions. Matches [v1 wo-http](../../reference/crates/wo-http/src/connection.rs)'s model verbatim. +2. **Per-connection state machine.** Each accepted socket fd is registered on the event loop with its own `Connection { state: Reading | Writing | Idle, parser, pending_response }`. Edge-triggered `EPOLLIN`/`EPOLLOUT` drive state transitions. Matches [v1 wo-http](../../.dev/reference/crates/wo-http/src/connection.rs)'s model verbatim. 3. **Router is pattern-matched at registration.** `Router::new().route("/api/articles/:id", Method::GET, handler)` resolves to a trie at boot. Per-request dispatch is a single trie walk — no axum-style type-erased layers. 4. **Handlers are `fn(&Request, &Engine) -> Response`.** Synchronous. The single-threaded event loop means a handler blocking is a bug; each handler must be a pure transformation over engine state. -5. **Module, not crate (yet).** Lives at `crates/rt/src/http/` with the same "extract when a second consumer shows up" rule as phase 02. The eventual home is the empty [`crates/http/`](../../crates/http/) sibling — but not in this phase. Paired with [phase 02's `crates/rt/src/runtime/`](./02-event-loop-epoll.md) (Go-style naming — `netpoll_epoll.rs`, `eventfd.rs`, …) which this module depends on for the `EventLoop` + raw syscall shims. Go's `src/net/http/` and `src/runtime/` split is the layout precedent; see [`reference/go/src/net/http/`](../../reference/go/src/net/http/). +5. **Module, not crate (yet).** Lives at `crates/rt/src/http/` with the same "extract when a second consumer shows up" rule as phase 02. The eventual home is the empty [`crates/http/`](../../crates/http/) sibling — but not in this phase. Paired with [phase 02's `crates/rt/src/runtime/`](./02-event-loop-epoll.md) (Go-style naming — `netpoll_epoll.rs`, `eventfd.rs`, …) which this module depends on for the `EventLoop` + raw syscall shims. Go's `src/net/http/` and `src/runtime/` split is the layout precedent; see [`.dev/reference/go/src/net/http/`](../../.dev/reference/go/src/net/http/). ## Scope @@ -20,12 +20,12 @@ A non-blocking HTTP/1.1 server module that accepts connections, parses requests, | File | Responsibility | Port source | | --- | --- | --- | -| `mod.rs` | Re-exports `Listener`, `Connection`, `Request`, `Response`, `Router`, `Method`, `Status` | [`reference/crates/wo-http/src/lib.rs`](../../reference/crates/wo-http/src/lib.rs) (4 LOC) | -| `listener.rs` | `Listener { fd }` wrapping `socket + bind + listen + accept4(SOCK_NONBLOCK \| SOCK_CLOEXEC)`; integrates with `EventLoop` | [`reference/crates/wo-http/src/listener.rs`](../../reference/crates/wo-http/src/listener.rs) (202 LOC) | -| `connection.rs` | Per-fd state machine: drain request bytes, parse, dispatch, drain response bytes, keep-alive or close | [`reference/crates/wo-http/src/connection.rs`](../../reference/crates/wo-http/src/connection.rs) (202 LOC) | -| `request.rs` | Incremental HTTP/1.1 request parser: request line, headers, optional body. `Content-Length` only (no chunked request bodies in Stage 2 — they don't appear in the samples) | [`reference/crates/wo-http/src/request.rs`](../../reference/crates/wo-http/src/request.rs) (158 LOC) | -| `response.rs` | Response builder + writer: status line, headers, body (fixed or chunked) | [`reference/crates/wo-http/src/response.rs`](../../reference/crates/wo-http/src/response.rs) (110 LOC) | -| `route.rs` | Trie-based router: static paths + `:param` segments. `Router::route(method, path, handler) -> Router` | [`reference/crates/wo-route/src/router.rs`](../../reference/crates/wo-route/src/router.rs) (127 LOC) + [`pattern.rs`](../../reference/crates/wo-route/src/pattern.rs) (146 LOC) | +| `mod.rs` | Re-exports `Listener`, `Connection`, `Request`, `Response`, `Router`, `Method`, `Status` | [`.dev/reference/crates/wo-http/src/lib.rs`](../../.dev/reference/crates/wo-http/src/lib.rs) (4 LOC) | +| `listener.rs` | `Listener { fd }` wrapping `socket + bind + listen + accept4(SOCK_NONBLOCK \| SOCK_CLOEXEC)`; integrates with `EventLoop` | [`.dev/reference/crates/wo-http/src/listener.rs`](../../.dev/reference/crates/wo-http/src/listener.rs) (202 LOC) | +| `connection.rs` | Per-fd state machine: drain request bytes, parse, dispatch, drain response bytes, keep-alive or close | [`.dev/reference/crates/wo-http/src/connection.rs`](../../.dev/reference/crates/wo-http/src/connection.rs) (202 LOC) | +| `request.rs` | Incremental HTTP/1.1 request parser: request line, headers, optional body. `Content-Length` only (no chunked request bodies in Stage 2 — they don't appear in the samples) | [`.dev/reference/crates/wo-http/src/request.rs`](../../.dev/reference/crates/wo-http/src/request.rs) (158 LOC) | +| `response.rs` | Response builder + writer: status line, headers, body (fixed or chunked) | [`.dev/reference/crates/wo-http/src/response.rs`](../../.dev/reference/crates/wo-http/src/response.rs) (110 LOC) | +| `route.rs` | Trie-based router: static paths + `:param` segments. `Router::route(method, path, handler) -> Router` | [`.dev/reference/crates/wo-route/src/router.rs`](../../.dev/reference/crates/wo-route/src/router.rs) (127 LOC) + [`pattern.rs`](../../.dev/reference/crates/wo-route/src/pattern.rs) (146 LOC) | Total: ~949 LOC ported. Most of it is mechanical adaptation from v1; the namespace + the `Interest` enum change from phase 02 are the only non-trivial edits. @@ -76,7 +76,7 @@ loop { 2. An integration test (also in `crates/rt/tests/http_smoke.rs` or similar) spawns the binary, sends three `curl` equivalents using `std::net::TcpStream`, validates status codes and bodies. 3. All 14 existing `rt` tests still pass. 4. `wo run docs/examples/blog` unchanged — axum path still drives the real CLI. -5. `cargo build` at root; `cd reference/crates && cargo build` still green. +5. `cargo build` at root; `cd .dev/reference/crates && cargo build` still green. ## Non-scope @@ -97,4 +97,4 @@ cargo run --bin wo -- run docs/examples/blog # axum path unchanged ## After this phase -Phase 04 takes the same in-memory `Engine` that the axum router serves and points the phase-03 router at it instead. Removing `tokio`, `axum`, `tower` is a consequence; the behaviour visible to `reference/rest/blog.rest` does not change. +Phase 04 takes the same in-memory `Engine` that the axum router serves and points the phase-03 router at it instead. Removing `tokio`, `axum`, `tower` is a consequence; the behaviour visible to `.dev/reference/rest/blog.rest` does not change. diff --git a/docs/plan/done/04-cutover-remove-tokio-axum.md b/docs/plan/done/04-cutover-remove-tokio-axum.md index 2c9faa5..06e5df1 100644 --- a/docs/plan/done/04-cutover-remove-tokio-axum.md +++ b/docs/plan/done/04-cutover-remove-tokio-axum.md @@ -4,7 +4,7 @@ ## Goal -Flip the `wo` binary off the tokio + axum stack and onto the phase-02 event loop + phase-03 HTTP server. Delete three dependencies from `crates/rt/Cargo.toml`. REST behaviour visible to [`reference/rest/blog.rest`](../../reference/rest/blog.rest) does not change — same status codes, same response bodies, same endpoint paths. +Flip the `wo` binary off the tokio + axum stack and onto the phase-02 event loop + phase-03 HTTP server. Delete three dependencies from `crates/rt/Cargo.toml`. REST behaviour visible to [`.dev/reference/rest/blog.rest`](../../.dev/reference/rest/blog.rest) does not change — same status codes, same response bodies, same endpoint paths. This is the first phase where the dependency count goes *down*. Phases 02 and 03 were additive; this one is the switch. @@ -77,13 +77,13 @@ Twelve handlers total — one pair per `{list, get, create, update, delete}` × 1. **`cargo build`** at root — compiles with four deps (not seven). 2. **`cargo test --lib`** — all 14 existing `rt` unit tests still pass. A new test in `src/server.rs` exercises the router build from a compiled catalog (no HTTP, just static registration). -3. **End-to-end REST smoke — the 20-assertion battery from [`reference/rest/blog.rest`](../../reference/rest/blog.rest)** must pass byte-identical to Stage 2 today. Script: +3. **End-to-end REST smoke — the 20-assertion battery from [`.dev/reference/rest/blog.rest`](../../.dev/reference/rest/blog.rest)** must pass byte-identical to Stage 2 today. Script: ```bash WO_LISTEN=127.0.0.1:8765 cargo run --bin wo -- run docs/examples/blog & # ... curl each block, check expected status ``` 4. **Graceful shutdown.** SIGINT on the process exits cleanly (no panic, no orphan fds). Validate with `strace -f -e signalfd4,close` on shutdown. -5. **`cd reference/crates && cargo build && cargo test`** still green. +5. **`cd .dev/reference/crates && cargo build && cargo test`** still green. 6. **Dep audit.** `cargo tree -p rt --depth 1` shows `libc` as the only non-transitive external dep beyond `anyhow`, `serde`, `serde_json`. ## Non-scope @@ -107,11 +107,11 @@ cargo test --lib # 14 + any new server.rs tes WO_LISTEN=127.0.0.1:8765 cargo run --bin wo -- run docs/examples/blog & PID=$! sleep 2 -# every block in reference/rest/blog.rest, via curl, checking %{http_code} +# every block in .dev/reference/rest/blog.rest, via curl, checking %{http_code} # (copy-paste the 20-assertion script from the Stage 2 turn that verified blog.rest) kill $PID -cd reference/crates && cargo build && cargo test # v1 untouched +cd .dev/reference/crates && cargo build && cargo test # v1 untouched ``` ## After this phase