From 69fcef31fca2d7037bb6ba8ab97b63100e173cba Mon Sep 17 00:00:00 2001 From: Rohan Port Date: Thu, 16 Jul 2026 11:15:29 +1000 Subject: [PATCH] fix(doctor): run TUI on blocking thread to prevent single-worker starvation run_tui's loop has no .await points, so scheduling it with tokio::spawn on a single-worker-thread runtime (the default on single-vCPU hosts) starves every other task, including the sweep it's meant to display, for as long as it runs. Move it to spawn_blocking's dedicated pool instead. Co-authored-by: Claude --- crates/bestool/src/actions/tamanu/doctor.rs | 2 +- crates/bestool/src/actions/tamanu/doctor/tui.rs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/crates/bestool/src/actions/tamanu/doctor.rs b/crates/bestool/src/actions/tamanu/doctor.rs index a86850e9..3c96f9d6 100644 --- a/crates/bestool/src/actions/tamanu/doctor.rs +++ b/crates/bestool/src/actions/tamanu/doctor.rs @@ -492,7 +492,7 @@ fn setup_progress( } let (tx, rx) = mpsc::unbounded_channel(); let names = selected_names.to_vec(); - let handle = tokio::spawn(tui::run_tui(names, source, rx)); + let handle = tokio::task::spawn_blocking(move || tui::run_tui(names, source, rx)); (Some(tx), Some(handle)) } diff --git a/crates/bestool/src/actions/tamanu/doctor/tui.rs b/crates/bestool/src/actions/tamanu/doctor/tui.rs index a5f4748d..e56d374c 100644 --- a/crates/bestool/src/actions/tamanu/doctor/tui.rs +++ b/crates/bestool/src/actions/tamanu/doctor/tui.rs @@ -121,7 +121,18 @@ impl Drop for TerminalGuard { /// Run the live TUI until either the sweep finishes (progress channel closes /// and every selected check has been seen) or the user interrupts (Ctrl+C / q). -pub async fn run_tui( +/// +/// This is synchronous and must be run via [`tokio::task::spawn_blocking`], not +/// `tokio::spawn`: its loop has no `.await` points (the terminal I/O and +/// `crossterm::event::poll` are plain blocking calls with no runtime +/// integration), so as an async task it would never yield back to the +/// executor. On a single-worker-thread runtime — the default on a +/// single-vCPU host — that starves every other task (the sweep itself +/// included) for as long as the TUI keeps running, which reads as a total, +/// permanent hang. `spawn_blocking` moves it onto Tokio's separate blocking +/// thread pool, which is sized independently of the CPU count, so it can +/// never contend with the async worker pool. +pub fn run_tui( selected_names: Vec<&'static str>, source: SweepSource, mut progress_rx: UnboundedReceiver,