Files
alkstore/tasks/sqlite-engine-locks.md

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
sqlite-engine-seam-tx
narrow low component implementation
wave-3
sqlite-engine

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 boxed Lock handle.
  • Duration guard (ADR-023 §2): ttl <= 0 is a rejection — Err, opaque Database shape — enforced at the trait-impl entry, before the substrate. The substrate's lock_renew already guards ttl_s <= 0 (its message preserved in the source chain where it fires); lock_acquire does 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_lock grants exclusively (second acquirer gets None); release makes it acquirable again
  • Duration guards: try_lock/renew with ttl <= 0 return opaque Err (Database) — never a granted lock, never a panic; substrate guard message preserved in the source chain on renew — pinned per ADR-023 §2's backlog row
  • renew extends from now (full window, not additive); foreign owner cannot renew; false on lapsed/re-acquired
  • release removes 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, not None — the substrate's lock_acquire is 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 kept lock_acquire verbatim). The handle returned is a fresh boxed Lock; renew remains the only refresh path.
  • Post-lapse release returns true while the stale row survives, false only once the row is gone/foreign — the substrate's lock_release is 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-op false. 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; the Database display is database error opaque). The substrate's direct-caller message ttl_s must be positive is asserted verbatim at the substrate call site (A-2 above).
  • Closed-store posture: try_lock/renew/release check the engine's closed flag (or the writer slot's unavailability) and fail closed with opaque Database — 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.rs follows the wave-3 bounded-wait posture for TTL expiry (second-precision unixepoch(); 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 on ttl <= 0 engine-side before the substrate, the closed-store check, then lock_acquire through the writer slot, Ok(None) = held elsewhere) and SqliteLockHandle (Lock impl: renew with the same duration guard over lock_renew's full-window update; consuming release(self: Box<Self>) over lock_release's owner-scoped delete). store.rs's try_lock stub replaced with the wiring. Six acceptance tests in store/lock_tests.rs pin 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.