From 7b92403abf2acb316084df983f51ebed8fd12cef Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Sat, 1 Aug 2026 21:46:32 +0800 Subject: [PATCH] Fix diagnostics cancellation interrupt race --- NEWS.md | 2 ++ R/task.R | 8 +++++--- tests/testthat/test-task.R | 26 ++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index e9797827..bd938176 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # languageserver 0.3.18 +- Retire persistent `callr` workers when cancelling tasks so a late interrupt + cannot leak into the next diagnostics run. - Add preferred quick fixes for common `lintr` diagnostics and a conflict-aware `source.fixAll` action, while making multi-line `nolint` actions apply to every affected line. diff --git a/R/task.R b/R/task.R index ed4bdbbe..66503b39 100644 --- a/R/task.R +++ b/R/task.R @@ -90,9 +90,11 @@ Task <- R6::R6Class("Task", private$cancelled <- TRUE if (!is.null(private$session)) { if (!identical(Sys.getenv("R_COVR"), "true")) { - # Do not close the session, it is persistent and managed by TaskManager. - # Just try to interrupt the ongoing computation. - private$session$interrupt() + # An interrupt can arrive after this call has completed and + # interrupt the next task on the persistent session. Retire + # the worker instead so cancellation cannot leak across tasks. + private$session$kill( + grace = 0, close_connections = FALSE) } } else if (!is.null(private$process) && private$process$is_alive()) { if (identical(Sys.getenv("R_COVR"), "true")) { diff --git a/tests/testthat/test-task.R b/tests/testthat/test-task.R index 83132291..5eef45f0 100644 --- a/tests/testthat/test-task.R +++ b/tests/testthat/test-task.R @@ -158,6 +158,32 @@ test_that("TaskManager refreshes pending task recency", { tm$stop() }) +test_that("Task cancellation retires its persistent session", { + withr::local_envvar(R_COVR = "false") + state <- "idle" + killed <- FALSE + interrupted <- FALSE + session <- list( + call = function(...) state <<- "busy", + get_state = function() state, + read = function() NULL, + kill = function(grace, close_connections) { + expect_equal(grace, 0) + expect_false(close_connections) + killed <<- TRUE + state <<- "finished" + }, + interrupt = function() interrupted <<- TRUE + ) + task <- create_task(function() NULL, list()) + + task$start(session) + task$kill() + + expect_true(killed) + expect_false(interrupted) +}) + test_that("TaskManager does not overprovision while a session starts", { tm <- TaskManager$new( "starting", use_session = TRUE, min_idle_sessions = 0,