Files
alkstore/tasks/release-flake-investigation.md
T
glm-5.3-flash 0344d101d1 release-flake-investigation: the fired lock-row trigger investigated and resolved — harness-model, fixed test-side
Instrumentation first: the tee-before-filter capture procedure of
record (demonstrated to preserve failing messages, immediately
vindicating itself — two natural repros captured verbatim), then the
arithmetic read against lock.rs/resolution.rs (delay-only argument
confirmed for the lapse legs, corrected for the contention leg:
second-precision now_unix stamps let a ttl=1 holder's exclusion
window collapse below a second), the conditions matrix (48+ rounds:
threads, concurrency with the SQLite column, cold/warm, CPU-load
stress), mechanism probes (1.2 s delay fails deterministically on the
exact assert; 200 ms lapses 2/10, frac-dependent), and the natural
repro (2/30 loaded solo runs, exact message at properties.rs:563).

Disposition: reproduced, root cause harness-model — the engine acted
per ADR-008 §7 throughout; the pre-fix row's contention leg asserted
a live-TTL state across an unbounded wall-clock gap. Trivially
test-side, so the fix landed in-session under the AC's exception:
lock_ttl_expiry_and_reacquisition is now two-legged (300 s exclusion
leg released; the 1 s lapse leg's post-sleep asserts stay
delay-only-safe); every pinned semantic survives.

Verified: 30/30 loaded runs of the fixed row green under the exact
repro condition; sqlite + mem columns green; workspace gates green
(build, live-pg workspace tests, clippy -D warnings, fmt); taskgraph
validate green.

Watch ledger: the lock row's flag retires; the trim row stays
watch-flagged (no repro, 10 focused + 7 loaded rounds); a new single
occurrence of row_stream_ordering_equivalence under synthetic load
(message captured — the row's drain window raced the pg bridge's
incomplete channel fill) is watch-flagged for review-wave-8's flake
outcome; no ride-along fix (instrumentation-first discipline).
Tasks: release-flake-investigation -> completed with Notes + Summary;
review-wave-5 ledger dated update.
2026-10-11 01:13:05 +00:00

