diff --git a/alkstore-contract-suite/src/properties.rs b/alkstore-contract-suite/src/properties.rs index d9fb8ec..f77d059 100644 --- a/alkstore-contract-suite/src/properties.rs +++ b/alkstore-contract-suite/src/properties.rs @@ -569,7 +569,7 @@ pub async fn lock_ttl_expiry_and_reacquisition(factory: &dyn StoreFactory) { // 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); + past_stamp_sleep(2_500).await; let second = store .try_lock("lapse", "b", 300) .await @@ -1680,8 +1680,14 @@ pub async fn payload_too_large_produced_on_pg(factory: &dyn StoreFactory) { /// clock read `> T + 1`; 2.5 s guarantees it, 3.5 s the `expires: 2` /// stamps). Timing posture: state outcomes are asserted, never /// durations. -fn past_stamp_sleep(millis: u64) { - std::thread::sleep(std::time::Duration::from_millis(millis)); +/// +/// Harness posture: **no blocking calls inside suite rows** — this +/// helper is async (`tokio::time::sleep`) and every row awaits it; +/// the mem engine's suite column runs the current-thread flavor +/// ([ADR-024](../../docs/architecture/decisions/024-mem-engine.md) +/// §5), which is what catches a blocking stall here. +async fn past_stamp_sleep(millis: u64) { + tokio::time::sleep(std::time::Duration::from_millis(millis)).await; } /// **Job-handle validity predicate** — the uniform rule (processing @@ -1785,7 +1791,7 @@ pub async fn job_handle_validity_predicate(factory: &dyn StoreFactory) { "claims hand out in enqueue order (equal priority)" ); let retry_before = lapsed_retry.job().clone(); - past_stamp_sleep(2_500); + past_stamp_sleep(2_500).await; assert!( !lapsed_ack.heartbeat(600).await.unwrap(), "late heartbeat must be refused: the deadline has lapsed" @@ -1845,7 +1851,7 @@ pub async fn job_handle_validity_predicate(factory: &dyn StoreFactory) { batch_live.heartbeat(600).await.unwrap(), "the renewal keeps this id's claim live through the batch" ); - past_stamp_sleep(2_500); + past_stamp_sleep(2_500).await; let counted = batch .ack_batch(&[batch_live_id, batch_lapse_id, 987_654_321]) .await @@ -1967,7 +1973,7 @@ pub async fn queue_depth_reclaim_and_dead_letter(factory: &dyn StoreFactory) { .job() .claim_expires_at .expect("claim stamps the deadline"); - past_stamp_sleep(2_500); + past_stamp_sleep(2_500).await; let second = depth .claim_one("w2") @@ -2002,7 +2008,7 @@ pub async fn queue_depth_reclaim_and_dead_letter(factory: &dyn StoreFactory) { .await .unwrap(); depth.claim_one("w1").await.unwrap().unwrap(); - past_stamp_sleep(2_500); + past_stamp_sleep(2_500).await; let at_budget = depth.claim_one("w2").await.unwrap().unwrap(); assert_eq!(at_budget.job().id, exhausted); assert_eq!( @@ -2010,7 +2016,7 @@ pub async fn queue_depth_reclaim_and_dead_letter(factory: &dyn StoreFactory) { at_budget.job().max_attempts, "the reclaim spent the budget" ); - past_stamp_sleep(2_500); + past_stamp_sleep(2_500).await; assert!( depth.claim_one("w3").await.unwrap().is_none(), "the budget-spent row dead-letters instead of reclaiming" @@ -2139,7 +2145,7 @@ pub async fn sweep_no_stranded_rows(factory: &dyn StoreFactory) { .enqueue(json!({"leg": "never-expiring"}), EnqueueOpts::default()) .await .unwrap(); - past_stamp_sleep(3_500); + past_stamp_sleep(3_500).await; let moved = sweeping.sweep_expired().await.unwrap(); assert_eq!( @@ -2195,7 +2201,7 @@ pub async fn sweep_no_stranded_rows(factory: &dyn StoreFactory) { .unwrap(); let kept_dies = keeping.claim_one("w1").await.unwrap().unwrap(); assert!(kept_dies.fail(None).await.unwrap()); - past_stamp_sleep(2_500); + past_stamp_sleep(2_500).await; let deleted = retained.sweep_expired().await.unwrap(); assert_eq!( @@ -2479,7 +2485,7 @@ pub async fn scheduler_bounded_catchup(factory: &dyn StoreFactory) { // Several boundaries elapse (the first sits one interval past // registration; a 4 s runner-less pause leaves at least three // elapsed) — still nothing, no runner in sight. - past_stamp_sleep(4_000); + past_stamp_sleep(4_000).await; collect_fires(store.as_ref(), "catchup", &mut fired).await; assert!( fired.is_empty(), diff --git a/tasks/suite-harness-current-thread.md b/tasks/suite-harness-current-thread.md index 33c606e..d9ff685 100644 --- a/tasks/suite-harness-current-thread.md +++ b/tasks/suite-harness-current-thread.md @@ -1,7 +1,7 @@ --- id: suite-harness-current-thread name: Contract-suite harness — async sleep in `past_stamp_sleep`, current-thread flavor readiness -status: pending +status: completed depends_on: [] scope: narrow risk: low @@ -38,12 +38,12 @@ This task is engine-independent and can run any time before ## Acceptance Criteria -- [ ] `past_stamp_sleep` is async (`tokio::time::sleep`) -- [ ] Both engines' existing suites re-run green with identical +- [x] `past_stamp_sleep` is async (`tokio::time::sleep`) +- [x] Both engines' existing suites re-run green with identical assertions and tolerances (determinism posture intact) -- [ ] The no-blocking-calls posture noted where the harness's +- [x] The no-blocking-calls posture noted where the harness's determinism posture is documented -- [ ] Clippy `-D warnings`, fmt clean; workspace gates green +- [x] Clippy `-D warnings`, fmt clean; workspace gates green ## References @@ -55,8 +55,28 @@ This task is engine-independent and can run any time before ## Notes -> To be filled by implementation agent +- The posture note went into `past_stamp_sleep`'s doc comment rather + than the factory contract notes: the posture is a harness rule about + row internals (what a row may call), not part of the + `StoreFactory` shape, and the doc comment sits exactly where a row + author reaches for a sleep. The comment cites ADR-024 §5 for the + current-thread flavor that enforces it. +- Five rows exercise the helper across its two use shapes (the + lapse-settle legs and the scheduler runner-less-pause leg): + `lock_ttl_expiry_and_reacquisition`, `job_handle_validity_predicate`, + `queue_depth_reclaim_and_dead_letter`, `sweep_no_stranded_rows`, + `scheduler_bounded_catchup`. All nine call sites gained `.await`; + assertions and tolerances untouched. ## Summary -> To be filled on completion \ No newline at end of file +`past_stamp_sleep` now awaits `tokio::time::sleep` instead of blocking +on `std::thread::sleep` (alkstore-contract-suite/src/properties.rs), +making every suite row current-thread-safe for the mem engine's +`#[tokio::test(flavor = "current_thread")]` column (ADR-024 §5 item 4); +the doc comment records the no-blocking-calls-inside-rows posture. +Both engines' `contract_suite` re-ran fully green (25/25 each, SQLite +15.0 s / Postgres 21.3 s — no timing drift, tolerances unchanged); the +full workspace build, tests, `clippy -D warnings`, and `fmt --check` +pass. PostgreSQL leg ran against the POC container (`pglo-poc`, port +15432, creds per docs/reviews/003 §env note). \ No newline at end of file