Keep a plan's backups in the store: a migration and the plan's read and write (issues 1065, 1066)
mesh/repo-check waiting in the build queue since 00:05: asked by the merge tool against trunk 7c9c187ca6e2
mesh/delivery stopped: Operator's word (2026-10-18 00:09): split the group. After mesh-catalog #353 merged on the group's Approve (23:48:51), #375's merg…
mesh/delivery-group group feat/backup-before-send stopped: a member was stopped
mesh/merge-gate building on shanks since 00:10

This commit is contained in:
2026-10-17 23:25:57 +02:00
parent 08e000bb56
commit 91a7c1eaff
3 changed files with 49 additions and 8 deletions
@@ -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;
+18 -8
View File
@@ -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, &times); err != nil {
&delivery, &merged, &commits, &times, &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
+25
View File
@@ -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)
}
}