diff --git a/crates/manager-core/src/queue_repo.rs b/crates/manager-core/src/queue_repo.rs index d45d701..588bceb 100644 --- a/crates/manager-core/src/queue_repo.rs +++ b/crates/manager-core/src/queue_repo.rs @@ -22,6 +22,13 @@ use uuid::Uuid; use crate::dead_letter_repo::{DeadLetterRepoError, NewDeadLetter}; +/// F-P-006: maximum rows scanned by `depth()` / `depth_pending()`. +/// Scripts calling `queue::depth(name)` per invocation paid an unbounded +/// COUNT(*); capping the scan at 10k lets the SDK report "10000" as a +/// floor for very-deep queues without ever touching more than that many +/// index rows. +const QUEUE_DEPTH_SCAN_CAP: i64 = 10_000; + #[derive(Debug, thiserror::Error)] pub enum QueueRepoError { #[error("database error: {0}")] @@ -262,11 +269,24 @@ impl QueueRepo for PostgresQueueRepo { } async fn depth(&self, app_id: AppId, queue_name: &str) -> Result { + // F-P-006: cap the scan at QUEUE_DEPTH_SCAN_CAP rows. Scripts + // calling queue::depth(name) per invocation were doing + // unbounded COUNT(*) over the queue_messages partial index; + // on a backed-up queue with millions of rows that meant a + // full-partition scan every call. The capped subquery means + // the worst-case cost is bounded; callers needing exact + // depth on huge queues should use the admin dashboard which + // tolerates the slower COUNT for its read frequency. let (n,): (i64,) = sqlx::query_as( - "SELECT COUNT(*) FROM queue_messages WHERE app_id = $1 AND queue_name = $2", + "SELECT COUNT(*) FROM ( \ + SELECT 1 FROM queue_messages \ + WHERE app_id = $1 AND queue_name = $2 \ + LIMIT $3 \ + ) sub", ) .bind(app_id.into_inner()) .bind(queue_name) + .bind(QUEUE_DEPTH_SCAN_CAP) .fetch_one(&self.pool) .await?; Ok(u64::try_from(n).unwrap_or(0)) @@ -274,13 +294,17 @@ impl QueueRepo for PostgresQueueRepo { async fn depth_pending(&self, app_id: AppId, queue_name: &str) -> Result { let (n,): (i64,) = sqlx::query_as( - "SELECT COUNT(*) FROM queue_messages \ - WHERE app_id = $1 AND queue_name = $2 \ - AND claim_token IS NULL \ - AND (deliver_after IS NULL OR deliver_after <= NOW())", + "SELECT COUNT(*) FROM ( \ + SELECT 1 FROM queue_messages \ + WHERE app_id = $1 AND queue_name = $2 \ + AND claim_token IS NULL \ + AND (deliver_after IS NULL OR deliver_after <= NOW()) \ + LIMIT $3 \ + ) sub", ) .bind(app_id.into_inner()) .bind(queue_name) + .bind(QUEUE_DEPTH_SCAN_CAP) .fetch_one(&self.pool) .await?; Ok(u64::try_from(n).unwrap_or(0))