Contract-suite lock rows (task suite-lock-rows): two version-stamped rows discharging the lock backlog rows — lock_ttl_expiry_and_reacquisition (the ADR-008 §7 guarantee row: a held lock excludes a second acquirer (contender None); after a 1 s TTL the exclusion lapses silently — no revocation event, no error — and a second owner acquires; the original holder's post-expiry renew is refused (false, the lost-it arm); the second owner's release frees the name for a third acquirer, which consumes cleanly; tolerance-sleep posture, state outcomes only; ADR-008 §7 + ADR-019 §1, duration-guard legs cross-referenced to duration_refusal_on_non_positive_ttl) and concurrent_try_lock_loser_is_a_value (the contention posture: four sequential contenders — two owners, repeated — against a held lock all land the clean None value, never Database, never a busy-throw; the backlog row's SQLite busy-path open question answered: no divergence from the pg reference; release-then-contend cycle proves the loser path leaves no state blocking a later acquire, granted to a former loser and consumed cleanly; ADR-008 §5 + §7 + ADR-019 §1) — wired into both engines' suite targets (SQLite tokio tests, pg harness_row!s), 23 rows per column up from 21. Dispositions in Notes: no divergence found (no fix/ADR call needed), ADR-023 §2 excluded from stamps (its guard property is the duration-refusal row's, cross-referenced), the backlog parenthetical re-acquire-does-not-refresh-TTL stays engine-pinned, renew-refusal asserted after the second owner's re-acquisition (strongest form), contenders distinct-owner only (same-owner re-acquire grants per the inherited substrate shape), and one unreproducible first-cold-run pg failure recorded (message lost to truncation; 13 subsequent clean runs incl. concurrent SQLite+pg — no timing hazard identified, the lapse assertions are post-sleep state outcomes). Drive-by: alkstore-contract-suite's tokio dep gained the macros feature — a pre-existing compile break in the crate's own tests/suite_harness.rs (since 7f749ac) failed the harness target and the workspace test gate on HEAD; test-side non-event (ADR-017 §2 class 4). Verified: cargo test -p alkstore-sqlite -p alkstore-postgres green (SQLite 121+23+9, pg 189+23 vs harness server on :15432, server-less pg skips clean), workspace cargo test green (12 binaries), clippy -D warnings, fmt clean

This commit is contained in:
glm-5.3-flash committed 2026-10-10 06:58:54 +00:00
1 parent 1d05df200e
commit 98de4a49cd
6 files changed
+285 -17

No files matched your search

+157
View File
@@ -525,6 +525,163 @@ pub async fn duration_refusal_on_non_positive_ttl(factory: &dyn StoreFactory) {
factory.teardown().await.expect("factory teardown");
}
/// **Lock TTL/expiry re-acquisition** — the ADR-008 §7 guarantee row:
/// a held lock excludes a second acquirer (`None` — the loser value,
/// never an error); after the TTL elapses exclusion lapses *silently*
/// — no revocation event, no error anywhere — and a second owner's
/// acquire succeeds; the original holder's post-expiry `renew` is
/// refused (`false` — the deadline-lapse refusal of the handle's
/// uniform predicate shape); and the second owner's `release` frees
/// the name for a third acquirer. Short-TTL (1 s) stamps with the
/// tolerance-sleep posture (state outcomes asserted, never durations;
/// sequential single-store drive).
///
/// Cross-reference: the duration-guard legs (`ttl <= 0` rejecting on
/// `try_lock`/`renew`) pin in `duration_refusal_on_non_positive_ttl`
/// (ADR-023 §2) — this row owns the TTL *lifecycle* (lapse,
/// silent-exclusion loss, re-acquisition, post-expiry refusal, and
/// name-freedom after release).
///
/// Contract stamp: ADR-008 §7 (the locks guarantee row — mutual
/// exclusion bounded by TTL + renewal; expiry is a silent loss of
/// exclusivity, not an error; exclusivity resumes with
/// re-acquisition); ADR-019 §1 (the `Lock` handle surface the
/// refusals ride — repeatable `renew` with the lost-it `false`, the
/// consuming `release`).
pub async fn lock_ttl_expiry_and_reacquisition(factory: &dyn StoreFactory) {
let store = factory.open().await.expect("factory opens a store");
let holder = store
.try_lock("lapse", "a", 1)
.await
.expect("try_lock on a free name must not error")
.expect("the first acquire holds the free name");
let contender = store
.try_lock("lapse", "b", 300)
.await
.expect("a contender's try_lock must not error");
assert!(
contender.is_none(),
"a held lock excludes a second acquirer"
);
// TTL expiry (a 1 s stamp; the sleep is bounded well past the
// second-precision stamp's resolution): exclusion lapses silently
// — the second owner's acquire is a clean Ok(Some) with no error
// preceding it and no revocation event to observe.
past_stamp_sleep(2_500);
let second = store
.try_lock("lapse", "b", 300)
.await
.expect("the post-expiry acquire must not error (silent lapse, no revocation event)")
.expect("after TTL expiry exclusion has silently lapsed: a second owner acquires");
assert!(
!holder
.renew(300)
.await
.expect("renew errs only on storage failure, never on lapse"),
"the original holder's post-expiry renew is refused: the lost-it arm"
);
assert!(
second
.release()
.await
.expect("the second owner's release errs only on storage failure"),
"the second owner's release deletes its own row"
);
let third = store
.try_lock("lapse", "c", 300)
.await
.expect("try_lock on the freed name must not error")
.expect("the released name is acquirable by a third owner");
assert!(
third
.release()
.await
.expect("the third release errs only on storage failure"),
"the third owner releases its own row (no residue for teardown)"
);
factory.teardown().await.expect("factory teardown");
}
/// **Concurrent `try_lock` loser is a value** — the contention
/// posture: with a lock held, contenders' `try_lock` calls return the
/// clean no-work value (`None`) on both engines — never a `Database`
/// error, never a busy-throw surfacing (the SQLite substrate's
/// busy-path is the risk site this row pins; the pg side is the
/// reference behavior the suite drives first). Contenders are driven
/// sequentially against one held lock (several distinct owners,
/// including a repeat contender), and a release-then-contend cycle
/// proves the loser path leaves no state behind that blocks a later
/// acquire — the former loser acquires cleanly on an empty field.
///
/// Cross-reference: the TTL-lapse arms of the loser value pin in
/// `lock_ttl_expiry_and_reacquisition` — this row owns the
/// *held-lock* loser path (the substrate's busy-path behavior is the
/// thing under test, the backlog row's open question).
///
/// Contract stamp: ADR-008 §5 (the value-not-error rule — `try_lock`
/// returning `Option<Lock>` is explicitly not a taxonomy item; a
/// loser `Database` error would be a contract violation); ADR-008 §7
/// (the exclusion row the contention composes with); ADR-019 §1 (the
/// handle surface the winner's consuming `release` rides).
pub async fn concurrent_try_lock_loser_is_a_value(factory: &dyn StoreFactory) {
let store = factory.open().await.expect("factory opens a store");
let holder = store
.try_lock("contended", "holder", 300)
.await
.expect("try_lock on a free name must not error")
.expect("the first acquire holds the free name");
// Sequential contenders against the held lock: every loser sees
// the clean `None` value — a `Database` error on any of them is
// the busy-path finding this row exists to catch.
for attempt in 0..4u32 {
let owner = if attempt.is_multiple_of(2) {
"contender-1"
} else {
"contender-2"
};
let out = store
.try_lock("contended", owner, 300)
.await
.expect("a contended try_lock must not error (no busy-throw surfacing)");
assert!(
out.is_none(),
"a held lock must lose contenders the None value, not a granted lock"
);
}
// Release-then-contend: the loser path left no state — an
// immediate re-contend after release grants, to a former loser's
// owner, and consumes cleanly.
assert!(
holder
.release()
.await
.expect("the winner's release errs only on storage failure"),
"the winner's release deletes its own row"
);
let winner = store
.try_lock("contended", "contender-1", 300)
.await
.expect("the post-release contend must not error")
.expect("the loser path left no state blocking a later acquire");
assert!(
winner
.release()
.await
.expect("release errs only on storage failure"),
"the new winner releases cleanly (no residue for teardown)"
);
factory.teardown().await.expect("factory teardown");
}
/// **`encode_payload` typed-failure round-trip** — enqueue and publish
/// store the exact serde_json serialization of the input `Value` and
/// decode back to `Value` equality; the helper's failure arm is typed