9.4 KiB
9.4 KiB
Cerebrum
OpenWolf's learning memory. General, reusable knowledge only — bug-specific history lives in .wolf/buglog.json, not here. Do not edit manually unless correcting an error. Last updated: 2026-08-16 (condensed from a ~40k-token file; see .wolf/buglog.json for full bug history)
User Preferences
- Run soak tests with specific round counts and deploy directories for precise regression tracking.
- Prefer parallel agent runs for independent analysis work; sequential for dependent tasks.
- Keep test logs for analysis, don't skip intermediate output even if a test hangs.
Key Learnings
- Build entry point:
flake.nixlives insrc/, not the repo root. Alwaysnix develop /abs/path/to/src --command bash -c '...'with an explicit path — Bash cwd isn't stable across calls, and a barecd src && nix developcan silently short-circuit. - Test coverage gaps: documented full-suite commands (
xce-build.sh <component>,make test) each cover only a subset — e.g. a component's default meson run misses newly-added standalone test binaries unless wired into itsRUN_TESTScase, and the CGO/apiserver backend suite needsmake test-backendwith CGO_CFLAGS/LDFLAGS reconstructed via pkg-config (package isc-capnproto, notcapnp_c). A green default build is not proof of full coverage — verify every new test binary actually runs. - Unity harness: use
UnitySetTestFile(__FILE__)(not nestedUNITY_BEGIN()/UNITY_END()) when multiple test-group files share one binary — nested BEGIN/END resets global counters and can mask most of the real suite exit code. Authoritative green = grep raw testlog for:FAIL:/ word-boundary\bTIMEOUT\b, never trust an aggregateFail:0alone. Also rule out a concurrent competing build/soak against the same build dir before treating a timeout/crash as a regression. - capnp/typemap conventions: every schema struct gets
$C.mapname(...); for a wire-only concept with no live C struct, hand-define a small matching struct intypemap.h. AData-typed field's generated composite codec is a silent no-op in this backend AND the generated_ptrwrapper doesn't zero-init — an unset field is written as literal uninitialized stack garbage (real crash risk). PreferList(UInt8)+ a$C.maplistcountfield, which this backend does generate real code for. A nested-struct getter can return an unresolved far pointer after a segment round-trip — callcapn_resolve()before reading its scalar fields (Text/Data getters resolve internally and mask this). - C comments: never write a bare
type*/(pointer type immediately followed by/) inside a/* */block comment — it silently closes the comment early, producing a cascade of unrelated syntax errors elsewhere. Recurred repeatedly; grep new/edited comments for[A-Za-z0-9_]\*/before building. - Thread/lock hazards on async-ified single-threaded code: a library/struct built for serialized calling (Raft state, SSL* objects, an epoll batch-dispatch loop) that later gets a "drive it on a thread pool" feature needs its own lock/refcount around the shared state — per-endpoint or per-callback serialization does not imply serialization across different endpoints. A same-thread same-batch use-after-free (one event's handler freeing something a later event in the identical batch still references) can look identical to a genuine cross-thread race.
- makeString() (lib/xstring.c) is rtti-backed, not malloc/strdup — only free it via its own module's rtti-aware teardown, or don't free it at all; a generic
free()/capnpfree_X()on it is heap corruption. Usestrdup()for fields a generatedfree_X()will free directly. - Threaded test fixtures: teardown for a fixture with real background threads must run unconditionally before any
TEST_ASSERT_*— a failed assertion longjmps past later code, orphaning the thread and corrupting the next test's fixture memory. - Live-soak validation discipline: a unit-test-green fix can still be unreachable/dead code in production — always re-run the exact live scenario end-to-end, not just the mechanism fixed. A later, differently-shaped soak failure after a fix isn't automatically a regression from that fix — check whether that failure point was ever exercised before.
- Sandbox quirks:
/tmpcan be overlayfs with intermittent fdatasync EIO — durability-sensitive tests need a real fs (e.g.~/tmpon ext4). This sandbox'sdatemay resolve to BusyBox (no%N, silently passes it through instead of erroring) — use bash's$EPOCHREALTIME/SECONDSbuiltins for timing instead. - HA election timer discipline (bug-1562): every early-return branch in a function driven by a one-shot timer (
ha_on_primary_ctrl_fd_rdhup's liveness-watchdog callback) must either re-arm the next timer itself or verify some OTHER live timer is actually still scheduled before skipping the re-arm — checking only elapsed-time-since-last-event (the 5.10b debounce branch) is not the same as checking an election is still in flight (the 5.6 guard's actual role/pre_vote_in_progress check). A silently-dropped re-arm freezes the node with zero CPU/log activity forever (looks identical to a clean idle state, not a crash) — diagnose via/proc/<pid>/task/*/wchan(all threads infutex_do_wait/do_epoll_waitwith no fds beyond listen+epoll = dropped timer, not deadlock) before reaching for gdb (ptrace is blocked in this sandbox,yama ptrace_scope=1+ no ancestor relationship to the target). - Reproducing HA bootstrap bugs: match the user's actual inter-node start stagger exactly — a tight ~0.5s stagger (the
/xce-deploy-haskill's default) can mask a bug that only triggers when a node sits solo for tens of seconds (real peers answering fast enough prevents the trap window from ever closing). Always ask/check the real timestamps before concluding "could not reproduce." - A soak's own "excluded"/PASS verdict is not a license to stop reading (bug-1563):
/xce-test-ha --chaotic's verdict deliberately excludesbug924_fdatasync(overlayfs EIO) from pass/fail — but a huge excluded count (300K+ in one run) or a different errno than the documented one (EINVAL, not EIO) is worth a manual look anyway. Found:fsync_worker's retry loop only backs off when NOT pending, and a sustained fdatasync failure makes "pending" permanently true (durable_lsn never advances on failure) — a real busy-spin/livelock, invisible to the soak's own job-loss/divergence checks since the other 2-of-3 standbys still satisfy majority. Lesson: skim excluded/noise counts for magnitude and errno before trusting a documented exclusion covers them.
Do-Not-Repeat
- Don't run
clang-format -ion whole existing files here — the repo isn't clang-format-clean at baseline, so a full reformat buries the real diff; recovering viagit checkout HEAD -- <file>on a file with other uncommitted work destroys that work irrecoverably. - Before citing a bug as "still open" or a spec as "blocked," cross-check
.wolf/buglog.json's actual fix status and the spec's own tasks.md/commit history — blocked_on notes and prior narrative claims (including this file's own past entries) can be stale. - Before trusting an "all tests pass, GO" claim on a stale/uncommitted tree, re-run the suite yourself — a buglog "FIXED" note can describe the intended fix while the code on disk is mid-revert from RED-phase construction.
- A generic retry/failover classifier that judges retriable-vs-terminal by a raw return value's magnitude (comparing against known errno values) is unsafe unless every wrapped call site shares the same success convention — a call that legitimately returns a variable count on success can numerically collide with an error code.
- When two independent "is it ready" signals exist server-side, a wait/convergence helper gating on only one does not imply the other is satisfied — check what the actual raced precondition depends on.
- Nix fixed-output-derivation hash mismatches from an upstream input pinning a mutable branch: re-locking flake.lock is a dead end once already at the latest commit; fix via a local
overrideAttrspinningsrcto an explicit immutable commit in the consuming repo. pkill/pgrepby bare process name (unscoped) can kill or match a different concurrent invocation's processes, including a sibling session's build/soak — always scope process matching to something unique to the current run (e.g. a workdir path).
Decision Log
- HA/consistency model follows Raft + CP strictly: majority ACK is the hard commit barrier for all state-changing writes (reject on timeout, never local-apply-and-degrade); the own-term commit rule (not a 4th "COMMITTING" state) is what gives a new leader safety.
- A controller process must NEVER self-exit/self-abort as a designed response to an operational condition (only a genuine crash or admin/supervisor kill) — an unrecoverable/fenced member logs loudly, stays permanently non-promotable, stays alive, and keeps retrying.
- Commit-first-then-reply: job/queue/host control ops validate → mutate(provisional) → WAL append → commit_submit(WAIT) → reply; no success reply before commit. Worker-reported fact entries instead follow pure Raft semantics — retained/retried indefinitely, no commit timeout.
- No client reply-wait timeout as a fix for a hung request — fail loud at the real transport/source instead of masking with a deadline backstop.
- Product is pre-release: no backward/mixed-version wire compatibility required; wire format changes may be made freely.