Acquire snapshots synchronously while blocking other requests/notifications#2765
Acquire snapshots synchronously while blocking other requests/notifications#2765andrewbranch merged 6 commits intomicrosoft:mainfrom
Conversation
internal/lsp/server.go
Outdated
| if req.ID != nil { | ||
| s.pendingClientRequestsMu.Lock() | ||
| delete(s.pendingClientRequests, *req.ID) | ||
| s.pendingClientRequestsMu.Unlock() | ||
| } |
There was a problem hiding this comment.
This deletion can now happen before doAsyncWork completes (or even runs), which I think means that requests can't be cancelled.
Probably this needs to be duplicated, or done in an IIFE so it can be deferred?
There was a problem hiding this comment.
Like:
func() {
defer func() {
s.pendingClientRequestsMu.Lock()
defer s.pendingClientRequestsMu.Unlock()
delete(s.pendingClientRequests, *req.ID)
}()
if doAsyncWork, err := s.handleRequestOrNotification(requestCtx, req); err != nil {
handleError(err)
} else if doAsyncWork != nil {
go func() {
if lsError := doAsyncWork(); lsError != nil {
handleError(lsError)
}
}()
}
}()|
I do feel like servicing every out-of-date request, especially for the most typical kind of client (e.g. editors) is not ideal - but probably something we can pull back on later. |
Implements suggestion from microsoft#2765 (comment) This ensures that the request is tracked in pendingClientRequests until both the sync and async portions of the work complete, preventing the entry from being deleted while async work is still running.
There was a problem hiding this comment.
Pull request overview
This PR fixes a race condition in the LSP server where snapshots were being acquired asynchronously, allowing file change notifications to be processed before in-flight requests had secured their snapshots. The fix restructures all LSP handlers to use a two-phase pattern: synchronous work (primarily snapshot acquisition) executes in the dispatch loop, blocking other requests, while the actual request processing runs asynchronously in a goroutine using the captured snapshot.
Changes:
- Modified handler signature from
func(...) errortofunc(...) (func() error, error)to separate sync and async work - Removed
isBlockingMethodlogic since all handlers now execute sync work in the dispatch loop - Updated all handler registration functions to support the new two-phase pattern
|
Sorryyyy |
Fixes #2690
Imagine a sequence of
When we saw the first change in the server dispatch loop, we would hold processing of the rest of the queue while that change gets pushed. Then we would resume and call
s.handleRequestOrNotificationwith the inlay hint one, and (without waiting for it to complete) handle the next change, holding any more pending requests. The problem is thats.handleRequestOrNotificationdidn't get passed a snapshot; it looked at the request method, looked up the correct handler in the server, which would then get acquire a snapshot (in the form of a LanguageService) and pass it to the handler. So there was a tiny bit of intermediate time that was available for the next change notification to sneak in.With this PR, handlers now have a sync portion that gets executed serially in the dispatch loop, which they use to secure a snapshot, and return a function of async work that uses the snapshot.
The downside of this is that if a client isn’t diligent about canceling old requests, we have no choice but to manifest a program for each one, even if we've built up changes that make them stale. The LSP spec says as much:
So I think this is the right approach.