Skip to content

Move direct engine state lifecycle to top-level callers#4928

Merged
denik merged 30 commits intomainfrom
denik/open-top
Apr 10, 2026
Merged

Move direct engine state lifecycle to top-level callers#4928
denik merged 30 commits intomainfrom
denik/open-top

Conversation

@denik
Copy link
Copy Markdown
Contributor

@denik denik commented Apr 10, 2026

Changes

  • Move state lifecycle (Open/Finalize) from internal methods to top-level command callers, so state is opened exactly once per command. Double-open panics.
  • Script-based bundle run no longer pulls remote state it doesn't need.
  • Skip the /api/2.0/preview/scim/v2/Me call during bundle run when only ErrorOnEmptyState is set.
  • Skip state file creation when deploy/destroy plans are empty.

Why

The previous pattern hid Open/Finalize inside low-level methods, making it hard to reason about when state was opened, whether it would be written, and whether double-opens could occur. Lifting the lifecycle to callers makes the invariant obvious and enforceable, and avoids unnecessary state I/O and API calls in read-only flows.

Preparation for WAL & metadata service.

Tests

  • Unit tests for DeploymentState: open/save/finalize round-trip, panic on double open, delete tracking.
  • Existing acceptance tests cover deploy, destroy, bundle run, and config-remote-sync flows.

denik added 28 commits April 10, 2026 12:14
- Open state once in process.go after PullResourcesState for direct engine
- Open state in destroy.go and configsync/diff.go before CalculatePlan
- Finalize state in deployCore/destroyCore after Apply, before PushResourcesState
- Remove Open from CalculatePlan, InitForApply, ExportState
- Remove Finalize from Apply (moved to callers)
- Add modified flag to DeploymentState to skip no-op Finalize
- Fix panic syntax (panic takes single arg)

Co-authored-by: Isaac
The bundle plan command uses PreDeployChecks without Deploy,
so state must also be opened when PreDeployChecks is set.

Co-authored-by: Isaac
These commands manage state outside ProcessOptions and need
explicit Open before calling statemgmt.Load for direct engine.

Co-authored-by: Isaac
Apply no longer calls Finalize internally, so callers that need
the state written to disk must call Finalize explicitly.

Co-authored-by: Isaac
- Serial numbers decrease because Finalize is now a no-op when state is
  unmodified (prevents spurious serial increments)
- "Updating deployment state..." no longer appears when deploy fails
  without modifying state (no state file to push)

Co-authored-by: Isaac
- Finalize: move `modified = false` after successful save, restore
  serial on failure so retries work correctly
- Open: set `modified = true` after migrateState changes the version,
  so Finalize persists migrated state

Add unit tests for the state lifecycle.

Co-authored-by: Isaac
Co-authored-by: Isaac
Centralize Open/Finalize pairing: process.go gets deferred Finalize
for all flows going through ProcessBundleRet. Exception flows
(destroy, run, dashboard, configsync/diff) that don't use process.go
also get deferred Finalize after their own Open calls.

Explicit Finalize in deployCore/destroyCore is kept because it must
run before PushResourcesState. Deferred call is a no-op after that
since Finalize is idempotent when state is unmodified.

Co-authored-by: Isaac
Per design: migrations happen on the fly each Open() and don't need
to be persisted. Only real state updates (SaveState, DeleteState)
cause Finalize to write to disk. This avoids spurious serial bumps.

Co-authored-by: Isaac
- DeleteState only sets modified when the key actually existed
- ValidatePlanAgainstState accepts *DeploymentState instead of re-reading the file

Co-authored-by: Isaac
Add PostStateFunc callback to ProcessOptions that runs within the state
lifecycle scope (after state open, before deferred Finalize). Add
NeedDirectState flag for flows that need state but don't set InitIDs etc.

- destroy: uses PostStateFunc to call phases.Destroy within state scope
- run: restructured into inline path (SkipInitialize) and normal path
  (full ProcessBundleRet with PostStateFunc)
- config-remote-sync: uses PostStateFunc; DetectChanges reuses
  b.DeploymentBundle for direct engine instead of creating a new one

Co-authored-by: Isaac
Scripts don't need state, so don't use InitIDs/ErrorOnEmptyState in
ProcessBundleRet. Instead, load state inside PostStateFunc only for
resource runs.

Co-authored-by: Isaac
- Revert broken outputs from previous InitIDs attempt
- Scripts now go through full ProcessBundleRet (with PullResourcesState)
  so request logs show additional state-reading calls

