Skip to content

Run Evaluate requests with timeout#1309

Open
lionel- wants to merge 5 commits into
mainfrom
debugger/evaluate-timeout
Open

Run Evaluate requests with timeout#1309
lionel- wants to merge 5 commits into
mainfrom
debugger/evaluate-timeout

Conversation

@lionel-

@lionel- lionel- commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Addresses posit-dev/positron#14481

Adds with_timeout() to run R code with a timeout and use that in the Evaluate handlers (in notebook and console paths). This prevents an inflooping (or long-running) expression in the Watch Pane from freezing the kernel.

  • with_timeout() runs a closure in a context where interrupts and polled events are restored. This allows R to check for interrupts, and allows polled events to check for a timeout.

  • We check for a timeout from polled events to avoid the complexity of checking time in a side thread. When a timeout is detected, we record that fact in a thread-local variable and signal an R interrupt.

  • The interrupt is either caught by with_timeout()'s try_catch(), or by an inner one running in the closure. Either way, with_timeout() returns whether the expression timed out to its caller.

This setup allows inner code to recover from the interrupt, which is not ideal. However this is much simpler than trying to propagate the interrupt with a Rust panic, since the latter approach would require making sure the panic does not cross a C stack which would be UB and likely to corrupt R's state. The simpler approach of propagating with Rust errors could in principle lead to surprising behaviour, but is simpler and safer to implement.

Regarding the safety of longjumping from polled events with an R interrupt, this should be sufficiently safe because:

  • This is equivalent to an R-level error and should be caught the same way by proper safeguards like try_catch().
  • This can only happen from within with_timeout(), which is a controlled environment that expects the interrupt to fire any time R is called.

We should still assume it's possible for some Rust destructors to get bypassed by the interrupt if the calling context is not super vigilant about wrapping in try_catch(). However the alternative of having Ark freeze with runaway evals is much worse.

This is Unix-only until we figure out #1222

Positron Release Notes

New Features

  • N/A

Bug Fixes

@lionel-
lionel- requested a review from DavisVaughan July 1, 2026 15:57

@DavisVaughan DavisVaughan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly want to talk through the ultimate plan with #1222 before committing to this, because relying on being able to poke an R API from polled-events / process-events might box us in a bit.

Comment thread crates/ark/src/console/console_repl.rs Outdated
}

// Interrupt any in-flight evaluations that have outlived their timeout
crate::timeout::check_timeout();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if we do #1222, where does this go?

#1222 removed polled_events() entirely.

In the current state of that PR, we are left with a process_events() hook for the debug_filter to still be checked occasionally (#1222 (comment)) but as you discovered in #1222 (comment) we shouldn't actually take over ptr_R_ProcessEvents because other packages like Quartz may use it instead.

So we are going to have to do something else there. I really wasn't thrilled with the idea of setting Callback (i.e. R_ProcessEvents) on Windows and R_PolledEvents on Unix as mentioned in #1222 (review). I remember sending this slack message about it

i would really rather not mix
* setting `R_ProcessEvents` on Windows via `CallBack`
* setting `R_PolledEvents` on Unix

that sounds like something that is likely to just confuse me greatly in the future, and is part of what im trying to get away from in this pr. it is very hard to keep what these do (and when they are called) straight in my head

since:
* we cant use `R_PolledEvents` on windows (doesnt exist)
* we cant use `R_ProcessEvents` on mac (Quartz needs it)

and because we only have 1 thing we need to regularly check, the `debug_filter` thing, i vote we get out of the business of setting `R_PolledEvents` and `R_ProcessEvents` at all and instead do one of two things

* the side thread that checks `debug_filter` and eat the 2mb cost
* drop caring about `debug_filter` at all for this nice use case. it isn't worth this amount of trouble IMO.

IMO not setting `R_PolledEvents` and `R_ProcessEvents` would be a huge win for us in terms of complexity and never having to think about this again

With the main conclusion there being that it would be nice to not use EITHER R_PolledEvents or R_ProcessEvents, to try to avoid all of the funny business with those, and instead maybe use a "watcher" thread of some kind.

The watcher thread could probably handle debug_filter flushing, but I don't think it could directly do crate::timeout::check_timeout() since that pokes an R API via crate::signals::set_interrupts_pending(true).

I'll also add that I'm not sure that calling this at R_PolledEvents / R_ProcessEvents time really reliably saves us. It still relies on someone calling these events handlers, and I'm pretty sure that arbitrary R code running in an Evaluate could just never check those, right? But maybe if that is the case, it means they wouldn't be able to respect an interrupt request either, so that's okay?

So I'm not really sure what the right thing to do is! But I would hate to add this and box ourselves out from being able to finish off #1222 when we were soooooo close to getting rid of interrupt tasks, so I think I'd like us to have a plan before merging this.

Comment thread crates/ark/src/dap/dap_jupyter_handler.rs Outdated
Comment thread crates/ark/src/dap/dap_server.rs
Comment thread crates/ark/src/r_task.rs
Comment thread crates/ark/src/dap/dap_server.rs
Comment thread crates/ark/src/timeout.rs Outdated
Comment thread crates/ark/src/timeout.rs Outdated
Comment on lines +117 to +120
let _polled = harp::raii::RLocal::new(
Some(crate::console::r_polled_events as unsafe extern "C-unwind" fn()),
unsafe { libr::R_PolledEvents },
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm you kind of want the opposite of RLocalPolledEventsSuspended, but that's a harp thing...

@lionel-
lionel- force-pushed the debugger/evaluate-timeout branch from e9e6620 to 2fb35f4 Compare July 17, 2026 09:27
@lionel-
lionel- force-pushed the debugger/evaluate-timeout branch from b22a265 to 4d6eb25 Compare July 17, 2026 11:49
@lionel-

lionel- commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on #1222 and cancellation tests now pass on Windows!

@lionel-
lionel- requested a review from DavisVaughan July 17, 2026 12:05
Comment thread crates/ark/src/timeout.rs
/// indicating whether the timeout fired.
///
/// Must run on the R thread.
pub(crate) fn with_timeout<F, T>(timeout: Duration, f: F) -> (harp::Result<T>, bool)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you hit a timeout, is it ever valid to look at harp::Result<T>?

My intuition is no, and that we should make that impossible via the type system, like

enum TimeoutResult<T> {
  Ok(harp::Result<T>),
  Timeout
}

Comment thread crates/ark/src/timeout.rs
//
//

//! A timeout that breaks runaway R-thread work with an interrupt.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//! A timeout that breaks runaway R-thread work with an interrupt.
//! A timeout that breaks runaway R-thread work with an interrupt

Random nit: I have explicit Claude.md instructions not to add a period after 1 liner code comments, which IIUC is our usual convention. I don't care that much, but I seem to notice it every time I see them 😆

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.

2 participants