Skip to content

Fixed a crash on completions with auto-imports racing with DidClose notification#2905

Draft
Andarist wants to merge 14 commits intomicrosoft:mainfrom
Andarist:fix/crash-autoimports-closed-file
Draft

Fixed a crash on completions with auto-imports racing with DidClose notification#2905
Andarist wants to merge 14 commits intomicrosoft:mainfrom
Andarist:fix/crash-autoimports-closed-file

Conversation

@Andarist
Copy link
Contributor

@Andarist Andarist commented Feb 25, 2026

Root cause behind the crash:

  • the completion request is split into 2 phases, a sync one and async one
  • in the async one the completion request might be retried on ErrNeedsAutoImports
  • but in between the first one and the second one a project change might come in (like DidClose or some other change)
  • currently, the retried request flushes the pending changes (like DidClose) and resends the adjusted request
  • that creates a mismatched code expectation because the closed file gets removed from the specifierCache and the request panics

There are 2 fixes in this PR:

  • a request should always be served for the change.RequestedFile. That entry isn't removed from the specifierCache. It's not up to the server to cancel a pending request even if the state changed.
  • a 2-phase request should use a consistent snapshot for the whole request to avoid its steps using different state and ultimately leading to confusing/erroneous results (although usually a small deviation there would usually be somewhat acceptable... I think it's still better to use a consistent state, and I think it's consistent with Acquire snapshots synchronously while blocking other requests/notifications #2765 ). For that reason, a snapshot gets acquired in the sync phase and reused by the async phase - achieving consistency.
Configuration close before completion close during async unopened file snapshot freezing (DidChange)
No fixes CRASH (no response) CRASH (nil deref panic) PASS FAIL (someVar missing)
Registry fix only PASS PASS PASS FAIL (someVar missing)
Snapshot freezing only CRASH (no response) PASS PASS PASS
Both fixes PASS PASS PASS PASS

fixes #2882

@andrewbranch
Copy link
Member

I have a branch with this in it as well that I passed off to @gabritto as she was working on fixing a related crash. I think my branch wasn't quite right or was insufficient; I'm not sure what the status is. However, in mine, after we clone the snapshot for auto-imports, we check to see if the session can adopt the new one as the latest snapshot - I think we'll want that functionality.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't really test this through fourslash so I had to resort to rolling a new test file that could use the NewLSPClient directly with a greater control over the performed actions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we write a test using session directly? I did this recently in https://github.com/microsoft/typescript-go/blob/main/internal/project/snapshot_test.go#L125 to test things related to the timing of changes being processed.
We may need a better testing setup for those kinds of bugs long-term, but maybe right now using session for testing could do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I planned to add a test using the session directly later on. I just wanted to have more real-life test cases for this too and focused on those here.

@Andarist Andarist marked this pull request as ready for review February 25, 2026 22:51
}
return nil, nil, fmt.Errorf("no project found for URI %s", uri)
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Andarist Here's how I was doing the work of re-using the snapshot + extra information computed in an old PR:
https://github.com/microsoft/typescript-go/pull/1871/changes#diff-b8a62f01d8c9b6ed8df3c61e62fe4b694885da67eadb682a4117ce2cb62b1bf9R754-R784

But the tricky part would be to figure out whether to release the snapshot or not, I'll have to think about that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for the snapshot lifetime handling, we can (1) invoke snapshot.dispose() inside snapshot.Deref() when the count reaches 0, and (2) make sure both session and the server call .Ref() and .Deref() when they acquire/release it. So if you did the whole thing I linked above of enqueuing of a snapshot with auto-imports to see if session can adopt it, that would come with a call to .Ref()/.Deref(), etc.

@Andarist Andarist requested a review from gabritto March 2, 2026 13:01
@Andarist Andarist marked this pull request as draft March 2, 2026 17:04
@Andarist Andarist force-pushed the fix/crash-autoimports-closed-file branch from 1c0137d to bdf37cf Compare March 3, 2026 09:39
@Andarist Andarist force-pushed the fix/crash-autoimports-closed-file branch from bdf37cf to 0c47159 Compare March 3, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Probable nil entry from specifier cache in auto-import completions

3 participants