Skip to content

Commit e17a3fa

Browse files
committed
perf(workflow): push workflow_ref (type) filter to the fast DB-page path
Filtering the /workflows list by type was slow: workflow_query_can_use_db_page excluded workflow_ref, so a filtered query fell back to list_all() — fetching every run with full blobs and filtering in memory (~6s). The journal backend's list/query_ids already support a workflow_ref filter, so thread it through list_page + query_ids and let workflow_ref stay on the bounded DB-page path. Filtered lists are now server-side bounded + accurate-total, same as unfiltered. Folded into rc.20.
1 parent f8ef728 commit e17a3fa

3 files changed

Lines changed: 34 additions & 11 deletions

File tree

crates/orchestrator-core/src/services/workflow_impl.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,11 @@ fn query_workflows(workflows: Vec<OrchestratorWorkflow>, query: &WorkflowQuery)
149149
}
150150

151151
fn workflow_query_can_use_db_page(query: &WorkflowQuery) -> bool {
152+
// `workflow_ref` (the type filter) is pushed down to the journal backend's
153+
// bounded list/query_ids, so it stays on the fast DB-page path; task_id /
154+
// phase_id / search still fall back to the in-memory scan.
152155
query.page.limit.is_some()
153156
&& matches!(query.sort, WorkflowQuerySort::StartedAt)
154-
&& query.filter.workflow_ref.is_none()
155157
&& query.filter.task_id.is_none()
156158
&& query.filter.phase_id.is_none()
157159
&& query.filter.search_text.as_deref().is_none_or(|value| value.trim().is_empty())
@@ -398,15 +400,16 @@ impl WorkflowServiceApi for FileServiceHub {
398400
// query for `total`, instead of query_ids + a per-id load loop (N+1 —
399401
// one RPC per run, which serialized behind other journal traffic and
400402
// made the list intermittently multi-second).
403+
let workflow_ref = query.filter.workflow_ref.as_deref();
401404
if query.page.offset == 0 {
402405
if let Some(limit) = query.page.limit {
403-
let (_ids, total) = manager.query_ids(query.page, query.filter.status)?;
404-
let items = manager.list_page(query.filter.status, limit)?;
406+
let (_ids, total) = manager.query_ids(query.page, query.filter.status, workflow_ref)?;
407+
let items = manager.list_page(query.filter.status, workflow_ref, limit)?;
405408
return Ok(ListPage::new(items, total, query.page));
406409
}
407410
}
408411
// Offset > 0: the journal list RPC has no offset, so keep the id-walk.
409-
let (ids, total) = manager.query_ids(query.page, query.filter.status)?;
412+
let (ids, total) = manager.query_ids(query.page, query.filter.status, workflow_ref)?;
410413
let mut items = Vec::with_capacity(ids.len());
411414
for id in ids {
412415
if let Ok(workflow) = manager.load(&id) {

crates/orchestrator-core/src/workflow/journal_client.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,11 +477,12 @@ pub(crate) fn list_page(
477477
plugin: &DiscoveredPlugin,
478478
project_root: &Path,
479479
status: Option<WorkflowStatus>,
480+
workflow_ref: Option<&str>,
480481
limit: usize,
481482
) -> Result<Vec<OrchestratorWorkflow>> {
482483
let query = JournalQuery {
483484
status: status.map(|s| vec![status_wire(s).to_string()]).unwrap_or_default(),
484-
workflow_ref: None,
485+
workflow_ref: workflow_ref.map(str::to_string),
485486
updated_since: None,
486487
limit: Some(limit as u32),
487488
};
@@ -593,10 +594,11 @@ pub(crate) fn query_ids(
593594
plugin: &DiscoveredPlugin,
594595
project_root: &Path,
595596
status: Option<WorkflowStatus>,
597+
workflow_ref: Option<&str>,
596598
) -> Result<Vec<String>> {
597599
let query = JournalQuery {
598600
status: status.map(|s| vec![status_wire(s).to_string()]).unwrap_or_default(),
599-
workflow_ref: None,
601+
workflow_ref: workflow_ref.map(str::to_string),
600602
updated_since: None,
601603
limit: Some(QUERY_IDS_LIMIT),
602604
};

crates/orchestrator-core/src/workflow/state_manager.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -368,13 +368,17 @@ impl WorkflowStateManager {
368368
pub fn list_page(
369369
&self,
370370
status: Option<crate::types::WorkflowStatus>,
371+
workflow_ref: Option<&str>,
371372
limit: usize,
372373
) -> Result<Vec<OrchestratorWorkflow>> {
373374
if let Some(plugin) = self.journal_plugin() {
374-
return super::journal_client::list_page(plugin, &self.project_root, status, limit);
375+
return super::journal_client::list_page(plugin, &self.project_root, status, workflow_ref, limit);
375376
}
376-
// SQLite dev fallback: fetch all and truncate (no plugin round-trips).
377+
// SQLite dev fallback: fetch all, filter by ref, truncate (no plugin round-trips).
377378
let mut all = self.list_all()?;
379+
if let Some(wref) = workflow_ref {
380+
all.retain(|w| w.workflow_ref.as_deref() == Some(wref));
381+
}
378382
all.truncate(limit);
379383
Ok(all)
380384
}
@@ -476,17 +480,31 @@ impl WorkflowStateManager {
476480
&self,
477481
page: ListPageRequest,
478482
status: Option<crate::types::WorkflowStatus>,
483+
workflow_ref: Option<&str>,
479484
) -> Result<(Vec<String>, usize)> {
480485
if let Some(plugin) = self.journal_plugin() {
481486
// The journal protocol's query_ids has no offset/total, so fetch the
482-
// full matching id set from the plugin and paginate client-side to
483-
// preserve the (page, total) contract.
484-
let all_ids = super::journal_client::query_ids(plugin, &self.project_root, status)?;
487+
// full matching id set from the plugin (status + workflow_ref filtered
488+
// server-side) and paginate client-side to preserve the (page, total)
489+
// contract.
490+
let all_ids = super::journal_client::query_ids(plugin, &self.project_root, status, workflow_ref)?;
485491
let total = all_ids.len();
486492
let (start, end) = page.bounds(total);
487493
let ids = all_ids.into_iter().skip(start).take(end.saturating_sub(start)).collect();
488494
return Ok((ids, total));
489495
}
496+
// SQLite dev fallback: a workflow_ref filter is applied in memory (small
497+
// local store) to avoid a combinatorial SQL branch; the plugin path above
498+
// is the production one.
499+
if let Some(wref) = workflow_ref {
500+
let mut runs = self.list_all()?;
501+
runs.retain(|w| status.is_none_or(|s| w.status == s) && w.workflow_ref.as_deref() == Some(wref));
502+
runs.sort_by(|a, b| b.started_at.cmp(&a.started_at).then_with(|| a.id.cmp(&b.id)));
503+
let total = runs.len();
504+
let (start, end) = page.bounds(total);
505+
let ids = runs[start..end].iter().map(|w| w.id.clone()).collect();
506+
return Ok((ids, total));
507+
}
490508
let conn = self.open_db()?;
491509

492510
let total: usize = match status {

0 commit comments

Comments
 (0)