feat(python-setup): recover from a constraint conflict with a Retry-as-DB-Connect fallback - #2181
Merged
rugpanov merged 9 commits intoSep 9, 2026
Conversation
…s-DB-Connect fallback *Why* The uv-native "set up Python environment" Full preset pins the cluster's dependencies, so uv sync can fail when those pins conflict with the user's own dependencies. Today that surfaces as the generic E_PROVISION toast, leaving the user to work out on their own that dropping the pins (the DB Connect preset) would resolve it. *What* - Add the distinct E_PROVISION_CONFLICT code to the setup-local result model (the CLI reserves it for a genuine pin-vs-local conflict; a generic sync failure stays E_PROVISION) with dedicated, actionable copy. - On a Full-preset conflict, replace the generic toast with three buttons: "Retry as DB Connect setup" (re-runs with --no-constraints, keeping matched Python + databricks-connect), "Open pyproject.toml", and "Show Logs". Other provision failures are unchanged. - Add a run (in-process callback) arm to PythonSetupErrorAction so those runtime-stateful buttons reuse the existing showError plumbing; extract runGuarded/runResolved so the retry re-enters the flow with the dbconnect preset without re-prompting the compute or the preset picker. - A conflict on a run that already skipped constraints falls back to the generic doc-link handling (no nonsensical, looping retry). The conflict is never report-worthy (it is the user's own dependencies). *Verification* - test:unit — 1110 passing (the 2 failures are the stale bundled-CLI version check, a local worktree artifact; CI fetches the pinned binary and passes). - New unit coverage: conflict copy + fallback doc link, run-action dispatch, openProjectFile, and the controller conflict->retry flow (re-run drops the pins and adopts, dbconnect telemetry, open pyproject, reportOffered=false, and the non-Full fallback). - tsc + eslint clean on changed files. Co-authored-by: Isaac <no-reply@databricks.com>
Contributor
Author
|
🤖 Integration tests running for |
…metry, and cover retry edge cases *Why* Multi-source review (Claude + Codex) flagged: E_PROVISION_CONFLICT silently dropped the soft "report to databricks/environments" log pointer that the same conflict carried while it was E_PROVISION; the retry re-entrancy and no-loop guarantees were untested; and the retry's `initial` telemetry trigger deserved a recorded rationale. *What* - Extend the constraints-report log pointer in formatSetupFailureDetail to cover E_PROVISION_CONFLICT (a pins-vs-local conflict is exactly where the published constraints may be at fault, and it carried the pointer before the CLI split the code out). Button behaviour is unchanged (still no report button). - Add tests: the DB Connect retry failing again gets the generic action, never a second Retry (no loop); and a Retry click while another run is in flight coalesces onto it rather than spawning a concurrent CLI process. Coalescing (not queuing) is deliberate — a queued dbconnect retry could clobber a run about to succeed. - Document why a constraint-conflict retry reports trigger `initial` (the failed Full run never marked the project ready; the `dbconnect` setupPreset distinguishes it). *Verification* - test:unit — 1113 passing (the 2 failures are the stale bundled-CLI version check, a local worktree artifact). New/updated specs for the log pointer, the no-loop path, and the in-flight coalescing all green. - tsc + eslint + prettier clean on changed files. Co-authored-by: Isaac <no-reply@databricks.com>
Contributor
Author
|
🤖 Integration tests running for |
…et-up project *Why* Re-review (Claude) flagged a footgun: the constraint-conflict toast lingers in the Notifications Center, so its "Retry as DB Connect" can be clicked long after a separate run has since provisioned the project. With no run in flight, that stale click would re-run with --no-constraints and silently downgrade the working Full environment. This is the first toast action that re-enters the mutating setup flow, so it introduces the hazard. *What* - The Retry callback now bails when the project is already set up (`readyRoots.has(cwd)`), so a stale click cannot re-provision. The immediate (intended) retry still runs — the failed Full run never marked the project ready. This also keeps the `trigger: "initial"` note accurate: the retry only ever runs while not ready. *Verification* - test:unit — 1114 passing (the 2 failures are the stale bundled-CLI version check, a local worktree artifact). New spec: a stale Retry after a later success does not re-run the CLI. - tsc + eslint + prettier clean on changed files. Co-authored-by: Isaac <no-reply@databricks.com>
Contributor
Author
|
🤖 Integration tests ❌ failed for |
*Why*
Local testing surfaced two UX rough edges on the new conflict toast: the retry
label overflowed the VS Code notification's button row (truncating a button),
and it read a little jargon-y on its own. Three buttons also crowded the row.
*What*
- Rename the retry action to "Retry DB Connect setup" — it mirrors the setup
picker's "DB Connect setup" tier 1:1 (what the user would otherwise choose),
and avoids CLI-internal wording ("constraints") that appears nowhere the user
can see.
- Add an `includeShowLogs` option to the `showError` seam and pass `false` for
the recoverable conflict, so its toast shows just
[Retry DB Connect setup] [Open pyproject.toml] — a self-service row whose own
buttons are the remedy. The output channel is still written and revealed, so
the log stays reachable. Every other error toast keeps its Show Logs button.
*Verification*
- test:unit — 1119 passing. New/updated specs: `showError` omits Show Logs when
`includeShowLogs` is false; the conflict path passes it while an ordinary
failure does not; label assertions updated.
- tsc + eslint + prettier clean on changed files.
Co-authored-by: Isaac <no-reply@databricks.com>
Contributor
Author
|
🤖 Integration tests running for |
rugpanov
marked this pull request as ready for review
September 8, 2026 16:01
*Why* To measure how often the "Retry DB Connect setup" button is clicked and how often that retry then succeeds — reusing the existing 1:1 python_env.setup.attempt → python_env.setup.result pairing rather than adding a new event. The only gap was that a conflict retry is indistinguishable from a first-time DB Connect pick: both record trigger "initial" + setupPreset "dbconnect". A discriminator on the trigger dimension closes it. *What* - Add "conflict_retry" to PythonSetupRunTrigger; update the trigger comment on python_env.setup.attempt and the PythonSetupAttempt doc to describe it. - Thread an explicit trigger through runResolved → recordAttempt. The normal path still derives readyRoots.has(cwd) ? "rerun" : "initial"; the retry re-entry in buildConflictRecoveryActions passes "conflict_retry". No structural change downstream — the paired result event already carries the outcome. Resulting metrics: (a) clicks = attempts with trigger=conflict_retry; (b) success = the paired result with outcome=ok; rate = (b)/(a), with the failure breakdown free from the same result event. The two no-op click paths (stale / guard-coalesced) record no attempt by design, so (a) stays the true denominator. *Verification* - test:unit — 1119 passing. The conflict→retry test now asserts the retry attempt records trigger "conflict_retry" and the paired result its outcome. - tsc + eslint + prettier clean on changed files. Co-authored-by: Isaac <no-reply@databricks.com>
Contributor
Author
|
🤖 Integration tests ❌ failed for |
*Why* The constraint-conflict "Retry DB Connect setup" recovery re-ran with --no-constraints, but the conflict fails in the provision phase *after* the runtime pins were already merged into pyproject.toml (diskMutated: true). --no-constraints only stops the retry from re-adding the pins; it does not remove the conflicting ones the first run already wrote, so the retry would resolve against the same conflict. *What* - Add an injected restoreProjectFile(projectRoot, backupPath) seam; the production wiring copies the CLI's backupPath over <projectRoot>/pyproject.toml (no new temp backup — reuses PythonSetupResult.backupPath). - Thread result.backupPath into buildConflictRecoveryActions. On Retry, restore the file first, then re-run runResolved(..., "dbconnect", "conflict_retry"). - Gate the recoverable-conflict path on a present backupPath: with no backup there is nothing to roll back to, so Retry is not offered and the run falls through to the generic doc-link action. - Restore runs inside runGuarded, so a failed restore throws before the CLI spawns or a retry attempt is recorded; showError's action-error handling logs it. Trim the now-duplicated rationale comments to a single home. *Verification* - New/updated unit tests: restore-before-CLI ordering, skipConstraints still set, successful retry adopts + marks ready, restore failure prevents the second CLI run and retry telemetry, missing backupPath does not offer Retry, and the wiring copies the backup over pyproject.toml. - Full unit suite green (1123 passing); tsc, ESLint, Prettier clean on changed files. Co-authored-by: Isaac <no-reply@databricks.com>
Contributor
Author
|
🤖 Integration tests running for |
*Why* Review of the constraint-conflict recovery surfaced two hardening gaps in the backup restore and two doc inaccuracies. The restore behavior itself (a full revert to the pre-merge backup) is kept as designed. *What* - restoreProjectFile now writes atomically: copy the backup to a temp sibling, then rename over pyproject.toml, so an interrupted/failed copy can't leave the project file truncated (temp cleaned up on error). - Reject a backupPath that resolves outside the project before copying, so a malformed CLI result can't land an arbitrary file as pyproject.toml. - Telemetry: clarify that conflict_retry counts recovery runs that started, not raw clicks (a failed-restore/coalesced/stale click records none). - README: note E_PROVISION_CONFLICT follows the same no-report policy as E_PROVISION and points at the restore-and-retry recovery. *Verification* - New wiring unit tests: atomic restore leaves no temp artifact; refuses an out-of-project backup and leaves pyproject.toml untouched; a failed copy leaves the file untouched with no temp left. Existing copy test still green. - Full unit suite green (1126 passing); tsc, ESLint, Prettier clean on changed files. Co-authored-by: Isaac <no-reply@databricks.com>
Contributor
Author
|
🤖 Integration tests ❌ failed for |
*Why* A second review round flagged that the recoverable-conflict gate keyed on `!== undefined`, so a (contract-forbidden) empty backupPath would still offer a "Retry DB Connect setup" button that can only throw — the restore rejects an empty/at-root path — while hiding "Show Logs". Truthiness is strictly safer. *What* - Gate the recovery on a truthy backupPath, so an empty string falls through to the ordinary doc-link handling (like a missing backup) instead of a broken Retry toast. - Make PythonSetupRunTrigger a type-only import (CODE_CONVENTIONS §7, new code). *Verification* - New unit test: an E_PROVISION_CONFLICT result with backupPath "" gets the generic "Resolve dependency conflicts" doc link and keeps Show Logs. - Full unit suite green (1127 passing); tsc, ESLint, Prettier clean on changed files. Co-authored-by: Isaac <no-reply@databricks.com>
Contributor
Author
|
🤖 Integration tests ✅ passed for |
misha-db
approved these changes
Sep 9, 2026
…ker' into rugpanov/python-setup-conflict-fallback # Conflicts: # packages/databricks-vscode/src/python-setup/utils/errorMessages.ts
Contributor
Author
|
🤖 Integration tests running for |
rugpanov
merged commit Sep 9, 2026
0a9ac53
into
rugpanov/python-setup-preset-picker
6 of 7 checks passed
rugpanov
added a commit
that referenced
this pull request
Sep 10, 2026
…s-DB-Connect fallback (#2186) ## Why The uv-native "set up Python environment" **Full** preset pins the cluster's dependencies, so `uv sync` can fail when those pins conflict with the user's own dependencies. The CLI now emits a distinct **`E_PROVISION_CONFLICT`** for that case (shipped in **CLI v1.16.0**, already bundled on `main`). Rather than the generic `E_PROVISION` toast ("uv could not resolve the project's dependencies… adjust your dependencies"), offer a one-click recovery: re-run as **DB Connect** (`--no-constraints`), which drops the conflicting cluster-dependency pins while keeping matched Python + databricks-connect. ## What - **`E_PROVISION_CONFLICT`** — a distinct result-model error code for a genuine pin-vs-local conflict (a generic sync failure stays `E_PROVISION`), with its own copy: *"The cluster dependencies conflict with your local dependencies, so uv sync couldn't resolve the environment."* - **Recovery UX** (Full-preset conflict only) — the generic toast is replaced with **Retry DB Connect setup** and **Open pyproject.toml**. "Show Logs" is dropped from this self-service toast (the channel is still revealed). - **Restore-then-retry** — the conflict fails *after* the runtime pins were merged into `pyproject.toml` (`diskMutated: true`, pre-merge file saved to the CLI's `backupPath`). So **Retry** first restores that backup, then re-runs with the `dbconnect` preset — `--no-constraints` alone would only stop the retry from re-adding pins, not remove those already on disk. The restore is atomic (temp sibling + rename) and refuses a `backupPath` resolving outside the project. When `backupPath` is absent/empty (e.g. a no-op-merge re-run), Retry isn't offered and the run falls back to the ordinary doc-link handling. - **Guards** — `runGuarded`/`runResolved` let the retry re-enter with the `dbconnect` preset directly (no re-prompt), coalesced so a click can't race an in-flight run; `dbconnect` skips constraints so the retry can't loop; a stale toast's Retry no-ops once the project is ready. - **Telemetry** — a `conflict_retry` value on the existing `python_env.setup.attempt` `trigger` dimension marks recovery runs, paired with the `python_env.setup.result` outcome for the recovery success rate. No new event. A conflict is never report-worthy (it's the user's own dependencies). ## Live against the shipped CLI Unlike when this was first written, **CLI v1.16.0 (on `main`) emits `E_PROVISION_CONFLICT`**, so the recovery path is now exercised end-to-end, not dormant. Note: the CLI only populates `backupPath` on the *first* merge; a re-run over an already-merged `pyproject.toml` is a no-op merge and returns an empty `backupPath`, in which case the run correctly falls through to the generic path (the truthy-`backupPath` guard). ## Testing - `test:unit`: new coverage for the conflict copy + fallback doc link, the `run`-action dispatch, `openProjectFile`, the controller conflict→retry flow (restore-before-retry ordering, `--no-constraints` + adopt on success, `conflict_retry` telemetry paired with its outcome, open pyproject at the run's cwd, `reportOffered: false`, missing/empty-`backupPath` and non-Full fallbacks, restore-failure aborts without a retry attempt), and the atomic/in-project restore wiring. - `tsc` + `eslint` + `prettier` clean on changed files. Supersedes #2181, which merged into the (now-defunct) preset-picker branch rather than `main`. This pull request and its description were written by Isaac. Co-authored-by: Isaac <no-reply@databricks.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The uv-native "set up Python environment" Full preset pins the cluster's
dependencies, so
uv synccan fail when those pins conflict with the user's owndependencies. Today that surfaces as the generic
E_PROVISIONtoast ("uv couldnot resolve the project's dependencies… adjust your dependencies"), which leaves
the user to work out on their own that dropping the pins — the DB Connect
preset (
--no-constraints) — would resolve it while keeping matched Python +databricks-connect.
What
E_PROVISION_CONFLICT— a distinct result-model error code for a genuinepin-vs-local conflict (a generic sync failure stays
E_PROVISION), with itsown actionable copy: "The cluster dependencies conflict with your local
dependencies, so uv sync couldn't resolve the environment."
two buttons: Retry DB Connect setup and Open pyproject.toml. "Show
Logs" is dropped from this toast (it's self-service — the channel is still
revealed), and every other provision failure is unchanged.
merged into
pyproject.toml(diskMutated: true, pre-merge file saved to theCLI's
backupPath). So Retry first restores that backup, then re-runs withthe
dbconnectpreset (--no-constraints) —--no-constraintsalone onlystops the retry from re-adding pins, it wouldn't remove the ones already on
disk. The restore is atomic (temp sibling + rename) and refuses a
backupPathoutside the project. With no
backupPath, Retry isn't offered and the runfalls back to the ordinary doc-link handling.
runaction arm —PythonSetupErrorActiongains an in-process-callbackarm alongside
url/command, so the two runtime-stateful buttons reuseshowError's existing plumbing (label de-dup, log reveal);showErrorgains anincludeShowLogsoption to drop the trailing button for this toast.runGuarded/runResolvedextracted fromrunSetup, so the retryre-enters the flow with the
dbconnectpreset directly — no re-prompt of thecompute or the preset picker, and guarded so a click can't race an in-flight
run (and the restore only fires when the retry actually runs). Because
dbconnectskips constraints, the retry can't itself surface a recoverableconflict (no loop), and a stale toast's Retry no-ops once the project is ready.
conflict_retryvalue on the existingpython_env.setup.attempttriggerdimension marks recovery runs, paired withthe
python_env.setup.resultoutcome for the recovery success rate. No newevent. A conflict is never report-worthy (it's the user's own dependencies).
Depends on the CLI conflict code (dormant until it ships)
E_PROVISION_CONFLICTis a CLI-emitted code that is not yet in a releasedCLI — the extension is coded against the contract, so the new recovery path is
fully unit-tested but stays inert (a real conflict still arrives as
E_PROVISIONtoday) until the CLI emits the distinct code and populates
backupPathon thatfailure (the restore-then-retry relies on it). This mirrors the stacked-on
picker PR's own posture. No end-to-end test is added for that reason.
Testing
test:unit: 1126 passing. New coverage: the conflict copy + fallback doc link,the
run-action dispatch,openProjectFile, the controller conflict→retryflow (restore-before-retry ordering,
--no-constraints+ adopt on success,conflict_retrytelemetry paired with its outcome, open pyproject at the run'scwd,
reportOffered: false, missing-backupPathand non-Full fallbacks,restore-failure aborts without a retry attempt), and the atomic/in-project
restore wiring.
tsc+eslint+prettierclean on changed files.Stacked on #2179 — review/merge that first.
This pull request and its description were written by Isaac.