Contract-suite rows (task suite-opts-backoff-rows): the backoff-curve equivalence row and the deferred enqueue-opts clock legs. backoff_curve_equivalence — the equal-jitter exponential pinned as the range, not a jitter label (ADR-010 §3): claim → retry(err, None) cycles on a backoff_base_s = 2 queue land the ranges [1,2], [2,4], [4,8] across attempts 1–3, each computed delay read back through get_job's resolved run_at minus a clock read taken before the retry — the lower bound race-free (the retry's internal clock read cannot precede the suite's, so the observed value over-reads the delay, never under-reads) and the upper bound carrying a one-second straddle tolerance for the integer-second stamp crossing a wall second; identical bounds on both engines is the equivalence pin (ADR-012 §2, the row body shared). The explicit-delay override legs ride the same row: retry(err, Some(5)) honored verbatim ([5, 6] under the same straddle tolerance) and retry(err, Some(0)) resolving ready-now per the boundary-total rule (ADR-023 §2), claimed within a bounded wait. enqueue_opts_resolution extended in place with the wave-5 legs its deferral note named — the run_at-alone literal leg (a future run_at is the row's ready time verbatim, a deterministic equality with no clock read involved; a past run_at stores the literal too and is claimable now through the run_at <= now predicate within a bounded wait, the run_at-ASC ordering making the observation unambiguous among the row's other legs) and the neither-field leg (ready at the enqueue instant, abs_diff(now) <= 5 tolerance-bounded proximity, never a tight timing assert); the deferral note replaced by the completion statement, ADR-023 §2 stamp accumulated per the convention (ADR-020 §1 governs the resolutions). Cap-leg disposition recorded in Notes for the review gate: the 1-hour cap is not suite-pinnable (capped attempts need minute-scale waits) and stays pinned engine-side on both engines' unit tests per the wave-3/4 reviews. Ready-now waits are bounded claim_one poll loops (deadline asserts only, sequential single-store drive), with the one draft defect the finding surfaced (a re-claim after the bounded wait consumed nothing; the helper returns the claimed handle) noted. Wired into both engines' suite targets (SQLite tokio test, pg harness_row!), 25 rows per column up from 24. Verified: SQLite column green server-less (25/25 suite, 189 lib), pg column green vs harness (postgres/poc@:15432 — 25/25 suite, 121 lib + 9 schema), 3 solo re-runs of each new/extended row per engine (determinism), server-less pg skips cleanly via the reachability gate, cargo test -p alkstore-sqlite -p alkstore-postgres green, clippy -D warnings, fmt clean

This commit is contained in:
glm-5.3-flash committed 2026-10-10 07:26:55 +00:00
1 parent 2a2ad7e11f
commit 36023914b2
5 files changed
+374 -30

No files matched your search

