Contract-suite queue-depth rows (task suite-queue-depth-rows): the job-handle validity predicate row (in-window heartbeat/ack landing, late-heartbeat/post-lapse-ack/retry/fail refusals past a 1 s stamp with the row untouched, ack_batch per-id predicate live-1/lapsed-0/nonexistent-0, fail(None)→"failed" and retry-at-budget→"max attempts exceeded"), the ADR-010 depth row (reclaim consumes an attempt with claimed_at/deadline refreshed and the original holder refusing, reclaim-exhaustion dead-lettering with the pre-claim sweep — get_job-visible "max attempts exceeded"+died_at, cancel unconditional delete with the not-an-interrupt refusal shape plus pending/dead/missing arms), and the no-stranded-rows sweep row (both states move with "expired", unexpired/never-expiring untouched, retention TTL enforcing with the moved+deleted sum, None=forever) — each version-stamped per the suite convention (ADR-010 §1–§5, ADR-019 §3, ADR-008 §5), wired into both engines' suite targets (SQLite tokio tests, pg harness_row!s). M-1's retention-failure pin dispositioned engine-side per the task's honest call: the storage-level DELETE failure is not injectable through the contract surface, so it lands in the SQLite substrate's trigger seam (sweep_rolls_back_the_retention_half_with_the_move — the move half rolls back with the retention half), with the pg twin verified structurally (both halves inside in_tx's frame) and the disposition recorded in the task Notes. Verified: sqlite suite 13/13, pg suite 13/13 vs harness twice (postgres/poc@:15432), substrate sweep-rollback tests 4/4, workspace build/test green, clippy -D warnings, fmt clean
This commit is contained in:
1 parent
250511d480
commit
5c9ae6a7fc
6 files changed
+746
-15
No files matched your search
@@ -795,6 +795,14 @@ mod queue_tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn block_dead_deletes(conn: &Connection) {
|
||||
conn.execute_batch(
|
||||
"CREATE TRIGGER test_block_dead_delete BEFORE DELETE ON __alkstore_dead
|
||||
BEGIN SELECT RAISE(ABORT, 'dead delete blocked'); END",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn undecodable_pending_job(conn: &Connection) -> i64 {
|
||||
let id = put(conn, "emails", "{}", now_unix(conn).unwrap(), 0, 3, None);
|
||||
conn.execute(
|
||||
@@ -1532,6 +1540,69 @@ mod queue_tests {
|
||||
assert!(conn.is_autocommit());
|
||||
}
|
||||
|
||||
// The M-1 follow-up (the waves-1–2 general review's §4 suggested add,
|
||||
// task suite-queue-depth-rows): the retention half's rollback. The
|
||||
// retention DELETE runs inside the same savepoint as the move half —
|
||||
// a failure in it rolls the live→dead move back too (no effects
|
||||
// partially applied by an Err sweep). Not injectable through the
|
||||
// contract surface (storage-level failure, no fault seam) — pinned
|
||||
// here at the substrate seam with a trigger-raised ABORT.
|
||||
#[test]
|
||||
fn sweep_rolls_back_the_retention_half_with_the_move() {
|
||||
let conn = db();
|
||||
let now: i64 = now_unix(&conn).unwrap();
|
||||
let aged = enqueue(
|
||||
&conn,
|
||||
"q",
|
||||
"{}",
|
||||
now,
|
||||
0,
|
||||
Some(now - 1),
|
||||
Stamps {
|
||||
max_attempts: 5,
|
||||
visibility_timeout_s: 300,
|
||||
backoff_base_s: 5,
|
||||
dead_letter_retention_s: Some(60),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(sweep_expired(&conn, "q", EXPIRED).unwrap(), 1);
|
||||
let aged_out = now - 61;
|
||||
conn.execute_batch(&format!(
|
||||
"UPDATE __alkstore_dead SET died_at = {aged_out} WHERE id = {aged}"
|
||||
))
|
||||
.unwrap();
|
||||
let fresh = put(&conn, "q", "{}", now, 0, 5, Some(now - 1));
|
||||
|
||||
block_dead_deletes(&conn);
|
||||
let err = sweep_expired(&conn, "q", EXPIRED)
|
||||
.expect_err("the failing retention DELETE must reach the caller");
|
||||
assert!(
|
||||
err.to_string().contains("dead delete blocked"),
|
||||
"got: {err}"
|
||||
);
|
||||
assert_eq!(
|
||||
(live_count(&conn, fresh), dead_count(&conn, fresh)),
|
||||
(1, 0),
|
||||
"the move half rolled back with the retention half"
|
||||
);
|
||||
assert_eq!(
|
||||
dead_count(&conn, aged),
|
||||
1,
|
||||
"the retention DELETE rolled back with it"
|
||||
);
|
||||
assert!(conn.is_autocommit());
|
||||
|
||||
conn.execute_batch("DROP TRIGGER test_block_dead_delete")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
sweep_expired(&conn, "q", EXPIRED).unwrap(),
|
||||
2,
|
||||
"unblocked, both halves land: one moved + one retention-deleted"
|
||||
);
|
||||
assert_eq!((live_count(&conn, fresh), dead_count(&conn, aged)), (0, 0));
|
||||
}
|
||||
|
||||
// ---------------- no-stranded-rows property ----------------
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user