- 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.
6.6 KiB
04 — Cutover: Remove tokio, axum, tower
Context sources: ./02-event-loop-epoll.md, ./03-hand-rolled-http.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 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)
- 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 viahttp-smoke. - Preserve the
Enginetrait surface.Arc<Mutex<Engine>>stays exactly ascrates/rt/src/engine.rshas it today. The routing layer incrates/rt/src/server.rs— the function that mapsservice restblocks to axumMethodRouter— gets rewritten to emit phase-03Router::route(...)calls instead. Same data flow, different transport. - No tokio — no async. Handlers become synchronous
fn(&Request, &Engine) -> Response. The single-threaded event loop already assumes this; removingasync fnplumbing simplifies the code.tokio::sync::Mutexbecomesstd::sync::Mutex(fine in a single-threaded loop since lock contention is impossible). signalfdreplacestokio::signal::ctrl_c(). Registered as another fd on the loop; reading a SIGINT cleanly exits the loop and closes outstanding connections.WO_LISTENenv var semantics unchanged. The127.0.0.1:8080default + theWO_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
[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-03http/module stay in place and now become the primary code path.
Handler signature change
Before (axum + tokio):
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):
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
cargo buildat root — compiles with four deps (not seven).cargo test --lib— all 14 existingrtunit tests still pass. A new test insrc/server.rsexercises the router build from a compiled catalog (no HTTP, just static registration).- End-to-end REST smoke — the 20-assertion battery from
reference/rest/blog.restmust pass byte-identical to Stage 2 today. Script:WO_LISTEN=127.0.0.1:8765 cargo run --bin wo -- run docs/examples/blog & # ... curl each block, check expected status - Graceful shutdown. SIGINT on the process exits cleanly (no panic, no orphan fds). Validate with
strace -f -e signalfd4,closeon shutdown. cd reference/crates && cargo build && cargo teststill green.- Dep audit.
cargo tree -p rt --depth 1showslibcas the only non-transitive external dep beyondanyhow,serde,serde_json.
Non-scope
- No JSON replacement.
serde+serde_jsonare still imported and used. Phase 05 removes them. - No inotify / sendfile. Stage 3 capabilities. Phases 07 and 08.
- No
io_uring. TheEventLoopkeeps usingepollhere; swapping is a later phase. - No crate extraction.
runtime/andhttp/stay insidecrates/rt/src/. The emptycrates/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'ssrc/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
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.