settings-3 task 3: update_settings hashes into the snapshot only when set
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+42
−12
internal/registry/hold_test.go+1 −1internal/registry/registry.go+12 −10internal/registry/registry_test.go+1 −1internal/registry/updatesettings_test.go+28 −0
diff --git a/internal/registry/hold_test.go b/internal/registry/hold_test.go| index 496f9b2cb..473a98762 100644 |
| --- a/internal/registry/hold_test.go |
| +++ b/internal/registry/hold_test.go |
| @@ -424,7 +424,7 @@ func TestFingerprintsUnchangedForUnheldRows(t *testing.T) { |
| t.Fatal(err) |
| } |
| const wantF = "2:f1013971d496e9401cd6a9928a1f4e18669deda40089d2c2a8b84263b32f34c6" |
| - const wantS = "2:178f6dc7e16d6e4226bf8f24acf88396dee2a2faf85b0306853d1c9ebf81a1b4" |
| + const wantS = "2:149e8f668771c7493bdb7d4245fdabe03e957b0909c4daa1d037484b11064b80" |
| if got, _ := u.Fingerprint(); got != wantF { |
| t.Fatalf("Fingerprint = %s, want %s", got, wantF) |
| } |
diff --git a/internal/registry/registry.go b/internal/registry/registry.go| index a92bbcebe..fe3457f65 100644 |
| --- a/internal/registry/registry.go |
| +++ b/internal/registry/registry.go |
| @@ -1028,8 +1028,8 @@ func (r *Registry) Delete(host string) error { |
| // SetEnvironment (`carlos route -environment -`), SetCaps (`carlos route |
| // -revoke`), SetTunnel (`carlos route -tunnel -`), SetUnit (`carlos route |
| // -unit -`), SetReplication (`carlos route -replication -`), SetSeed, |
| -// SetFleetBox, SetArgs, SetWorkerLimits, and for `provisioned` the row's |
| -// own deletion. |
| +// SetFleetBox, SetArgs, SetWorkerLimits, SetUpdateSettings, and for |
| +// `provisioned` the row's own deletion. |
| // |
| // `replication` is the one column with a SECOND clearing door, and it is not |
| // an operator's: SetHibernate clears it when a route stops hibernating, |
| @@ -3141,15 +3141,17 @@ var snapshotClaimCols = map[string]bool{ |
| "claim_row_id": true, |
| } |
| -// snapshotWhenSetCols are the good-build columns (zero-downtime spec §8.5). |
| -// A restore has to reproduce them, so they are hashed, but only when set and |
| -// tagged, on the hold_op rule: a registry that has never recorded a good |
| -// build digests exactly as it did before the columns existed, so upgrading |
| -// does not force a snapshot on every box. |
| +// snapshotWhenSetCols are the good-build columns (zero-downtime spec §8.5) |
| +// and the update-settings column. A restore has to reproduce them, so they |
| +// are hashed, but only when set and tagged, on the hold_op rule: a registry |
| +// that has never recorded a good build or set update settings digests exactly |
| +// as it did before the columns existed, so upgrading does not force a snapshot |
| +// on every box. |
| var snapshotWhenSetCols = map[string]bool{ |
| - "good_version": true, |
| - "good_digest": true, |
| - "good_demand": true, |
| + "good_version": true, |
| + "good_digest": true, |
| + "good_demand": true, |
| + "update_settings": true, |
| } |
| func (r *Registry) SnapshotFingerprint() (string, error) { |
diff --git a/internal/registry/registry_test.go b/internal/registry/registry_test.go| index f8ed1e8c1..ca70219d2 100644 |
| --- a/internal/registry/registry_test.go |
| +++ b/internal/registry/registry_test.go |
| @@ -83,7 +83,7 @@ func TestUpsertRoundTripsAllColumns(t *testing.T) { |
| CreatedAt: 1700000000, |
| GoodVersion: "good789", GoodDigest: "sha256:good", GoodDemand: "swap-demand", |
| ServingVersion: "serve012", |
| - UpdateSettings: "", |
| + UpdateSettings: `{"updates":"manual","channel":"stable"}`, |
| } |
| if err := reg.Upsert(want); err != nil { |
| t.Fatal(err) |
diff --git a/internal/registry/updatesettings_test.go b/internal/registry/updatesettings_test.go| index ec49b9a98..d397254c8 100644 |
| --- a/internal/registry/updatesettings_test.go |
| +++ b/internal/registry/updatesettings_test.go |
| @@ -52,3 +52,31 @@ func TestUpdateSettingsColumn(t *testing.T) { |
| t.Fatal("an unknown host was not an error") |
| } |
| } |
| + |
| +func TestUpdateSettingsMovesSnapshotFingerprint(t *testing.T) { |
| + reg, err := Open(filepath.Join(t.TempDir(), "r.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := reg.Upsert(Route{Host: "s.example.com", Kind: "instance", App: "km"}); err != nil { |
| + t.Fatal(err) |
| + } |
| + // SnapshotFingerprint with unset update_settings |
| + before, err := reg.SnapshotFingerprint() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + col := `{"updates":"manual","channel":"stable"}` |
| + if err := reg.SetUpdateSettings("s.example.com", col); err != nil { |
| + t.Fatal(err) |
| + } |
| + // SnapshotFingerprint with set update_settings must be different |
| + // (restores must reproduce the column value) |
| + after, err := reg.SnapshotFingerprint() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if after == before { |
| + t.Fatal("setting update_settings did not move SnapshotFingerprint — restore would miss the column") |
| + } |
| +} |