Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 22 additions & 11 deletions bundle/direct/dstate/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,26 @@ func (db *DeploymentState) GetResourceID(key string) string {
return db.stateIDs[key]
}

// GetOrInitLineage returns the deployment lineage, generating and storing a new
// one if the state does not have one yet. It is the single place the lineage is
// initialized, shared so the direct deployment engine (when it writes state, via
// Open/UpgradeToWrite) and DMS (when it records a deployment) always agree on the
// value.
//
// DMS needs this before the engine writes state: it records the version at lock
// time, which is before the engine assigns the lineage at plan-apply time.
// Seeding db.Data.Lineage here means the subsequent write reuses the same value
// instead of minting a different one.
//
// It does not take db.mu: Open and UpgradeToWrite already hold it, and the DMS
// recorder calls it during deploy setup, before any concurrent state writes.
func (db *DeploymentState) GetOrInitLineage() string {
if db.Data.Lineage == "" {
db.Data.Lineage = uuid.New().String()
}
return db.Data.Lineage
}

type (
// If true, then Open reads the WAL and merges it in the state. If false, and WAL is present, Open returns an error.
WithRecovery bool
Expand Down Expand Up @@ -213,13 +233,8 @@ func (db *DeploymentState) Open(ctx context.Context, path string, withRecovery W
return fmt.Errorf("failed to open WAL file %s: %w", walPath, err)
}
db.walFile = walFile
lineage := db.Data.Lineage
if lineage == "" {
// state file is new, does not have lineage yet; store lineage in the WAL only
lineage = uuid.New().String()
}
walHead := Header{
Lineage: lineage,
Lineage: db.GetOrInitLineage(),
Serial: db.Data.Serial + 1,
StateVersion: currentStateVersion,
CLIVersion: build.GetInfo().Version,
Expand Down Expand Up @@ -414,12 +429,8 @@ func (db *DeploymentState) UpgradeToWrite() error {
}
db.walFile = walFile

lineage := db.Data.Lineage
if lineage == "" {
lineage = uuid.New().String()
}
walHead := Header{
Lineage: lineage,
Lineage: db.GetOrInitLineage(),
Serial: db.Data.Serial + 1,
StateVersion: currentStateVersion,
CLIVersion: build.GetInfo().Version,
Expand Down
28 changes: 28 additions & 0 deletions bundle/direct/dstate/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,31 @@ func TestDeleteState(t *testing.T) {
assert.Empty(t, db3.GetResourceID("jobs.my_job"))
mustFinalize(t, &db3)
}

func TestGetOrInitLineageReadableBeforeWriteAndPersisted(t *testing.T) {
path := filepath.Join(t.TempDir(), "state.json")

// Fresh state opened read-only, as the deploy does before planning: no
// lineage yet.
var db DeploymentState
require.NoError(t, db.Open(t.Context(), path, WithRecovery(true), WithWrite(false)))
require.Empty(t, db.Data.Lineage)

// GetOrInitLineage initializes the lineage and makes it readable before any
// write (i.e. before planning), and is stable across calls.
lineage := db.GetOrInitLineage()
require.NotEmpty(t, lineage)
require.Equal(t, lineage, db.GetOrInitLineage())

// Upgrading to write reuses the same lineage (it goes into the WAL header),
// and a write makes it durable.
require.NoError(t, db.UpgradeToWrite())
require.NoError(t, db.SaveState("jobs.my_job", "123", map[string]string{}, nil))
mustFinalize(t, &db)

// Re-open: the persisted lineage matches the one read before the write.
var reopened DeploymentState
require.NoError(t, reopened.Open(t.Context(), path, WithRecovery(false), WithWrite(false)))
assert.Equal(t, lineage, reopened.Data.Lineage)
mustFinalize(t, &reopened)
}
Loading