Co-authored-by: Isaac
Use full ProcessBundleRet with AlwaysPull and NeedDirectState for run,
so state lifecycle is managed by process.go. Scripts get unnecessary
PullResourcesState but no state loading (ErrorOnEmptyState is only
called for resource runs inside PostStateFunc).

Fix non-deterministic request ordering in databricks-cli script tests
by using print_requests.py --sort.

Co-authored-by: Isaac
…pull

Scripts no longer go through ProcessBundleRet with AlwaysPull+NeedDirectState.
Instead, after Initialize, the run argument is resolved and scripts execute
immediately without state operations. Resource runs handle state inline
(matching the original main approach) with explicit Open/Finalize.

Restores the original test scripts and request assertions from main.

Co-authored-by: Isaac
- Revert acceptance/cmd/workspace/apps/run-local-node/output.txt to main
  (was overwritten by broad -update run, unrelated to state lifecycle)
- Add NeedDirectState to shouldReadState so callers don't need to also
  set AlwaysPull for the state block to execute

Co-authored-by: Isaac
Restructure cmd/bundle/run.go so that resource runs use ProcessBundleRet
with AlwaysPull, ErrorOnEmptyState, and NeedDirectState instead of
hand-rolling PullResourcesState, StateDB.Open, and Finalize.

Scripts are handled in PostInitFunc (before state is pulled) via
executeScript which replaces the process on success. Resource runs are
handled in PostStateFunc with state already opened by process.go.

Co-authored-by: Isaac
InitializeURLs makes an extra API call to get the workspace ID. It was
previously called for both InitIDs and ErrorOnEmptyState, but only
InitIDs callers (summary, open) need URLs. ErrorOnEmptyState callers
(run) do not.

Co-authored-by: Isaac
CalculatePlan now reads from in-memory StateDB.Data, not from disk.
The Finalize() call is still needed to persist state, but the comment
incorrectly stated it was needed "so CalculatePlan can read it".

Task: 011.md

Co-authored-by: Isaac
Return (false, err) instead of diags.Extend() in convertState which
returns (bool, error), not diag.Diagnostics.

Task: 012.md

Co-authored-by: Isaac
Avoid creating a state file when the plan has no operations,
which prevents spurious state files from being written on no-ops.

Co-authored-by: Isaac
NeedDirectState leaked the direct/terraform engine distinction into the
top-level ProcessOptions API. Since every caller that set NeedDirectState
also set PostStateFunc, use PostStateFunc != nil as the signal that the
state DB should be opened.

Co-authored-by: Isaac
@github-actions
Copy link
Copy Markdown

Approval status: pending

/bundle/ - needs approval

Files: bundle/configsync/diff.go, bundle/deploy/terraform/check_dashboards_modified_remotely.go, bundle/direct/bind.go, bundle/direct/bundle_apply.go, bundle/direct/bundle_plan.go, bundle/direct/dstate/state.go, bundle/direct/dstate/state_test.go, bundle/direct/pkg.go, bundle/phases/deploy.go, bundle/phases/destroy.go, bundle/statemgmt/check_running_resources.go, bundle/statemgmt/state_load.go, bundle/statemgmt/upload_state_for_yaml_sync.go
Suggested: @shreyas-goenka
Also eligible: @pietern, @andrewnester, @anton-107, @lennartkats-db

/cmd/bundle/ - needs approval

Files: cmd/bundle/config_remote_sync.go, cmd/bundle/deployment/migrate.go, cmd/bundle/destroy.go, cmd/bundle/generate/dashboard.go, cmd/bundle/run.go, cmd/bundle/utils/process.go
Suggested: @shreyas-goenka
Also eligible: @pietern, @andrewnester, @anton-107, @lennartkats-db

Any maintainer (@andrewnester, @anton-107, @pietern, @shreyas-goenka, @simonfaltum) can approve all areas.
See OWNERS for ownership rules.

@denik denik marked this pull request as draft April 10, 2026 10:44
@denik denik marked this pull request as ready for review April 10, 2026 13:09
@denik denik enabled auto-merge April 10, 2026 13:10
@denik denik added this pull request to the merge queue Apr 10, 2026
Merged via the queue into main with commit c0801d4 Apr 10, 2026
20 of 21 checks passed
@denik denik deleted the denik/open-top branch April 10, 2026 14:02
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.

1 participant