--- id: pg-engine-locks name: Postgres engine — named locks (`try_lock`, `Lock` handle, duration guards) status: pending depends_on: [pg-engine-seam-tx] scope: narrow risk: low impact: component level: implementation tags: [wave-4, postgres-engine] --- ## Description Implement the named-lock mechanism's pg arm — the smallest mechanism, and the one with the strongest POC ground (the pg POC's lock probe pinned acquire/release/renew with TTL and expiry re-acquisition directly). Over the schema task's locks table: - **`try_lock(name, owner, ttl)`** — shared-namespace + non-empty owner validation (`InvalidName`); the **duration guard** (`ttl <= 0` → opaque `Err(Database)`, detail in the source chain — ADR-023 §2's duration kind; an exclusion mechanism must not silently hand back a lease the caller believes it holds); the closed-store check; then the acquire op on a pool connection: opportunistic-expiry- delete + insert-or-reacquire + read-back (the substrate's `lock_acquire` semantics, re-derived — the POC's probe shape). `Ok(None)` = held elsewhere (no-work is a value); `Ok(Some(boxed Lock))` on grant. - **`Lock` handle** (`renew`, consuming `release`): - `renew(ttl)` — the same duration guard; the new **full TTL window from now** (not additive); `false` = lost it (expired and re-acquired by another owner — the owner-scoped update's 0-rows). - `release(self: Box)` — consuming; the owner-scoped delete; `true` = was held (row gone by this owner's hand), `false` = row-less or foreign (the substrate's row-scoped truth-teller posture — no handle-local "held" flag caching). - **Guarantee row posture** (ADR-008 §7): mutual exclusion bounded by TTL + renewal; expiry lapses *silently* (no revocation event — expired rows are deleted opportunistically by the next acquire's expiry-delete, no general expiry sweeper — the inherited posture, documented); re-acquirable after expiry (the POC-pinned property — pin it engine-side here; the contract-suite row remains for the suite itself). - **Same-owner re-acquire**: pin the inherited disposition (the substrate's shape: the same owner's re-acquire keeps the original row, TTL *not* refreshed, reports granted — the SQLite task's pinned posture; engine-sqlite.md's mapping row calls it inherited deliberately, and the pg arm should match it for cross-engine equivalence — flag in Notes if the pg realization differs). - **Post-lapse release arms**: pin both (the stale holder's release deletes its own stale row → `true`; after a foreign re-acquire → `false`) — the SQLite task's pinned shape, matched here. - **Closed-store posture**: `try_lock`/`renew`/`release` fail closed with opaque `Database` (the engine-wide shape). - **No `lock_tx`** — deliberate absence (core-contract.md's named- locks section); nothing to build, just don't invent one. ## Acceptance Criteria - [ ] `try_lock`: validation (name + owner), duration guard (`ttl <= 0` → opaque `Err`, detail in the source chain), closed-store check, then acquire — `Ok(None)` on held-elsewhere - [ ] Exclusion + loser shape pinned (two owners, one wins); expiry re-acquisition pinned (the POC's property, engine-side) - [ ] `renew`: duration guard; full-window reset; `false` on lost/expired-and-reacquired - [ ] `release`: consuming, owner-scoped, both boolean arms pinned (stale-row delete = `true`; foreign/row-less = `false`) - [ ] Same-owner re-acquire disposition pinned (and flagged if it differs from the SQLite arm's) - [ ] Silent lapse posture documented (no revocation event; no general expiry sweeper) - [ ] `cargo test -p alkstore-postgres` (harness server), clippy `-D warnings`, fmt clean; gates green server-less ## References - docs/architecture/core-contract.md (named locks section) - docs/architecture/decisions/008-contract-v1-pinning.md §7 (guarantee row) - docs/architecture/decisions/023-fourth-review-round.md §2 (duration kind) - docs/architecture/engine-postgres.md (Mapping the contract: named locks) - docs/research/poc-pg-posture-findings.md (the lock probe row) - alkstore-sqlite/src/lock.rs (the structural twin — match its pinned dispositions) - alkstore/src/lock.rs (the trait) ## Notes > To be filled by implementation agent ## Summary > To be filled on completion