fix(vscode): stop the LSP restarting on unrelated configuration changes - #6057
Conversation
The extension restarted the language server on every configuration change in the editor and allowed those restarts to overlap, which showed up as `Client got disposed and can't be restarted` plus a stream of `command '...' already exists` errors. Two changes: 1. Filter the configuration event. `extension.ts` subscribed to `workspace.onDidChangeConfiguration`, which fires for every setting in the editor including ones written by other extensions, and restarted unconditionally. It now restarts only when a section the server reads is affected — `sqlmesh` (`projectPaths`, `lspEntrypoint`) or `python.defaultInterpreterPath`. This is why running any python command in a VS Code terminal killed the extension: the Python extension touches its own settings in response, and that was enough to restart the server. It also explains why running a copy of the same interpreter from a different path did not reproduce it, and why a subshell, `su`, `uvx` or a notebook did not either — none of those make the Python extension write a setting. 2. Serialize restarts. `restart()` stops a client and starts another, so concurrent restarts leave the outgoing client's command registrations colliding with the incoming one's and leave requests aimed at a client that has already been disposed. Restarts now run one at a time, with triggers that arrive mid-restart collapsing into a single rerun rather than queueing one restart each. An explicit restart is never downgraded to an automatic one when the two are coalesced. Both helpers are kept free of `vscode` imports so they are covered by `vitest` in the `test-vscode` stage. Fixes SQLMesh#5920 Fixes SQLMesh#5642 Signed-off-by: Adegbite Ayoade <tripleaceme@gmail.com>
|
@cmgoffena13 — this is the #5920 / #5642 one you pointed me at, ready for a look whenever you have time. Both issues turned out to share a root cause: It's still showing zero checks, since the workflow run needs maintainer approval. |
|
@tripleaceme -- I am not that familiar with JavaScript so kind of going on good faith here. Ran this through AI and here's some feedback on a bug: The settings filter looks right. Seems like a successful sign in for Tobiko Cloud stalls it |
Review feedback on SQLMesh#6057. The restart failure handler ran inside the serialized task, and the not_signed_in branch of it signs the user in and then restarts the client again. That restart waited on the run that was still waiting on the handler, so a successful Tobiko Cloud sign-in never completed and the server stayed stopped. The run now records the error and returns; the caller handles it once the run has finished, so the restart triggered by signing in starts a fresh run. The error is claimed by whichever caller reads it first, so callers coalesced into the same run do not each report it. Covered by a test that mirrors the shape of the real restart. That test only works because the simulated run awaits before it reports failure: without that await the re-entrant call lands in the synchronous prefix of the run, before it is recorded as in flight, and no deadlock is possible. Verified it times out against the previous arrangement. Signed-off-by: Adegbite Ayoade <tripleaceme@gmail.com>
…lsp-restart-storm
|
Thanks @cmgoffena13 — the bug is real, and it's fixed in 9e787b5. Good catch; a successful Tobiko Cloud sign-in would indeed have stalled with the server stopped. The chain, confirmed by reproducing it:
The fix: the run now records the error and returns, and the caller handles it after the run has finished. The restart triggered by signing in therefore starts a fresh run. The error is claimed by whichever caller reads it first, so callers coalesced into the same run don't each report the same failure. One thing worth knowing about the test, because my first attempt was wrong in a way that looked fine. The deadlock only happens if the run awaits something before the re-entrant call. The real code has The test lives in |
|
@tripleaceme thanks, glad it was useful. One last thing: for new files, you need to add an SPDX license header per CONTRIBUTING.md ex. |
Signed-off-by: Adegbite Ayoade <tripleaceme@gmail.com>
|
Done in 067d511. The four new files ( |
Description
Fixes #5920. Fixes #5642.
Both issues are the same bug: the extension restarted the language server far more often than it needed to, and let those restarts overlap. That produced the errors reported in both —
Client got disposed and can't be restarted, and a stream ofcommand 'sqlmesh.external_model_update_columns' already exists.1. The LSP restarted on every configuration change in the editor
extension.tssubscribed toworkspace.onDidChangeConfigurationand restarted unconditionally:That event fires for any setting in the editor, including settings written by other extensions. It now restarts only when a section the server actually reads is affected —
sqlmesh(which coversprojectPathsandlspEntrypoint, both of which decide how the server is launched) orpython.defaultInterpreterPath(the server runs inside that interpreter).This accounts for the observations in #5642 that were otherwise hard to explain:
su,uvx, a subshell or a notebook did not reproduce it either — none of those go through VS Code's shell integration, so no setting is written.2. Restarts could overlap
LSPClient.restart()isstop()thenstart(). With a burst of triggers, several of those interleave: the outgoing client's command registrations collide with the incoming client's, and in-flight requests are aimed at a client that has already been disposed — exactly the two error messages in the reports.Restarts are now serialized. Triggers arriving mid-restart collapse into a single rerun rather than queueing one restart each, since all a trigger needs is for a restart to have happened after it. An explicit restart (
sqlmesh.restart, sign-in, format) is never downgraded to an automatic one when coalesced with one.A related finding, deliberately not changed here
initializePython()inutilities/common/python.tshas no call sites, soonDidChangePythonInterpreterEventis never fired and theonDidChangePythonInterpreter(() => restartLsp())subscription inextension.tsis currently inert. Two consequences worth flagging:onDidChangeActiveEnvironmentPathwithout comparing against the path it last saw, which would reintroduce a restart storm.I left it alone rather than widen this PR — happy to follow up if you'd like it wired up with a path comparison, or removed.
Test Plan
New unit tests, both in modules kept free of
vscodeimports sovitestcan cover them in thetest-vscodestage:configurationChange.test.ts— restarts forsqlmesh.projectPaths,sqlmesh.lspEntrypointandpython.defaultInterpreterPath; does not restart foreditor.fontSize,workbench.colorTheme,files.autoSave,python.terminal.activateEnvironment,python.analysis.typeCheckingModeorterminal.integrated.env.linux. The last three are the regression tests for the terminal scenario in SQLMesh extension clashes with running sqlmesh in terminal #5642.coalesceAsync.test.ts— runs immediately when idle; four calls during a run collapse into exactly one rerun; every coalesced caller resolves; a later call still runs after a failure.Ran both CI job commands locally:
I have not reproduced the crash end to end in a live VS Code session —
test-vscode-e2eis still disabled, and the trigger needs a real Python extension writing settings. The causal chain is established from the code path plus the reporter's process-of-elimination in #5642, and the unit tests pin the specific settings that must and must not cause a restart. Worth a sanity check from someone who can reproduce the original crash.Checklist
make styleand fixed any issuespnpm run ci; no Python changed, somake fast-testis unaffected by this PRgit commit -s) per the DCOcc @cmgoffena13 — picking this up per your note on #6054. Checked both issues for assignees and linked PRs beforehand; both were unassigned with nothing linked.