Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Notable changes between releases. Detailed migration notes for storage transitio

### Fixed

- **`awa.jobs` is receipt-aware and no longer pathologically slow under backlog ([#422](https://github.com/hardbyte/awa/issues/422), migration v044).** The SQL-compat view had two defects found during the 0.6.1→0.7 upgrade rehearsal. *Performance:* the available-branch filter spelled `lane_seq >= sequence_next_value(claims.seq_name)` inline against the partitioned `ready_entries` scan; `sequence_next_value` is a VOLATILE PL/pgSQL function, so the planner could neither use the predicate as an index bound nor cache it across rows — every ready child was scanned in full and each surviving row paid a dynamic-SQL catalog round-trip (measured ~90ms → ~6ms for the branch on a 20k-row backlog; the gap grows with un-pruned sealed generations). v044 materializes the per-lane cursors once per statement — the same shape the claim path has used since v027/v039 — and reading each cursor once is also a more consistent snapshot than per-row evaluation. *Correctness:* receipt claims that never materialized into `leases` (row-local `lease_claims`, and by default compact `lease_claim_batches`) were invisible, so `SELECT state, count(*) FROM awa.jobs` reported `running=0` while workers held live work. The view now carries the open-receipt legs the admin surface shipped in #410 (`admin::state_counts` / `queue_storage::open_receipt_running_claims_sql`), projecting the claim ledger's own attempt / claim-time / deadline values and anti-joined against closures, closure batches, materialised leases, and terminal/deferred/DLQ supersession — a job reports at most one state outside the brief post-commit cursor-lag window the claim protocol itself has (identical to the admin surfaces; it self-heals on the lane's next claim). Rows previously returned are unchanged; the running rows are additive. **No 0.6.x backport is required:** released 0.6.x workers have no schema-version gate at startup, so both upgrade orderings keep working — only a ≤0.6.x *migrator* refuses via the #392 fail-safe, which is correct (past-v043 migrations are applied by the 0.7 binary). Verified by the compat matrix against pinned release artifacts (`scripts/compat-matrix.sh` now runs a `forward-0.6.6` leg alongside 0.6.2/0.6.0/0.5.7) plus a released-binary enqueue→claim→complete rehearsal on a v044 database.

- **`awa migrate --extract-to` wrote only the first 15 migrations.** The target path was built by substituting the migration description into the filename, so v017 — whose description contains `/` — resolved to a nested directory that does not exist. The command aborted there, leaving a partial extraction that looked plausible but silently omitted two thirds of the schema, breaking the documented external-runner workflow for every range reaching v017. Only path separators are now substituted, so every filename this tool has already published stays byte-identical — re-extracting into an existing directory cannot leave two files for the same version. `V17` and `V21` are the only names that change, and neither could previously be written at all. All paths are computed and checked for collisions before anything is written, and a write failure names the file. A test asserts every migration is extracted with byte-identical SQL.

- **`python -m awa migrate --sql` matches the Rust CLI's transaction wrapper.** The two CLIs are documented as interchangeable, so the Python one emitted unwrapped SQL for the same `| psql` workflow. It now renders the identical wrapper and accepts the same `--no-transaction` escape hatch, taking the lock key from the new `awa.migration_lock_key()` binding rather than a duplicated literal.
Expand Down
577 changes: 577 additions & 0 deletions awa-model/migrations/v044_jobs_compat_receipt_running.sql

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion awa-model/src/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use sqlx::{Connection, PgPool};
use tracing::{info, warn};

/// Current schema version.
pub const CURRENT_VERSION: i32 = 43;
pub const CURRENT_VERSION: i32 = 44;

/// Migrations that require an exclusive (no-live-runtime) upgrade window.
///
Expand Down Expand Up @@ -256,6 +256,11 @@ const MIGRATIONS: &[(i32, &str, &[&str])] = &[
"Append-only ring-rotation ledgers and terminal-rollup deltas (#371)",
&[V18_UP, V23_UP, V43_UP],
),
(
44,
"Refresh jobs_compat(): materialized claim cursors and receipt-plane running rows (#422)",
&[V44_UP],
),
];

const V1_UP: &str = include_str!("../migrations/v001_canonical_schema.sql");
Expand Down Expand Up @@ -300,6 +305,7 @@ const V40_UP: &str = include_str!("../migrations/v040_finalize_with_drain_runtim
const V41_UP: &str = include_str!("../migrations/v041_queue_runtime_overrides.sql");
const V42_UP: &str = include_str!("../migrations/v042_compact_deadline_claims.sql");
const V43_UP: &str = include_str!("../migrations/v043_ring_rotation_ledger.sql");
const V44_UP: &str = include_str!("../migrations/v044_jobs_compat_receipt_running.sql");

/// Old version numbers from pre-0.4 releases that used V3/V4/V5 numbering.
/// Also tolerates the unreleased inline-V6 branch numbering used during review.
Expand Down
100 changes: 100 additions & 0 deletions awa/tests/queue_storage_runtime_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14908,3 +14908,103 @@ async fn test_queue_storage_lowering_enqueue_shards_drains_existing_rows() {

client.shutdown(Duration::from_secs(5)).await;
}

/// The migration-owned `awa.jobs` compat view must surface receipt-plane
/// running claims (`#422`): jobs held by open `lease_claim_batches` (and
/// legacy row-local `lease_claims`) previously vanished from the view
/// entirely — running=0 while workers held live work. The view also computes
/// per-lane claim cursors once per statement (v044) instead of evaluating the
/// volatile `sequence_next_value` per ready row, which is what made the view
/// pathologically slow under an un-pruned backlog.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_jobs_compat_view_reports_receipt_running_claims() {
let (_db_guard, pool) = setup_pool(6).await;
let queue = "qs_compat_receipt_running";
let schema = "awa_qs_compat_running";
let store = create_store_with_config(
&pool,
QueueStorageConfig {
schema: schema.to_string(),
queue_slot_count: 4,
lease_slot_count: 2,
claim_slot_count: 2,
lease_claim_receipts: true,
..Default::default()
},
)
.await;

for id in 0..3 {
enqueue_job(
&pool,
&store,
&CompleteJob { id },
InsertOpts {
queue: queue.to_string(),
..Default::default()
},
)
.await;
}

async fn read_state_counts(pool: &sqlx::PgPool, queue: &str) -> Vec<(String, i64)> {
sqlx::query_as::<_, (String, i64)>(
"SELECT state::text, count(*)::bigint FROM awa.jobs WHERE queue = $1 GROUP BY state",
)
.bind(queue)
.fetch_all(pool)
.await
.expect("read compat-view states")
}

let baseline = read_state_counts(&pool, queue).await;
assert_eq!(
baseline,
vec![("available".to_string(), 3)],
"before claiming, all enqueued jobs must be available and nothing else may appear"
);

// Claim through the runtime batch API so the claim cursor advances
// post-commit exactly as a real worker would; receipts mode records the
// claims as open compact batches rather than materialised leases.
let claimed = store
.claim_runtime_batch(&pool, queue, 2, Duration::from_secs(30))
.await
.expect("claim two jobs into the receipt plane");
assert_eq!(claimed.len(), 2, "two jobs must be claimed");

let after = read_state_counts(&pool, queue).await;
let mut states: Vec<(String, i64)> = after;
states.sort();
assert_eq!(
states,
vec![
("available".to_string(), 1),
("running".to_string(), 2),
],
"open receipt claims must be visible as running (#422), with the unclaimed remainder available"
);

// Running rows must read from the claim ledger, not the pre-claim ready
// row: attempt is the incremented claim attempt, attempted_at is the
// claim time, and the 30s claim deadline surfaces as deadline_at.
type RunningProjection = (i16, Option<DateTime<Utc>>, Option<DateTime<Utc>>);
let running_rows: Vec<RunningProjection> = sqlx::query_as(
"SELECT attempt, attempted_at, deadline_at FROM awa.jobs \
WHERE queue = $1 AND state = 'running' ORDER BY id",
)
.bind(queue)
.fetch_all(&pool)
.await
.expect("read running-row projections");
assert_eq!(running_rows.len(), 2);
for (attempt, attempted_at, deadline_at) in &running_rows {
assert_eq!(*attempt, 1, "running rows must project the claimed attempt");
let claimed_at = attempted_at.expect("running rows must expose the claim time");
let deadline = deadline_at.expect("the 30s claim deadline must surface on running rows");
assert!(
deadline > claimed_at,
"deadline_at must be the claim time plus the claim deadline"
);
}
}
2 changes: 1 addition & 1 deletion correctness/storage/MAPPING.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ The TLA+ lifecycle model does not represent the completed-history rollup cache,

`AwaSegmentedStorage.tla` models storage state, not every SQL projection over that state. Public and admin reads are therefore refinement obligations on the SQL implementation:

- `awa.jobs` / `awa.jobs_compat()` is the compatibility view used by SQL adapters and operational queries. Migration `awa-model/migrations/v028_ready_tombstones.sql` keeps queue-storage available rows shard-aware, uses the sequence-backed claim cursor, and skips `ready_tombstones`; lease-backed rows join ready bodies by `(queue, priority, enqueue_shard, lane_seq)`.
- `awa.jobs` / `awa.jobs_compat()` is the compatibility view used by SQL adapters and operational queries. Migration `awa-model/migrations/v028_ready_tombstones.sql` keeps queue-storage available rows shard-aware, uses the sequence-backed claim cursor, and skips `ready_tombstones`; lease-backed rows join ready bodies by `(queue, priority, enqueue_shard, lane_seq)`. Migration `v044_jobs_compat_receipt_running.sql` materializes the per-lane claim cursors once per statement (the inline VOLATILE `sequence_next_value` filter defeated index bounds and re-evaluated per row) and adds the open-receipt running legs (row-local `lease_claims` + compact `lease_claim_batches`, anti-joined against closures/closure-batches/leases/terminal/deferred/DLQ), matching the admin read side (#410, #422).
- `awa-model/src/admin.rs::queue_storage_current_jobs_cte`, `awa-model/src/admin.rs::state_counts`, and `awa-worker/src/client.rs::health_check` are the Rust read-side equivalents. They must preserve the same enqueue-shard predicates or multi-shard queues can overcount available rows or hydrate a row from the wrong shard.
- `test_queue_storage_multi_shard_public_available_counts_are_exact` is the code-level regression test for this projection boundary. A future TLA+ projection model could make this formal by deriving `JobsCompatAvailable` from the storage variables and asserting it equals `CurrentReady`; the current storage model stops at the underlying lifecycle and prune state.

Expand Down
2 changes: 1 addition & 1 deletion docs/0.7-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ this roadmap and not yet filed.
| 036 | Public surface stability policy | Accepted | #369 / D6 |
| 037 | Canonical engine deprecation & removal | Accepted | #370 / D2 |
| 042 | Caller-owned finalization transactions | Accepted | #401 / #342 |
| 03x | Segment/cursor storage evolution | Draft from E1/E3; status decided at Gate A | #295 |
| 044 | Segment/cursor storage evolution | Gate A decided 2026-08-22: RFC graduates to 0.8 with evidence; allocator ideas shipped in place via v027–v043 | #295 |

---

Expand Down
Loading
Loading