Complete pending server-initiated request promises on default executor
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
CancellationExceptionwe handle ourselves, which, since it blocks on asend-notificationcall, 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-executoroption 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.
I think this is mergeable. What do you think about letting users override the executor though?
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 :)
@mainej @ericdallo -- I've added a :response-executor option, which defaults to :default (= ForkJoinPool/commonPool). Users can:
- specify
:current-threadfor the old behaviour - use
:vthreadif they running on a recent JVM - specify their own executor instance
I've also added tests for that.
@ferdinand-beyer could you an entry on CHANGELOG.md please?
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:
:testfor configuring the test classpath with:extra-pathsand:extra-deps, enabled during development:test/runor:kaochato 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.
That's ok add that alias! Thank you!
Will wait for https://github.com/clojure-lsp/lsp4clj/pull/44 merge to do a release
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.