Skip to content

Commit a913fbc

Browse files
authored
perf(db): count rows in the database instead of fetching them to count (#3387)
1 parent 8024a15 commit a913fbc

10 files changed

Lines changed: 504 additions & 51 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/api-core/src/handlers/compute_allocation.rs

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -451,17 +451,9 @@ pub(crate) async fn update(
451451
instance_type_id: Some(instance_type_id.to_string()),
452452
};
453453

454-
let instance_count = instance::find_ids(&mut txn, filter).await?.len();
454+
let instance_count = instance::count_ids(&mut txn, filter).await?;
455455

456-
if instance_count
457-
> new_tenant_allocation_total
458-
.try_into()
459-
.map_err(|e: TryFromIntError| CarbideError::Internal {
460-
message: format!(
461-
"unable to compare current instance and allocation counts - {e}"
462-
),
463-
})?
464-
{
456+
if instance_count > i64::from(new_tenant_allocation_total) {
465457
return Err(CarbideError::FailedPrecondition(format!(
466458
"requested update would decrease allocation count ({new_tenant_allocation_total}) below existing instances count ({instance_count})"
467459
))
@@ -594,17 +586,9 @@ pub(crate) async fn delete(
594586
instance_type_id: Some(allocation.instance_type_id.to_string()),
595587
};
596588

597-
let instance_count = instance::find_ids(&mut txn, filter).await?.len();
589+
let instance_count = instance::count_ids(&mut txn, filter).await?;
598590

599-
if instance_count
600-
> new_tenant_allocation_total
601-
.try_into()
602-
.map_err(|e: TryFromIntError| CarbideError::Internal {
603-
message: format!(
604-
"unable to compare current instance and allocation counts - {e}"
605-
),
606-
})?
607-
{
591+
if instance_count > i64::from(new_tenant_allocation_total) {
608592
return Err(CarbideError::FailedPrecondition(format!(
609593
"requested delete would decrease allocation count ({new_tenant_allocation_total}) below existing instances count ({instance_count})"
610594
))

crates/api-core/src/instance/mod.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -638,10 +638,13 @@ pub async fn batch_allocate_instances(
638638
instance_type_id: Some(instance_type_id.to_string()),
639639
};
640640

641-
let new_total_instance_count =
642-
req_count + db::instance::find_ids(&mut txn, filter).await?.len();
641+
// Saturate rather than wrap: an absurd request count then trips the
642+
// limit check (fail closed) instead of going negative past it.
643+
let new_total_instance_count = i64::try_from(req_count)
644+
.unwrap_or(i64::MAX)
645+
.saturating_add(db::instance::count_ids(&mut txn, filter).await?);
643646

644-
if new_total_instance_count > compute_allocation_total as usize {
647+
if new_total_instance_count > i64::from(compute_allocation_total) {
645648
// # enforce_if_present: Instance type not required in creation request. If sent and allocations are found for instance type ID, enforce it; otherwise, it's like no limits.
646649
// # always: Instance type not required in creation request. If sent, enforce allocations. If none are found, its a constraint value of 0 (i.e., you get nothing / default-deny).
647650
// # warn_only (default): Instance type not required in creation request. If sent in and allocations are found, don't enforce, but log what would have happened if they were enforced.

crates/api-db/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ carbide-ipxe-renderer = { path = "../ipxe-renderer" }
4444
#these are alphabetized
4545
async-trait = { workspace = true }
4646
chrono = { workspace = true }
47+
const_format = { workspace = true }
4748
domain = { workspace = true }
4849
eyre = { workspace = true }
4950
futures = { workspace = true }

crates/api-db/src/explored_endpoints.rs

Lines changed: 171 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use std::net::IpAddr;
1818

1919
use chrono::{DateTime, Utc};
2020
use config_version::ConfigVersion;
21+
use const_format::concatcp;
2122
use mac_address::MacAddress;
2223
use model::firmware::FirmwareComponentType;
2324
use model::machine_boot_interface::MachineBootInterface;
@@ -189,33 +190,107 @@ pub async fn find_all(txn: impl DbReader<'_>) -> Result<Vec<ExploredEndpoint>, D
189190
.map_err(|e| DatabaseError::new("explored_endpoints find_all", e))
190191
}
191192

193+
/// The WHERE clause matching endpoints still in preingestion that are neither
194+
/// waiting for a site-explorer refresh nor in an error state. If
195+
/// LastExplorationError is completely nonexistent it is NULL; if it is there
196+
/// and indicates a null value it is 'null'.
197+
///
198+
/// [`find_preingest_not_waiting_not_error`] and
199+
/// [`count_preingest_not_waiting_not_error`] both build their queries from
200+
/// this, so the row-returning and counting variants cannot drift apart.
201+
const PREINGEST_NOT_WAITING_NOT_ERROR_WHERE: &str = "(preingestion_state IS NULL OR preingestion_state->'state' != '\"complete\"')
202+
AND waiting_for_explorer_refresh = false
203+
AND (exploration_report->'LastExplorationError' IS NULL OR exploration_report->'LastExplorationError' = 'null')";
204+
192205
/// find_preingest_not_waiting gets everything that is still in preingestion that isn't waiting for site explorer to refresh it again and isn't in an error state.
193206
pub async fn find_preingest_not_waiting_not_error(
194207
txn: impl DbReader<'_>,
195208
) -> Result<Vec<ExploredEndpoint>, DatabaseError> {
196-
let query = "SELECT * FROM explored_endpoints
197-
WHERE (preingestion_state IS NULL OR preingestion_state->'state' != '\"complete\"')
198-
AND waiting_for_explorer_refresh = false
199-
AND (exploration_report->'LastExplorationError' IS NULL OR exploration_report->'LastExplorationError' = 'null')"; // If LastExplorationError is completely notexistant it is NULL, if it is there and indicates a null value it is 'null'.
209+
const QUERY: &str = concatcp!(
210+
"SELECT * FROM explored_endpoints
211+
WHERE ",
212+
PREINGEST_NOT_WAITING_NOT_ERROR_WHERE
213+
);
200214

201-
sqlx::query_as::<_, DbExploredEndpoint>(query)
215+
sqlx::query_as::<_, DbExploredEndpoint>(QUERY)
202216
.fetch_all(txn)
203217
.await
204218
.map(|endpoints| endpoints.into_iter().map(Into::into).collect())
205219
.map_err(|e| DatabaseError::new("explored_endpoints find_preingest_not_waiting", e))
206220
}
207221

208-
/// find_preingest_installing returns the endpoints where wew are waiting for firmware installs
222+
/// Counts the endpoints still in preingestion that are neither waiting for a
223+
/// site-explorer refresh nor in an error state.
224+
///
225+
/// Callers that only need the number of such endpoints (e.g. a metric gauge)
226+
/// use this instead of `find_preingest_not_waiting_not_error(..).len()`: it runs
227+
/// the same predicate but selects a scalar `count(*)`, so the database neither
228+
/// returns nor decodes the per-row `exploration_report` jsonb blob. Unlike that
229+
/// row-returning twin, the count also includes rows whose `exploration_report`
230+
/// or `preingestion_state` would fail to deserialize (COUNT never decodes them)
231+
/// — intentional for a metrics counter.
232+
pub async fn count_preingest_not_waiting_not_error(
233+
txn: impl DbReader<'_>,
234+
) -> Result<i64, DatabaseError> {
235+
const QUERY: &str = concatcp!(
236+
"SELECT count(*) FROM explored_endpoints
237+
WHERE ",
238+
PREINGEST_NOT_WAITING_NOT_ERROR_WHERE
239+
);
240+
241+
sqlx::query_scalar(QUERY).fetch_one(txn).await.map_err(|e| {
242+
DatabaseError::new(
243+
"explored_endpoints count_preingest_not_waiting_not_error",
244+
e,
245+
)
246+
})
247+
}
248+
249+
/// The WHERE clause matching endpoints waiting on a firmware install.
250+
///
251+
/// [`find_preingest_installing`] and [`count_preingest_installing`] both build
252+
/// their queries from this, so the row-returning and counting variants cannot
253+
/// drift apart.
254+
const PREINGEST_INSTALLING_WHERE: &str = "preingestion_state->'state' = '\"upgradefirmwarewait\"'";
255+
256+
/// find_preingest_installing returns the endpoints where we are waiting for firmware installs.
257+
///
258+
/// The metrics caller now uses [`count_preingest_installing`]; this
259+
/// row-returning form remains for callers that need the endpoints themselves
260+
/// and anchors the count's parity test.
209261
pub async fn find_preingest_installing(
210262
txn: impl DbReader<'_>,
211263
) -> Result<Vec<ExploredEndpoint>, DatabaseError> {
212-
let query = "SELECT * FROM explored_endpoints WHERE preingestion_state->'state' = '\"upgradefirmwarewait\"'";
264+
const QUERY: &str = concatcp!(
265+
"SELECT * FROM explored_endpoints WHERE ",
266+
PREINGEST_INSTALLING_WHERE
267+
);
213268

214-
sqlx::query_as::<_, DbExploredEndpoint>(query)
269+
sqlx::query_as::<_, DbExploredEndpoint>(QUERY)
215270
.fetch_all(txn)
216271
.await
217272
.map(|endpoints| endpoints.into_iter().map(Into::into).collect())
218-
.map_err(|e| DatabaseError::new("explored_endpoints find_preingest_not_waiting", e))
273+
.map_err(|e| DatabaseError::new("explored_endpoints find_preingest_installing", e))
274+
}
275+
276+
/// Counts the endpoints waiting for a firmware install to finish.
277+
///
278+
/// The counting counterpart to [`find_preingest_installing`]: callers that only
279+
/// need the number (e.g. a metric gauge) use this so the database returns a
280+
/// single scalar rather than every matching row's `exploration_report` jsonb.
281+
/// Unlike that row-returning twin, the count also includes rows whose
282+
/// `exploration_report` or `preingestion_state` would fail to deserialize
283+
/// (COUNT never decodes them) — intentional for a metrics counter.
284+
pub async fn count_preingest_installing(txn: impl DbReader<'_>) -> Result<i64, DatabaseError> {
285+
const QUERY: &str = concatcp!(
286+
"SELECT count(*) FROM explored_endpoints WHERE ",
287+
PREINGEST_INSTALLING_WHERE
288+
);
289+
290+
sqlx::query_scalar(QUERY)
291+
.fetch_one(txn)
292+
.await
293+
.map_err(|e| DatabaseError::new("explored_endpoints count_preingest_installing", e))
219294
}
220295

221296
/// find_all_no_upgrades returns all explored endpoints that site explorer has been able to probe, but ignores anything currently undergoing an upgrade
@@ -827,3 +902,90 @@ pub async fn set_pause_ingestion_and_poweron(
827902
.map_err(|e| DatabaseError::query(query, e))?;
828903
Ok(())
829904
}
905+
906+
#[cfg(test)]
907+
mod count_preingest_tests {
908+
use super::*;
909+
910+
/// An `UpgradeFirmwareWait` state — the one the "installing" predicate keys
911+
/// on. Built from the real enum so the row-returning path can deserialize it.
912+
fn installing_state() -> PreingestionState {
913+
PreingestionState::UpgradeFirmwareWait {
914+
task_id: "task-1".to_string(),
915+
final_version: "1.2.3".to_string(),
916+
upgrade_type: FirmwareComponentType::default(),
917+
power_drains_needed: None,
918+
firmware_number: None,
919+
}
920+
}
921+
922+
/// Inserts an explored endpoint with a fat, *decodable* `exploration_report`
923+
/// blob and the given preingestion `state`. Both are built from the real
924+
/// model types so the row-returning path can deserialize them — which is
925+
/// exactly the per-row cost the count path avoids.
926+
async fn seed_endpoint(txn: &mut PgConnection, addr: &str, state: PreingestionState) {
927+
// A real report with a deliberately large field, so the row-returning
928+
// path has genuine multi-KB jsonb to decode; the count path never
929+
// touches it.
930+
let report = EndpointExplorationReport {
931+
model: Some("x".repeat(4096)),
932+
..Default::default()
933+
};
934+
sqlx::query(
935+
"INSERT INTO explored_endpoints (address, exploration_report, version, preingestion_state, waiting_for_explorer_refresh) \
936+
VALUES ($1::inet, $2, 'V1-T1733777281821769', $3, false)",
937+
)
938+
.bind(addr)
939+
.bind(sqlx::types::Json(report))
940+
.bind(sqlx::types::Json(state))
941+
.execute(&mut *txn)
942+
.await
943+
.expect("seed explored_endpoint");
944+
}
945+
946+
/// `count_preingest_not_waiting_not_error` returns the same tally as
947+
/// `find_preingest_not_waiting_not_error(..).len()`. The win: the count
948+
/// query returns one scalar, whereas the find query returns every matching
949+
/// row and decodes each row's multi-KB `exploration_report` jsonb — so the
950+
/// win is rows + per-row jsonb decode, N -> 0.
951+
#[crate::sqlx_test]
952+
async fn count_matches_find_not_waiting_not_error(pool: sqlx::PgPool) {
953+
let mut txn = pool.begin().await.unwrap();
954+
// Three endpoints still in preingestion (not complete), not waiting, no error.
955+
seed_endpoint(&mut txn, "10.0.0.1", PreingestionState::Initial).await;
956+
seed_endpoint(&mut txn, "10.0.0.2", PreingestionState::RecheckVersions).await;
957+
seed_endpoint(&mut txn, "10.0.0.3", installing_state()).await;
958+
// One that is complete -> excluded by the predicate.
959+
seed_endpoint(&mut txn, "10.0.0.4", PreingestionState::Complete).await;
960+
961+
let rows = find_preingest_not_waiting_not_error(&mut *txn)
962+
.await
963+
.unwrap();
964+
let count = count_preingest_not_waiting_not_error(&mut *txn)
965+
.await
966+
.unwrap();
967+
968+
assert_eq!(rows.len(), 3, "three endpoints match the predicate");
969+
assert_eq!(count, 3, "count agrees with the row count");
970+
assert_eq!(count, rows.len() as i64);
971+
}
972+
973+
/// `count_preingest_installing` returns the same tally as
974+
/// `find_preingest_installing(..).len()` without returning/decoding the
975+
/// per-row jsonb reports.
976+
#[crate::sqlx_test]
977+
async fn count_matches_find_installing(pool: sqlx::PgPool) {
978+
let mut txn = pool.begin().await.unwrap();
979+
seed_endpoint(&mut txn, "10.0.1.1", installing_state()).await;
980+
seed_endpoint(&mut txn, "10.0.1.2", installing_state()).await;
981+
// Not installing -> excluded.
982+
seed_endpoint(&mut txn, "10.0.1.3", PreingestionState::Initial).await;
983+
984+
let rows = find_preingest_installing(&mut *txn).await.unwrap();
985+
let count = count_preingest_installing(&mut *txn).await.unwrap();
986+
987+
assert_eq!(rows.len(), 2, "two endpoints are installing firmware");
988+
assert_eq!(count, 2, "count agrees with the row count");
989+
assert_eq!(count, rows.len() as i64);
990+
}
991+
}

0 commit comments

Comments
 (0)