Skip to content

feat(mcp): preemptive task cancellation via duckdb_interrupt (#111) - #113

Merged
jrosskopf merged 2 commits into
mainfrom
feature/gh-111-task-interrupt
Sep 1, 2026
Merged

feat(mcp): preemptive task cancellation via duckdb_interrupt (#111)#113
jrosskopf merged 2 commits into
mainfrom
feature/gh-111-task-interrupt

Conversation

@jrosskopf

@jrosskopf jrosskopf commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Makes MCP task cancellation preemptive: tasks/cancel and graceful shutdown now interrupt the in-flight DuckDB query instead of waiting for the work to notice a cooperative cancel flag. A 100-billion-row async query that would run ~32s is cancelled in ~1s.

How it works

  • QueryExecutor::interrupt() calls duckdb_interrupt on the executor's connection; the running duckdb_query returns an error that surfaces as a thrown exception.
  • A small thread-id → QueryExecutor* registry lets the executor self-register (RAII ActiveExecGuard) for the duration of each query, so another thread can interrupt whatever query runs on a given worker thread — without threading an executor handle through executeToolDatabaseManagerQueryExecutor.
  • MCPTaskManager gains a per-task interrupt hook. The worker publishes [tid]{ interruptActiveExecutor(tid); } while running; cancel() and shutdown() fire it under the manager mutex. The cancelled flag still wins over the resulting error, so an interrupted task lands in cancelled, not failed.
  • shutdown() interrupts running tasks before joining, so a graceful stop no longer blocks on a multi-minute query.

Test plan

  • C++ unit: QueryExecutor::interrupt stops an in-flight query
  • C++ unit: tasks/cancel fires the preemptive interrupt hook
  • Integration: long (100B-row) async query cancelled in ~1s, task reaches cancelled
  • Full C++ suite green (666 cases, 6198 assertions)
  • Full Tasks integration suite green (7/7, incl. restart durability)

Closes #111

… task cancel (#111)

First increment toward interruptible tasks: QueryExecutor already owns its
duckdb_connection for its lifetime, so expose interrupt() which calls
duckdb_interrupt(conn) from another thread. Additive and REST-safe (the REST
path never calls it); a running query returns an error, surfaced as an
exception by execute().

Verified by a unit test that starts a huge range-count on one thread and
interrupts it from another — it stops in ~1.4s (vs many seconds unbudged) and
throws. Next: thread this through a cancellable async-tool execution path and
wire tasks/cancel + shutdown to it (see the plan on #111).
- Add a thread-id registry so a QueryExecutor self-registers for the
  duration of its DuckDB query; another thread can then interrupt it by
  thread id without threading an executor handle through the whole
  executeTool -> DatabaseManager -> QueryExecutor pipeline.
- MCPTaskManager gains a per-task interrupt hook: the worker publishes an
  interrupt closure while running, and tasks/cancel and shutdown fire it
  to stop the in-flight query preemptively instead of waiting for
  cooperative polling. Cancelled-flag still wins over the resulting error,
  so an interrupted task lands in 'cancelled', not 'failed'.
- shutdown() now interrupts running tasks before joining, so a graceful
  stop no longer blocks on a multi-minute query finishing on its own.
- Tests: unit test that cancel fires the interrupt hook; integration test
  that a long (100B-row) async query is cancelled in ~1s instead of ~32s.
@jrosskopf
jrosskopf marked this pull request as ready for review September 1, 2026 01:32
@jrosskopf
jrosskopf merged commit 46ed6b6 into main Sep 1, 2026
22 checks passed
@jrosskopf
jrosskopf deleted the feature/gh-111-task-interrupt branch September 1, 2026 02:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP Tasks: interrupt in-flight queries on tasks/cancel (duckdb_interrupt)

1 participant