From 5854b4f08cf8c089b5e8c3176f610217eacf27ca Mon Sep 17 00:00:00 2001 From: Alejandro Do Nascimento Mora Date: Thu, 27 Aug 2026 11:26:46 +0200 Subject: [PATCH] fix: make a double ctrl-c actually stop in-flight copies `Worker::run` selected between the hard-shutdown token and `tokio::spawn(run_inner(..))`. When the hard-shutdown branch won, the `JoinHandle` was dropped, which detaches the task instead of stopping it: the in-progress copy kept streaming, committed its transaction and marked the task complete after we'd already printed "Terminating immediately." and "Copied 0B from 0 chunks". Take an `AbortHandle` before the select and abort the inner task on hard shutdown. This also un-gates `double_ctrl_c_stops_hard` on macOS, where the stale comment blamed the platform for what was this bug, and removes the CI flake: whether the detached copy's completion line escaped before the process exited was a pure race, lost on slower runners. Claude-Session: https://claude.ai/code/session_01MphA7yobURuWfDNk8RdY83 --- src/workers.rs | 19 +++++++++++++++++-- tests/integration/main.rs | 5 ----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/workers.rs b/src/workers.rs index b1a32ab..20e6e59 100644 --- a/src/workers.rs +++ b/src/workers.rs @@ -168,13 +168,28 @@ impl Worker { let target = self.target; let task_count = self.task_count; + // Note: we spawn run_inner as a separate task, to prevent it from blocking + // the select below. + let inner = tokio::spawn(Self::run_inner( + graceful_shutdown.clone(), + source, + target, + task_count, + task, + )); + // Dropping a `JoinHandle` detaches the task instead of stopping it, so a + // hard shutdown must abort it explicitly. Otherwise the in-progress copy + // keeps running (and can even commit and report itself as copied) after + // we've told the user we're terminating immediately. + let abort_inner = inner.abort_handle(); + tokio::select! { _ = hard_shutdown.0.cancelled() => { debug!("worker received hard shutdown"); + abort_inner.abort(); Ok(WorkerResult::HardShutdown) }, - // Note: we spawn run_inner as a separate task, to prevent it from blocking this select. - res = tokio::spawn(Self::run_inner(graceful_shutdown.clone(), source, target, task_count, task)) => { + res = inner => { match res { Ok(r) => r, Err(join_error) => { diff --git a/tests/integration/main.rs b/tests/integration/main.rs index 82221cd..1c45c52 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -1893,11 +1893,6 @@ fn wait_for_message_in_output(output: &mut T, message: &str) -> Result< bail!("message '{message}' not found in output") } -// This test fails under macos, because double ctrl-c does not actually -// hard-stop the program on that platform. Inserting a `yield_now` into the -// tight loop in `copy_from_source_to_sink` fixes the problem, but it needs -// further investigation. -#[cfg(not(target_os = "macos"))] #[test] #[allow(clippy::zombie_processes)] fn double_ctrl_c_stops_hard() -> Result<()> {