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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Notable changes between releases. Detailed migration notes for storage transitio

- Queue-storage job lists without a kind filter select receipt candidate IDs before loading their ready bodies. Kind filters still apply before the limit.

### Fixed

- Admin job lists and kind counts work on a queue-storage schema installed with more claim slots than the default, instead of failing with `invalid physical claim slot`.

## [0.6.14] — 2026-11-08

Recommended for everyone on 0.6.13 using queue storage. A busy queue no longer stalls promotion of the others, prune backs off cleanly under lock contention, new queue lanes are usable by every runtime role, and jobs no longer stay running, run again or get misreported after priority aging.
Expand Down
11 changes: 4 additions & 7 deletions awa-model/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4076,7 +4076,7 @@ async fn queue_storage_base_kind_counts(
Ok(counts)
}

// The result is bounded by the configured claim-ring slot count, not by
// The result is bounded by the installed claim-ring slot count, not by
// retained job volume. Include all populated slots, including old generations.
async fn queue_storage_populated_claim_slots(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
Expand All @@ -4098,14 +4098,11 @@ async fn queue_storage_populated_claim_slots(
sqlx::query("RELEASE SAVEPOINT awa_claim_slots")
.execute(tx.as_mut())
.await?;
// A schema-only admin store carries the default slot count, so take the
// installed slot numbers as they are rather than bounding them by it.
slots
.into_iter()
.map(|slot| {
usize::try_from(slot)
.ok()
.filter(|slot| *slot < store.claim_slot_count())
.ok_or_else(|| AwaError::Validation(format!("invalid physical claim slot {slot}")))
})
.map(|slot| crate::queue_storage::ring_slot_number(slot, "claim"))
.collect()
}

Expand Down
2 changes: 1 addition & 1 deletion awa-model/src/queue_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ fn claim_closure_batch_child_name(schema: &str, slot: usize) -> String {
format!("{schema}.lease_claim_closure_batches_{slot}")
}

fn ring_slot_number(slot: i32, ring: &str) -> Result<usize, AwaError> {
pub(crate) fn ring_slot_number(slot: i32, ring: &str) -> Result<usize, AwaError> {
usize::try_from(slot).map_err(|_| {
AwaError::Validation(format!(
"invalid {ring} ring slot {slot}: slot must be non-negative"
Expand Down
61 changes: 61 additions & 0 deletions awa/tests/queue_storage_runtime_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17552,6 +17552,67 @@ async fn test_receipt_admin_scope_materialized_control() {
receipt_admin_reads_preserve_scope_and_disposition(false, false).await;
}

#[tokio::test]
async fn test_schema_only_admin_reads_claims_in_installed_slot() {
let (_db_guard, pool) = setup_pool(6).await;
let schema = "awa_qs_admin_wide_claims";
let queue = "admin_wide_claims";
let default_claim_slots = QueueStorageConfig::default().claim_slot_count;
let store = create_store_with_config(
&pool,
QueueStorageConfig {
claim_slot_count: default_claim_slots + 2,
..unique_cancel_store_config(schema)
},
)
.await;
for _ in 0..default_claim_slots {
assert!(matches!(
store.rotate_claims(&pool).await.unwrap(),
RotateOutcome::Rotated { .. }
));
}
let job_id = enqueue_job(
&pool,
&store,
&CompleteJob { id: 1 },
InsertOpts {
queue: queue.into(),
..Default::default()
},
)
.await;
let claims = store
.claim_runtime_batch(&pool, queue, 1, Duration::ZERO)
.await
.unwrap();
assert_eq!(claims.len(), 1);
assert_eq!(claims[0].claim.claim_slot, default_claim_slots as i32);

let running = admin::list_jobs(
&pool,
&admin::ListJobsFilter {
state: Some(JobState::Running),
queue: Some(queue.into()),
..Default::default()
},
)
.await
.expect("list running jobs through a schema-only store");
assert_eq!(
running.iter().map(|job| job.id).collect::<Vec<_>>(),
vec![job_id]
);
let kinds = admin::job_kind_overviews(&pool)
.await
.expect("count kinds through a schema-only store");
let complete = kinds
.iter()
.find(|kind| kind.kind == CompleteJob::kind())
.unwrap();
assert_eq!((complete.job_count, complete.queue_count), (1, 1));
}

async fn claim_aged_rescue_body_job(
schema: &str,
receipts: bool,
Expand Down
Loading