From 347ab72331719ae13c8b84634400d30b8dc87e8a Mon Sep 17 00:00:00 2001 From: Bare7a Date: Thu, 5 Nov 2026 12:01:00 +0200 Subject: [PATCH] fix(BAR-34): Stops a cancelled MCP call at once, even before its query starts --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 2 +- crates/barsql-app/src/mcp.rs | 15 ++++++++++++--- crates/barsql-app/src/mcp/tests.rs | 13 +++++++++++++ 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0978b83..8882d52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -641,7 +641,7 @@ dependencies = [ [[package]] name = "barsql" -version = "1.2.5" +version = "1.2.6" dependencies = [ "anyhow", "barsql-app", @@ -656,7 +656,7 @@ dependencies = [ [[package]] name = "barsql-app" -version = "1.2.5" +version = "1.2.6" dependencies = [ "async-channel", "barsql-core", @@ -684,7 +684,7 @@ dependencies = [ [[package]] name = "barsql-core" -version = "1.2.5" +version = "1.2.6" dependencies = [ "dirs 7.0.0", "hex", @@ -697,7 +697,7 @@ dependencies = [ [[package]] name = "barsql-db" -version = "1.2.5" +version = "1.2.6" dependencies = [ "anyhow", "async-channel", @@ -734,7 +734,7 @@ dependencies = [ [[package]] name = "barsql-io" -version = "1.2.5" +version = "1.2.6" dependencies = [ "barsql-core", "barsql-db", @@ -749,7 +749,7 @@ dependencies = [ [[package]] name = "barsql-sql" -version = "1.2.5" +version = "1.2.6" dependencies = [ "barsql-core", "libc", @@ -761,7 +761,7 @@ dependencies = [ [[package]] name = "barsql-storage" -version = "1.2.5" +version = "1.2.6" dependencies = [ "barsql-core", "serde", @@ -772,7 +772,7 @@ dependencies = [ [[package]] name = "barsql-ui" -version = "1.2.5" +version = "1.2.6" dependencies = [ "anyhow", "async-channel", diff --git a/Cargo.toml b/Cargo.toml index 989b8fa..c8fc0a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "3" members = ["crates/*", "xtask"] [workspace.package] -version = "1.2.5" +version = "1.2.6" edition = "2024" license = "GPL-3.0-only" publish = false diff --git a/crates/barsql-app/src/mcp.rs b/crates/barsql-app/src/mcp.rs index 8290bd7..f976aae 100644 --- a/crates/barsql-app/src/mcp.rs +++ b/crates/barsql-app/src/mcp.rs @@ -157,15 +157,24 @@ impl Server { } } - async fn call(self: Arc, id: Value, params: Value, tab: String, cancelled: watch::Receiver) { + async fn call(self: Arc, id: Value, params: Value, tab: String, mut cancelled: watch::Receiver) { let tool = params["name"].as_str().unwrap_or_default(); let args = ¶ms["arguments"]; let started = Instant::now(); let result = if *cancelled.borrow() { Err(CANCELLED.into()) } else { - let run = AssertUnwindSafe(self.run(tool, args, &tab, cancelled.clone())); - run.catch_unwind().await.unwrap_or_else(|panic| Err(failed(QueryError::from_panic(panic)))) + let run = AssertUnwindSafe(self.run(tool, args, &tab, cancelled.clone())) + .catch_unwind() + .map(|result| result.unwrap_or_else(|panic| Err(failed(QueryError::from_panic(panic))))); + // Anything but a change, which rolls back by itself, stops at once, even before its query starts. + // Err is a newer call taking the id, not a cancel. + tokio::select! { + result = run => result, + Ok(_) = cancelled.wait_for(|stop| *stop), if tool != "run_change" => { + Err(failed(QueryError::cancelled())) + } + } }; // Gives the connection back at once: SQLite has only one. self.bar.cleanup_tab(&tab).await; diff --git a/crates/barsql-app/src/mcp/tests.rs b/crates/barsql-app/src/mcp/tests.rs index 3de3136..4a6cc92 100644 --- a/crates/barsql-app/src/mcp/tests.rs +++ b/crates/barsql-app/src/mcp/tests.rs @@ -271,6 +271,19 @@ fn a_reused_request_id_leaves_the_newer_call_cancellable() { assert!(s.settled(), "the cancel reached it"); } +// Holding the engines lock parks the call in reload_connections, before its query registers. +#[test] +fn a_cancel_before_the_query_registers_still_stops_it() { + let s = setup(); + let engines = lock(&s.server.bar.inner.engines); + s.query(4, ENDLESS); + std::thread::sleep(Duration::from_millis(200)); + s.cancel(4); + drop(engines); + assert!(s.settled(), "the cancel was lost"); + assert!(s.replies.try_recv().is_err(), "a cancelled call answered"); +} + #[test] fn cancelling_a_change_takes_its_question_down_and_runs_nothing() { let s = setup();