5.9 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | |||
|---|---|---|---|---|---|---|---|---|---|---|---|
| sqlite-engine-locks | SQLite engine — named locks (`try_lock`, `Lock` handle, duration guards) | completed |
|
narrow | low | component | implementation |
|
Description
Implement Store::try_lock and the Lock trait over the substrate's
lock ops (lock_acquire, lock_release, lock_renew):
try_lock(name, owner, ttl)— validated entry point (name: shared-namespace rules; owner: non-empty only);None= someone holds it (no-work is a value). Returns the boxedLockhandle.- Duration guard (ADR-023 §2):
ttl <= 0is a rejection —Err, opaqueDatabaseshape — enforced at the trait-impl entry, before the substrate. The substrate'slock_renewalready guardsttl_s <= 0(its message preserved in the source chain where it fires);lock_acquiredoes not — the engine guards both call sites uniformly so the rule is contract-side, not substrate-side (the pg engine implements the same rule with no inherited guard at all). Lock::renew(ttl)— same duration guard; a new full TTL window from now;false= lost it (expired and re-acquired elsewhere).Lock::release(self)— consuming;true= was held;false= already expired/re-acquired (no-op, not an error).- Exclusion lapses silently at TTL expiry (no revocation event) — the substrate's expired-row delete on next acquire is the mechanism; pin the lapse-then-reacquire shape in tests.
Acceptance Criteria
try_lockgrants exclusively (second acquirer getsNone); release makes it acquirable again- Duration guards:
try_lock/renewwithttl <= 0return opaqueErr(Database) — never a granted lock, never a panic; substrate guard message preserved in the source chain onrenew— pinned per ADR-023 §2's backlog row renewextends from now (full window, not additive); foreign owner cannot renew;falseon lapsed/re-acquiredreleaseremoves only the owner's row;false= no-op shape- TTL lapse: after expiry, another owner acquires (silent lapse)
cargo test -p alkstore-sqlite, clippy-D warnings, fmt clean
References
- docs/architecture/core-contract.md (locks section, numeric-domain rules)
- docs/architecture/decisions/008-contract-v1-pinning.md §7
- docs/architecture/decisions/019-mechanism-handle-surfaces.md §1
- docs/architecture/decisions/023-fourth-review-round.md §2
- alkstore-sqlite/src/substrate/ops.rs (lock_* fns)
Notes
Decisions of record the description didn't pin:
- Same-owner re-acquire returns
Some, notNone— the substrate'slock_acquireis opportunistic-expiry-delete +INSERT OR IGNORE+ read-back: the same owner's re-acquire keeps the original row (TTL not refreshed) and reports granted. Pinned as the inherited shape (engine-sqlite.md's mapping row: "re-acquire does not refresh TTL — inherited deliberately"; the research register's D-4 keptlock_acquireverbatim). The handle returned is a fresh boxedLock;renewremains the only refresh path. - Post-lapse release returns
truewhile the stale row survives,falseonly once the row is gone/foreign — the substrate'slock_releaseis a blind(name, owner)delete and expired rows persist until the next acquire's opportunistic delete (no general lock-expiry sweep, upstream-inherited). So immediately after a lapse the stale holder's release deletes its own stale row (true); after a foreign re-acquire (or any row-less state) it is the no-opfalse. Tests pin both arms; the contract text's "true = was held" reads as row-scoped, which the substrate defines. - The duration-guard detail rides the source chain, not
Display— the engine guard's message ("ttl must be a positive duration (seconds), got {ttl}") is the#[source]inner error (ADR-008 §5's chain discipline; theDatabasedisplay isdatabase erroropaque). The substrate's direct-caller messagettl_s must be positiveis asserted verbatim at the substrate call site (A-2 above). - Closed-store posture:
try_lock/renew/releasecheck the engine's closed flag (or the writer slot's unavailability) and fail closed with opaqueDatabase— the engine-wide shape every wave-3 mechanism carries; release preserves the substrate row delete as the truth-teller rather than caching a handle-local "held" flag. store/lock_tests.rsfollows the wave-3 bounded-wait posture for TTL expiry (second-precisionunixepoch(); no tight-race sleeps); the short-window lapse steps stamp short expiries directly through a separate connection (the queue tests' honest cross-connection probe shape) instead of sleeping out multi-second windows.
Summary
alkstore-sqlite/src/lock.rs: the acquire path (try_lock— shared-namespace + non-empty owner validation, the ADR-023 §2 duration guard onttl <= 0engine-side before the substrate, the closed-store check, thenlock_acquirethrough the writer slot,Ok(None)= held elsewhere) andSqliteLockHandle(Lockimpl:renewwith the same duration guard overlock_renew's full-window update; consumingrelease(self: Box<Self>)overlock_release's owner-scoped delete).store.rs'stry_lockstub replaced with the wiring. Six acceptance tests instore/lock_tests.rspin the exclusion/loser shapes, both duration guards + the substrate's verbatim message, full-window renew and stale-holder refusals, owner-scoped release with both boolean arms, the silent lapse-then-reacquire shape, and the cross-instance re-acquire (the verification backlog's SQLite re-acquisition pin at engine level; the contract-suite row remains for the suite itself). Verified:cargo build,cargo test(workspace; 177 + 25 + 3 pass),cargo clippy --all-targets -- -D warnings,cargo fmt --check— all clean.