Skip to content

Commit 73a5e8c

Browse files
authored
Remove interrupt tasks entirely (#1222)
Closes #1187 In #1187 we realized that our "interrupt time" tasks have never been running on Windows, and...no one has complained. Practically, this means that LSP features like completions or hover have not been running on Windows when R is busy, i.e. when R is running some long running computation, even if that code is checking for interrupts periodically via `R_CheckUserInterrupt()`. Unix machines, on the other hand, do return completions or hover results at interrupt time, but it is fairly unreliable and highly subject to the R code being run, i.e. this video shows how when you run a long running `lm()`, you can basically hang the LSP on a Mac because it almost never calls `R_CheckUserInterrupt()`: https://github.com/user-attachments/assets/52d54ca3-47f2-4ae5-a693-81b1b8d9bc02 We've long through that doing extensive work at R interrupt time is rather dangerous (you could accidentally load a namespace mid-R execution, which would be wild), so we've wanted to remove these. And with oak, we are going to rely on interrupt time less and less anyways, because more things can be done statically without talking to the main R session. So this PR removes interrupt tasks entirely. I'll leave more notes inline.
1 parent 6f7584e commit 73a5e8c

21 files changed

Lines changed: 521 additions & 278 deletions

crates/ark/src/console.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ use harp::utils::r_typeof;
9292
use harp::CONSOLE_THREAD_ID;
9393
use libr::R_BaseNamespace;
9494
use libr::R_ProcessEvents;
95-
use libr::R_RunPendingFinalizers;
9695
use libr::Rf_ScalarInteger;
9796
use libr::Rf_error;
9897
use libr::Rf_findVarInFrame;
@@ -130,8 +129,7 @@ use console_filter::ConsoleFilter;
130129
pub use console_repl::catching_panics;
131130
pub(crate) use console_repl::console_inputs;
132131
pub(crate) use console_repl::r_busy;
133-
#[cfg(unix)]
134-
pub(crate) use console_repl::r_polled_events;
132+
pub(crate) use console_repl::r_interrupt_events;
135133
pub(crate) use console_repl::r_read_console;
136134
pub(crate) use console_repl::r_show_message;
137135
pub(crate) use console_repl::r_suicide;
@@ -238,7 +236,6 @@ pub struct Console {
238236
autoprint_output: String,
239237

240238
/// Channel to send and receive tasks from `QueuedRTask`s
241-
tasks_interrupt_rx: Receiver<QueuedRTask>,
242239
tasks_idle_rx: Receiver<QueuedRTask>,
243240
tasks_idle_any_rx: Receiver<QueuedRTask>,
244241
try_idle_rx: Receiver<TryIdleTask>,

crates/ark/src/console/console_filter.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,6 @@ impl ConsoleFilter {
263263
/// Check for timeout and handle state transitions.
264264
/// Timeout means we didn't reach ReadConsole to confirm debug output,
265265
/// so we emit the accumulated content back to the user.
266-
#[cfg(any(unix, test))]
267266
pub(super) fn check_timeout(&mut self) -> Option<String> {
268267
self.drain_on_timeout()
269268
}

crates/ark/src/console/console_repl.rs

Lines changed: 134 additions & 107 deletions
Large diffs are not rendered by default.

crates/ark/src/lsp/backend.rs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use std::path::PathBuf;
1111
use std::sync::atomic::Ordering;
1212
use std::sync::Arc;
13+
use std::time::Duration;
1314

1415
use amalthea::comm::server_comm::ServerStartMessage;
1516
use amalthea::comm::server_comm::ServerStartedMessage;
@@ -89,7 +90,7 @@ macro_rules! cast_response {
8990
},
9091
RequestResponse::Crashed(err) => {
9192
// Notify user that the LSP has crashed and is no longer active
92-
report_crash();
93+
report_crash($self.client()).await;
9394

9495
// The backtrace is reported via `err` and eventually shows up
9596
// in the LSP logs on the client side
@@ -103,21 +104,34 @@ macro_rules! cast_response {
103104
}};
104105
}
105106

106-
fn report_crash() {
107+
/// Send via `request::ShowMessageRequest` not `notification::ShowMessage` so that we can
108+
/// ensure that the message has been received on the frontend side. We are about to shut
109+
/// the LSP down, and sending out a fire-and-forget notification often won't get sent out
110+
/// before shutdown occurs. The request returns control to us when the user acknowledges
111+
/// the message. It doesn't matter if that takes awhile because we shut down right after,
112+
/// and we've already flipped the `LSP_HAS_CRASHED` global flag, but we do bound it with
113+
/// a 5 second timeout just in case the user ignores the message entirely, so we can still
114+
/// shutdown.
115+
async fn report_crash(client: &Client) {
107116
let user_message = concat!(
108117
"The R language server has crashed and has been disabled. ",
109118
"Smart features such as completions will no longer work in this session. ",
110119
"Please report this crash to https://github.com/posit-dev/positron/issues ",
111120
"with full logs (see https://positron.posit.co/troubleshooting.html#python-and-r-logs)."
112121
);
113-
114-
// NOTE: This is a legit use of interrupt-time task. No R access here, and
115-
// we need to go through Console since it owns the UI comm.
116-
r_task(|| {
117-
if let Some(ui) = Console::get().ui_comm() {
118-
ui.show_message(String::from(user_message));
119-
}
122+
let request = client.send_request::<request::ShowMessageRequest>(ShowMessageRequestParams {
123+
typ: MessageType::ERROR,
124+
message: String::from(user_message),
125+
actions: None,
120126
});
127+
match tokio::time::timeout(Duration::from_secs(5), request).await {
128+
Ok(result) => {
129+
result.log_err();
130+
},
131+
Err(_) => {
132+
log::warn!("Timed out waiting for frontend to acknowledge LSP crash notification");
133+
},
134+
}
121135
}
122136

123137
#[derive(Debug)]
@@ -233,6 +247,9 @@ struct Backend {
233247
/// Channel for communication with the main loop.
234248
events_tx: TokioUnboundedSender<Event>,
235249

250+
/// Copy of the Client, for reporting crash messages.
251+
client: Client,
252+
236253
/// Handle to the LSP loops. Drop it to shut the loops down and drop all
237254
/// owned state.
238255
_main_loop: LoopHandles,
@@ -261,6 +278,10 @@ impl Backend {
261278
.send(Event::Lsp(LspMessage::Notification(notif)))
262279
.unwrap();
263280
}
281+
282+
fn client(&self) -> &Client {
283+
&self.client
284+
}
264285
}
265286

266287
#[tower_lsp::async_trait]
@@ -569,7 +590,7 @@ pub(crate) fn start_lsp(
569590
let (shutdown_tx, mut shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);
570591

571592
let init = |client: Client| {
572-
let state = GlobalState::new(client, r_home, console_notification_tx);
593+
let state = GlobalState::new(client.clone(), r_home, console_notification_tx);
573594
let events_tx = state.events_tx();
574595

575596
// Start main loop and hold onto the handle that keeps it alive
@@ -591,6 +612,7 @@ pub(crate) fn start_lsp(
591612
Backend {
592613
shutdown_tx,
593614
events_tx,
615+
client,
594616
_main_loop: main_loop,
595617
}
596618
};

crates/ark/src/r_task.rs

Lines changed: 61 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,11 @@ use crate::console::Console;
2727
use crate::console::ConsoleOutputCapture;
2828
use crate::fixtures::r_test_init;
2929

30-
/// Task channels for interrupt-time tasks
31-
static INTERRUPT_TASKS: LazyLock<TaskChannels> = LazyLock::new(TaskChannels::new);
32-
33-
/// Task channels for idle-time tasks
30+
/// Task channels for idle-time tasks (top-level only)
3431
static IDLE_TASKS: LazyLock<TaskChannels> = LazyLock::new(TaskChannels::new);
3532

36-
/// Task channels for idle tasks that run at any idle prompt (top-level or browser)
33+
/// Task channels for idle tasks that run at any idle prompt (top-level or browser, but
34+
/// not input)
3735
static IDLE_ANY_TASKS: LazyLock<TaskChannels> = LazyLock::new(TaskChannels::new);
3836

3937
/// Rendezvous channel for try-idle tasks. `bounded(0)` means `try_send`
@@ -87,17 +85,15 @@ impl TaskChannels {
8785
}
8886
}
8987

90-
/// Returns receivers for interrupt, idle, debug-idle, and try-idle tasks.
88+
/// Returns receivers for idle, idle-any, and try-idle tasks.
9189
/// Initializes the task channels if they haven't been initialized yet.
9290
/// Can only be called once (intended for `Console` during init).
9391
pub(crate) fn take_receivers() -> (
94-
Receiver<QueuedRTask>,
9592
Receiver<QueuedRTask>,
9693
Receiver<QueuedRTask>,
9794
Receiver<TryIdleTask>,
9895
) {
9996
(
100-
INTERRUPT_TASKS.take_rx(),
10197
IDLE_TASKS.take_rx(),
10298
IDLE_ANY_TASKS.take_rx(),
10399
TRY_IDLE.rx.lock().unwrap().take().unwrap(),
@@ -226,7 +222,7 @@ impl std::task::Wake for RTaskWaker {
226222
}
227223

228224
impl RTaskStartInfo {
229-
pub(crate) fn new(idle: bool) -> Self {
225+
pub(crate) fn new() -> Self {
230226
let thread = std::thread::current();
231227
let thread_id = thread.id();
232228
let thread_name = thread
@@ -236,7 +232,7 @@ impl RTaskStartInfo {
236232
.to_owned();
237233

238234
let start_time = std::time::Instant::now();
239-
let span = tracing::trace_span!("R task", thread = thread_name, interrupt = !idle,);
235+
let span = tracing::trace_span!("R task", thread = thread_name);
240236

241237
Self {
242238
thread_id,
@@ -269,6 +265,9 @@ impl RTaskStartInfo {
269265
// running, so borrowing is allowed even though we send it to another
270266
// thread. See also `Crossbeam::thread::ScopedThreadBuilder` (from which
271267
// `r_task()` is adapted) for a similar approach.
268+
//
269+
// `r_task()`s run via `IDLE_ANY_TASKS`, i.e. they run at top level, and at a debugger
270+
// prompt (but notably not the input prompt as a way to reduce risk of reentrancy).
272271

273272
pub fn r_task<'env, F, T>(f: F) -> T
274273
where
@@ -324,9 +323,9 @@ where
324323
let task = QueuedRTask::Sync(RTaskSync {
325324
fun: closure,
326325
status_tx: Some(status_tx),
327-
start_info: RTaskStartInfo::new(false),
326+
start_info: RTaskStartInfo::new(),
328327
});
329-
INTERRUPT_TASKS.tx().send(task).unwrap();
328+
IDLE_ANY_TASKS.tx().send(task).unwrap();
330329

331330
// Block until we get the signal that the task has started
332331
let status = status_rx.recv().unwrap();
@@ -376,23 +375,18 @@ where
376375

377376
/// An async task to be run on the R thread.
378377
///
379-
/// Construct via `RTask::interrupt`, `RTask::idle`, or `RTask::idle_any_prompt`
380-
/// when spawning from the R thread. Use the `Send` variants
381-
/// (`RTask::send_interrupt`, etc.) when spawning from other threads.
378+
/// Construct via [RTask::idle] or [RTask::idle_any_prompt] when spawning from the R
379+
/// thread. Use the `Send` variants ([RTask::send_idle], etc.) when spawning from other
380+
/// threads.
382381
///
383-
/// For idle modes, console output is automatically captured during the task's
384-
/// execution via a `ConsoleOutputCapture` passed to the closure.
382+
/// Console output is automatically captured during the task's execution via a
383+
/// `ConsoleOutputCapture` passed to the closure.
385384
pub(crate) enum RTask {
386-
/// Run at the next interrupt check. Must be spawned from the R thread.
387-
Interrupt(BoxFuture<'static, ()>),
388385
/// Run when R is at a top-level idle prompt. Must be spawned from the R thread.
389386
Idle(BoxFuture<'static, ()>),
390387
/// Run when R is at any idle prompt (top-level or browser). Must be spawned
391388
/// from the R thread.
392389
IdleAnyPrompt(BoxFuture<'static, ()>),
393-
/// Like `Interrupt`, but can be spawned from any thread. The constructor
394-
/// enforces `Send` on the closure.
395-
SendInterrupt(BoxFuture<'static, ()>),
396390
/// Like `Idle`, but can be spawned from any thread. The constructor
397391
/// enforces `Send` on the closure.
398392
SendIdle(BoxFuture<'static, ()>),
@@ -402,14 +396,6 @@ pub(crate) enum RTask {
402396
}
403397

404398
impl RTask {
405-
pub(crate) fn interrupt<F, Fut>(fun: F) -> Self
406-
where
407-
F: FnOnce() -> Fut + 'static,
408-
Fut: Future<Output = ()> + 'static,
409-
{
410-
RTask::Interrupt(Box::pin(fun()))
411-
}
412-
413399
pub(crate) fn idle<F, Fut>(fun: F) -> Self
414400
where
415401
F: FnOnce(ConsoleOutputCapture) -> Fut + 'static,
@@ -418,7 +404,7 @@ impl RTask {
418404
RTask::Idle(Self::pin_with_capture(fun))
419405
}
420406

421-
#[allow(unused)]
407+
#[expect(unused)]
422408
pub(crate) fn idle_any_prompt<F, Fut>(fun: F) -> Self
423409
where
424410
F: FnOnce(ConsoleOutputCapture) -> Fut + 'static,
@@ -427,38 +413,36 @@ impl RTask {
427413
RTask::IdleAnyPrompt(Self::pin_with_capture(fun))
428414
}
429415

430-
fn pin_with_capture<F, Fut>(fun: F) -> BoxFuture<'static, ()>
416+
/// [Self::idle_any_prompt()], but without capture support
417+
///
418+
/// Can be useful for spawning long running event loops that don't emit R output to
419+
/// avoid [Self::pin_with_capture()]'s behavior of setting `options(warn = 1)` for the
420+
/// life of `fun`.
421+
pub(crate) fn idle_any_prompt_without_capture<F, Fut>(fun: F) -> Self
431422
where
432-
F: FnOnce(ConsoleOutputCapture) -> Fut + 'static,
423+
F: FnOnce() -> Fut + 'static,
433424
Fut: Future<Output = ()> + 'static,
434425
{
435-
Box::pin(async move {
436-
let capture = if Console::is_initialized() {
437-
Console::get_mut().start_capture()
438-
} else {
439-
// Unit tests run without a Console. The dummy capture is
440-
// inert and doesn't interact with Console state.
441-
debug_assert!(stdext::IS_TESTING);
442-
ConsoleOutputCapture::dummy()
443-
};
444-
fun(capture).await
445-
})
426+
RTask::IdleAnyPrompt(Box::pin(fun()))
446427
}
447428

448-
pub(crate) fn send_interrupt<F, Fut>(fun: F) -> Self
429+
pub(crate) fn send_idle<F, Fut>(fun: F) -> Self
449430
where
450-
F: FnOnce() -> Fut + 'static + Send,
431+
F: FnOnce(ConsoleOutputCapture) -> Fut + 'static + Send,
451432
Fut: Future<Output = ()> + 'static,
452433
{
453-
RTask::SendInterrupt(Box::pin(fun()))
434+
RTask::SendIdle(Self::pin_with_capture(fun))
454435
}
455436

456-
pub(crate) fn send_idle<F, Fut>(fun: F) -> Self
437+
/// For rare performance sensitive cases where you'd like to avoid the cost of
438+
/// poking R options via [Self::pin_with_capture()] and you know you don't need
439+
/// the safety of capturing output because you aren't running R code
440+
pub(crate) fn send_idle_without_capture<F, Fut>(fun: F) -> Self
457441
where
458-
F: FnOnce(ConsoleOutputCapture) -> Fut + 'static + Send,
442+
F: FnOnce() -> Fut + 'static + Send,
459443
Fut: Future<Output = ()> + 'static,
460444
{
461-
RTask::SendIdle(Self::pin_with_capture(fun))
445+
RTask::SendIdle(Box::pin(fun()))
462446
}
463447

464448
pub(crate) fn send_idle_any_prompt<F, Fut>(fun: F) -> Self
@@ -468,49 +452,62 @@ impl RTask {
468452
{
469453
RTask::SendIdleAnyPrompt(Self::pin_with_capture(fun))
470454
}
455+
456+
// Note that `start_capture()` sets `options(warn = 1)` and this persists
457+
// for the life of `f`, so avoid using capturing on long running event loops
458+
// that don't emit R output
459+
fn pin_with_capture<F, Fut>(fun: F) -> BoxFuture<'static, ()>
460+
where
461+
F: FnOnce(ConsoleOutputCapture) -> Fut + 'static,
462+
Fut: Future<Output = ()> + 'static,
463+
{
464+
Box::pin(async move {
465+
let capture = if Console::is_initialized() {
466+
Console::get_mut().start_capture()
467+
} else {
468+
// Unit tests run without a Console. The dummy capture is
469+
// inert and doesn't interact with Console state.
470+
debug_assert!(stdext::IS_TESTING);
471+
ConsoleOutputCapture::dummy()
472+
};
473+
fun(capture).await
474+
})
475+
}
471476
}
472477

473478
/// Spawn an async task on the R thread.
474479
///
475-
/// For `Send` variants (`RTask::send_interrupt`, etc.) this can be called from
480+
/// For `Send` variants ([RTask::send_idle], etc.) this can be called from
476481
/// any thread. Non-`Send` variants must be called from the R thread.
477482
pub(crate) fn spawn(task: RTask) {
478483
if stdext::IS_TESTING && !Console::is_initialized() {
479484
let _lock = harp::fixtures::R_TEST_LOCK.lock();
480485
let fut = match task {
481-
RTask::Interrupt(fut) |
482486
RTask::Idle(fut) |
483487
RTask::IdleAnyPrompt(fut) |
484-
RTask::SendInterrupt(fut) |
485488
RTask::SendIdle(fut) |
486489
RTask::SendIdleAnyPrompt(fut) => fut,
487490
};
488491
futures::executor::block_on(fut);
489492
return;
490493
}
491494

492-
let needs_r_thread = matches!(
493-
task,
494-
RTask::Interrupt(_) | RTask::Idle(_) | RTask::IdleAnyPrompt(_)
495-
);
495+
let needs_r_thread = matches!(task, RTask::Idle(_) | RTask::IdleAnyPrompt(_));
496496
if needs_r_thread && !Console::on_main_thread() {
497497
let thread = std::thread::current();
498498
let name = thread.name().unwrap_or("<unnamed>");
499499
panic!("`spawn()` must be called from the R thread, not thread '{name}'");
500500
}
501501

502-
let (fut, tasks_tx, only_idle) = match task {
503-
RTask::Interrupt(fut) | RTask::SendInterrupt(fut) => (fut, INTERRUPT_TASKS.tx(), false),
504-
RTask::Idle(fut) | RTask::SendIdle(fut) => (fut, IDLE_TASKS.tx(), true),
505-
RTask::IdleAnyPrompt(fut) | RTask::SendIdleAnyPrompt(fut) => {
506-
(fut, IDLE_ANY_TASKS.tx(), true)
507-
},
502+
let (fut, tasks_tx) = match task {
503+
RTask::Idle(fut) | RTask::SendIdle(fut) => (fut, IDLE_TASKS.tx()),
504+
RTask::IdleAnyPrompt(fut) | RTask::SendIdleAnyPrompt(fut) => (fut, IDLE_ANY_TASKS.tx()),
508505
};
509506

510507
let task = QueuedRTask::Async(RTaskAsync {
511508
fut,
512509
tasks_tx: tasks_tx.clone(),
513-
start_info: RTaskStartInfo::new(only_idle),
510+
start_info: RTaskStartInfo::new(),
514511
});
515512

516513
tasks_tx.send(task).unwrap();

0 commit comments

Comments
 (0)