Skip stale overlay paths in markProjectsAffectedByConfigChanges - #4790
Skip stale overlay paths in markProjectsAffectedByConfigChanges#4790blixt wants to merge 1 commit into
Conversation
The config file registry caches config file lookups (configFileNames) for open files and removes an entry when the file's Closed event is processed during a snapshot clone. The session-level overlay map, however, is updated eagerly by flushChangesLocked before the clone runs. If a snapshot update is interrupted between those two steps - e.g. by a panic recovered at the request boundary (Server.recover), which leaves the session alive - the overlay is gone but the registry entry survives. The next config file create/change/delete in an ancestor directory of the stale entry (or an excessive-watch-event cache invalidation) then reports the stale path in changeFileResult.affectedFiles, and markProjectsAffectedByConfigChanges crashes dereferencing the missing overlay - inside Session.updateSnapshot while snapshotMu is held, so a host that recovers the panic is left with a permanently wedged session. Skip affectedFiles paths that no longer have an overlay: the producers of affectedFiles have already dropped the stale cache entry, and a file that is no longer open has no default project to recompute, so the registry self-heals. Open files in the same batch are still recomputed and affected projects are still marked dirty. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
I don't understand Claude's addiction with adding entirely new test files.
There's a perfectly good test file already
There was a problem hiding this comment.
I'm very sorry this made it to your attention, the intent was actually for me to draft it and give it a human touch (the agent took me too literally when I said "so we can create a PR later"). Let me know if I should delete it and resend it when it's actually ready.
There was a problem hiding this comment.
Yes, we already have a test file for this code, projectcollectiobuilder_test.go
There was a problem hiding this comment.
Pull request overview
Prevents stale closed-file registry entries from crashing snapshot updates during config changes.
Changes:
- Skips affected paths without overlays.
- Adds a regression test reproducing the interrupted-update state.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
internal/project/projectcollectionbuilder.go |
Guards stale overlay lookups. |
internal/project/projectcollectionbuilder_staleoverlay_test.go |
Tests config reload behavior with a stale overlay path. |
| if logger != nil { | ||
| logger.Logf("Skipping default project recomputation for %s: no overlay (file is no longer open)", path) | ||
| } | ||
| continue |
Problem
We embed
internal/projectin a long-running server and see recurring fatal crashes with this stack (currently several per day across our fleet):The crash site is:
affectedFilesis keyed by the config file registry'sconfigFileNamescache, and the unchecked map lookup assumes every cached path is still overlay-backed. When a stale entry survives for a file that is no longer open, the lookup returns a nil*OverlayandFileName()panics.Root cause
The two states advance at different times:
flushChangesLocked→overlayFS.processChanges, when pending changes are flushed.configFileRegistryBuilder.didCloseFile, which deletes theconfigFileNamesentry) only happens later, when the snapshot clone processes that flush'sFileChangeSummary.If a snapshot update is interrupted between those two steps, the Closed event is consumed forever while the registry entry survives. The most realistic interruption is a panic during the snapshot update that gets recovered at the request boundary —
lsp.Server.recoverdoes exactly this and keeps the session alive (as does any embedding host with per-request recovery).After that, the very next
tsconfig.jsoncreate/change/delete in an ancestor directory of the stale entry — or any excessive-watch-eventinvalidateCachepass, which reports every cached path — puts the stale path intoaffectedFilesand crashes. Worse, the panic fires insideSession.updateSnapshotwhilesnapshotMuis held, so a host that recovers it is left with a permanently wedged session (every subsequent request blocks on the leaked lock).Fix
Skip
affectedFilespaths that no longer have an overlay. This is safe and self-healing rather than signal-hiding:affectedFiles(invalidateCacheand the created/deleted-config handling inDidChangeFiles) have already removed the staleconfigFileNamesentry by the time the consumer runs, so skipping leaves no stale state behind — the registry converges back to a consistent state on the first config event instead of crashing.ensureConfiguredProjectAndAncestorsForFileon it would operate on a closed file.affectedProjectsmarking is untouched, so config-change invalidation behavior is preserved.Test
TestMarkProjectsAffectedByConfigChangesStaleOverlayis a white-box regression test that opens two files under one tsconfig, severs a snapshot update between the overlay-map commit and the registry bookkeeping using the session's own flush machinery (queue a Close, callflushChangesLocked, never clone — the exact state a recovered mid-update panic leaves), then changes the tsconfig.main).targetchange takes effect), the open file's config lookup is recomputed, and the stale entry is gone.go test ./internal/project/...and-raceon the new test pass.Disclosure
This patch was authored with AI assistance (Claude Code); I have read and understood the change and will respond to review feedback myself.