8.1 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | |||
|---|---|---|---|---|---|---|---|---|---|---|---|
| suite-lock-rows | Contract-suite rows — lock TTL/expiry re-acquisition + concurrent try_lock loser | completed | narrow | medium | phase | implementation |
|
Description
Add the named-locks contract rows to
alkstore-contract-suite/src/properties.rs and wire them into both
engines' suite targets, discharging two backlog rows
(core-contract.md §Verification backlog): "Named-lock TTL/expiry
re-acquisition on SQLite" (pinned on pg by the POC's lock probe; on
SQLite it rests on the forked substrate's lock machinery — the suite
pins both engines to identical behavior) and "Concurrent try_lock
loser/error behavior on SQLite" (the substrate's busy-path under lock
contention is the thing to pin — the pg side returns cleanly).
Rows to add:
lock_ttl_expiry_and_reacquisition— the ADR-008 §7 guarantee row: mutual exclusion bounded by TTL + renewal. Pin: a held lock excludes a second acquirer (None); after TTL expiry exclusion lapses silently (no revocation event, no error — a second owner acquires); the original holder's post-expiryrenewis refused (false— the deadline-lapse refusal, ADR-010 §2's uniform predicate shape on the lock handle);releaseon the second owner frees the name for a third acquirer. Short TTL (1 s) + tolerance sleeps — sequential single-store, fits the determinism posture.concurrent_try_lock_loser_is_a_value— contention posture: with a lock held, contendertry_lockcalls return the clean no-work value (None) on both engines — never aDatabaseerror, never a busy-throw surfacing (the SQLite substrate's busy-path is the risk site; the pg side is the reference behavior). Drive several contenders sequentially against one held lock, and a release-then-contend cycle proving the loser path leaves no state that blocks a later acquire.
Risk note: what the SQLite busy-path actually returns under
contention is the open question this row exists to answer — if it
surfaces Database where pg returns None, that is a real
conformance finding: stop, record it in Notes, and raise it (an
engine fix or an ADR-level asymmetry call) rather than pinning the
divergence silently.
Acceptance Criteria
- Two rows exist, version-stamped (ADR-008 §7, ADR-019 §1, ADR-023 §2 as applicable)
- Rows wired into both engines'
contract_suite.rstargets - SQLite column green server-less; pg column green against the harness server
- Any engine divergence found is recorded in Notes with its disposition (fix or documented asymmetry) — not pinned silently
cargo test -p alkstore-sqlite -p alkstore-postgresgreen (pg rows skip cleanly server-less); clippy-D warnings; fmt clean
References
- docs/architecture/core-contract.md §Verification backlog (lock TTL/expiry re-acquisition; concurrent try_lock loser)
- docs/architecture/decisions/008-contract-v1-pinning.md §7
- docs/architecture/decisions/019-mechanism-handle-surfaces.md §1
- docs/architecture/engine-sqlite.md (the substrate's lock machinery)
Notes
Decisions of record the description didn't pin:
- The open question is answered: no divergence — the SQLite
substrate's busy-path under lock contention never surfaces
Database: contendedtry_lockreturns the same cleanOk(None)value as pg (the engines route lock ops through the writer-slot-serialized surface / pool acquire respectively, and both acquire ops are opportunistic-expiry-delete + insert-or-ignore + read-back, so a held row reads as foreign — the loser value — on both). No engine fix and no ADR-level asymmetry call was needed; the row pins the convergence rather than recording one. - Stamp scope — ADR-023 §2 is not on either row's stamp list:
the duration-guard behavior it pins (
ttl <= 0rejecting ontry_lock/renew) isduration_refusal_on_non_positive_ttl's property (cross-referenced in the TTL row's doc text, as are that row's legs from this one). The rows carry stamps for the behavior they test: ADR-008 §7 (the guarantee row / contention composition), ADR-008 §5 (the loser-value-not-error rule the contention row pins), ADR-019 §1 (theLockhandle surface). - Backlog-row parenthetical stays engine-side — the backlog
row's "re-acquire-does-not-refresh-TTL behavior is upstream's,
inherited deliberately" note remains pinned by the SQLite
engine tests (
store/lock_tests.rs's mapping-row pin); the suite row pins the task description's five lifecycle legs only and does not re-pin the same-owner-re-acquire-returns-Some / TTL-not-refreshed shape (deliberate scope: that shape is the acquisition semantics' non-lapse twin, both engine tests pin it, and neither task text asked the suite to own it). - renew-refusal ordering — the original holder's
renewis asserted after the second owner's re-acquisition (the strongest form: the row the refuser targets belongs to another owner), rather than immediately post-lapse while the stale row survives; both forms landfalseon both engines, so only the strongest is pinned. - Contenders are distinct-owner only — the contention row
never drives a same-owner
try_lockagainst a held lock: the inherited substrate shape means a same-owner re-acquire grants (Some, original row kept) rather than losing, so including it in a "loser is a value" row would misdescribe the property. - One unreproducible pg failure, first cold run — the very
first pg full-suite run failed
row_lock_ttl_expiry_and_ reacquisition; the failure message was lost to output truncation and the row passed on every subsequent run (5 isolated full-suite runs, 3 concurrent SQLite+pg full-suite runs, 5 focused--test-threads=16runs — 13 clean total). No divergence observed, no repro found; recorded per the stream- rows task's precedent. The TTL legs' assertions are post-sleep state outcomes (a slower clock can only delay the lapse, never un-lapse it), so no timing hazard is identified.
Summary
What landed, verified how:
Two rows added to alkstore-contract-suite/src/properties.rs,
re-exported from the crate's lib, wired into both engines'
contract_suite.rs targets (SQLite: direct test fns; pg: the
harness_row! macro with unique factory tags) — 23 rows in each
engine's column, up from 21:
lock_ttl_expiry_and_reacquisition— the ADR-008 §7 guarantee row: held → contenderNone; post-1 s-TTL silent lapse (second owner acquires, no error, no revocation event); original holder's post-expiryrenewrefused (false); second owner'sreleasefrees the name for a third acquirer. Stamped ADR-008 §7 + ADR-019 §1; duration-guard legs cross-referenced toduration_refusal_on_non_positive_ttl(ADR-023 §2).concurrent_try_lock_loser_is_a_value— the contention posture: four sequential contenders (two distinct owners, repeated) against a held lock all get the cleanNonevalue — neverDatabase, never a busy-throw (the backlog row's SQLite busy-path open question, answered: no divergence from the pg reference) — plus a release-then-contend cycle granting a former loser (no blocking state left) and consuming cleanly. Stamped ADR-008 §5 + §7 + ADR-019 §1.
Drive-by fix: alkstore-contract-suite/Cargo.toml's tokio dep
gained the macros feature — a pre-existing break (since 7f749ac
added the dep without it) left the crate's own
tests/suite_harness.rs target failing to compile, which failed
cargo test -p alkstore-contract-suite and the workspace gate on
HEAD before this task. Test-side non-event (ADR-017 §2 class 4);
harness target green (3 tests).
Verification: cargo test -p alkstore-sqlite -p alkstore-postgres green (SQLite lib 121 + suite 23/23 +
harness-shape; pg lib 189 + suite 23/23 against the harness
server on :15432), server-less pg rows skip cleanly, full
workspace cargo test green (12 binaries), cargo clippy --workspace --all-targets -- -D warnings clean, cargo fmt --check clean. Stability: 13 clean runs of the pg suite's lock
rows/full suite as recorded in Notes.