Open micahscopes opened 7 months ago
Notes from review with @sbillig and @Y-Nak:
select!
statement containing all the stream handlers)
tracing
for logging, it will help guarantee that logs are correctly orderedsalsa's snapshot mechanism is similar to a rwlock but avoids deadlocks in case of cycles
This is not correct. What Snapshot
does is almost the same as what RwLock
does, so it's possible to introduce a deadlock either way. But it's rather difficult to cause a deadlock as long as we don't try to mutate the db from the salsa tracked function (e.g.,) by sending an event to the main thread to let the thread mutate the db.
E.g.,
#[salsa::tracked]
// Sender needs to implement `Clone` and `Hash` to be an argument of
// a salsa-tracked function, but I ignore the fact for simplicity.
fn rename(db: &dyn Db, rename_event: Event, tx: Sender ) {
// Perform Reneming.
// ...
// Send an event to the main thread, and the main thread will try to mutate the database.
// This might cause a deadlock.
tx.send(Event::SourceTextChanged)
// ...
}
Another possibility for the deadlock is not related to mutability thing, i.e., the deadlock situation might happen even if we only use &Db
in muti-thread settings. But salsa detects this deadlock situation and raises a cycle error, which is, of course, nice.
Please refer to the below links for more information.
This PR introduces significant changes to the language server, focusing on improving concurrency and LSP functionality. The language server has been rewritten based on
tower-lsp
to support concurrent execution of tasks.Here's a detailed overview of the key components and their interconnections:
server
moduleServer
: This struct handles the I/O of the Language Server Protocol (LSP). It receives requests and notifications from the client and sends responses and notifications back to the client. TheServer
struct uses thetower-lsp
crate, which provides a clean and easy-to-use API for implementing LSP servers. Because state is omitted from theServer
struct, it functions essentially as an async task manager for LSP events.MessageSenders
andMessageReceivers
: These structs are generated by a procedural macro inlanguage-server-macro
using information from thetower_lsp::LanguageServer
trait implementation. They are used for communication between theServer
andBackend
via tokio MPSC and oneshot channels.backend
moduleBackend
: This struct has exclusive ownership of language server state, which is stored inLanguageServerDatabase
andWorkspace
members, and mediates all state modifications. Struct methods defined onBackend
are used to handle stream events set up in thestreams
module.Workspace
: This struct has been refined to avoid excessive reliance on mutable references. It represents the current state of the user's workspace, including the open files and their contents. It functions as an index of salsa inputs to be used withLanguageServerDatabase
.LanguageServerDatabase
: This struct provides access to functionality in compiler crates. It implements Salsa'sParallelDatabase
trait to enable parallel salsa queries via theSnapshot
mechanism.functionality
modulefunctionality::streams
: This module sets up streams for handling LSP events on the backend side. It uses thetokio::select!
macro to concurrently listen to multiple streams and handle events as they arrive. Streams allow for declarative control over the order in which LSP events are processed and concise expression of additional reactive behavior. I'm especially happy with the chunked+debounced diagnostics stream and its handler, to give an example.functionality::handlers
: This module contains functions for handling different LSP events. Each function corresponds to a specific LSP event and contains the logic for processing that event and producing an appropriate response.functionality::handlers::Backend::handle_diagnostics
functionality
support implementations of various LSP functionality, including improved go-to definition, hover info and diagnostics functionalityThis architecture is designed to handle concurrent execution of tasks efficiently and to provide a clean separation between the I/O handling (
Server
) and the actual processing of LSP events (Backend
). This separation makes concurrency easier to reason about and allows for parallel execution of expensive tasks. The use of tokio channels and streams allows for efficient handling of concurrent tasks and provides control over the order of task execution.Changes and features
tower-lsp
crateServer
) and language server state (Backend
) via tokio channelson_change
handlerMore changes, following the initial review
select!
loop for mutating handlers and multiple spawns for read-only handlerstracing
for logging to ensure logs are correctly orderedconsole_subscriber
for tokio console supportBackend
exclusive ownership of workspace and database state; no locks neededFunctionality upgrades
Not urgent/maybe
Initial impressions (previously)
Update: the uncertainties below have been addressed by a channel/stream augmentation to the tower-lsp implementation
Managing request handler execution order
tower-lsp
doesn't really provide a way of managing the order of handler execution. Neither doeslsp-server
for that matter. What exactly should this execution dependency graph look like in various cases? It's hard to foresee what this will look like as more and more LSP functionality gets implemented, but it's clear that we need a way to control the order of task execution somehow or another.As a simple example, if I were to rename a file in vscode it could trigger multiple handlers (
did_open
,did_close
,watched_files_did_change
) concurrently.How can we ensure that this pair of LSP events gets handled appropriately? We're not just opening any file, we're opening a renamed file and we need to ensure that the workspace cache is updated to reflect this before executing diagnostics. The
watched_files_did_change
handler ends up being redundant in this case if it gets executed after thedid_open
handler, but in the case where a file gets directly renamed outside of the LSP client, it's still important.In this example, it's not a big deal to check for deleted (renamed) files in both handlers, since those checks are relatively short lived given how infrequently they'd be executed. But it'll be important to ensure that e.g. diagnostics are run carefully, that the salsa inputs are set up correctly before running diagnostics or other complex tasks. It's also important to ensure that expensive tasks aren't triggered redundantly in parallel.
Shared state and deadlocks
Related to the issue of concurrency... How do we manage sharing of the salsa database and inputs cache in a concurrent environment?
In this prototype the salsa db and workspace cache are currently shared via
std::sync::Mutex
locks and the language server client is shared viatokio::sync::Mutex
locks. This works just fine but it requires care not to cause deadlocks. The tokio shared state docs were very helpful for me in understanding this better.Useful info
tower-lsp
andlsp-server
tower-lsp