Skip to content

Commit eac345c

Browse files
committed
Run the main loop on its own thread
1 parent 4348824 commit eac345c

3 files changed

Lines changed: 77 additions & 50 deletions

File tree

crates/ark/src/lsp/backend.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ use crate::lsp::input_boundaries::InputBoundariesParams;
4747
use crate::lsp::input_boundaries::InputBoundariesResponse;
4848
use crate::lsp::main_loop::Event;
4949
use crate::lsp::main_loop::GlobalState;
50+
use crate::lsp::main_loop::LoopHandles;
5051
use crate::lsp::main_loop::TokioUnboundedSender;
5152
use crate::lsp::statement_range;
5253
use crate::lsp::statement_range::StatementRangeParams;
@@ -232,9 +233,9 @@ struct Backend {
232233
/// Channel for communication with the main loop.
233234
events_tx: TokioUnboundedSender<Event>,
234235

235-
/// Handle to main loop. Drop it to cancel the loop, all associated tasks,
236-
/// and drop all owned state.
237-
_main_loop: tokio::task::JoinSet<()>,
236+
/// Handle to the LSP loops. Drop it to shut the loops down and drop all
237+
/// owned state.
238+
_main_loop: LoopHandles,
238239
}
239240

240241
impl Backend {

crates/ark/src/lsp/handler.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ impl Lsp {
3939
) -> Self {
4040
let rt = Builder::new_multi_thread()
4141
.enable_all()
42-
// One for the main loop and one spare
42+
// Workers serve tower-lsp, the auxiliary loop, and the diagnostics
43+
// queue. The main loop runs on its own thread.
4344
.worker_threads(2)
4445
// Used for diagnostics
4546
.max_blocking_threads(2)

crates/ark/src/lsp/main_loop.rs

Lines changed: 71 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ use oak_scan::ScanRequest;
2626
use oak_scan::ScanScheduler;
2727
use oak_semantic::library::Library;
2828
use stdext::result::ResultExt;
29+
use stdext::spawn;
2930
use tokio::runtime::Handle;
30-
use tokio::runtime::RuntimeFlavor;
3131
use tokio::sync::mpsc;
3232
use tokio::sync::mpsc::unbounded_channel as tokio_unbounded_channel;
33+
use tokio::sync::oneshot;
3334
use tokio::task::JoinHandle;
3435
use tower_lsp::jsonrpc;
3536
use tower_lsp::lsp_types;
@@ -159,6 +160,21 @@ pub(crate) struct GlobalState {
159160
events_rx: TokioUnboundedReceiver<Event>,
160161
}
161162

163+
/// Owns the running LSP loops. Dropping it shuts them down.
164+
///
165+
/// - The auxiliary loop is a runtime task, aborted when `_aux_loop` drops.
166+
/// - The main loop runs on its own thread. Dropping `_main_shutdown_tx` closes
167+
/// the channel the loop selects on, so it breaks, drops the owned
168+
/// `GlobalState`, and the thread exits. We hold the thread's handle but don't
169+
/// join it on drop (it winds down on its own), so dropping never blocks the
170+
/// caller.
171+
#[derive(Debug)]
172+
pub(crate) struct LoopHandles {
173+
_main_loop: std::thread::JoinHandle<()>,
174+
_main_shutdown_tx: oneshot::Sender<()>,
175+
_aux_loop: tokio::task::JoinSet<()>,
176+
}
177+
162178
/// Non-cloneable, per-session state mutated only by exclusive handlers.
163179
/// Sits alongside [`WorldState`] (which is cloneable for snapshot
164180
/// handlers); state that can't be cloned lives here instead.
@@ -267,37 +283,66 @@ impl GlobalState {
267283
self.events_tx.clone()
268284
}
269285

270-
/// Start the main and auxiliary loops
286+
/// Start the main and auxiliary loops.
271287
///
272-
/// Returns a `JoinSet` that holds onto all tasks and state owned by the
273-
/// event loop. Drop it to cancel everything and shut down the service.
274-
pub(crate) fn start(self) -> tokio::task::JoinSet<()> {
275-
let mut set = tokio::task::JoinSet::<()>::new();
276-
277-
// Spawn latency-sensitive auxiliary loop. Must be first to initialise
278-
// global transmission channel.
279-
let aux = AuxiliaryState::new(self.client.clone());
280-
set.spawn(async move { aux.start().await });
281-
282-
// Spawn main loop
283-
set.spawn(async move { self.main_loop().await });
288+
/// The returned [`LoopHandles`] owns everything the loops need. Drop it to
289+
/// shut the loops down and release the owned state.
290+
pub(crate) fn start(self) -> LoopHandles {
291+
let mut aux = tokio::task::JoinSet::<()>::new();
292+
293+
// The auxiliary loop is fully async and never blocks. Must be started
294+
// first to initialise the global transmission channel.
295+
let aux_state = AuxiliaryState::new(self.client.clone());
296+
aux.spawn(async move { aux_state.start().await });
297+
298+
// Since the main loop owns the Salsa DB and writes to it, we run on its
299+
// own thread instead of a Tokio worker. Salsa writes are potentially
300+
// blocking until the writer gains exclusive access. If background tasks
301+
// holding clones of the DB are stuck on the same thread as the main
302+
// loop, the LSP deadlocks. This can be avoided by wrapping writes in
303+
// `block_in_place()` but the safer structure is to have it run on an OS
304+
// thread that we're in control of.
305+
let (shutdown_tx, shutdown_rx) = oneshot::channel();
306+
let handle = Handle::current();
307+
let main_loop = spawn!("oak-main-loop", move || {
308+
handle.block_on(self.main_loop(shutdown_rx));
309+
});
284310

285-
set
311+
LoopHandles {
312+
_main_shutdown_tx: shutdown_tx,
313+
_aux_loop: aux,
314+
_main_loop: main_loop,
315+
}
286316
}
287317

288318
/// Run main loop
289319
///
290320
/// This takes ownership of all global state and handles one by one LSP
291321
/// requests, notifications, and other internal events.
292-
async fn main_loop(mut self) {
322+
async fn main_loop(mut self, mut shutdown_rx: oneshot::Receiver<()>) {
293323
loop {
294-
let event = self.next_event().await;
295-
if let Err(err) = self.handle_event(event).await {
296-
lsp::log_error!("Failure while handling event:\n{err:?}")
324+
tokio::select! {
325+
_ = &mut shutdown_rx => {
326+
lsp::log_info!("Main loop stopping: handle dropped");
327+
break;
328+
},
329+
event = self.events_rx.recv() => {
330+
let Some(event) = event else {
331+
lsp::log_info!("Main loop stopping: event channel closed");
332+
break;
333+
};
334+
if let Err(err) = self.handle_event(event).await {
335+
lsp::log_error!("Failure while handling event:\n{err:?}")
336+
}
337+
}
297338
}
298339
}
299340
}
300341

342+
/// Pull the next event off the channel. Only the test pump uses this; the
343+
/// running loop selects on the channel directly so it can also watch for
344+
/// shutdown.
345+
#[cfg(test)]
301346
async fn next_event(&mut self) -> Event {
302347
self.events_rx.recv().await.unwrap()
303348
}
@@ -351,10 +396,10 @@ impl GlobalState {
351396
state_handlers::did_change_watched_files(params, &mut self.world, &mut self.lsp_state, &self.events_tx)?;
352397
},
353398
LspNotification::DidOpenTextDocument(params) => {
354-
block_for_write(|| state_handlers::did_open(params, &mut self.world))?;
399+
state_handlers::did_open(params, &mut self.world)?;
355400
},
356401
LspNotification::DidChangeTextDocument(params) => {
357-
block_for_write(|| state_handlers::did_change(params, &mut self.lsp_state, &mut self.world))?;
402+
state_handlers::did_change(params, &mut self.lsp_state, &mut self.world)?;
358403
},
359404
LspNotification::DidSaveTextDocument(_params) => {
360405
// Currently ignored
@@ -479,13 +524,11 @@ impl GlobalState {
479524
// kicked off. The buffer-drain inside `apply_scan_completed` uses
480525
// this set as its watcher-event `skip` argument.
481526
let editor_owned: HashSet<FilePath> = self.world.open_files.keys().cloned().collect();
482-
let followups = block_for_write(|| {
483-
self.lsp_state.oak_scheduler.apply_scan_completed(
484-
&mut self.world.db,
485-
scan,
486-
&editor_owned,
487-
)
488-
});
527+
let followups = self.lsp_state.oak_scheduler.apply_scan_completed(
528+
&mut self.world.db,
529+
scan,
530+
&editor_owned,
531+
);
489532
lsp::log_info!("Dispatching {n} followup scan requests", n = followups.len());
490533
dispatch_scan_requests(&self.events_tx, followups);
491534

@@ -849,24 +892,6 @@ pub(crate) fn log(level: lsp_types::MessageType, message: String) {
849892
};
850893
}
851894

852-
/// Run a blocking Salsa write without stranding the runtime.
853-
///
854-
/// Salsa writes may block the current thread until it gains exclusive access to
855-
/// the DB. This requires processing/cancelling background tasks that hold DB
856-
/// clones, e.g. diagnostics tasks. If the dispatching of these tasks lives in
857-
/// tokio tasks, there is a risk of deadlock. We call `block_in_place()` to let
858-
/// Tokio know the thread is about to potentially block, allowing tasks to be
859-
/// scheduled on other worker threads.
860-
///
861-
/// `block_in_place()` panics on a current-thread runtime, which the tests use, so
862-
/// fall back to calling `f` directly there.
863-
fn block_for_write<T>(f: impl FnOnce() -> T) -> T {
864-
match Handle::try_current().map(|handle| handle.runtime_flavor()) {
865-
Ok(RuntimeFlavor::MultiThread) => tokio::task::block_in_place(f),
866-
_ => f(),
867-
}
868-
}
869-
870895
/// Spawn a blocking task
871896
///
872897
/// This runs tasks that do semantic analysis on a separate thread pool to avoid

0 commit comments

Comments
 (0)