Skip to main content

php_lsp/backend/
offload.rs

1//! Run CPU-bound handler work off the async runtime worker.
2//!
3//! tower-lsp drives every connection's requests from the same small tokio
4//! worker pool. A handler that walks an AST, scans the workspace index, or
5//! shells out to an external process inline blocks that worker — every
6//! other in-flight request (on this connection or, with a multi-threaded
7//! runtime, any other task sharing the pool) stalls until it finishes. See
8//! the git history for the long tail of `fix(lsp): run X off the request
9//! loop` commits this fixed one handler at a time before this module existed.
10//!
11//! [`run`] and [`run_gated`] are the two sanctioned ways to leave the async
12//! runtime worker: `tokio::task::spawn_blocking` plus uniform panic
13//! handling. A `spawn_blocking` panic is delivered as `Err(JoinError)`
14//! rather than propagating like a normal panic; before this module several
15//! call sites discarded that `Err` via `.unwrap_or_default()` with no
16//! logging at all, so a panicking closure failed silently. Both helpers log
17//! unconditionally and return `None`, so callers can never reintroduce that
18//! gap by accident — `.unwrap_or_default()`, `.unwrap_or(fallback)`, and
19//! `let Some(x) = ... else { return .. }` all stay available on the
20//! `Option` they return.
21//!
22//! `run_gated` additionally passes through a named [`DebugGate`](super::debug_gate::DebugGate)
23//! section first, so a responsiveness regression test can pin that specific
24//! closure in flight and assert the connection still answers other requests
25//! meanwhile (see `debug_gate` module docs). Reach for `run` when a closure
26//! has no such test.
27
28use std::sync::Arc;
29
30use super::debug_gate::DebugGate;
31
32/// Runs `f` on the blocking thread pool. Returns `None` (logging via
33/// `label`) if `f` panics.
34pub async fn run<F, R>(label: &'static str, f: F) -> Option<R>
35where
36    F: FnOnce() -> R + Send + 'static,
37    R: Send + 'static,
38{
39    match tokio::task::spawn_blocking(f).await {
40        Ok(r) => Some(r),
41        Err(e) => {
42            tracing::warn!("{label} panicked: {e}");
43            None
44        }
45    }
46}
47
48/// Same as [`run`], but `f` first passes through `section` on `gate` — a
49/// no-op outside test builds (see `debug_gate`).
50pub async fn run_gated<F, R>(gate: &Arc<DebugGate>, section: &'static str, f: F) -> Option<R>
51where
52    F: FnOnce() -> R + Send + 'static,
53    R: Send + 'static,
54{
55    let gate = Arc::clone(gate);
56    run(section, move || {
57        gate.pass(section);
58        f()
59    })
60    .await
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[tokio::test]
68    async fn returns_value_when_no_panic() {
69        let r = run("test", || 42).await;
70        assert_eq!(r, Some(42));
71    }
72
73    #[tokio::test]
74    async fn returns_none_on_panic() {
75        let r: Option<i32> = run("test", || panic!("boom")).await;
76        assert_eq!(r, None);
77    }
78
79    #[tokio::test]
80    async fn gated_returns_value_when_no_panic() {
81        let gate = Arc::new(DebugGate::default());
82        let r = run_gated(&gate, "sec", || 7).await;
83        assert_eq!(r, Some(7));
84    }
85
86    #[tokio::test]
87    async fn gated_returns_none_on_panic() {
88        let gate = Arc::new(DebugGate::default());
89        let r: Option<i32> = run_gated(&gate, "sec", || panic!("boom")).await;
90        assert_eq!(r, None);
91    }
92}