eclipse-xtext / eclipse-xtext/xtext
Deadlock potential in LSP RequestManager (and it's usages in rename & quickfix implementations)
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 831
- Forks
- 330
- Avg merge
- 3d 7h
- Merged PRs (30d)
- 12
Description
It's currently possible to induce a deadlock in the RequestManager by doing the following:
- Read Request [R1] is queued, then executed.
RequestManager.queueis empty again andRequestManager.parallelis executing theRunnableof [R1]. - Write Request [W] is queued, then executed. The implementation of
WriteRequestawaits the completion of all current Requests (which were also marked as cancelled). During this time it blocksRequestManager.queue. - [R1] now starts another Read Request [R2] and waits for its completion.
- [R2] is now queued, but has to wait for [W] to complete, which waits for [R1] to complete, which waits for [R2] to complete. Hence deadlock.
I think this behaviour would actually theoretically be acceptable, though I'd explicitly document, that you may not join the resulting futures unless you can guarantee that you're not already in a Request.
The issue is, that this exact situation can already happen with the way it's being used in the default XText implementations.
Specifically I found it for the Rename request and in the ChangeConverter2, which is used for quickfixes. Here is the code paths explained:
ChangeConverter2
This path is probably the worse one (and the one we actually ran into with our quickfixes, since some of them take quite long to calculate.
The ChangeConverter2 uses ILanguageServerAccess#doRead to access the document and then immediately awaits it:
ILanguageServerAccess#doRead does start another Read Request:
However, this code already potentially runs in a request. E.g. in the default diagnostics path:
Rename Deadlock
The code path for a rename request looks as follows:
Here the requestManager is immediately called and a read request is queued. Inside of that read request, we execute the RenameService2:
Here line 95 and 146 are the issue. They queue a new read request and then await it. So if a write request is queued in the (admitedly short) time frame of the start of the request until the rename service queues its read request, you'll get a dead lock.
(the same applies for PrepareRename)
Suggested fix
ILanguageServerAccess already offers a doSyncRead method, which is identical to the doRead method, with the exception that it does NOT queue a new Read Request:
vs
By using that method, we can now prevent a deadlock.
For the Rename path, this should be quite obviously fine (as it's guaranteed to run inside a read request already, so it doesn't interfere with the scheduling done via the RequestManager).
For the ChangeConverter2 it's less obvious for me. In the quickfix path, it should be required, but I am not sure if there are other code paths where the asynchronous request is necessary.
I can't say I understand why this was ever done in an asynchronous manner in the first place, tbh. I haven't come up with a scenario that would require or desire that.
Maybe you still know the reasoning behind it, and whether it's fine to also change it to a synchronous method?
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with RequestManager and the doRead/doSyncRead implementations in LanguageServerImpl, then trace the linked ChangeConverter2, RenameService2, and quickfix paths. Verify how nested reads interact with queued writes, and confirm that rename, prepare-rename, and quickfix flows no longer create the described wait cycle while preserving the intended request semantics.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- backend-api-design, devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100