@@ -26,10 +26,11 @@ use oak_scan::ScanRequest;
2626use oak_scan:: ScanScheduler ;
2727use oak_semantic:: library:: Library ;
2828use stdext:: result:: ResultExt ;
29+ use stdext:: spawn;
2930use tokio:: runtime:: Handle ;
30- use tokio:: runtime:: RuntimeFlavor ;
3131use tokio:: sync:: mpsc;
3232use tokio:: sync:: mpsc:: unbounded_channel as tokio_unbounded_channel;
33+ use tokio:: sync:: oneshot;
3334use tokio:: task:: JoinHandle ;
3435use tower_lsp:: jsonrpc;
3536use 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