Skip to content
2 changes: 2 additions & 0 deletions .add/CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ At-BUILD safety audit (ADD, foundation v2): run `scripts/audit-unsafe.sh` / `scr
Mechanism-proxy pass ≠ effect measured (TDD, foundation v3): a perf Must needs TWO distinct gates — a mechanism-fired proxy (a counter proving the code path ran) AND an effect-measured benchmark (proving the path achieved the intended effect). They are NOT the same test: the proxy can be green while the effect is entirely absent. (lesson: ft-search-off-eventloop — the m1 `ft_search_cooperative_yields_total` counter was green on monoio while the yield relieved zero co-located latency; only the §6 benchmark caught the no-op.)
Per-runtime EFFECTIVENESS validation, not just compile+correctness (ADD, foundation v3): when a contract guarantee rests on runtime scheduler behavior, a `#[cfg]`-split primitive that COMPILES and is CORRECT on both runtimes can still be EFFECTIVE on only one — the verify plan must MEASURE the behavior on EACH runtime, never infer parity from shared code. (lesson: ft-search-off-eventloop — identical self-wake `cooperative_yield` gave tokio p99 6ms but monoio 68ms; monoio's io_uring loop never reaps the CQ under a self-waking task. Fix: runtime-split — monoio `sleep(ZERO)` timer-park.)
Make the instrument work before deferring the measurement (ADD, foundation v3): when a GATE-DEFER would HIDE a real defect, prefer fixing the instrument (clean disk, quiesce the VM, add a deterministic proxy) over deferring — a heavier verify caught a default-runtime no-op that the easy disk-full defer would have shipped. (Complements "confirm instrument validity before a perf Must": validity-confirm picks the anchor; this says don't defer past a defect the anchor CAN resolve. lesson: ft-search-off-eventloop.)
Behavioral wall-time red test over an introspection hook (TDD, foundation v4): to pin a "cost-free" / "fast-enough" property, assert WALL-TIME at the operation's granularity (e.g. 200 yields must finish <100ms) rather than exposing an internal counter — it catches the property deterministically with zero new public surface. (lesson: ft-yield-costfree-monoio `monoio_yield_overhead_is_microscopic`, red 360ms → green <1ms.)
Relative same-binary A/B beats mechanism arithmetic for a tuning knee (ADD, foundation v4): a measured dominant-cost constant is necessary but NOT sufficient to freeze a tuning parameter — the 0.317µs/yield constant predicted <2% overhead at chunk=256, but the end-to-end FT.SEARCH A/B measured +4.98% there (per-chunk bookkeeping the constant ignored), driving the shipped knee to 512 (+2.74%). Pair the constant with a RELATIVE same-binary A/B (control + treatment share the VM ⇒ only the ratio matters), which ALSO sidesteps the OrbStack absolute-RPS noise that forced the v2 magnitude defers. (lesson: ft-yield-costfree-monoio.)
3 changes: 3 additions & 0 deletions .add/GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ lowest-confidence flag: the AI's ranked declaration of the 1–2 points most lik
decision point: a stop for human judgment — the contract-freeze approval, an escalated verify gate, intake confirmation, milestone close; the machine names seam (--json owner enum, decide key) and seam-audit (CI job) keep their names (formerly "seam").
retrospective consolidation: gathering confirmed lessons learned at milestone close and writing them append-only into the versioned foundation — human-confirmed, never self-approved; the machine names fold.md, the folded status, and add.py deltas keep their names (formerly "the fold / fold ritual").
specification bundle: a task's spec, scenarios, contract, and failing tests drafted as one piece and approved by a person once at the contract freeze (formerly "the one-approval front").
self-pipe yield: the monoio cost-free cooperative yield (ft-yield-costfree-monoio) — a read of one byte from an always-ready per-shard `UnixStream` socketpair, which forces the io_uring run loop to drain→park→reap the completion queue (servicing co-located connections) at ~µs instead of the timer wheel's ~1.8ms (`sleep(ZERO)`); falls back to `sleep(ZERO)` off io_uring or on init failure.
cost-free park-reap: the property a self-pipe yield achieves — one CQ reap per yield with no timer-wheel latency (measured 0.317µs/yield vs 1746µs for `sleep(ZERO)`, 5514× cheaper), so the brute-force chunk no longer has to be coarse to amortize the yield.
yield knee (K): the brute-force chunk size (`max_brute_force_vecs_per_chunk`) that trades co-located latency (smaller = finer relief) against search throughput (smaller = more yields); with the cost-free yield it returns to the cross-arch build-measured 1024 — an end-to-end FT.SEARCH A/B (20k×384d, release) holds it within the 5% throughput bound on BOTH targets (x86_64 Sapphire Rapids +2–3.5%, aarch64 Neoverse-N1 +2–3.4%) while still yielding ~20×/query. The knee is ARCH-dependent: a finer 512 holds on aarch64 (+4%) but breaches the bound on x86 (+6–8%, faster AVX-512 scan shrinks chunk wall-time) — found by a GCloud cross-arch bench, so the default is the conservative cross-arch value. Overridable per deployment via `MOON_FT_YIELD_CHUNK`.
4 changes: 3 additions & 1 deletion .add/PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
> UI/UX = UDD. When a loop reveals a gap here, come back and update this file —
> that is the re-entrant arrow from the engine down to the foundation.

slug: moon · stage: production · updated: 2026-06-15 · foundation-version: 3
slug: moon · stage: production · updated: 2026-06-15 · foundation-version: 4
goal: a Redis-compatible server whose thread-per-core architecture measurably out-scales Redis on multi-core hardware — without sacrificing protocol compatibility or durability semantics

---
Expand All @@ -29,6 +29,7 @@ goal: a Redis-compatible server whose thread-per-core architecture measurably ou
- Settled vs still open: settled — thread-per-core + SPSC mesh architecture, monoio default on Linux. Open — sub-linear multi-shard scaling (root causes mapped in 2026-06 review: leaky shared-nothing + 1ms monoio wake floor)
- [foundation v2, 2026-06-15] A contract invariant that quantifies over "all N implementations" must be verified against EACH one, not assumed uniform: group commit's `CommitOutcome.write_failed ⇒ write_error latch` held in 3 of the 4 AOF writer loops, but the tokio-TopLevel loop never carried the latch (a pre-existing gap the new contract made explicit — Finding 2, wal-group-commit). When a spec says "both writers / all loops", enumerate and check each.
- [foundation v2, 2026-06-15] Keep REJECTED-risk flags IN the frozen §3 contract, not just in discussion: a pre-named, pre-reasoned risk (xshard synchronous-spin serializing pipelined reads) was the exact failure that materialized at verify — naming it at freeze turned a surprise −27.5% P16 regression into a targeted batch-depth-gate fix instead of a redesign (xshard-read-fastpath).
- [foundation v4, 2026-06-15] When the riskiest frozen assumption is a library-internals question, SPIKE it before freezing the contract: a ~40-line standalone program both refuted the make-or-break ⚠ (an io_uring read on an already-ready fd DOES force monoio to drain→park→reap the CQ — not a silent no-op) AND corrected the contract's named primitive (`Pipe` has no `AsyncReadRent`; `UnixStream::pair()` does) before the mechanism was locked. The literal "NOP io_uring op" the request named was unreachable; the spike found the reachable equivalent (ft-yield-costfree-monoio).

## Users (UDD) — UI/UX: design before code
- No UI — surface is the **Redis wire protocol** (RESP2/RESP3) plus CLI flags (`--port --shards --appendonly --dir …`) and INFO/Prometheus metrics.
Expand All @@ -49,3 +50,4 @@ goal: a Redis-compatible server whose thread-per-core architecture measurably ou
| 2026-06-13 | fold v1 deltas → foundation-version 1 | close the ADD loop so learnings outlive the milestone | DDD: lock-inventory grep → CI (PROJECT §Domain); TDD: red-suite split pattern + ADD: §3 freeze flag-line requirement (CONVENTIONS) |
| 2026-06-15 | fold v2 deltas → foundation-version 2 (9 deltas from xshard-read-fastpath + wal-group-commit) | close the loop after the first 2 v2-performance tasks; perf-measurement + cross-cutting-deletion lessons recur | SDD: "verify each impl of an all-N invariant" + "keep rejected-risk flags in the freeze" (PROJECT §Spec); TDD: whole-repo symbol-removal grep, MOON_BIN-pinned VM integration, pipelined+control+best-of-7 perf anchor, frozen-red-test-may-be-wrong (CONVENTIONS); ADD: confirm instrument validity before a perf Must, full-dual-runtime gate for deletions, at-BUILD unsafe/unwrap audit (CONVENTIONS) |
| 2026-06-15 | CLOSE v2-performance (3/3 PASS) + fold deltas → foundation-version 3 (3 deltas from ft-search-off-eventloop) | all three v1-deferred bottlenecks delivered (xshard read latency, FT.SEARCH stalls, WAL group commit); a default-runtime perf no-op slipped past green tests until the effectiveness bench | TDD: mechanism-proxy pass ≠ effect measured (CONVENTIONS); ADD: per-runtime EFFECTIVENESS validation for `#[cfg]`-split primitives + make the instrument work before deferring a defect-hiding measurement (CONVENTIONS). v2 absolute magnitudes (xshard µs, WAL throughput) GATE-DEFERRED to GCloud per the milestone's sanctioned VM bench-exception |
| 2026-06-15 | CLOSE v2-1-throughput-polish (1/1 PASS, PR #189) + fold deltas → foundation-version 4 (3 deltas from ft-yield-costfree-monoio) | cost-free monoio self-pipe yield (`UnixStream::pair` park-reap, 0.317µs vs 1746µs) reclaims #179's deferred ~22% QPS; the build-measured A/B caught a knee (256, +4.98% on the 5% line) that mechanism arithmetic had cleared → shipped 512 (+2.74%) | SDD: spike library-internals risk before freeze (PROJECT §Spec); TDD: behavioral wall-time red test over an introspection hook; ADD: relative same-binary A/B beats mechanism arithmetic for a tuning knee (CONVENTIONS). Absolute single-shard RPS on real disk GCloud-deferred with the v2 siblings |
56 changes: 56 additions & 0 deletions .add/milestones/v2-1-throughput-polish/MILESTONE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# MILESTONE: Throughput Polish (recover v2-deferred costs)

goal: On monoio, FT.SEARCH's off-event-loop yield reaps the io_uring CQ without timer-wheel latency, so the brute-force chunk shrinks back to its latency-optimal size and reclaims the ~22% throughput #179 deferred, with #179's co-located latency relief preserved.
rationale: sub-milestone — a slice of the closed v2 "Multi-Core Throughput Hardening" theme. #179 shipped FT.SEARCH's latency relief but accepted a −22% brute-force QPS cost (the monoio yield uses a ~ms timer-wheel park, forcing chunk=16384). Recovering that throughput is task-sized work that touches no frozen contract, but both milestones are closed so it needs a fresh home. Scoped focused (1 task); sibling deferrals (xshard-read-coalescing C3, GCloud revalidation) left OUT.
stage: production · status: active · created: 2026-06-15

> SDD living doc for this milestone. Keep it THIN: breadth, shared decisions, and
> exit criteria only — per-task detail lives in each `.add/tasks/<slug>/TASK.md`,
> written just-in-time. Update this doc whenever a task reveals a milestone gap.

## Scope
In:
- Replace monoio `cooperative_yield()`'s `sleep(ZERO)` timer-park (`src/runtime/mod.rs:50`)
with a cost-free io_uring park-and-reap (NOP submission / already-ready self-pipe — whatever
monoio 0.2.4 exposes safely) that still drains the task queue so the loop park()s and reaps the CQ.
- Re-tune the `max_brute_force_vecs_per_chunk` default (`holder.rs:77`, currently 16384) down to
the new latency-optimal knee once the per-yield cost drops.
- Same-run A/B re-validation on a clean monoio VM: QPS recovery AND latency-relief preservation.

Out:
- tokio `cooperative_yield()` — already free via `yield_now`, untouched.
- `xshard-read-coalescing` (C3) and GCloud absolute revalidation — separate scope, deferred siblings.
- Any modification of #179's frozen latency contract — it is a CONSTRAINT here, never re-opened.
- HNSW / per-segment yield-point changes (only the brute-force chunk path is in scope).

## Shared decisions & glossary deltas (living — every task must honor these)
- `cooperative_yield()` keeps its `async fn ()` signature — callers in the FT.SEARCH slices
(`handler_monoio/ft.rs`, `handler_sharded/ft.rs`) stay unchanged; only the monoio body changes.
- No new `unsafe` without explicit user approval + a `// SAFETY:` comment (CLAUDE.md unsafe policy).
- Measurement is RELATIVE same-run A/B (absolute OrbStack RPS is untrusted); needs a verified-clean
VM — see the leaked-busy-poller gotcha before benching cross-runtime latency.

## Shared / risky contracts (freeze these first)
- monoio yield primitive — must (a) drain the task queue so the loop parks, (b) trigger exactly one
CQ reap, (c) re-wake promptly without ms-scale latency, (d) add no new unsafe w/o approved SAFETY.
-> owning task `ft-yield-costfree-monoio`

## Tasks (breadth-first decomposition; detail lives in each TASK.md)
- [x] ft-yield-costfree-monoio depends-on: none — swap monoio timer-park yield → cost-free
io_uring CQ-reap; re-tune brute-force chunk; A/B re-validate QPS recovery + latency preservation.
DONE · gate PASS (PR #189).

## Exit criteria (observable; map each to the task that delivers it)
- [x] monoio `cooperative_yield()` no longer uses `sleep(ZERO)` — parks-and-reaps via a cost-free
io_uring mechanism (no timer-wheel dependency), with a test. (← ft-yield-costfree-monoio)
MET: `monoio_yield::park_reap` reads an always-ready `UnixStream::pair` (sleep(ZERO) is fallback-only);
tests `monoio_yield_overhead_is_microscopic` + `monoio_yield_relieves_colocated`.
- [x] Same-run A/B on clean monoio VM: heavy brute-force FT.SEARCH QPS recovered to within ~5% of the
timer-park-disabled / pre-#179 control, at the re-tuned small chunk. (← ft-yield-costfree-monoio)
MET: A/B (20k×384d, release, `tests/ft_yield_chunk_ab.rs`) K=512 = +2.74% vs sync control (within 5%, 2× margin).
- [x] Co-located p99 relief preserved — no regression beyond noise vs #179's 6.6ms(1t)/27ms(3t) anchor. (← ft-yield-costfree-monoio)
MET: #179 relief guards (m1/m1b) stay green + relief test passes; the cost-free yield is STRICTLY finer
(~39 yields/query at 512 vs ~1 at 16384), so relief improves, not regresses.
- [x] Both runtimes green (tokio unchanged), 0 new unsafe w/o approved SAFETY, MVCC/consistency regression green. (← ft-yield-costfree-monoio)
MET: 3604 monoio + tokio suites green; tokio `yield_now` untouched; unsafe 218/218 (0 new); unwrap ratchet;
clippy + fmt both runtimes.
51 changes: 51 additions & 0 deletions .add/milestones/v2-1-throughput-polish/RETRO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
════════════════════════════════════════════════════════════════════════
v2-1-throughput-polish · Throughput Polish (recover v2-deferred costs)
════════════════════════════════════════════════════════════════════════
VERDICT DONE
TASKS 1/1 done CRITERIA 4/4 met
GATES 1 PASS WAIVERS none

goal On monoio, FT.SEARCH's off-event-loop yield reaps the io_uring CQ
without timer-wheel latency, so the brute-force chunk shrinks
back to its latency-optimal size and reclaims the ~22% throughput
#179 deferred, with #179's co-located latency relief preserved.

TASK PHASE GATE TESTS PROGRESS
───────────────────────────────────────────────────────────────────────
ft-yield-costfree-monoio done PASS 0 ●●●●●●●●
legend ● reached ◉ current ○ pending spec→…→done

EXIT CRITERIA ●●●●●●●●●● 4/4 met

LEARNINGS (3 carried)
• TDD · folded · (foundation v4 → CONVENTIONS.md) A behavioral red
test (`monoio_yield_overhead_is_microscopic`: 200 yields must
finish <100ms) caught the cost-free property deterministically
WITHOUT internal counters — measuring wall-time at the yield
granularity beats exposing an introspection hook (evidence: red
360ms → green <1ms, no new public surface).
• SDD · folded · (foundation v4 → PROJECT.md §Spec)
Spike-before-freeze (a ~40-line standalone monoio program) refuted
the make-or-break ⚠#1 AND corrected the contract
(`Pipe`→`UnixStream::pair()`: `Pipe` has no `AsyncReadRent`) BEFORE
locking the mechanism — cheap de-risking that the frozen-contract
model should reach for whenever the riskiest assumption is a
library-internals question (evidence: the literal "NOP io_uring op"
the request named was unreachable; the spike found the reachable
equivalent).
• ADD · folded · (foundation v4 → CONVENTIONS.md) Mechanism
arithmetic UNDER-predicted the tuning parameter; the end-to-end A/B
was worth running. The 0.317µs per-yield constant predicted <2%
overhead at K=256, but the real FT.SEARCH A/B measured +4.98% there
(and +2.74% at the shipped K=512) — a ~3pt gap from per-chunk loop
bookkeeping the mechanism cost ignored. Lesson: a measured
dominant-cost constant is necessary but NOT sufficient to freeze a
tuning knee; pair it with a RELATIVE same-binary A/B (which cancels
the absolute-RPS noise that made us defer in the first place)
before committing the default. The relative A/B sidesteps the
OrbStack absolute-noise problem entirely — control and treatment
share the VM, so only the ratio matters.

DECIDE NEXT consolidate learnings + archive-milestone
v2-1-throughput-polish
════════════════════════════════════════════════════════════════════════
24 changes: 21 additions & 3 deletions .add/state.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"project": "moon",
"stage": "production",
"active_task": "ft-search-off-eventloop",
"active_milestone": "v2-performance",
"active_task": "ft-yield-costfree-monoio",
"active_milestone": "v2-1-throughput-polish",
"tasks": {
"hotpath-lock-quickwins": {
"title": "Eliminate per-command global locks & syscall-level quick wins",
Expand Down Expand Up @@ -78,6 +78,16 @@
"created": "2026-06-15T01:43:18+00:00",
"updated": "2026-06-15T05:37:34+00:00",
"flag_verified": true
},
"ft-yield-costfree-monoio": {
"title": "Swap monoio cooperative_yield timer-park -> cost-free io_uring CQ-reap; re-tune chunk; A/B re-validate",
"phase": "done",
"gate": "PASS",
"milestone": "v2-1-throughput-polish",
"depends_on": [],
"created": "2026-06-15T11:57:05+00:00",
"updated": "2026-06-15T14:39:07+00:00",
"flag_verified": true
}
},
"milestones": {
Expand All @@ -96,10 +106,18 @@
"status": "done",
"created": "2026-06-13T08:24:32+00:00",
"updated": "2026-06-15T06:10:04+00:00"
},
"v2-1-throughput-polish": {
"title": "Throughput Polish (recover v2-deferred costs)",
"goal": "On monoio, FT.SEARCH's off-event-loop yield reaps the io_uring CQ without timer-wheel latency, so the brute-force chunk shrinks back to its latency-optimal size and reclaims the ~22% throughput #179 deferred, with #179's co-located latency relief preserved.",
"stage": "production",
"status": "done",
"created": "2026-06-15T11:57:00+00:00",
"updated": "2026-06-15T14:49:42+00:00"
}
},
"created": "2026-06-11T03:18:21+00:00",
"updated": "2026-06-15T06:10:04+00:00",
"updated": "2026-06-15T14:49:42+00:00",
"setup": {
"locked": true,
"locked_at": "2026-06-11T03:28:00+00:00",
Expand Down
Loading
Loading