This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

zio / docs / adoption-guide.md
6.3 kB 135 lines
1# adopting zio in your project 2 3a practical guide for swapping `std.Io.Threaded` for `zio.ZioBackend` in a 4zig 0.16 application. written for any project; [zlay](https://tangled.org/zat.dev/zlay) 5(an AT Protocol relay holding ~2,800 websocket subscriptions) is the worked 6example throughout because it's the first real consumer. 7 8companion docs: [adoption.md](adoption.md) (the original per-project 9analysis), [benchmarks.md](benchmarks.md), [baselines.md](baselines.md). 10 11## should you adopt it? 12 13zio pays off when you have **many concurrent blocking-style io tasks** 14hundreds-plus of `io.concurrent` calls that each spend their life in 15`net.Stream` reads/writes. that's where thread-per-task costs you 16O(conns × stacks) memory and O(conns) threads, and zio gives you fibers on 17one loop thread instead (zlay: 2,800 threads → a handful; ~200× less RSS in 18the bench). 19 20you don't need it when concurrency is low (a CLI, a modest server), when 21your tasks are CPU- or file-bound rather than socket-bound, or when 22`Io.Threaded` simply isn't hurting you. threads are great until they aren't. 23 24## the swap 25 26zio is an ordinary package. no compiler fork, no stdlib patch, no changes to 27libraries that take `io` as a parameter (they can't tell the difference — 28that's the point of the vtable). 29 30```zig 31// build.zig.zon 32.zio = .{ .url = "https://tangled.org/zzstoatzz.io/zio/archive/<ref>.tar.gz", .hash = "..." }, 33``` 34 35```zig 36// before (zlay main.zig) 37const Backend = Io.Threaded; 38var backend: Backend = undefined; 39backend = Io.Threaded.init(allocator, io_opts); 40const io = backend.io(); 41 42// after 43const zio = @import("zio"); 44var backend: zio.ZioBackend = undefined; 45try backend.init(allocator, io_opts); // io_opts forwards to the inner Threaded 46try backend.start(); // spawns the loop thread 47const io = backend.io(); 48``` 49 50everything downstream that receives `io` — websocket clients, http servers, 51protocol libraries — is unchanged. 52 53## what runs where 54 55`ZioBackend` is a hybrid: it wraps a real `Io.Threaded` and routes each 56vtable call by caller domain. 57 58| you call | from a fiber (inside `io.concurrent` task) | from anywhere else | 59|---|---|---| 60| `io.concurrent` | fiber spawned directly | fiber spawned via injection | 61| `Future.await/cancel` | parks the fiber | blocks the thread (futex) | 62| `net` connect/accept/read/write | parks on poller readiness | delegates to Threaded | 63| `io.sleep` | sched timer heap | delegates | 64| `Io.Mutex`/`Condition`/`Queue` | fiber-aware futex (parks fiber) | real futex | 65| DNS (`netLookup`) | runs on a Threaded thread; fiber parks | delegates | 66| files, processes, everything else | **delegates — runs on the loop thread** | delegates | 67 68that last row is the one rule you must design around. 69 70## the four rules 71 721. **fiber tasks should be socket-shaped.** anything a fiber calls that 73 isn't overridden runs *blocking on the loop thread* and stalls every 74 other fiber. sockets, sleeps, sync, DNS: fine. file io, process spawns, 75 database client calls: keep them off fibers (rule 2). 76 772. **keep a second, threaded io for blocking work.** the pattern zlay 78 already runs (its `pool_io`): construct a plain `Io.Threaded` alongside 79 zio and hand it to components that block — database pools, disk 80 persistence, worker pools. cross-domain handoff through `Io.Mutex` / 81 `Io.Queue` is safe: zio's futex ops wake both domains. (this "two-io 82 split" is not a workaround; it's the design. blocking work *should* be 83 on threads.) 84 853. **an fd belongs to the domain that created it.** sockets opened by a 86 fiber are non-blocking and parked on zio's poller; sockets opened on the 87 threaded side are blocking. don't ship an fd across domains mid-life. 88 (zio flips listener fds non-blocking on first fiber accept, so 89 listen-on-main / accept-on-fiber is supported.) 90 914. **cancellation is prompt but cooperative.** `future.cancel(io)` wakes the 92 task's fiber wherever it's parked (io, futex, timer, await) and the next 93 io/sleep call returns `error.Canceled`. a fiber that never calls back 94 into `io` can't be canceled — same contract as Threaded's checkpoints. 95 96## gotchas we hit so you don't 97 98- **`readSliceShort` is not a message-read.** it parks until the buffer 99 fills; short reads happen only at stream end. an echo server using it with 100 a 32-byte buffer on 11-byte messages deadlocks — on any backend. use 101 exact-size reads (`readSliceAll`) for framed protocols, or take what 102 `Reader` buffered. 103- **never rely on closing an fd to wake a waiter.** kqueue silently drops a 104 closed fd's filters — the wakeup is lost, the fiber parks forever. use 105 timed waits for shutdown-observing loops; zio's engine/sched expose them, 106 and the vtable's own ops re-check cancellation on every wake. 107- **shutdown order**: cancel/await your fibers, then `backend.deinit()`. 108 deinit stops the loop thread; fibers still parked at that point never ran 109 their cleanup. 110 111## rollout discipline (from zlay's ops history) 112 1131. land the dependency + `const Backend` switch behind a build option, so 114 `-Dbackend=threaded` and `-Dbackend=zio` build the same code. 1152. green test suite on both backends. 1163. soak against a simulator or staging traffic — never iterate on the 117 backend in production (zlay learned this with the previous evented 118 attempt; see the april 2026 ops changelog). 1194. canary with explicit rollback triggers and post-restart checks; watch 120 thread count and RSS — they should fall off a cliff, and if they don't, 121 fibers aren't carrying your sockets. 122 123## current maturity (2026-07-12) 124 125honest status, not marketing: 126 127- proven: the full vtable test matrix (Debug + ReleaseSafe, macos + linux, 128 aarch64; x86_64 correctness under emulation), the fiber gauntlet, the 129 reconnect storm, cross-domain sync, prompt cancellation. 130- not yet: guard-page mmap fiber stacks (malloc stacks commit ~256 KB per 131 fiber today), TLS-over-fibers measurement (wss:// paths — expected 132 transparent, not yet benchmarked), multi-loop sharding (one loop thread; 133 plenty for hundreds-of-fps workloads, measure before assuming for yours), 134 io_uring (epoll/kqueue only), non-stream sockets on fibers (UDP delegates 135 to threads), production soak time (zero, until zlay).