Simplify orphan reconciliation to a single SQL statement
Runs::scan_orphans materialized a Vec<Run> just to log entries,
Runs::reconcile_orphans then ran two separate UPDATE statements,
and the caller looped every repo to call into a per-repo method —
all to do something that's a single global UPDATE on state IN
('pending', 'active').

Replace both methods with a free function reconcile_orphans(db_path)
that runs one UPDATE across all repos, logs an aggregate count, and
returns. Tests for the now-removed scan_orphans drop; the reconcile
tests inspect state via Run::open.

Assisted-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
change znrxuwqnokuymsullyvrklpnpwuxkqlv
commit 5b1851df78983472b284fe054bd7ac83ba88e7a1
author Alpha Chen <alpha@kejadlen.dev>
date
parent qtsutwpt
diff --git a/src/bin/quire/server.rs b/src/bin/quire/server.rs
index cb56ef6..55a1a30 100644
--- a/src/bin/quire/server.rs
+++ b/src/bin/quire/server.rs
@@ -46,10 +46,8 @@ pub async fn run(quire: &Quire) -> Result<()> {
     quire::db::migrate(&mut db).into_diagnostic()?;
     drop(db);
 
-    // Scan for orphaned runs from a previous server instance.
-    for repo in quire.repos().context("failed to list repos")? {
-        repo.runs(&db_path).reconcile_orphans()?;
-    }
+    // Reconcile any orphaned runs from a previous server instance.
+    quire::ci::reconcile_orphans(&db_path)?;
 
     let quire_handle = quire.clone();
     let event_handle = tokio::spawn(event_listener(listener, quire_handle));
diff --git a/src/ci/mod.rs b/src/ci/mod.rs
index 5ac86f5..a40e717 100644
--- a/src/ci/mod.rs
+++ b/src/ci/mod.rs
@@ -13,7 +13,7 @@ pub(crate) mod error;
 
 pub use error::{Error, Result};
 pub use pipeline::{DefinitionError, Diagnostic, Job, Pipeline, PipelineError, StructureError};
-pub use run::{Executor, Run, RunMeta, RunState, Runs, materialize_workspace};
+pub use run::{Executor, Run, RunMeta, RunState, Runs, materialize_workspace, reconcile_orphans};
 
 /// A resolved commit reference.
 ///
@@ -368,10 +368,16 @@ mod tests {
         )
         .expect("trigger_ref should succeed");
 
-        // Verify a run was created in complete/.
-        let runs = repo.runs(&quire.db_path());
-        let orphans = runs.scan_orphans().expect("scan");
-        assert!(orphans.is_empty(), "run should be complete, not orphaned");
+        // Verify the run completed (no pending or active rows left behind).
+        let conn = crate::db::open(&quire.db_path()).expect("db");
+        let count: i64 = conn
+            .query_row(
+                "SELECT COUNT(*) FROM runs WHERE state IN ('pending', 'active')",
+                [],
+                |row| row.get(0),
+            )
+            .expect("count");
+        assert_eq!(count, 0, "run should be complete, not orphaned");
     }
 
     #[test]
diff --git a/src/ci/run.rs b/src/ci/run.rs
index 5c98d9f..a61a2ac 100644
--- a/src/ci/run.rs
+++ b/src/ci/run.rs
@@ -176,66 +176,25 @@ impl Runs {
             base_dir: self.base_dir.clone(),
         })
     }
+}
 
