mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 15:40:18 +00:00
fix: N+1 query pattern in event trigger loop (routine_engine) (#1163)
* fix: N+1 query pattern in event trigger loop (routine_engine) * fix: linter
This commit is contained in:
@@ -139,6 +139,32 @@ impl RoutineEngine {
|
||||
let cache = self.event_cache.read().await;
|
||||
let mut fired = 0;
|
||||
|
||||
// Collect routine IDs for batch query
|
||||
let routine_ids: Vec<Uuid> = cache
|
||||
.iter()
|
||||
.filter_map(|matcher| match matcher {
|
||||
EventMatcher::Message { routine, .. } => Some(routine.id),
|
||||
EventMatcher::System { .. } => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
if routine_ids.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Single batch query instead of N queries
|
||||
let concurrent_counts = match self
|
||||
.store
|
||||
.count_running_routine_runs_batch(&routine_ids)
|
||||
.await
|
||||
{
|
||||
Ok(counts) => counts,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to batch-load concurrent counts: {}", e);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
for matcher in cache.iter() {
|
||||
let (routine, re) = match matcher {
|
||||
EventMatcher::Message { routine, regex } => (routine, regex),
|
||||
@@ -164,8 +190,9 @@ impl RoutineEngine {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Concurrent run check
|
||||
if !self.check_concurrent(routine).await {
|
||||
// Concurrent run check (using batch-loaded counts)
|
||||
let running_count = concurrent_counts.get(&routine.id).copied().unwrap_or(0);
|
||||
if running_count >= routine.guardrails.max_concurrent as i64 {
|
||||
tracing::trace!(routine = %routine.name, "Skipped: max concurrent reached");
|
||||
continue;
|
||||
}
|
||||
@@ -197,6 +224,35 @@ impl RoutineEngine {
|
||||
let cache = self.event_cache.read().await;
|
||||
let mut fired = 0;
|
||||
|
||||
// Collect routine IDs for batch query
|
||||
let routine_ids: Vec<Uuid> = cache
|
||||
.iter()
|
||||
.filter_map(|matcher| match matcher {
|
||||
EventMatcher::System { routine } => Some(routine.id),
|
||||
EventMatcher::Message { .. } => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
if routine_ids.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Single batch query instead of N queries
|
||||
let concurrent_counts = match self
|
||||
.store
|
||||
.count_running_routine_runs_batch(&routine_ids)
|
||||
.await
|
||||
{
|
||||
Ok(counts) => counts,
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to batch-load concurrent counts for system events: {}",
|
||||
e
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
for matcher in cache.iter() {
|
||||
let routine = match matcher {
|
||||
EventMatcher::System { routine } => routine,
|
||||
@@ -248,7 +304,9 @@ impl RoutineEngine {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !self.check_concurrent(routine).await {
|
||||
// Concurrent run check (using batch-loaded counts)
|
||||
let running_count = concurrent_counts.get(&routine.id).copied().unwrap_or(0);
|
||||
if running_count >= routine.guardrails.max_concurrent as i64 {
|
||||
tracing::debug!(routine = %routine.name, "Skipped: max concurrent reached");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
//! Routine-related RoutineStore implementation for LibSqlBackend.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use libsql::params;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{
|
||||
LibSqlBackend, ROUTINE_COLUMNS, ROUTINE_RUN_COLUMNS, fmt_opt_ts, fmt_ts, get_i64, opt_text,
|
||||
opt_text_owned, row_to_routine_libsql, row_to_routine_run_libsql,
|
||||
LibSqlBackend, ROUTINE_COLUMNS, ROUTINE_RUN_COLUMNS, fmt_opt_ts, fmt_ts, get_i64, get_text,
|
||||
opt_text, opt_text_owned, row_to_routine_libsql, row_to_routine_run_libsql,
|
||||
};
|
||||
use crate::agent::routine::{Routine, RoutineRun, RunStatus};
|
||||
use crate::db::RoutineStore;
|
||||
@@ -409,6 +411,57 @@ impl RoutineStore for LibSqlBackend {
|
||||
}
|
||||
}
|
||||
|
||||
async fn count_running_routine_runs_batch(
|
||||
&self,
|
||||
routine_ids: &[Uuid],
|
||||
) -> Result<HashMap<Uuid, i64>, DatabaseError> {
|
||||
if routine_ids.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
|
||||
let mut counts = HashMap::new();
|
||||
let conn = self.connect().await?;
|
||||
|
||||
// Query all running routines and filter in memory
|
||||
// This is simpler for libSQL than building dynamic parameter lists
|
||||
let mut rows = conn
|
||||
.query(
|
||||
"SELECT routine_id, COUNT(*) as cnt FROM routine_runs
|
||||
WHERE status = 'running'
|
||||
GROUP BY routine_id",
|
||||
params![],
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DatabaseError::Query(format!("Failed to batch count running routines: {}", e))
|
||||
})?;
|
||||
|
||||
let routine_id_set: HashSet<Uuid> = routine_ids.iter().copied().collect();
|
||||
|
||||
while let Some(row) = rows
|
||||
.next()
|
||||
.await
|
||||
.map_err(|e| DatabaseError::Query(e.to_string()))?
|
||||
{
|
||||
let id_str: String = get_text(&row, 0);
|
||||
let id = Uuid::parse_str(&id_str)
|
||||
.map_err(|e| DatabaseError::Query(format!("Invalid routine UUID: {}", e)))?;
|
||||
|
||||
// Only include if this routine ID was requested
|
||||
if routine_id_set.contains(&id) {
|
||||
let cnt: i64 = get_i64(&row, 1);
|
||||
counts.insert(id, cnt);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure all requested IDs are in the map (defaults to 0 for no running runs)
|
||||
for id in routine_ids {
|
||||
counts.entry(*id).or_insert(0);
|
||||
}
|
||||
|
||||
Ok(counts)
|
||||
}
|
||||
|
||||
async fn link_routine_run_to_job(
|
||||
&self,
|
||||
run_id: Uuid,
|
||||
|
||||
@@ -387,6 +387,10 @@ pub trait RoutineStore: Send + Sync {
|
||||
limit: i64,
|
||||
) -> Result<Vec<RoutineRun>, DatabaseError>;
|
||||
async fn count_running_routine_runs(&self, routine_id: Uuid) -> Result<i64, DatabaseError>;
|
||||
async fn count_running_routine_runs_batch(
|
||||
&self,
|
||||
routine_ids: &[Uuid],
|
||||
) -> Result<HashMap<Uuid, i64>, DatabaseError>;
|
||||
async fn link_routine_run_to_job(
|
||||
&self,
|
||||
run_id: Uuid,
|
||||
|
||||
@@ -487,6 +487,15 @@ impl RoutineStore for PgBackend {
|
||||
self.store.count_running_routine_runs(routine_id).await
|
||||
}
|
||||
|
||||
async fn count_running_routine_runs_batch(
|
||||
&self,
|
||||
routine_ids: &[Uuid],
|
||||
) -> Result<std::collections::HashMap<Uuid, i64>, DatabaseError> {
|
||||
self.store
|
||||
.count_running_routine_runs_batch(routine_ids)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn link_routine_run_to_job(
|
||||
&self,
|
||||
run_id: Uuid,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
//! PostgreSQL store for persisting agent data.
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
#[cfg(feature = "postgres")]
|
||||
use deadpool_postgres::{Config, Pool};
|
||||
@@ -1294,6 +1297,42 @@ impl Store {
|
||||
Ok(row.get("cnt"))
|
||||
}
|
||||
|
||||
/// Batch-load concurrent run counts for multiple routines in a single query.
|
||||
/// Returns a map where missing routine IDs default to 0.
|
||||
#[cfg(feature = "postgres")]
|
||||
pub async fn count_running_routine_runs_batch(
|
||||
&self,
|
||||
routine_ids: &[Uuid],
|
||||
) -> Result<HashMap<Uuid, i64>, DatabaseError> {
|
||||
if routine_ids.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
|
||||
let conn = self.conn().await?;
|
||||
let rows = conn
|
||||
.query(
|
||||
"SELECT routine_id, COUNT(*) as cnt FROM routine_runs
|
||||
WHERE routine_id = ANY($1) AND status = 'running'
|
||||
GROUP BY routine_id",
|
||||
&[&routine_ids],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut counts = HashMap::new();
|
||||
for row in rows {
|
||||
let id: Uuid = row.get("routine_id");
|
||||
let cnt: i64 = row.get("cnt");
|
||||
counts.insert(id, cnt);
|
||||
}
|
||||
|
||||
// Ensure all requested IDs are in the map (defaults to 0 for no running runs)
|
||||
for id in routine_ids {
|
||||
counts.entry(*id).or_insert(0);
|
||||
}
|
||||
|
||||
Ok(counts)
|
||||
}
|
||||
|
||||
/// Link a routine run to a dispatched job.
|
||||
pub async fn link_routine_run_to_job(
|
||||
&self,
|
||||
|
||||
Reference in New Issue
Block a user