+3 -3
View File
@@ -38,9 +38,9 @@ pub mod version_stamp;
pub use factory::StoreFactory; pub use factory::StoreFactory;
pub use properties::{ pub use properties::{
concurrent_try_lock_loser_is_a_value, duration_refusal_on_non_positive_ttl, backoff_curve_equivalence, concurrent_try_lock_loser_is_a_value,
enqueue_opts_resolution, extent_clamp_semantics, in_tx_reads_see_own_writes, duration_refusal_on_non_positive_ttl, enqueue_opts_resolution, extent_clamp_semantics,
job_handle_validity_predicate, lock_ttl_expiry_and_reacquisition, in_tx_reads_see_own_writes, job_handle_validity_predicate, lock_ttl_expiry_and_reacquisition,
name_validation_rejects_empty_and_reserved, outbox_enqueue_tx_commit_atomicity, name_validation_rejects_empty_and_reserved, outbox_enqueue_tx_commit_atomicity,
payload_round_trip_stores_exact_encoding, payload_too_large_never_produced_on_sqlite, payload_round_trip_stores_exact_encoding, payload_too_large_never_produced_on_sqlite,
payload_too_large_produced_on_pg, publish_with_key_tx_commit_atomicity, payload_too_large_produced_on_pg, publish_with_key_tx_commit_atomicity,
+266 -6
View File
@@ -894,15 +894,22 @@ pub async fn in_tx_reads_see_own_writes(factory: &dyn StoreFactory) {
/// `EnqueueOpts::max_attempts` override riding over them — `get_job` /// `EnqueueOpts::max_attempts` override riding over them — `get_job`
/// shows resolved values, never the raw opts. /// shows resolved values, never the raw opts.
/// ///
/// The `run_at`-alone-literal and neither-field-resolves-to-now legs /// Wave-5 completion — the two clock-adjacent legs the earlier note
/// are wall-clock-adjacent; they pin with wave 5's equivalence row /// deferred pin here: the `run_at`-alone literal leg (a future
/// (this row's precedence/resolution legs are the engine-agnostic /// `run_at` is the row's ready time *verbatim* — no arithmetic, a
/// claim). /// deterministic equality; a past `run_at` is ready *now* — the
/// boundary-total rule, claimable through the `run_at <= now`
/// predicate within a bounded wait) and the neither-field leg (ready
/// now at the enqueue instant — tolerance-bounded proximity of the
/// stamp to the clock read, never a tight timing assert).
/// ///
/// Contract stamp: ADR-020 §1 (delay wins over `run_at`); §2 /// Contract stamp: ADR-020 §1 (delay wins over `run_at`; `run_at`
/// alone is literal; neither field resolves to now); §2
/// (relative `expires` → absolute row value); §3/§3a (the derived /// (relative `expires` → absolute row value); §3/§3a (the derived
/// default stamps — plain queue 300/3/5/none, per-job override); /// default stamps — plain queue 300/3/5/none, per-job override);
/// ADR-012 §2 (engine-side arithmetic pinned equivalent). /// ADR-023 §2 (the boundary-total rule — a past ready time is ready
/// now, never rejected); ADR-012 §2 (engine-side arithmetic pinned
/// equivalent).
pub async fn enqueue_opts_resolution(factory: &dyn StoreFactory) { pub async fn enqueue_opts_resolution(factory: &dyn StoreFactory) {
let store = factory.open().await.expect("factory opens a store"); let store = factory.open().await.expect("factory opens a store");
@@ -993,6 +1000,259 @@ pub async fn enqueue_opts_resolution(factory: &dyn StoreFactory) {
"the per-job override rides over the derived defaults, others untouched" "the per-job override rides over the derived defaults, others untouched"
); );
// Wave-5 clock legs (the run_at-alone and neither-field pins).
// Future run_at alone: the ready time is the literal — stored
// verbatim, no arithmetic, no clock read involved (a deterministic
// equality, not a timing assert).
let literal = 1_800_000_000;
let literal_id = queue
.enqueue(
json!({"leg": "run-at-literal"}),
EnqueueOpts {
run_at: Some(literal),
..Default::default()
},
)
.await
.unwrap();
let literal_job = queue.get_job(literal_id).await.unwrap().unwrap();
assert_eq!(
literal_job.run_at, literal,
"run_at alone is the row's ready time verbatim (delay absent)"
);
// Past run_at alone: ready now (the boundary-total rule) —
// claimable within a bounded wait. The claim ordering (run_at ASC)
// hands it out ahead of the every same-now row above, so the
// observation is unambiguous even against the row's other legs.
let past = unix_now() - 30;
let past_id = queue
.enqueue(
json!({"leg": "run-at-past"}),
EnqueueOpts {
run_at: Some(past),
..Default::default()
},
)
.await
.unwrap();
assert_eq!(
queue.get_job(past_id).await.unwrap().unwrap().run_at,
past,
"a past run_at stores the literal too — ready-now is the claim semantics"
);
let claimed_past = tokio::time::timeout(std::time::Duration::from_secs(5), async {
loop {
if let Some(h) = queue.claim_one("wave5-past").await.unwrap() {
let id = h.job().id;
if id == past_id {
break id;
}
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
})
.await
.expect("a past run_at resolves ready-now: claimable within the bounded wait");
assert_eq!(
claimed_past, past_id,
"the past-run_at row is the ready-now claim (ordering: run_at ASC)"
);
// Neither field: ready now at the enqueue instant — the stamp sits
// close to the clock read (tolerance-bounded proximity, never a
// tight timing assert).
let neither_id = queue
.enqueue(json!({"leg": "neither"}), EnqueueOpts::default())
.await
.unwrap();
let now = unix_now();
assert!(
queue
.get_job(neither_id)
.await
.unwrap()
.unwrap()
.run_at
.abs_diff(now)
<= 5,
"neither field resolves to now: the stamp is within the tolerance \
of the clock read"
);
factory.teardown().await.expect("factory teardown");
}
/// The backoff row's bounded-wait claim: poll `claim_one` on `queue`
/// until `id` comes out and return that claim's handle (claim
/// ordering keeps single-job queues unambiguous; the deadline is the
/// safety bound — the assertions are on state outcomes, and the claim
/// is consumed here, not re-taken afterwards).
async fn await_claimable(
queue: &dyn alkstore::Queue,
id: i64,
max_wait: std::time::Duration,
label: &str,
) -> Box<dyn alkstore::JobHandle> {
let deadline = std::time::Instant::now() + max_wait;
loop {
match queue.claim_one("backoff-w").await.unwrap() {
Some(h) if h.job().id == id => return h,
_ => {}
}
assert!(
std::time::Instant::now() < deadline,
"{label}: the row never became ready within the bounded wait"
);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
}
/// **Backoff-curve equivalence across engines** — the equal-jitter
/// exponential curve (ADR-010 §3): `retry(err, None)` computes
/// `delay ∈ [base·2^(a−1)/2, base·2^(a−1)]` from the job's stamped
/// `backoff_base_s` (the attempt index `a` the row's post-claim
/// `attempts`), capped at 1 hour — *the range, not a jitter label, is
/// the definition*, and identical bounds across engines is the
/// equivalence pin (ADR-012 §2: engine-side arithmetic, suite-pinned
/// identical outputs). The row drives claim → `retry(err, None)`
/// cycles on a queue with `backoff_base_s = 2` (integer-second
/// resolution: the ranges [1,2], [2,4], [4,8] land across the first
/// three attempts), reading each computed delay back through
/// `get_job`'s resolved `run_at` minus a clock read taken before the
/// retry, asserting range membership per attempt — with a
/// one-second straddle tolerance on the upper side only (the retry's
/// internal clock read and the suite's may cross a wall second; the
/// lower side is race-free, the retry's clock read cannot precede the
/// suite's). The inter-attempt waits are tolerance-bounded and
/// sequential (the row waits for each `run_at` to pass through
/// bounded-wait claims).
///
/// The explicit-delay override legs: `retry(err, Some(d))` is honored
/// verbatim — `Some(5)` lands the row ready at the retry instant plus
/// exactly 5 s (the same straddle tolerance), and `Some(0)` resolves
/// ready-now (the boundary-total rule: a zero delay is claimable at
/// the retry's own second, within a bounded wait).
///
/// Cap-leg disposition: the 1-hour cap is not suite-pinnable (reaching
/// capped attempts requires waiting out minute-scale delays); it stays
/// pinned engine-side — both engines' unit tests pin the curve against
/// the ADR text including the cap.
///
/// Contract stamp: ADR-010 §3 (the equal-jitter exponential, the range
/// as definition, the 1-hour cap); ADR-020 §1 (the explicit-delay
/// override resolves to the row's ready time — `Some(0)` ready now);
/// ADR-012 §2 (engine-side arithmetic pinned identical); ADR-023 §2
/// (the boundary-total rule — a zero delay is ready now, never
/// rejected).
pub async fn backoff_curve_equivalence(factory: &dyn StoreFactory) {
let store = factory.open().await.expect("factory opens a store");
let queue = store
.queue(
"curve",
alkstore::QueueOpts {
max_attempts: 7,
backoff_base_s: 2,
..Default::default()
},
)
.await
.unwrap();
let id = queue
.enqueue(json!({"leg": "curve"}), EnqueueOpts::default())
.await
.unwrap();
// Three curve cycles at the first three attempts (base 2 s:
// ranges [1,2], [2,4], [4,8] — the integer-second row resolution
// kept honest by the 1-second straddle tolerance on the upper
// side). The now-read before the retry makes the lower bound
// race-free: the retry's internal clock read cannot precede it, so
// the observed value over-reads the delay, never under-reads.
for (attempt, lower, upper) in [(1i64, 1i64, 2i64), (2, 2, 4), (3, 4, 8)] {
let held = await_claimable(
queue.as_ref(),
id,
std::time::Duration::from_secs(upper as u64 + 10),
&format!("attempt {attempt}"),
)
.await;
assert_eq!(
held.job().attempts,
attempt,
"the attempt index is the claim count"
);
let now = unix_now();
assert!(
held.retry(Some("boom".to_string()), None)
.await
.expect("an in-window retry lands")
);
let job = queue
.get_job(id)
.await
.unwrap()
.expect("the retried row lives");
assert_eq!(
job.state,
JobState::Pending,
"the retry left the row pending"
);
let observed = job.run_at - now;
assert!(
(lower..=upper + 1).contains(&observed),
"attempt {attempt}: the computed delay must land in \
[{lower}, {upper}] (one-second straddle tolerance), got {observed}"
);
}
// Explicit-delay override: Some(5) is honored — ready at the retry
// instant plus exactly 5 s (the same straddle tolerance), not the
// attempt-4 curve range [8, 16].
let held = await_claimable(
queue.as_ref(),
id,
std::time::Duration::from_secs(10),
"Some(5) leg",
)
.await;
let now = unix_now();
assert!(
held.retry(Some("boom".to_string()), Some(5))
.await
.expect("an in-window retry lands")
);
let job = queue.get_job(id).await.unwrap().unwrap();
let observed = job.run_at - now;
assert!(
(5..=6).contains(&observed),
"retry(err, Some(5)) is honored verbatim (± one second straddle), \
got {observed}"
);
// Some(0): the boundary-total rule — ready now. The retry's own
// second already satisfies the claim predicate, so the bounded
// wait is short and the row lands promptly.
let held = await_claimable(
queue.as_ref(),
id,
std::time::Duration::from_secs(8),
"Some(0) leg",
)
.await;
assert!(
held.retry(Some("boom".to_string()), Some(0))
.await
.expect("an in-window retry lands")
);
await_claimable(
queue.as_ref(),
id,
std::time::Duration::from_secs(5),
"Some(0) resolves ready-now (the boundary-total rule)",
)
.await;
factory.teardown().await.expect("factory teardown"); factory.teardown().await.expect("factory teardown");
} }
+15 -10
View File
@@ -163,16 +163,16 @@ async fn harness_ready() -> bool {
mod rows { mod rows {
use alkstore_contract_suite::properties::{ use alkstore_contract_suite::properties::{
concurrent_try_lock_loser_is_a_value, drop_rollback_leaves_no_ghosts, backoff_curve_equivalence, concurrent_try_lock_loser_is_a_value,
duration_refusal_on_non_positive_ttl, enqueue_opts_resolution, extent_clamp_semantics, drop_rollback_leaves_no_ghosts, duration_refusal_on_non_positive_ttl,
in_tx_reads_see_own_writes, job_handle_validity_predicate, enqueue_opts_resolution, extent_clamp_semantics, in_tx_reads_see_own_writes,
lock_ttl_expiry_and_reacquisition, name_validation_rejects_empty_and_reserved, job_handle_validity_predicate, lock_ttl_expiry_and_reacquisition,
outbox_enqueue_tx_commit_atomicity, payload_round_trip_stores_exact_encoding, name_validation_rejects_empty_and_reserved, outbox_enqueue_tx_commit_atomicity,
payload_too_large_produced_on_pg, publish_with_key_tx_commit_atomicity, payload_round_trip_stores_exact_encoding, payload_too_large_produced_on_pg,
queue_depth_reclaim_and_dead_letter, receiver_close_and_save_arms, publish_with_key_tx_commit_atomicity, queue_depth_reclaim_and_dead_letter,
scheduler_boundary_fires, scheduler_bounded_catchup, scheduler_leadership_discipline, receiver_close_and_save_arms, scheduler_boundary_fires, scheduler_bounded_catchup,
stream_ordering_equivalence, sweep_no_stranded_rows, trim_to_semantics, scheduler_leadership_discipline, stream_ordering_equivalence, sweep_no_stranded_rows,
wake_receiver_shapes, with_tx_panicking_closure_rolls_back, trim_to_semantics, wake_receiver_shapes, with_tx_panicking_closure_rolls_back,
}; };
use super::PgFactory; use super::PgFactory;
@@ -243,6 +243,11 @@ mod rows {
enqueue_opts_resolution, enqueue_opts_resolution,
"row-opts-resolution" "row-opts-resolution"
); );
harness_row!(
row_backoff_curve_equivalence,
backoff_curve_equivalence,
"row-backoff-curve"
);
harness_row!( harness_row!(
row_receiver_close_and_save_arms, row_receiver_close_and_save_arms,
receiver_close_and_save_arms, receiver_close_and_save_arms,
+15 -10
View File
@@ -67,16 +67,16 @@ impl StoreFactory for SqliteFactory {
mod rows { mod rows {
use alkstore_contract_suite::properties::{ use alkstore_contract_suite::properties::{
concurrent_try_lock_loser_is_a_value, drop_rollback_leaves_no_ghosts, backoff_curve_equivalence, concurrent_try_lock_loser_is_a_value,
duration_refusal_on_non_positive_ttl, enqueue_opts_resolution, extent_clamp_semantics, drop_rollback_leaves_no_ghosts, duration_refusal_on_non_positive_ttl,
in_tx_reads_see_own_writes, job_handle_validity_predicate, enqueue_opts_resolution, extent_clamp_semantics, in_tx_reads_see_own_writes,
lock_ttl_expiry_and_reacquisition, name_validation_rejects_empty_and_reserved, job_handle_validity_predicate, lock_ttl_expiry_and_reacquisition,
outbox_enqueue_tx_commit_atomicity, payload_round_trip_stores_exact_encoding, name_validation_rejects_empty_and_reserved, outbox_enqueue_tx_commit_atomicity,
payload_too_large_never_produced_on_sqlite, publish_with_key_tx_commit_atomicity, payload_round_trip_stores_exact_encoding, payload_too_large_never_produced_on_sqlite,
queue_depth_reclaim_and_dead_letter, receiver_close_and_save_arms, publish_with_key_tx_commit_atomicity, queue_depth_reclaim_and_dead_letter,
scheduler_boundary_fires, scheduler_bounded_catchup, scheduler_leadership_discipline, receiver_close_and_save_arms, scheduler_boundary_fires, scheduler_bounded_catchup,
stream_ordering_equivalence, sweep_no_stranded_rows, trim_to_semantics, scheduler_leadership_discipline, stream_ordering_equivalence, sweep_no_stranded_rows,
wake_receiver_shapes, with_tx_panicking_closure_rolls_back, trim_to_semantics, wake_receiver_shapes, with_tx_panicking_closure_rolls_back,
}; };
use super::SqliteFactory; use super::SqliteFactory;
@@ -135,6 +135,11 @@ mod rows {
enqueue_opts_resolution(&factory("row-opts-resolution")).await; enqueue_opts_resolution(&factory("row-opts-resolution")).await;
} }
#[tokio::test(flavor = "multi_thread")]
async fn row_backoff_curve_equivalence() {
backoff_curve_equivalence(&factory("row-backoff-curve")).await;
}
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn row_receiver_close_and_save_arms() { async fn row_receiver_close_and_save_arms() {
receiver_close_and_save_arms(&factory("row-receiver-arms")).await; receiver_close_and_save_arms(&factory("row-receiver-arms")).await;
+75 -1
View File
@@ -1,7 +1,7 @@
--- ---
id: suite-opts-backoff-rows id: suite-opts-backoff-rows
name: Contract-suite rows — backoff-curve equivalence + the deferred enqueue-opts clock legs name: Contract-suite rows — backoff-curve equivalence + the deferred enqueue-opts clock legs
status: pending status: completed
depends_on: [] depends_on: []
scope: narrow scope: narrow
risk: low risk: low
@@ -76,6 +76,80 @@ Work:
> To be filled by implementation agent > To be filled by implementation agent
- **The 1-hour cap leg is not suite-pinned** (as pre-decided): reaching
capped attempts requires waiting out minute-scale delays. The cap
stays pinned engine-side — both engines' unit tests pin the curve
against the ADR text including the cap (SQLite's
`backoff_curve_lands_in_the_pinned_range` and the pg twin, verified
in the wave-3/4 rounds); the disposition is recorded in the row's
doc comment for the review gate.
- **Observation tolerance convention for the curve row**: the
computed delay is read back as `get_job`'s `run_at` minus a clock
read taken *before* the `retry(err, None)` call. That direction is
deliberate and race-free on the lower side: the retry's internal
clock read (SQLite's `unixepoch()` in-statement, pg's Rust-side
engine read) cannot precede the suite's, so the observed value
*over-reads* the delay, never under-reads — the pinned lower bound
is exact. The upper bound carries a one-second straddle tolerance
(retry commit and suite read may cross a wall second, and the
engines' stamps are integer seconds, ADR-010 §3's integerized
resolution). Asserted as a `(lower..=upper+1)` range membership; the
*identical* assertion on both engines is the equivalence pin
(ADR-012 §2's identical-outputs rule) — the row body is shared, so
the bounds are identical by construction.
- **Base 2 s kept the integer-second row resolution honest** per the
task text: ranges [1,2], [2,4], [4,8] across attempts 1–3 (the a=1
pin is inherently weak at one-second resolution — lower bound 1 with
upper ±1 — but the attempt-2/3 ranges are genuinely discriminating,
and the explicit-delay legs pin the exact-literal resolution).
- **Past-run_at leg stores the literal too**: the pin on the past leg
is the *claim semantics* (ready-now through the `run_at <= now`
predicate, bounded-wait claimable) and the stored literal (no
arithmetic on any form) — "resolves to ready now" is the claim-
predicate reading of the boundary-total rule (ADR-023 §2), not a
clamping of the stored value; both engines store verbatim
(`resolution.rs` twins verified).
- **Claim-polling observation posture**: both rows' ready-now waits
are bounded `claim_one` poll loops (100/50 ms cadence), deadline
asserts only — no timing-value assertions, single-store sequential
drive within the module doc's determinism posture.
- **One unreproducibility note on the helper**: the first draft made
`await_claimable` re-claim after the bounded wait, which found
nothing (the bounded wait consumed the claim); the helper returns
the claimed handle. Found on the first SQLite run, no engine-side
hazard.
- No ADR amendments needed; no divergences found (both engines'
arithmetic agreed out of the box — the equivalence pin held on the
first full run pairs).
## Summary ## Summary
> To be filled on completion > To be filled on completion
`enqueue_opts_resolution` extended in place with the wave-5
completion legs (its deferral note replaced by the completion
statement, ADR-023 §2 stamp accumulated): the `run_at`-alone literal
leg (future `run_at` = the row's ready time verbatim — a
deterministic equality, no clock read involved; past `run_at` stored
verbatim too and *claimable now* within a bounded-wait claim, the
run_at-ASC ordering making the observation unambiguous among the
row's other legs) and the neither-field leg (ready now at the
enqueue instant, `abs_diff(now) <= 5` tolerance-bounded proximity —
never a tight timing assert). New `backoff_curve_equivalence` row:
claim → `retry(err, None)` cycles on a `backoff_base_s = 2` queue
(ranges [1,2], [2,4], [4,8] per attempt, asserted as `(lower..=upper+1)`
range membership of `run_at − now_pre` with the race-free lower
bound and one-second straddle tolerance documented), reading each
computed delay back through `get_job`'s resolved `run_at`; the
explicit-delay override legs ride the same row (`Some(5)` honored
verbatim in [5, 6]; `Some(0)` resolving ready-now per the
boundary-total rule, claimed within a bounded wait). Stamped ADR-010
§3, ADR-020 §1, ADR-012 §2, ADR-023 §2. Both rows wired into both
engines' `contract_suite.rs` targets (SQLite `#[tokio::test]`, pg
`harness_row!`) — 25 rows per column, up from 24. Verified: SQLite
column green server-less (25/25 suite, 189 lib), pg column green vs
the harness server (postgres/poc@:15432 — 25/25 suite, 121 lib + 9
schema), 3 solo re-runs of each new/extended row per engine
(determinism), server-less pg column skips cleanly (25 "passed" via
the reachability gate), `cargo test -p alkstore-sqlite -p
alkstore-postgres` green, clippy `-D warnings`, fmt clean.