Move web-view DB access off the async runtime
Wrap load_runs and load_run_detail in tokio::task::spawn_blocking so
the synchronous rusqlite queries dont block tokio workers. Replace
expect("db mutex poisoned") with propagated errors that surface as
500s instead of panicking the worker.

Assisted-by: GLM-5.1 via pi
change nzlvlwvpyuyrnymnutvwvssytpqrskyt
commit 21ada87c63e976b56541972e6e4e4faec6d2cae5
author Alpha Chen <alpha@kejadlen.dev>
date
parent znztvvvv
diff --git a/src/quire/web/db.rs b/src/quire/web/db.rs
index 647d606..20198f8 100644
--- a/src/quire/web/db.rs
+++ b/src/quire/web/db.rs
@@ -32,7 +32,10 @@ pub struct ShEvent {
 }
 
 pub fn load_runs(quire: &Quire, repo: &str) -> Result<Vec<RunRow>> {
-    let db = quire.db_pool().lock().expect("db mutex poisoned");
+    let db = quire
+        .db_pool()
+        .lock()
+        .map_err(|_| crate::error::Error::Io(std::io::Error::other("db mutex poisoned")))?;
     let mut stmt = db.prepare(
         "SELECT id, state, sha, ref_name, queued_at_ms, started_at_ms, finished_at_ms
          FROM runs WHERE repo = ?1
@@ -65,7 +68,10 @@ pub struct RunDetail {
 }
 
 pub fn load_run_detail(quire: &Quire, repo: &str, run_id: &str) -> Result<RunDetail> {
-    let db = quire.db_pool().lock().expect("db mutex poisoned");
+    let db = quire
+        .db_pool()
+        .lock()
+        .map_err(|_| crate::error::Error::Io(std::io::Error::other("db mutex poisoned")))?;
 
     let run = db.query_row(
         "SELECT id, state, sha, ref_name, queued_at_ms, started_at_ms, finished_at_ms
diff --git a/src/quire/web/handlers.rs b/src/quire/web/handlers.rs
index 207546c..5ef3658 100644
--- a/src/quire/web/handlers.rs
+++ b/src/quire/web/handlers.rs
@@ -89,9 +89,10 @@ pub async fn run_list(State(quire): State<Quire>, AxumPath(repo): AxumPath<Strin
         _ => return StatusCode::NOT_FOUND.into_response(),
     };
 
-    let runs = match db::load_runs(&quire, &repo_name) {
-        Ok(r) => r,
-        Err(e) => {
+    let q = quire.clone();
+    let runs = match tokio::task::spawn_blocking(move || db::load_runs(&q, &repo_name)).await {
+        Ok(Ok(r)) => r,
+        Ok(Err(e)) => {
             tracing::error!(repo = %repo, error = %display_chain(&e), "failed to load runs");
             return render_error(
                 repo_display,
@@ -100,6 +101,10 @@ pub async fn run_list(State(quire): State<Quire>, AxumPath(repo): AxumPath<Strin
                 display_chain(&e).to_string(),
             );
         }
+        Err(_) => {
+            tracing::error!("spawn_blocking task panicked");
+            return StatusCode::INTERNAL_SERVER_ERROR.into_response();
+        }
     };
 
     let template_runs: Vec<RunListRow> = runs
@@ -137,11 +142,14 @@ pub async fn run_detail(
         return StatusCode::NOT_FOUND.into_response();
     }
 
-    let result = db::load_run_detail(&quire, &repo_name, &run_id);
-    let detail = match result {
-        Ok(d) => d,
-        Err(ref e) if is_no_rows(e) => return StatusCode::NOT_FOUND.into_response(),
-        Err(e) => {
+    let q = quire.clone();
+    let rn = repo_name.clone();
+    let ri = run_id.clone();
+    let detail = match tokio::task::spawn_blocking(move || db::load_run_detail(&q, &rn, &ri)).await
+    {
+        Ok(Ok(d)) => d,
+        Ok(Err(ref e)) if is_no_rows(e) => return StatusCode::NOT_FOUND.into_response(),
+        Ok(Err(e)) => {
             tracing::error!(repo = %repo, run_id = %run_id, error = %display_chain(&e), "failed to load run detail");
             return render_error(
                 repo_display,
@@ -150,6 +158,10 @@ pub async fn run_detail(
                 display_chain(&e).to_string(),
             );
         }
+        Err(_) => {
+            tracing::error!("spawn_blocking task panicked");
+            return StatusCode::INTERNAL_SERVER_ERROR.into_response();
+        }
     };
 
     let detail_run = DetailRun {