- Board renamed docs/plan/00-kanban.md -> docs/00-status.md and rebuilt: ▶ NEXT PLAN pointer (iteration 4 — emitter, corpus, `woc build`) then six buckets — stories, in progress, done, pending, discarded, learnings. It covered only the Rust runtime before, so the whole OOP track was invisible. All 16 inbound refs repointed; `Kanban:` banners renamed to `Status:`. - New discarded.md (settled rejections with reasons: inheritance, `abstract`, Money/SKU/Float, Dynamic/cast/macro/extern, AOT-to-C, Menhir, shared engine state) and learnings.md (plumbed≠enforced, vacuous goldens, exit-0-wrong- output, malloc-path ASan trick, deferred checks that never reach the VM). - RECOVERED docs/plan/exploration/blue-green-vm/00-vision.md — gone from disk, never committed (gitignored path), cited by five docs incl. principle 12. Root cause was broader: all seven forward-roadmap plans in docs/superpowers/plans/ were untracked and ignored, on one disk only. Dropped the docs ignore rules with a do-not-re-add note; added __pycache__/*.pyc. - Repaired broken links across docs/, 270 -> 36: fixes a regression from the earlier reference/ -> .dev/reference/ move (relative paths at ../../ and deeper were skipped), plus depth and reorg drift. The 36 residual point at content that does not exist and need decisions, not paths. - New spec docs/superpowers/specs/2026-08-10-logwatcher-gap-closure-design.md, applied: `and`/`or` verdict row; Part 3 gains `env` (six modules), swaps time.mono for iso/local, adds 22 bare core builtins; throw/time.mono/is cut (0 uses in the sample). Plan 8: Task 2 gains and/or, Task 5 drops throw, abstract+`is` task deleted, 8/9 renumber to 7/8. Plan 9 gains core builtins. Plan 10 gains the 307 -> 0 diagnostic gate. WO-E205 re-filed unreachable-by- design. types.ml header drops its false satisfaction-set claim. 00-code- review.md reduced to a stub — its rival Phase 1-4 roadmap retired.
119 lines
6.6 KiB
Markdown
119 lines
6.6 KiB
Markdown
# 04 — Cutover: Remove tokio, axum, tower
|
||
|
||
**Context sources:** [`./02-event-loop-epoll.md`](./02-event-loop-epoll.md), [`./03-hand-rolled-http.md`](./03-hand-rolled-http.md), [`../01-problem.md`](../../01-problem.md).
|
||
|
||
## 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`](../../../.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.
|
||
|
||
## Design decisions (locked)
|
||
|
||
1. **Atomic swap, single commit.** Don't run tokio and the new loop in parallel in production. Flip the binary's `main()` in one change. Phase 03 already gave us confidence the new stack works end-to-end via `http-smoke`.
|
||
2. **Preserve the `Engine` trait surface.** `Arc<Mutex<Engine>>` stays exactly as `crates/rt/src/engine.rs` has it today. The routing layer in `crates/rt/src/server.rs` — the function that maps `service rest` blocks to axum `MethodRouter` — gets rewritten to emit phase-03 `Router::route(...)` calls instead. Same data flow, different transport.
|
||
3. **No tokio — no async.** Handlers become synchronous `fn(&Request, &Engine) -> Response`. The single-threaded event loop [already assumes this](../../runtime/database/02-wo-language.md#concurrency-model); removing `async fn` plumbing simplifies the code. `tokio::sync::Mutex` becomes `std::sync::Mutex` (fine in a single-threaded loop since lock contention is impossible).
|
||
4. **`signalfd` replaces `tokio::signal::ctrl_c()`.** Registered as another fd on the loop; reading a SIGINT cleanly exits the loop and closes outstanding connections.
|
||
5. **`WO_LISTEN` env var semantics unchanged.** The `127.0.0.1:8080` default + the `WO_LISTEN=...` override stays exactly as today. Operators don't notice the change.
|
||
|
||
## Scope
|
||
|
||
### Files rewritten inside `crates/rt/`
|
||
|
||
| File | Change | Notes |
|
||
| --- | --- | --- |
|
||
| `src/bin/wo.rs` | Replace `tokio::runtime::Builder::new_current_thread` + `axum::serve` with `EventLoop` + `Listener` + `Router` wiring | The `run()` / `serve()` fns fuse into a single synchronous `run()` that drives the loop |
|
||
| `src/server.rs` | Replace `axum::Router` construction + axum handlers (`async fn list_h(State(st): State<TypeState>) -> impl IntoResponse`) with phase-03 `Router::route(...)` + sync handlers | Handler bodies are otherwise untouched: `engine.lock().list(&ty).map(Json)` logic flows through |
|
||
| `src/engine.rs` | `tokio::sync::Mutex` → `std::sync::Mutex`; `.lock().await` → `.lock().unwrap()` | Only the wrapper changes; row logic intact |
|
||
|
||
### `Cargo.toml` delta
|
||
|
||
```diff
|
||
[dependencies]
|
||
anyhow = "1"
|
||
serde = { version = "1", features = ["derive"] }
|
||
serde_json = "1"
|
||
-tokio = { version = "1", features = ["rt", "macros", "net", "signal", "sync", "time"] }
|
||
-axum = "0.7"
|
||
-tower = "0.4"
|
||
libc = "0.2"
|
||
```
|
||
|
||
Three deps gone. Four remaining: `anyhow`, `serde`, `serde_json`, `libc`.
|
||
|
||
### Files deleted
|
||
|
||
- None. The phase-02 `runtime/` module and phase-03 `http/` module stay in place and now become the primary code path.
|
||
|
||
## Handler signature change
|
||
|
||
**Before (axum + tokio):**
|
||
|
||
```rust
|
||
async fn list_h(State(st): State<TypeState>) -> impl IntoResponse {
|
||
let eng = st.engine.lock().await;
|
||
match eng.list(&st.ty) {
|
||
Ok(rows) => (StatusCode::OK, Json(json!(rows))).into_response(),
|
||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||
}
|
||
}
|
||
```
|
||
|
||
**After (sync, event loop):**
|
||
|
||
```rust
|
||
fn list_h(_req: &Request, st: &TypeState) -> Response {
|
||
let eng = st.engine.lock().unwrap();
|
||
match eng.list(&st.ty) {
|
||
Ok(rows) => Response::ok().json(&serde_json::json!(rows)),
|
||
Err(e) => Response::status(Status::INTERNAL_SERVER_ERROR).body(e.to_string()),
|
||
}
|
||
}
|
||
```
|
||
|
||
Twelve handlers total — one pair per `{list, get, create, update, delete}` × four types. Mechanical rewrite.
|
||
|
||
## Exit criteria
|
||
|
||
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`](../../../.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.
|
||
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
|
||
|
||
- **No JSON replacement.** `serde` + `serde_json` are still imported and used. Phase 05 removes them.
|
||
- **No inotify / sendfile.** Stage 3 capabilities. Phases 07 and 08.
|
||
- **No `io_uring`.** The `EventLoop` keeps using `epoll` here; swapping is a later phase.
|
||
- **No crate extraction.** `runtime/` and `http/` stay inside `crates/rt/src/`. The empty `crates/http/` sibling crate waits for a second consumer; `runtime/` doesn't have a sibling slot (it's the binary's private kernel-primitive layer, analogous to Go's `src/runtime/` staying internal to the toolchain).
|
||
|
||
## Risk
|
||
|
||
The `.rest` files are the safety net — 20 assertions that every Stage 2 endpoint returns the expected status. If one breaks after the cutover, the fix is almost always in the handler rewrite (sync semantics + the new `Response::json(...)` helper). No transport-layer regression should survive phase 03's `http-smoke` passing.
|
||
|
||
## Verification
|
||
|
||
```bash
|
||
cargo build # 4 deps, no tokio/axum/tower
|
||
cargo test --lib # 14 + any new server.rs tests green
|
||
|
||
# end-to-end
|
||
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}
|
||
# (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
|
||
```
|
||
|
||
## After this phase
|
||
|
||
Phase 05 removes `serde` + `serde_json` by writing a minimal JSON parser + emitter against the new `http::Response::json()` surface. At the end of phase 06, `crates/rt/Cargo.toml` is down to `libc` alone — the stated end goal.
|