lsp4clj icon indicating copy to clipboard operation
lsp4clj copied to clipboard

Complete pending server-initiated request promises on default executor

Open ferdinand-beyer opened this issue 1 year ago • 1 comments

There's a subtle detail on Promesa promises (CompletableFuture): Unless an executor is specified, callbacks will be executed on the thread that completed the promise.

For lsp4clj, this means that users need to be careful when using send-request and coercing the result to a Promesa promise: The code run in handlers such as p/then or p/catch will end up running in a core.async dispatch thread, as the promise is completed from within a go-loop.

Since go blocks are executed in a fixed-size thread pool, one should not block in go blocks. Consequently, users should not block in code that handles the send-request promise, unless they explicitly move it to a different executor.

This is easy to get wrong, especially with Promesa's convenience macros like p/let, which will coerce the PendingRequest to a promise without a dedicated executor. Furthermore, Promesa does not expose CompletionStage.exceptionallyAsync(), i.e. it does not support specifying an executor with p/catch, therefore it is not trivial to ensure all continuation happens in another thread.

This PR attempts to improve this by completing the promise in a thread from the :default executor ((ForkJoinPool/commonPool)), instead of the calling thread.


Some caveats to this approach:

  • There's some overhead even when the promise does not have any continuation handlers (apart from the CancellationException we handle ourselves, which, since it blocks on a send-notification call, should also run on a suitable executor)
  • We don't allow users to customise the executor, but use promesa's default, which is (ForkJoinPool/commonPool). Users might prefer to use a virtual thread executor instead. We could add a :response-executor option for this?

The change to .cljfmt.edn is unrelated, but required for more recent version of cljfmt, as used by Calva, for example. This could be moved to a different PR of course.

ferdinand-beyer avatar Jul 19 '24 12:07 ferdinand-beyer

I think this is mergeable. What do you think about letting users override the executor though?

ferdinand-beyer avatar Jul 31 '24 07:07 ferdinand-beyer

What do you think about letting users override the executor though?

TBH I like it to give a option, if people want to do that, they probably know what they are doing :)

ericdallo avatar Aug 05 '24 12:08 ericdallo

@mainej @ericdallo -- I've added a :response-executor option, which defaults to :default (= ForkJoinPool/commonPool). Users can:

  • specify :current-thread for the old behaviour
  • use :vthread if they running on a recent JVM
  • specify their own executor instance

I've also added tests for that.

ferdinand-beyer avatar Aug 06 '24 15:08 ferdinand-beyer

@ferdinand-beyer could you an entry on CHANGELOG.md please?

ericdallo avatar Aug 08 '24 16:08 ericdallo

Done. Also updated the README.

I sneaked in a change to deps.edn. I hope that this is OK.

The reason is that with my tooling (Calva), I'd like to have the test directory in my classpath, but without running the tests. I normally use two aliases:

  • :test for configuring the test classpath with :extra-paths and :extra-deps, enabled during development
  • :test/run or :kaocha to use the test runner of my choice

Did not want to mess with that here, keeping clojure -M:test to run the tests, so I added a new :dev alias for my needs.

ferdinand-beyer avatar Aug 09 '24 09:08 ferdinand-beyer

That's ok add that alias! Thank you!

ericdallo avatar Aug 10 '24 14:08 ericdallo

Will wait for https://github.com/clojure-lsp/lsp4clj/pull/44 merge to do a release

ericdallo avatar Aug 10 '24 14:08 ericdallo

Will wait for #44 merge to do a release

Makes sense! Will update that one to resolve the conflict and add the :response-executor to the chan-server docstring.

ferdinand-beyer avatar Aug 10 '24 14:08 ferdinand-beyer