-    /// Find runs stuck in `pending` or `active` states.
-    pub fn scan_orphans(&self) -> Result<Vec<Run>> {
-        let db = crate::db::open(&self.db_path)?;
-        let mut stmt = db.prepare(
-            "SELECT id, state FROM runs WHERE state IN ('pending', 'active') AND repo = ?1",
-        )?;
-        let rows = stmt.query_map(rusqlite::params![&self.repo], |row| {
-            let id: String = row.get(0)?;
-            let state_str: String = row.get(1)?;
-            Ok((id, state_str))
-        })?;
-
-        let mut orphans = Vec::new();
-        for row in rows {
-            let (id, state_str) = row?;
-            let state: RunState = state_str.parse().expect("DB enforces valid states");
-            orphans.push(Run {
-                db_path: self.db_path.clone(),
-                id,
-                state,
-                base_dir: self.base_dir.clone(),
-            });
-        }
-        Ok(orphans)
-    }
-
-    /// Reconcile orphaned runs from a previous server instance.
-    ///
-    /// - `pending` orphans are moved to `failed` (created but never started).
-    /// - `active` orphans are moved to `failed` (no live runner).
-    pub fn reconcile_orphans(&self) -> Result<()> {
-        let orphans = self.scan_orphans()?;
-        for orphan in &orphans {
-            tracing::warn!(
-                run_id = %orphan.id(),
-                state = ?orphan.state(),
-                "found orphaned run"
-            );
-        }
-
-        let now = Timestamp::now().as_millisecond();
-        let db = crate::db::open(&self.db_path)?;
-
-        // Active orphans → failed
-        db.execute(
-            "UPDATE runs SET state = 'failed', finished_at_ms = ?1, container_id = NULL, failure_kind = 'orphaned'
-             WHERE state = 'active' AND repo = ?2",
-            rusqlite::params![now, &self.repo],
-        )?;
-
-        // Pending orphans → failed (created but never executed).
-        db.execute(
-            "UPDATE runs SET state = 'failed', finished_at_ms = ?1, failure_kind = 'orphaned'
-             WHERE state = 'pending' AND repo = ?2",
-            rusqlite::params![now, &self.repo],
-        )?;
-
-        Ok(())
+/// Move every `pending` or `active` run to `failed` with
+/// `failure_kind = 'orphaned'`. Called once at server startup to clean
+/// up runs left behind by a prior instance. Operates across all repos —
+/// orphans aren't a per-repo concern.
+pub fn reconcile_orphans(db_path: &Path) -> Result<()> {
+    let now = Timestamp::now().as_millisecond();
+    let db = crate::db::open(db_path)?;
+    let count = db.execute(
+        "UPDATE runs SET state = 'failed', finished_at_ms = ?1,
+         container_id = NULL, failure_kind = 'orphaned'
+         WHERE state IN ('pending', 'active')",
+        rusqlite::params![now],
+    )?;
+    if count > 0 {
+        tracing::warn!(count, "reconciled orphaned runs");
     }
+    Ok(())
 }
 
 /// A CI run backed by a SQLite row.
@@ -918,74 +877,51 @@ mod tests {
     }
 
     #[test]
-    fn scan_orphans_finds_pending() {
+    fn reconcile_fails_pending_orphans() {
         let (_dir, quire) = tmp_quire();
         let runs = test_runs(&quire);
         let run = runs.create(&test_meta()).expect("create");
+        let id = run.id().to_string();
 
-        let orphans = runs.scan_orphans().expect("scan");
-        assert_eq!(orphans.len(), 1);
-        assert_eq!(orphans[0].id(), run.id());
-        assert_eq!(orphans[0].state(), RunState::Pending);
-    }
-
-    #[test]
-    fn scan_orphans_finds_active() {
-        let (_dir, quire) = tmp_quire();
-        let runs = test_runs(&quire);
-        let mut run = runs.create(&test_meta()).expect("create");
-        run.transition(RunState::Active).expect("transition");
+        reconcile_orphans(&quire.db_path()).expect("reconcile");
 
-        let orphans = runs.scan_orphans().expect("scan");
-        assert_eq!(orphans.len(), 1);
-        assert_eq!(orphans[0].state(), RunState::Active);
+        let reopened = Run::open(quire.db_path(), id, runs.base_dir.clone()).expect("reopen");
+        assert_eq!(reopened.state(), RunState::Failed);
     }
 
     #[test]
-    fn scan_orphans_skips_complete() {
+    fn reconcile_fails_active_orphans() {
         let (_dir, quire) = tmp_quire();
         let runs = test_runs(&quire);
         let mut run = runs.create(&test_meta()).expect("create");
-        run.transition(RunState::Active).expect("transition");
-        run.transition(RunState::Complete).expect("transition");
-
-        let orphans = runs.scan_orphans().expect("scan");
-        assert!(orphans.is_empty(), "complete runs are not orphans");
-    }
-
-    #[test]
-    fn scan_orphans_empty_when_no_runs() {
-        let (_dir, quire) = tmp_quire();
-        let runs = test_runs(&quire);
-        assert!(runs.scan_orphans().expect("scan").is_empty());
-    }
-
-    #[test]
-    fn reconcile_fails_pending_orphans() {
-        let (_dir, quire) = tmp_quire();
-        let runs = test_runs(&quire);
-        let run = runs.create(&test_meta()).expect("create");
+        run.transition(RunState::Active).expect("to active");
         let id = run.id().to_string();
 
-        runs.reconcile_orphans().expect("reconcile");
+        reconcile_orphans(&quire.db_path()).expect("reconcile");
 
-        // Pending orphan should be moved to failed.
         let reopened = Run::open(quire.db_path(), id, runs.base_dir.clone()).expect("reopen");
         assert_eq!(reopened.state(), RunState::Failed);
     }
 
     #[test]
-    fn reconcile_fails_active_orphans() {
+    fn reconcile_leaves_complete_runs_alone() {
         let (_dir, quire) = tmp_quire();
         let runs = test_runs(&quire);
         let mut run = runs.create(&test_meta()).expect("create");
         run.transition(RunState::Active).expect("to active");
+        run.transition(RunState::Complete).expect("to complete");
         let id = run.id().to_string();
 
-        runs.reconcile_orphans().expect("reconcile");
+        reconcile_orphans(&quire.db_path()).expect("reconcile");
 
         let reopened = Run::open(quire.db_path(), id, runs.base_dir.clone()).expect("reopen");
-        assert_eq!(reopened.state(), RunState::Failed);
+        assert_eq!(reopened.state(), RunState::Complete);
+    }
+
+    #[test]
+    fn reconcile_is_a_noop_when_no_runs() {
+        let (_dir, quire) = tmp_quire();
+        reconcile_orphans(&quire.db_path()).expect("reconcile");
     }
 
     fn load(source: &str) -> Pipeline {