diff --git a/internal/inventory/migrations/0103-a-walk-keeps-its-backups.sql b/internal/inventory/migrations/0103-a-walk-keeps-its-backups.sql new file mode 100644 index 00000000..4a85a32f --- /dev/null +++ b/internal/inventory/migrations/0103-a-walk-keeps-its-backups.sql @@ -0,0 +1,6 @@ +-- A walk keeps the backups it takes before it asks its first tier (novox/hq ADR 0404 decisions 2 to 4, issue 1065). +-- +-- Per machine and module: asked, pending, done or failed, with the backup's own words, and the machines with no backup +-- holder. Read by whoever says the walk (`plan-moved`). Null for a walk that moves nothing a backup protects, and for +-- a walk kept before this. +alter table release_plan add column backups jsonb; diff --git a/internal/inventory/plans.go b/internal/inventory/plans.go index 68b02bda..da3dad4a 100644 --- a/internal/inventory/plans.go +++ b/internal/inventory/plans.go @@ -746,7 +746,12 @@ func writePlan(ctx context.Context, tx pgx.Tx, p *Plan, epoch *int64) (int64, ti if err != nil { return 0, time.Time{}, err } - var release, delivery, commits, times []byte + var release, delivery, commits, times, backups []byte + if p.Backups != nil { + if backups, err = json.Marshal(p.Backups); err != nil { + return 0, time.Time{}, err + } + } if p.Times != nil { if times, err = json.Marshal(p.Times); err != nil { return 0, time.Time{}, err @@ -774,18 +779,18 @@ func writePlan(ctx context.Context, tx pgx.Tx, p *Plan, epoch *int64) (int64, ti var revision int64 err = tx.QueryRow(ctx, `insert into release_plan (id, repository, commit_hash, created, updated, state, tier, tiers, modules, note, - branch, tier_entered, revision, epoch, release, delivery, merged_at, commits, times) - values ($1, $2, $3, $4, now(), $5, $6, $7, $8, $9, $10, $11, 1, $13, $14, $15, $16, $17, $18) + branch, tier_entered, revision, epoch, release, delivery, merged_at, commits, times, backups) + values ($1, $2, $3, $4, now(), $5, $6, $7, $8, $9, $10, $11, 1, $13, $14, $15, $16, $17, $18, $19) on conflict (id) do update set updated = now(), state = excluded.state, tier = excluded.tier, tiers = excluded.tiers, modules = excluded.modules, note = excluded.note, branch = excluded.branch, tier_entered = excluded.tier_entered, revision = release_plan.revision + 1, epoch = excluded.epoch, release = excluded.release, delivery = excluded.delivery, repository = excluded.repository, commit_hash = excluded.commit_hash, merged_at = excluded.merged_at, commits = excluded.commits, - created = excluded.created, times = excluded.times + created = excluded.created, times = excluded.times, backups = excluded.backups where release_plan.revision = $12 returning revision`, p.ID, p.Repository, p.Commit, p.Created, p.State, p.Tier, tiers, modules, p.Note, p.Branch, entered, - p.Revision, epoch, release, delivery, mergedAt(p.Merged), commits, times).Scan(&revision) + p.Revision, epoch, release, delivery, mergedAt(p.Merged), commits, times, backups).Scan(&revision) if errors.Is(err, pgx.ErrNoRows) { // The row is there and at another revision — moved since this was read, or there already // when this one is new: either way not this writer's to overwrite. (A plan saved before plans @@ -854,7 +859,7 @@ func (i *Inventory) PlanByID(ctx context.Context, id string) (Plan, error) { func (i *Inventory) plans(ctx context.Context, tail string, args ...any) ([]Plan, error) { rows, err := i.store.Pool().Query(ctx, `select id, repository, commit_hash, created, updated, state, tier, tiers, modules, note, branch, - coalesce(tier_entered, created), revision, coalesce(epoch, 0), release, delivery, merged_at, commits, times + coalesce(tier_entered, created), revision, coalesce(epoch, 0), release, delivery, merged_at, commits, times, backups from release_plan `+tail, args...) if err != nil { return nil, err @@ -863,14 +868,19 @@ func (i *Inventory) plans(ctx context.Context, tail string, args ...any) ([]Plan var out []Plan for rows.Next() { var p Plan - var tiers, modules, release, delivery, commits, times []byte + var tiers, modules, release, delivery, commits, times, backups []byte var epoch int64 var merged *time.Time if err := rows.Scan(&p.ID, &p.Repository, &p.Commit, &p.Created, &p.Updated, &p.State, &p.Tier, &tiers, &modules, &p.Note, &p.Branch, &p.TierEntered, &p.Revision, &epoch, &release, - &delivery, &merged, &commits, ×); err != nil { + &delivery, &merged, &commits, ×, &backups); err != nil { return nil, err } + if len(backups) > 0 { + if err := json.Unmarshal(backups, &p.Backups); err != nil { + return nil, err + } + } if len(times) > 0 { if err := json.Unmarshal(times, &p.Times); err != nil { return nil, err diff --git a/internal/inventory/plans_test.go b/internal/inventory/plans_test.go index 831a63b5..819b393c 100644 --- a/internal/inventory/plans_test.go +++ b/internal/inventory/plans_test.go @@ -198,3 +198,28 @@ func TestASchemaReachIsKeptByBuild(t *testing.T) { t.Fatalf("reach %d %v %v", reach, known, err) } } + +// A plan keeps its backups through the store, timestamps to the nanosecond (novox/hq ADR 0404, issue 1065). +func TestAPlanKeepsItsBackupsInTheStore(t *testing.T) { + inv := ForTest(t) + ctx := t.Context() + at := time.Now().UTC() + p := Plan{ID: "plan-backups", Repository: "novox/mesh-catalog", Commit: "abc", Created: at, State: PlanBuilding, + Tiers: [][]string{{"mail"}}, Modules: map[string]*PlanModule{"mail": {}}, + Backups: &PlanBackups{State: BackupPending, NoHolder: []string{"bare"}, + Asked: []PlanBackup{{Machine: "ace", Module: "mail", State: BackupPending, AskedAt: &at}}}} + if err := inv.SavePlan(ctx, &p); err != nil { + t.Fatal(err) + } + got, err := inv.PlanByID(ctx, p.ID) + if err != nil || got.Backups == nil || !reflect.DeepEqual(got.Backups, p.Backups) { + t.Fatalf("kept %+v (%v), wrote %+v", got.Backups, err, p.Backups) + } + got.Backups.State = BackupDone + if err := inv.SavePlan(ctx, &got); err != nil { + t.Fatal(err) + } + if again, _ := inv.PlanByID(ctx, p.ID); again.Backups == nil || again.Backups.State != BackupDone { + t.Fatalf("the change was not kept: %+v", again.Backups) + } +}