-
Notifications
You must be signed in to change notification settings - Fork 60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement proper client request support #134
Conversation
Once hooked up, this should allow us to route responses from clients back to the `Printer` for handling.
Since the Printer itself doesn't do much beyond check whether the server is initialized, perhaps we can combine `Router` and `Printer` together into a single struct.
This should more accurately reflect what this object does, since it no longer just prints notifications but also routes requests and responses.
This is to be more consistent with the body of send_notification()`.
This change ensures that we only ever insert the request ID into `pending_requests` if it was actually successfully sent over the channel.
This is because `dashmap` indirectly makes use of standard library features which requires a new version of Rust. We also revert the changes from #111 to see if the `rustfmt` inconsistencies have been fixed in Rust 1.40.
Having `MessageSender` be a newtype is less important than `MessageStream` because it is not intended to be exposed to downstream users as a public API, and removing the manual `Sink` implementation cleans up quite a bit of unneeded boilerplate.
This should adhere to the upstream LSP specification, which defines the JSON-RPC error code `-32002` to mean "server not initialized."
Sorry for the massive pull request, @icsaszar! Feel free to review this at your leisure. |
Local tests of However, please note that this branch hasn't yet been tested with multiple concurrent client requests, so it is not definitively proven yet that awaiting multiple concurrent client responses simultaneously does not block the executor. |
The request/response routing works roughly as follows:
|
The lockless concurrent hashmap was chosen for the sake of better performance when compared to an We could set up a benchmark for both implementations and count the messages per second with a certain number of concurrent client requests all firing at once, if we were interested in finding out. But note that this might require a little extra tweaking since |
d6f5df7
to
fc5ac6a
Compare
Very nice and clean implementation! The PR might seem big and scary, but as presented by the diff viewer in Clion everything was easy to follow. The only thing that comes to my mind is using a tokio::sync::oneshot channel instead of polling the map. The changes in the client would look something like this: async fn send_request<R>(&self, params: R::Params) -> Result<R::Result>
{
// ...
let (tx, rx) = tokio::sync::oneshot::channel();
self.pending_requests.insert(id, tx);
let response = rx.await.unwrap();
// ...
} pub(super) fn new(
sender: Sender<String>,
mut receiver: Receiver<Output>,
initialized: Arc<AtomicBool>,
) -> Self {
let pending_requests = Arc::new(DashMap::default());
let pending = pending_requests.clone();
tokio::spawn(async move {
while let Some(response) = receiver.next().await {
match response.id() {
Id::Num(id) => {
if let Some(tx) = pending.remove(id) {
tx.send(response);
} else {
error!("received response from client with no matching request"),
}
}
_ => error!("received response from client with non-numeric ID"),
}
}
});
// ...
} But since currently there's no objective way to compare the 2 options (performance vs memory usage), by suggestion would be to go forward with the current version, maybe open an issue for this suggestion alongside another issue to investigate testing and benchmarking. |
Thanks for the review! I like your I don't really know how efficient |
This changes the `pending_requests` map to use a oneshot async channel to simplify the code and better capitalize on the async runtime. The previous implementation used a `loop` to poll the `pending_requests` hashmap and explicitly yield execution back to the executor by calling `tokio::task::yield_now()` if the client's response hasn't been received yet by the server. This should hopefully be a bit more efficient, given that `futures::channel::oneshot::channel()` is presumably using the underlying runtime's task scheduler more efficiently. This assumption hasn't been tested with dedicated benchmarks, however, but the code is much easier to reason about, though. Original suggestion here: #134 (comment)
This changes the `pending_requests` map to use a oneshot async channel to simplify the code and better capitalize on the async runtime. The previous implementation used a `loop` to poll the `pending_requests` hashmap and explicitly yield execution back to the executor by calling `tokio::task::yield_now()` if the client's response hasn't been received yet by the server. This should hopefully be a bit more efficient, given that `futures::channel::oneshot::channel()` is presumably using the underlying runtime's task scheduler more efficiently. This assumption hasn't been tested with dedicated benchmarks, however, but the code is much easier to reason about, though. See original suggestion here: #134 (comment) We also add a private `RequestMap` type alias to help `rustc` infer the type of the `pending_requests` map inside the `tokio::spawn()` async block.
This changes the `pending_requests` map to use a oneshot async channel to simplify the code and better capitalize on the async runtime. The previous implementation used a `loop` to poll the `pending_requests` hashmap and explicitly yield execution back to the executor by calling `tokio::task::yield_now()` if the client's response hasn't been received yet by the server. This should hopefully be a bit more efficient, given that `futures::channel::oneshot::channel()` is presumably using the underlying runtime's task scheduler more efficiently. This assumption hasn't been tested with dedicated benchmarks, however, but at least the code is much easier to reason about. See original suggestion here: #134 (comment) We also add a private `RequestMap` type alias to help `rustc` infer the type of the `pending_requests` map inside the `tokio::spawn()` async block.
This changes the `pending_requests` map to use a oneshot async channel to simplify the code and better capitalize on the async runtime. The previous implementation used a `loop` to poll the `pending_requests` hashmap and explicitly yield execution back to the executor by calling `tokio::task::yield_now()` if the client's response hasn't been received yet by the server. This should hopefully be a bit more efficient, given that `futures::channel::oneshot::channel()` is presumably using the underlying runtime's task scheduler more efficiently. This assumption hasn't been tested with dedicated benchmarks, however, but at least the code is much easier to reason about. See original suggestion here: #134 (comment) We also add a private `RequestMap` type alias to help `rustc` infer the type of the `pending_requests` map inside the `tokio::spawn()` async block.
This changes the `pending_requests` map to use a oneshot async channel to simplify the code and better capitalize on the async runtime. The previous implementation used a `loop` to poll the `pending_requests` hashmap and explicitly yield execution back to the executor by calling `tokio::task::yield_now()` if the client's response hasn't been received yet by the server. This should hopefully be a bit more efficient, given that `futures::channel::oneshot::channel()` is presumably using the underlying runtime's task scheduler more efficiently. This assumption hasn't been tested with dedicated benchmarks, however, but at least the code is much easier to reason about. See original suggestion here: #134 (comment) We also add a private `RequestMap` type alias to help `rustc` infer the type of the `pending_requests` map inside the `tokio::spawn()` async block.
Added
Printer
(now renamedClient
).window/showMessageRequest
workspace/configuration
workspace/workspaceFolders
Changed
Printer
toClient
.LanguageServer
trait method type signatures to referenceclient
andClient
.Client::send_notification()
toClient::send_custom_notification()
. This is to avoid a name clash with a new internal method and to be more explicit during usage.workspace/applyEdit
request toasync fn
which returns anApplyWorkspaceEditResponse
.Fixed
Closes #13 and closes #128.
CC @icsaszar