carlos / platform Public

settings-3 task 3: the registry's update_settings column

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 5cf6fd2cff58725631dbd94b9885098362543081 parent 891945c
6 files changed, +98 −7
  • internal/registry/coldhandover.go +1 −1
  • internal/registry/hold_test.go +1 −1
  • internal/registry/registry.go +38 −5
  • internal/registry/registry_test.go +1 −0
  • internal/registry/remote_dispatch.go +3 −0
  • internal/registry/updatesettings_test.go +54 −0
diff --git a/internal/registry/coldhandover.go b/internal/registry/coldhandover.go
index 4ca2b9292..4d82e992d 100644
--- a/internal/registry/coldhandover.go
+++ b/internal/registry/coldhandover.go
@@ -135,7 +135,7 @@ func (r *Registry) RestoreClaimRows(ctx context.Context, id, retirement string,
return ErrClaimOwnership
}
t := next.Route
- values := []any{t.Host, t.Slug, t.Kind, t.Socket, t.Addr, t.Status, t.LastError, t.DBPath, t.Version, t.PrevVersion, t.App, t.Pool, t.Region, t.Channel, b2iDB(t.Hibernate), b2iDB(t.Canary), t.Replication, t.NextDue, t.Backing, b2iDB(t.StatusNamed), b2iDB(t.Platform), t.DNSStatus, t.DNSCheckedAt, t.CertExpiresAt, t.Account, t.Environment, t.Caps, t.Tunnel, b2iDB(t.Provisioned), t.Seed, t.Unit, t.FleetBox, t.Addrs, t.Health, t.Steering, t.Gate, t.CreatedAt, t.Args, t.IdleSeconds, t.MaxRunSeconds, t.Replicas, b2iDB(t.Replica), t.ClaimIncarnation, t.ClaimStamp, t.ClaimOwner, t.ClaimRowID, t.HoldOp, t.GoodVersion, t.GoodDigest, t.GoodDemand, t.ServingVersion}
+ values := []any{t.Host, t.Slug, t.Kind, t.Socket, t.Addr, t.Status, t.LastError, t.DBPath, t.Version, t.PrevVersion, t.App, t.Pool, t.Region, t.Channel, b2iDB(t.Hibernate), b2iDB(t.Canary), t.Replication, t.NextDue, t.Backing, b2iDB(t.StatusNamed), b2iDB(t.Platform), t.DNSStatus, t.DNSCheckedAt, t.CertExpiresAt, t.Account, t.Environment, t.Caps, t.Tunnel, b2iDB(t.Provisioned), t.Seed, t.Unit, t.FleetBox, t.Addrs, t.Health, t.Steering, t.Gate, t.CreatedAt, t.Args, t.IdleSeconds, t.MaxRunSeconds, t.Replicas, b2iDB(t.Replica), t.ClaimIncarnation, t.ClaimStamp, t.ClaimOwner, t.ClaimRowID, t.HoldOp, t.GoodVersion, t.GoodDigest, t.GoodDemand, t.ServingVersion, t.UpdateSettings}
if _, err = tx.ExecContext(ctx, `INSERT INTO routes (`+routeCols+`) VALUES (`+strings.TrimSuffix(strings.Repeat("?,", len(values)), ",")+`)`, values...); err != nil {
return err
}
diff --git a/internal/registry/hold_test.go b/internal/registry/hold_test.go
index 473a98762..496f9b2cb 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:149e8f668771c7493bdb7d4245fdabe03e957b0909c4daa1d037484b11064b80"
+ const wantS = "2:178f6dc7e16d6e4226bf8f24acf88396dee2a2faf85b0306853d1c9ebf81a1b4"
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 091d2a804..a92bbcebe 100644
--- a/internal/registry/registry.go
+++ b/internal/registry/registry.go
@@ -292,6 +292,13 @@ type Route struct {
// different questions (SnapshotFingerprint's own doc comment).
Steering string
+ // UpdateSettings is the instance's effective update settings
+ // (release.UpdateSettings' canonical JSON, "" = today's adoption),
+ // converged by provision.Reconcile from the record and the app policy.
+ // Root reads it (plan 4). Deliberately NOT in Fingerprint(): it
+ // changes what root installs, not what the edge serves.
+ UpdateSettings string
+
// Replicas is the account's replica placements, comma-joined placement
// codes in canonical (sorted, deduplicated) order — the registry half of
// #162. "" on every row that predates the column = no replicas =
@@ -574,6 +581,7 @@ CREATE TABLE IF NOT EXISTS routes (
`ALTER TABLE routes ADD COLUMN good_digest TEXT NOT NULL DEFAULT ''`,
`ALTER TABLE routes ADD COLUMN good_demand TEXT NOT NULL DEFAULT ''`,
`ALTER TABLE routes ADD COLUMN serving_version TEXT NOT NULL DEFAULT ''`,
+ `ALTER TABLE routes ADD COLUMN update_settings TEXT NOT NULL DEFAULT ''`,
} {
if _, err := r.sql.Exec(stmt); err != nil && !strings.Contains(err.Error(), "duplicate column") {
return err
@@ -851,7 +859,7 @@ replication, next_due, backing, status_named, platform, dns_status,
dns_checked_at, cert_expires_at, account, environment, caps, tunnel,
provisioned, seed, unit, fleet_box, addrs, health, steering, gate, created_at,
args, idle_seconds, max_run_seconds, replicas, replica, claim_incarnation, claim_stamp, claim_owner, claim_row_id, hold_op,
-good_version, good_digest, good_demand, serving_version`
+good_version, good_digest, good_demand, serving_version, update_settings`
func scanRoute(scan func(...any) error) (Route, error) {
var t Route
@@ -863,7 +871,7 @@ func scanRoute(scan func(...any) error) (Route, error) {
&t.Environment, &t.Caps, &t.Tunnel, &prov, &t.Seed, &t.Unit, &t.FleetBox,
&t.Addrs, &t.Health, &t.Steering, &t.Gate, &t.CreatedAt,
&t.Args, &t.IdleSeconds, &t.MaxRunSeconds, &t.Replicas, &rep, &t.ClaimIncarnation, &t.ClaimStamp, &t.ClaimOwner, &t.ClaimRowID, &t.HoldOp,
- &t.GoodVersion, &t.GoodDigest, &t.GoodDemand, &t.ServingVersion)
+ &t.GoodVersion, &t.GoodDigest, &t.GoodDemand, &t.ServingVersion, &t.UpdateSettings)
t.Hibernate = hib != 0
t.Canary = can != 0
t.StatusNamed = sn != 0
@@ -1109,7 +1117,7 @@ func (r *Registry) Upsert(t Route) error {
}
res, err := r.sql.Exec(`
INSERT INTO routes (`+routeCols+`)
-VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
+VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(host) DO UPDATE SET
slug=excluded.slug, kind=excluded.kind, socket=excluded.socket,
addr=excluded.addr,
@@ -1137,7 +1145,8 @@ ON CONFLICT(host) DO UPDATE SET
gate=CASE WHEN excluded.gate = '' THEN routes.gate ELSE excluded.gate END,
args=CASE WHEN excluded.args = '' THEN routes.args ELSE excluded.args END,
idle_seconds=CASE WHEN excluded.idle_seconds = 0 THEN routes.idle_seconds ELSE excluded.idle_seconds END,
- max_run_seconds=CASE WHEN excluded.max_run_seconds = 0 THEN routes.max_run_seconds ELSE excluded.max_run_seconds END
+ max_run_seconds=CASE WHEN excluded.max_run_seconds = 0 THEN routes.max_run_seconds ELSE excluded.max_run_seconds END,
+ update_settings=CASE WHEN excluded.update_settings = '' THEN routes.update_settings ELSE excluded.update_settings END
WHERE routes.claim_incarnation=excluded.claim_incarnation AND routes.claim_stamp=excluded.claim_stamp
AND routes.claim_owner=excluded.claim_owner AND routes.claim_row_id=excluded.claim_row_id`,
t.Host, t.Slug, t.Kind, t.Socket, t.Addr, t.Status, t.LastError,
@@ -1148,7 +1157,7 @@ AND routes.claim_owner=excluded.claim_owner AND routes.claim_row_id=excluded.cla
b2i(t.Provisioned), t.Seed, t.Unit, t.FleetBox, t.Addrs, t.Health,
t.Steering, t.Gate, t.CreatedAt, t.Args, t.IdleSeconds, t.MaxRunSeconds,
t.Replicas, b2i(t.Replica), t.ClaimIncarnation, t.ClaimStamp, t.ClaimOwner, t.ClaimRowID, t.HoldOp,
- t.GoodVersion, t.GoodDigest, t.GoodDemand, t.ServingVersion)
+ t.GoodVersion, t.GoodDigest, t.GoodDemand, t.ServingVersion, t.UpdateSettings)
if err != nil {
return err
}
@@ -1836,6 +1845,30 @@ func (r *Registry) SetSteering(host, steering string) error {
return nil
}
+// SetUpdateSettings repoints a route's effective update settings as a narrow
+// UPDATE: reconcile converges it from the record every pass, in both
+// directions, including down to "". Errors on an unmatched host, like
+// SetSteering.
+func (r *Registry) SetUpdateSettings(host, col string) error {
+ if r.remote != nil {
+ _, err := r.remote.call(nil, remoteRequest{Method: "SetUpdateSettings", Host: host, UpdateSettings: col}, nil)
+ return err
+ }
+
+ res, err := r.sql.Exec(`UPDATE routes SET update_settings = ? WHERE host = ?`, col, host)
+ if err != nil {
+ return err
+ }
+ n, err := res.RowsAffected()
+ if err != nil {
+ return err
+ }
+ if n == 0 {
+ return fmt.Errorf("no route for host %q", host)
+ }
+ return nil
+}
+
// SetReplicas repoints a route's replica placement set (#162) as a narrow
// UPDATE: the provision pass converges this column every tick from the
// record, and the record's value is authoritative in BOTH directions —
diff --git a/internal/registry/registry_test.go b/internal/registry/registry_test.go
index b162bcc8b..f8ed1e8c1 100644
--- a/internal/registry/registry_test.go
+++ b/internal/registry/registry_test.go
@@ -83,6 +83,7 @@ func TestUpsertRoundTripsAllColumns(t *testing.T) {
CreatedAt: 1700000000,
GoodVersion: "good789", GoodDigest: "sha256:good", GoodDemand: "swap-demand",
ServingVersion: "serve012",
+ UpdateSettings: "",
}
if err := reg.Upsert(want); err != nil {
t.Fatal(err)
diff --git a/internal/registry/remote_dispatch.go b/internal/registry/remote_dispatch.go
index fc085286a..9d84778b3 100644
--- a/internal/registry/remote_dispatch.go
+++ b/internal/registry/remote_dispatch.go
@@ -52,6 +52,7 @@ type remoteRequest struct {
Addrs string
Health string
Steering string
+ UpdateSettings string
Replicas string
Replica bool
Hash string
@@ -209,6 +210,8 @@ func (r *Registry) dispatchRemote(ctx context.Context, q remoteRequest, callback
err = r.SetUpstreams(q.Host, q.Addr, q.Addrs, q.Health)
case "SetSteering":
err = r.SetSteering(q.Host, q.Steering)
+ case "SetUpdateSettings":
+ err = r.SetUpdateSettings(q.Host, q.UpdateSettings)
case "SetReplicas":
err = r.SetReplicas(q.Host, q.Replicas)
case "SetReplica":
diff --git a/internal/registry/updatesettings_test.go b/internal/registry/updatesettings_test.go
new file mode 100644
index 000000000..ec49b9a98
--- /dev/null
+++ b/internal/registry/updatesettings_test.go
@@ -0,0 +1,54 @@
+package registry
+
+import (
+ "path/filepath"
+ "testing"
+)
+
+func TestUpdateSettingsColumn(t *testing.T) {
+ reg, err := Open(filepath.Join(t.TempDir(), "r.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := reg.Upsert(Route{Host: "a.example.com", Kind: "instance", App: "km"}); err != nil {
+ t.Fatal(err)
+ }
+ rt, _ := reg.Get("a.example.com")
+ if rt.UpdateSettings != "" {
+ t.Fatalf("a new row has settings %q", rt.UpdateSettings)
+ }
+ before, err := reg.Fingerprint()
+ if err != nil {
+ t.Fatal(err)
+ }
+ col := `{"updates":"manual","channel":"stable"}`
+ if err := reg.SetUpdateSettings("a.example.com", col); err != nil {
+ t.Fatal(err)
+ }
+ if rt, _ = reg.Get("a.example.com"); rt.UpdateSettings != col {
+ t.Fatalf("column = %q", rt.UpdateSettings)
+ }
+ after, err := reg.Fingerprint()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if after != before {
+ t.Fatal("update settings changed the edge's fingerprint")
+ }
+ // A full-row Upsert that does not carry the column keeps it.
+ if err := reg.Upsert(Route{Host: "a.example.com", Kind: "instance", App: "km"}); err != nil {
+ t.Fatal(err)
+ }
+ if rt, _ = reg.Get("a.example.com"); rt.UpdateSettings != col {
+ t.Fatalf("Upsert cleared the column: %q", rt.UpdateSettings)
+ }
+ if err := reg.SetUpdateSettings("a.example.com", ""); err != nil {
+ t.Fatal(err)
+ }
+ if rt, _ = reg.Get("a.example.com"); rt.UpdateSettings != "" {
+ t.Fatal("the narrow setter could not clear the column")
+ }
+ if err := reg.SetUpdateSettings("nobody.example.com", col); err == nil {
+ t.Fatal("an unknown host was not an error")
+ }
+}