Skip to content

Commit 794cb85

Browse files
authored
Use an RwLock for AUXILIARY_EVENT_TX (#668)
* Use an `RwLock` for `AUXILIARY_EVENT_TX` We expect it to be write locked very rarely, so it should still be pretty cheap to get access to this safely * Panic if we can't access the aux sender
1 parent ceaf332 commit 794cb85

File tree

1 file changed

+47
-29
lines changed

1 file changed

+47
-29
lines changed

crates/ark/src/lsp/main_loop.rs

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use std::collections::HashMap;
99
use std::future;
1010
use std::pin::Pin;
11+
use std::sync::RwLock;
1112

1213
use anyhow::anyhow;
1314
use futures::StreamExt;
@@ -34,17 +35,20 @@ use crate::lsp::state_handlers::ConsoleInputs;
3435
pub(crate) type TokioUnboundedSender<T> = tokio::sync::mpsc::UnboundedSender<T>;
3536
pub(crate) type TokioUnboundedReceiver<T> = tokio::sync::mpsc::UnboundedReceiver<T>;
3637

37-
// The global instance of the auxiliary event channel, used for sending log
38-
// messages or spawning threads from free functions. Since this is an unbounded
39-
// channel, sending a log message is not async nor blocking. Tokio senders are
40-
// Send and Sync so this global variable can be safely shared across threads.
41-
//
42-
// Note that in case of duplicate LSP sessions (see
43-
// https://github.com/posit-dev/ark/issues/622 and
44-
// https://github.com/posit-dev/positron/issues/5321), it's possible for older
45-
// LSPs to send log messages and tasks to the newer LSPs.
46-
static mut AUXILIARY_EVENT_TX: std::cell::OnceCell<TokioUnboundedSender<AuxiliaryEvent>> =
47-
std::cell::OnceCell::new();
38+
/// The global instance of the auxiliary event channel, used for sending log messages or
39+
/// spawning threads from free functions. Since this is an unbounded channel, sending a
40+
/// log message is not async nor blocking. Tokio senders are Send and Sync so this global
41+
/// variable can be safely shared across threads.
42+
///
43+
/// LSP sessions can be restarted or reconnected at any time, which is why this is an
44+
/// `RwLock`, but we expect that to be very rare. Read locking is not expected to be
45+
/// contentious.
46+
///
47+
/// Note that in case of duplicate LSP sessions (see
48+
/// https://github.com/posit-dev/ark/issues/622 and
49+
/// https://github.com/posit-dev/positron/issues/5321), it's possible for older
50+
/// LSPs to send log messages and tasks to the newer LSPs.
51+
static AUXILIARY_EVENT_TX: RwLock<Option<TokioUnboundedSender<AuxiliaryEvent>>> = RwLock::new(None);
4852

4953
// This is the syntax for trait aliases until an official one is stabilised.
5054
// This alias is for the future of a `JoinHandle<anyhow::Result<T>>`
@@ -400,17 +404,16 @@ impl AuxiliaryState {
400404
// Channels for communication with the auxiliary loop
401405
let (auxiliary_event_tx, auxiliary_event_rx) = tokio_unbounded_channel::<AuxiliaryEvent>();
402406

403-
// Set global instance of this channel. This is used for interacting
404-
// with the auxiliary loop (logging messages or spawning a task) from
405-
// free functions.
406-
unsafe {
407-
if let Some(val) = AUXILIARY_EVENT_TX.get_mut() {
408-
// Reset channel if already set. Happens e.g. on reconnection after a refresh.
409-
*val = auxiliary_event_tx;
410-
} else {
411-
// Set channel for the first time
412-
AUXILIARY_EVENT_TX.set(auxiliary_event_tx).unwrap();
413-
}
407+
// Set global instance of this channel. This is used for interacting with the
408+
// auxiliary loop (logging messages or spawning a task) from free functions.
409+
// Unfortunately this can theoretically be reset at any time, i.e. on reconnection
410+
// after a refresh, which is why we need an RwLock. This is the only place we take
411+
// a write lock though. We panic if we can't access the write lock, as that implies
412+
// the auxiliary loop has gone down and something is very wrong. We hold the lock
413+
// for as short as possible, hence the extra scope.
414+
{
415+
let mut tx = AUXILIARY_EVENT_TX.write().unwrap();
416+
*tx = Some(auxiliary_event_tx);
414417
}
415418

416419
// List of pending tasks for which we manage the lifecycle (mainly relay
@@ -487,17 +490,30 @@ impl AuxiliaryState {
487490
}
488491
}
489492

490-
fn auxiliary_tx() -> &'static TokioUnboundedSender<AuxiliaryEvent> {
493+
fn with_auxiliary_tx<F, T>(f: F) -> T
494+
where
495+
F: FnOnce(&TokioUnboundedSender<AuxiliaryEvent>) -> T,
496+
{
497+
let auxiliary_event_tx = AUXILIARY_EVENT_TX
498+
.read()
499+
.expect("Can lock auxiliary event sender.");
500+
491501
// If we get here that means the LSP was initialised at least once. The
492502
// channel might be closed if the LSP was dropped, but it should exist.
493-
unsafe { AUXILIARY_EVENT_TX.get().unwrap() }
503+
let auxiliary_event_tx = auxiliary_event_tx
504+
.as_ref()
505+
.expect("LSP should have been initialized at least once by now.");
506+
507+
f(auxiliary_event_tx)
494508
}
495509

496510
fn send_auxiliary(event: AuxiliaryEvent) {
497-
if let Err(err) = auxiliary_tx().send(event) {
498-
// The error includes the event
499-
log::warn!("LSP is shut down, can't send event:\n{err:?}");
500-
}
511+
with_auxiliary_tx(|auxiliary_event_tx| {
512+
if let Err(err) = auxiliary_event_tx.send(event) {
513+
// The error includes the event
514+
log::warn!("LSP is shut down, can't send event:\n{err:?}");
515+
}
516+
})
501517
}
502518

503519
/// Send a message to the LSP client. This is non-blocking and treated on a
@@ -510,7 +526,9 @@ pub(crate) fn log(level: lsp_types::MessageType, message: String) {
510526

511527
// Check that channel is still alive in case the LSP was closed.
512528
// If closed, fallthrough.
513-
if let Ok(_) = auxiliary_tx().send(AuxiliaryEvent::Log(level, message.clone())) {
529+
if let Ok(_) = with_auxiliary_tx(|auxiliary_event_tx| {
530+
auxiliary_event_tx.send(AuxiliaryEvent::Log(level, message.clone()))
531+
}) {
514532
return;
515533
}
516534

0 commit comments

Comments
 (0)