254 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: release-flake-investigation
name: Flake investigation — the pg lock-row trigger fired (dedicated session)
status: completed
depends_on: []
scope: moderate
risk: medium
impact: component
level: implementation
tags: [wave-8, postgres, flake-watch]
---
## Description
The wave-5 gate's flake-watch flag, carried to this pass
(`docs/plans/implementation.md` §Wave 8; `tasks/review-wave-5.md`
§Notes 6). The watch's trigger has **fired**:
`row_lock_ttl_expiry_and_reacquisition` failed a second time (the
ledger update, 2026-10-10, from the `pg-fix-scheduler-fire-wake`
session — one occurrence in one full sequential pg suite run, failure
message lost to output filtering, then green on two subsequent full
runs and six focused runs). Per the ledger's own prescription, a
**dedicated investigative session (instrumentation + focused repro
loop) is owed before any postulate-and-fix**. The trim row
(`row_trim_to_semantics`, one development-time failure) stays
watch-flagged; if it reproduces in this session's loops, it joins the
investigation.
The investigation's shape (the ledger's terms, not a
postulate-and-fix license):
1. **Instrument first**: capture the failing row's full assertion
output (the prior failures' messages were lost to truncation/filtering
— fix that blind spot before anything else: run with
`--nocapture`, or a file-tee'd harness, or a retry-wrapper that
preserves output on failure).
2. **Focused repro loop**: the row under stress — many consecutive
runs, varied `--test-threads`, solo vs concurrent-with-SQLite-column,
cold vs warm server — the conditions matrix the two prior
occurrences happened under (cold first run; full sequential run).
A deliberate slow-clock/lapse-boundary probe (the row's lapse
assertions are post-sleep states a slower clock can only delay,
never un-lapse — verify that argument by reading the row's actual
sleep/tolerance arithmetic against the engine's lock expiry
implementation, not by trusting it).
3. **Disposition**: either (a) reproduced → root-cause analysis → the
fix is its own follow-up task (engine bug vs harness-model, called
explicitly — the review-002 precedent is that a "flake" was a real
engine bug); or (b) not reproduced → the bounded-investigation
record (conditions matrix, instrumented output, the read-back of
the delay-only argument) with the watch posture updated (keep
watching vs instrument permanently vs accept). Both outcomes are
acceptable task completions; an uninvestigated shrug is not.
Requires the harness Postgres server (the POC container posture —
`docs/reviews/003-wave-5-general-review.md` §Op note has the env).
## Acceptance Criteria
- [x] The output-capture blind spot fixed (a failing run's assertion
message is preserved by the recorded procedure)
- [x] The conditions matrix actually run (counts + configurations
recorded in Notes)
- [x] The row's sleep/tolerance arithmetic read against the pg lock
expiry implementation; the delay-only argument confirmed or
corrected
- [x] Disposition recorded: reproduced (→ root cause + follow-up task
created) or not-reproduced (→ bounded-investigation record +
watch posture updated) — **reproduced, root cause harness-model;
trivially test-side, fixed in-session per the AC's exception**
- [x] If the trim row reproduces: it joins the investigation and its
outcome is recorded too — did **not** reproduce (10 focused +
7 loaded full-suite rounds green); stays watch-flagged (n/a)
- [x] Workspace gates green at session end (any fix is a follow-up
task, not a ride-along — unless the root cause is trivially
test-side, in which case fix + full gates + Notes)
## References
- tasks/review-wave-5.md §Notes 6 (the ledger + the trigger update)
- docs/plans/implementation.md §Wave 8 (the watch carriage)
- docs/reviews/003-wave-5-general-review.md (the concurrence read +
the harness-credentials op note)
- docs/reviews/002-wave-4-general-review.md (the flake-was-a-real-bug
precedent, F-1)
## Notes
> Investigation of record (2026-10-11). Every result below was run
> this session; raw logs under `/tmp/opencode/flake-inv/` (session
> scratch, not committed — the counts and shapes are recorded here).
**1. Output capture — the blind spot fixed and demonstrated.** The
procedure of record for every pg-column investigation run:
```
ALKSTORE_PG_HOST=localhost ALKSTORE_PG_PORT=15432 \
ALKSTORE_PG_USER=postgres ALKSTORE_PG_PASSWORD=poc ALKSTORE_PG_DB=blobs \
cargo test -p alkstore-postgres --test contract_suite [filter] -- \
--nocapture 2>&1 | tee <log>
```
Tee to a file **before** any filter (`grep`-only pipelines were how
the first and second occurrences — and this session initially re-lost
a failing run's identity — lost the messages). Verified by run: a
temporary deliberately-failing probe test executed through the
procedure preserved its full assertion message and backtrace in the
tee'd log (probe file removed after); the same procedure captured both
natural reproductions below verbatim. Corollary recorded for the skip
posture: `harness_ready()`'s skip prints "1 passed … 0.00s" — a
result-line scan alone cannot distinguish a skipped row from a
passing one; check the runtime or run with `--nocapture` and grep for
the skip marker.
**2. The arithmetic, read against the implementation.** `now_unix()`
is second-precision (`as_secs`,
`alkstore-postgres/src/resolution.rs:24-29`). A holder acquiring with
`ttl = 1` at wall instant `W0` stamps `expires_at = floor(W0) + 1`;
the acquire's expiry predicate is `expires_at <= now` with
second-precision reads (`alkstore-postgres/src/lock.rs::lock_acquire`:
opportunistic expired-row DELETE → PK-guarded ON CONFLICT DO NOTHING
INSERT → owner read-back). lapses when the contender's read reaches
`floor(W0) + 1` — i.e. after only `1 − frac(engine-now)` seconds,
arbitrarily close to zero (and the engine's clock read rides *before*
the acquire's round trips, so the effective headroom is shorter than
the post-read arithmetic implies). The ledger's delay-only argument:
- **Confirmed for the row's post-sleep lapse assertions** — slower
clocks only make expiry more certain, and once the second owner's
row is in place, owner-scoped `renew` (UPDATE … WHERE name AND
owner) and owner-scoped `release` cannot resurrect the original
holder's row; the `lock_acquire` read-back cannot grant two owners
(the PK guard). Never un-lapsed by lateness.
- **Corrected/extended in two places.** (a) It covers only the
post-sleep legs: the row's *contention* leg
(`contender.is_none()`, `properties.rs:563` in the pre-fix row)
asserts a **live-TTL state** — the one place the row asserts "not
yet lapsed", and under second-precision stamps the `ttl = 1`
holder's guaranteed exclusion window can be under a second. Any
scheduling/server stall (≳ ~1 s) between the holder's and the
contender's `try_lock` calls lapses the holder with the engine
acting fully per contract. (b) Strictly, a *backwards* clock step
(NTP slew) could also un-lapse the post-sleep asserts — not covered
by "slow clock" framing; rare in the container posture; recorded,
no action.
**3. Conditions matrix (pre-fix row).** All against the harness
container (`pglo-poc`, :15432), workspace clean:
| ID | Condition | Runs | Result |
|---|---|---|---|
| A1 | solo row, warm, `--test-threads=1` | 20 | all green (4.1–4.8 s) |
| A2 | solo row, warm, `--test-threads=6` | 10 | all green |
| B1 | full pg suite, `--test-threads=1` | 2 | green (99.8 / 100.0 s) |
| B2 | full pg suite, default (8) | 2 | green (21.1 / 21.3 s) |
| C | full pg suite concurrent with full SQLite column | 2 | green (pg 21.0 / 21.7; sq 13.3 / 16.3) |
| D | cold server (docker restart + readiness-wait before first run) | 3 × solo + 1 × full | green (full: 20.8 s). An earlier trio of "runs" were skips (0.00 s) caught by §1's corollary |
| E | trim row focused, `--test-threads=6` | 10 | all green — the trim row did **not** reproduce |
| F | full pg suite under 8 CPU burners (`sha256sum /dev/zero` ×8) | 6 | **F1 FAILED 24/1** — failing row's identity lost (this session's own grep-before-tee; from F2 on every run teed first); F2–F5 green; **F6 FAILED `row_stream_ordering_equivalence`** (see §6) |
**4. Mechanism probes (temporary test target, removed after
recording).** A probe mirrored the row's body with a `tokio::time::sleep`
interposed between the holder's and contender's `try_lock` calls:
- **1.2 s delay → deterministic failure** on the exact leg, exact
message: panic "a held lock excludes a second acquirer" at the
contention assert; probe log prints the arithmetic (holder stamp
`floor(1791680021)+1`, contender read `floor(1791680022)` →
`expires_at <= now` true → granted to the contender).
- **200 ms delay, 10 iterations → 2/10 lapsed** (frac-dependent —
the lapse lands whenever the holder's engine-now read sits late in
its second), confirming the boundary sits at ~`1 − frac` seconds,
far inside ordinary scheduling jitter.
**5. Natural repro captured.** Solo pre-fix row, 30 consecutive runs
under the §F burners, full output tee'd: **2/30 FAILED** at
`properties.rs:563` with the exact message "a held lock excludes a
second acquirer" (runs 17 and 23). Message capture procedure held.
Combined with §3's probes: mechanism demonstrated, boundary mapped,
and the failure occurs under ordinary CPU starvation with no other
pathology.
**6. Disposition: reproduced → root cause harness-model (test-side),
fixed in-session under the AC's trivially-test-side exception.** The
engine's behavior conforms to ADR-008 §7 at every step — exclusion
*bounded by TTL*, silent lapse, no revocation event; the row wrote
`ttl = 1` into a hold it then asserted across an unbounded wall-clock
gap. This is the review-002 precedent's inverse: the "flake" was not
the engine, it was the row's contention leg. Fix landed in
`alkstore-contract-suite/src/properties.rs::lock_ttl_expiry_and_reacquisition`:
a two-leg shape — the **exclusion leg** holds a long TTL (300 s, a
span the row's own duration cannot cross — wall-clock independent)
and releases it; the **lapse leg** keeps the 1 s stamp and the
tolerance-sleep posture (its asserts are post-sleep states the fix
preserves as delay-only-safe). Every pinned semantic survives
(exclusion-while-held, silent lapse, re-acquisition, post-expiry
renew `false`, second's release, third-owner freedom); the row doc
records the two-leg posture and why. No follow-up task created (no
engine change; the row change is the fix).
**7. Fix verification.** 30/30 loaded solo runs of the fixed row
green under the exact repro condition (§F burners); 3× focused green
on SQLite (2.7–2.8 s) and 3× on mem (2.5 s) columns; full workspace
gates green — `cargo build`; `cargo test` live for the pg column (127
lib + 25 suite + 9 schema vs the harness) with sqlite 193 + 25, mem
102, core 25, suite harness 3+1 all green; `cargo clippy --all-targets
-- -D warnings`; `cargo fmt --check`.
**8. Watch posture after this session.** The lock row's watch flag
**retires** (root-caused, test-side fixed, repro condition survives
the fix on both sides of it: 30/30 loaded green). The trim row stays
**watch-flagged** — 10 focused + every loaded full-suite round green
this session, no repro. **New ledger observation**: one
`row_stream_ordering_equivalence` failure under 8-burner load (§F6,
message captured): `left: [1]` vs `right: [1, 2, 3, 4, 5, 6]` at
`properties.rs:3052` — the row's drain window (`recv()` once, then
`try_recv()` until `None`) raced the pg bridge's not-yet-complete
fill (`run_subscribe_loop` sends the attach-drain pages into the
bounded mpsc sequentially; a descheduled bridge task between sends
leaves the consumer's `try_recv` pump momentarily exhausted). No
ordering violation, no delivery failure — a scheduling lottery in the
same class as the lock row's contention leg, in a row whose
compare-vectors are state-derivable (all six publishes fully stored
pre-attach). Single occurrence under synthetic load beyond any
dev/gate condition; recorded in the ledger
(`tasks/review-wave-5.md` §Notes 6, dated 2026-10-11) and carried to
the `review-wave-8` gate's flake outcome. No ride-along fix —
instrumentation first remains the discipline.
## Summary
> Filled by the implementation agent (2026-10-11):
The fired trigger was investigated per the ledger's prescription:
instrumentation first (tee-before-filter procedure of record,
demonstrated to preserve failing messages — and immediately vindicated
by two natural reproductions), the arithmetic read against
`lock.rs`/`resolution.rs` (delay-only argument confirmed for the
lapse legs, corrected for the contention leg), the conditions matrix
run (48+ rounds across thread counts, concurrency, cold/warm, and
under CPU load), mechanism probes with a mirroring test target, and
the disposition call: **reproduced — root cause is the row's own
shape (harness-model, not the engine)**: second-precision stamps let
a `ttl = 1` holder's exclusion window collapse below a second, so the
contention assert races the holder's own expiry under ordinary
scheduling stalls. Fixed in-session under the AC's trivially-test-side
exception (two-leg row: long-TTL exclusion leg + short-TTL lapse
leg); 30/30 loaded verification runs green under the exact repro
condition; all three suite columns green; workspace gates green
(build/test live-pg/clippy/fmt). The trim row did not reproduce and
stays watch-flagged; a new single-occurrence observation
(`row_stream_ordering_equivalence` under synthetic load, message
captured) is recorded in the ledger and rides the `review-wave-8`
gate's flake outcome.