carlos / platform Public

provision: adopt an existing route whose shape matches an appearing record

Backfilling the fleet becomes a pure bucket-side write per route — describe
what is already there and the next serial bump adopts it, with no box
commands. The match test excludes Channel/Environment/Hibernate, which are
the columns the record is taking ownership of, and refuses to adopt a row
with a non-empty Backing — a record has no field for it (box-scoped operator
column), so it can never describe a carlos-app@<host> unit tenant.

Adoption grants the removal phase authority over the row, so shapeMatches
compares the derived Socket and DBPath too. The Channel/Environment
converge is factored into convergeOwned and shared by the owned-route
converge branch and the adoption branch, so there is one place these two
columns are written from a record, not two.

Design: docs/superpowers/specs/2026-08-10-route-records-design.md §4.2
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell f7649830e13f079a3956118625929655cb207996 parent 11dd85b
4 files changed, +328 −13
  • internal/provision/reconcile.go +83 −13
  • internal/provision/reconcile_test.go +196 −0
  • internal/registry/registry.go +29 −0
  • internal/registry/registry_test.go +20 −0
diff --git a/internal/provision/reconcile.go b/internal/provision/reconcile.go
index 9847fb897..66e93c12b 100644
--- a/internal/provision/reconcile.go
+++ b/internal/provision/reconcile.go
@@ -70,6 +70,62 @@ func routeFor(rec Record, dataDir, seed string) registry.Route {
return rt
}
+// shapeMatches reports whether an existing route is the same ROUTE the
+// record describes — the gate on adoption (design §4.2), and so the gate on
+// this pass acquiring removal authority over a row it did not mint.
+//
+// Channel, Environment and Hibernate are deliberately absent: those are the
+// columns the record is taking ownership OF, and a hand-added route on the
+// wrong channel is the whole reason to adopt one. Requiring them to agree
+// would make adoption impossible in exactly the case it exists for.
+//
+// Backing is deliberately required to be empty on the existing row. A
+// record has no field for Backing — it is a box-scoped operator column,
+// alongside capability grants and tunnels (registry.Route.Backing's own
+// doc comment) — so a record can never describe a carlos-app@<host> unit
+// tenant at all. A row this pass cannot state the Backing of is a row this
+// pass must not claim to describe, no matter how well everything else
+// lines up.
+//
+// Everything else that identifies the route is compared, including the
+// derived Socket and DBPath — two rows agreeing on kind and app but not on
+// where the database lives are not the same route, and adopting across that
+// difference would point a running process at a second copy of its own data.
+func shapeMatches(existing, want registry.Route) bool {
+ return existing.Backing == "" &&
+ existing.App == want.App &&
+ existing.Account == want.Account &&
+ existing.Kind == want.Kind &&
+ existing.Addr == want.Addr &&
+ existing.Unit == want.Unit &&
+ existing.Slug == want.Slug &&
+ existing.Pool == want.Pool &&
+ existing.Region == want.Region &&
+ existing.Socket == want.Socket &&
+ existing.DBPath == want.DBPath
+}
+
+// convergeOwned applies the record's Channel and Environment onto a route it
+// owns — the two columns the record is authoritative for once it owns the
+// row, whether ownership was just granted by adoption or was already held
+// by an earlier pass. Both the owned-route converge branch and the
+// adoption branch call this, so there is exactly one place these two
+// columns get written from a record, never two copies drifting apart.
+func convergeOwned(reg *registry.Registry, existing registry.Route, rec Record) {
+ if rec.Channel != "" && existing.Channel != rec.Channel {
+ if err := reg.SetChannel(rec.Host, rec.Channel); err != nil {
+ log.Printf("provision: converge channel on %s: %v", rec.Host, err)
+ } else {
+ log.Printf("provision: converged %s channel %s -> %s", rec.Host, existing.Channel, rec.Channel)
+ }
+ }
+ if existing.Environment != rec.Environment {
+ if err := reg.SetEnvironment(rec.Host, rec.Environment); err != nil {
+ log.Printf("provision: converge environment on %s: %v", rec.Host, err)
+ }
+ }
+}
+
// Reconcile materializes bucket instance records into registry routes for
// THIS agent's region and pools: mint on appearance, converge seed, remove
// on record deletion — removal only for routes this pass (or an earlier one
@@ -286,18 +342,7 @@ func Reconcile(ctx context.Context, s release.Store, reg *registry.Registry, cfg
// deploy's SetVersions or the activator's SetNextDue would
// clobber those, and Upsert cannot express "clear Environment"
// at all (it preserves a non-empty value by design).
- if rec.Channel != "" && existing.Channel != rec.Channel {
- if err := reg.SetChannel(rec.Host, rec.Channel); err != nil {
- log.Printf("provision: converge channel on %s: %v", rec.Host, err)
- } else {
- log.Printf("provision: converged %s channel %s -> %s", rec.Host, existing.Channel, rec.Channel)
- }
- }
- if existing.Environment != rec.Environment {
- if err := reg.SetEnvironment(rec.Host, rec.Environment); err != nil {
- log.Printf("provision: converge environment on %s: %v", rec.Host, err)
- }
- }
+ convergeOwned(reg, existing, rec)
// Absent means "this record does not speak to hibernation" —
// never a derivation stamped over an operator's own setting.
if rec.Hibernate != nil && existing.Hibernate != *rec.Hibernate {
@@ -329,8 +374,33 @@ func Reconcile(ctx context.Context, s release.Store, reg *registry.Registry, cfg
want.Socket, existing.Socket, want.DBPath, existing.DBPath, rec.Host)
}
default:
- log.Printf("provision: %s exists but was not minted by this pass (app %q acct %q) — leaving it alone",
+ // A route exists that this pass did not mint. If it is the same
+ // route the record describes, adopt it — that is what makes
+ // backfilling the fleet a bucket-side write rather than a box
+ // session (design §4.2). If it is not, leave it exactly as alone
+ // as before: adoption grants this pass's removal phase authority
+ // over the row, so the match test is the only thing standing
+ // between a backfill and a retired production route.
+ want := routeFor(rec, cfg.DataDir, seed)
+ if !shapeMatches(existing, want) {
+ log.Printf("provision: %s exists but is not the route this record describes "+
+ "(app %q/%q kind %q/%q addr %q/%q) — leaving it alone",
+ rec.Host, existing.App, want.App, existing.Kind, want.Kind, existing.Addr, want.Addr)
+ continue
+ }
+ if err := reg.SetProvisioned(rec.Host, true); err != nil {
+ log.Printf("provision: adopt %s: %v", rec.Host, err)
+ continue
+ }
+ log.Printf("provision: adopted existing route %s (app %q acct %q) — this record now owns its channel and environment",
rec.Host, rec.App, rec.Account)
+ // Hibernate is deliberately NOT converged here even when
+ // explicit: this first pass over a hand-added route should
+ // change what it serves and who owns the column, not whether a
+ // live process is allowed to sleep. It converges on the next
+ // pass through the owned branch above, by which point the
+ // adoption is already visible in the log.
+ convergeOwned(reg, existing, rec)
}
}
diff --git a/internal/provision/reconcile_test.go b/internal/provision/reconcile_test.go
index 27609993b..ef6f30117 100644
--- a/internal/provision/reconcile_test.go
+++ b/internal/provision/reconcile_test.go
@@ -917,3 +917,199 @@ func TestReconcileDeclinesUnreadableTrashMarker(t *testing.T) {
t.Fatalf("an unreadable trash marker still minted a route: %+v %v", rt, err)
}
}
+
+// Design §4.2 — the payoff. Backfilling the fleet is a pure bucket-side
+// write per route: describe what is already there, and the next serial bump
+// adopts it. No carlos add, no SSM session, no on-box verb.
+func TestReconcileAdoptsMatchingHandAddedRoute(t *testing.T) {
+ s, sqid := setup(t)
+ rec := acmeRecord(sqid)
+ rec.Channel = "edge"
+ if err := CreateRecord(ctx, s, rec); err != nil {
+ t.Fatal(err)
+ }
+ // The route as an operator hand-added it before records existed: the
+ // same shape, a different channel, and no Provisioned flag.
+ handAdded := registry.Route{
+ Host: "acme.go.tito.io", Slug: "acme", Kind: "instance",
+ Socket: "/run/carlos/acme.go.tito.io.sock", DBPath: "/data/acme.go.tito.io.db",
+ App: "tito", Account: sqid, Pool: "tito", Region: "ie",
+ Channel: "stable", Hibernate: true, Status: "ready",
+ }
+ reg := regWith(t, handAdded)
+ cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"}
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ got := *mustGet(t, reg, "acme.go.tito.io")
+ if !got.Provisioned {
+ t.Fatal("a matching hand-added route was not adopted")
+ }
+ if got.Channel != "edge" {
+ t.Errorf("channel = %q, want edge — adoption should converge in the same pass", got.Channel)
+ }
+}
+
+// The match test EXCLUDES the three columns the record is taking ownership
+// of. Requiring them to agree first would make adoption impossible in
+// exactly the case it exists for — a hand-added route on the wrong channel
+// is the whole reason to adopt it.
+func TestReconcileAdoptionIgnoresOwnedColumns(t *testing.T) {
+ s, sqid := setup(t)
+ rec := acmeRecord(sqid)
+ rec.Channel = "edge"
+ if err := CreateRecord(ctx, s, rec); err != nil {
+ t.Fatal(err)
+ }
+ handAdded := registry.Route{
+ Host: "acme.go.tito.io", Slug: "acme", Kind: "instance",
+ Socket: "/run/carlos/acme.go.tito.io.sock", DBPath: "/data/acme.go.tito.io.db",
+ App: "tito", Account: sqid, Pool: "tito", Region: "ie",
+ Channel: "canary/rehearsal", Environment: "staging", Hibernate: false, Status: "ready",
+ }
+ reg := regWith(t, handAdded)
+ cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"}
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ if !mustGet(t, reg, "acme.go.tito.io").Provisioned {
+ t.Error("adoption refused over a Channel/Environment/Hibernate difference")
+ }
+}
+
+// A shape that differs is left exactly as alone as it is today. This is the
+// guard that stands between "adopt on match" and reconcile's removal phase
+// acquiring authority over a route it does not describe.
+func TestReconcileRefusesToAdoptMismatchedRoute(t *testing.T) {
+ s, sqid := setup(t)
+ if err := CreateRecord(ctx, s, acmeRecord(sqid)); err != nil {
+ t.Fatal(err)
+ }
+ mismatched := registry.Route{
+ Host: "acme.go.tito.io", Slug: "acme", Kind: "instance",
+ Socket: "/run/carlos/acme.go.tito.io.sock", DBPath: "/data/acme.go.tito.io.db",
+ App: "other", Account: sqid, Pool: "tito", Region: "ie",
+ Channel: "stable", Hibernate: true, Status: "ready",
+ }
+ reg := regWith(t, mismatched)
+ before := *mustGet(t, reg, "acme.go.tito.io")
+ cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"}
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ after := *mustGet(t, reg, "acme.go.tito.io")
+ if after != before {
+ t.Errorf("a mismatched route was touched: before %+v after %+v", before, after)
+ }
+ if after.Provisioned {
+ t.Error("a mismatched route was adopted — removal authority granted over a route the record does not describe")
+ }
+}
+
+// A record has no field for Backing — registry.Route.Backing's own doc
+// comment calls it a box-scoped operator column, alongside capability
+// grants and tunnels — so a record can never describe a carlos-app@<host>
+// unit tenant at all. Even a row that otherwise matches in every column
+// must not be adopted: adoption would hand this pass's removal phase
+// authority over a row it cannot even state the shape of.
+func TestReconcileRefusesToAdoptBackedRoute(t *testing.T) {
+ s, sqid := setup(t)
+ if err := CreateRecord(ctx, s, acmeRecord(sqid)); err != nil {
+ t.Fatal(err)
+ }
+ handAdded := registry.Route{
+ Host: "acme.go.tito.io", Slug: "acme", Kind: "instance",
+ Socket: "/run/carlos/acme.go.tito.io.sock", DBPath: "/data/acme.go.tito.io.db",
+ App: "tito", Account: sqid, Pool: "tito", Region: "ie",
+ Channel: "stable", Hibernate: true, Status: "ready",
+ Backing: registry.BackingUnit,
+ }
+ reg := regWith(t, handAdded)
+ before := *mustGet(t, reg, "acme.go.tito.io")
+ cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"}
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ after := *mustGet(t, reg, "acme.go.tito.io")
+ if after != before {
+ t.Errorf("a Backing-owned route was touched: before %+v after %+v", before, after)
+ }
+ if after.Provisioned {
+ t.Error("a carlos-app@ unit tenant was adopted — a record cannot state its Backing")
+ }
+}
+
+// An adopted route is removable by the pass that adopted it, on the same
+// terms as one it minted: the record disappearing is the bucket's own
+// decision to retire the instance.
+func TestReconcileRemovesAdoptedRouteWhenRecordGoes(t *testing.T) {
+ s, sqid := setup(t)
+ if err := CreateRecord(ctx, s, acmeRecord(sqid)); err != nil {
+ t.Fatal(err)
+ }
+ handAdded := registry.Route{
+ Host: "acme.go.tito.io", Slug: "acme", Kind: "instance",
+ Socket: "/run/carlos/acme.go.tito.io.sock", DBPath: "/data/acme.go.tito.io.db",
+ App: "tito", Account: sqid, Pool: "tito", Region: "ie",
+ Channel: "stable", Hibernate: true, Status: "ready",
+ }
+ reg := regWith(t, handAdded)
+ cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"}
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ if err := DeleteRecord(ctx, s, sqid, "tito", "acme.go.tito.io"); err != nil {
+ t.Fatal(err)
+ }
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ rt, err := reg.Get("acme.go.tito.io")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if rt != nil {
+ t.Errorf("adopted route survived its record's deletion: %+v", rt)
+ }
+}
+
+// The other side of that coin, and the one that matters more: a route no
+// record ever described is never removed, no matter how many passes run.
+// Adoption is the ONLY way this pass acquires removal authority, and this
+// test is what stands between a backfill and a retired production route.
+func TestReconcileNeverRemovesUnadoptedRoute(t *testing.T) {
+ s, sqid := setup(t)
+ // A record for some OTHER host, so the pass runs a full removal phase
+ // with a non-empty record set rather than bailing early.
+ if err := CreateRecord(ctx, s, acmeRecord(sqid)); err != nil {
+ t.Fatal(err)
+ }
+ handAdded := registry.Route{
+ Host: "console.go.tito.io", Kind: "instance", App: "tito", Account: sqid,
+ Pool: "tito", Region: "ie", Channel: "stable", Addr: "127.0.0.1:8090",
+ Unit: "console.service", Status: "ready",
+ }
+ reg := regWith(t, handAdded)
+ // Upsert stamps CreatedAt; read the row back so the final comparison
+ // isn't comparing against a zero value the row never actually had.
+ handAdded = *mustGet(t, reg, "console.go.tito.io")
+ cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"}
+ for i := 0; i < 3; i++ {
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ }
+ got, err := reg.Get("console.go.tito.io")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got == nil {
+ t.Fatal("a hand-added route with no record was removed")
+ }
+ if got.Provisioned {
+ t.Error("a hand-added route with no record was adopted")
+ }
+ if *got != handAdded {
+ t.Errorf("a hand-added route with no record was modified: %+v", got)
+ }
+}
diff --git a/internal/registry/registry.go b/internal/registry/registry.go
index 8fee34533..bf5d47076 100644
--- a/internal/registry/registry.go
+++ b/internal/registry/registry.go
@@ -1074,6 +1074,35 @@ func (r *Registry) SetSeed(host, seed string) error {
return nil
}
+// SetProvisioned stamps (or clears) the flag that marks a route as the
+// instance-provisioning reconcile pass's own — a narrow UPDATE, same
+// rationale as SetSeed. It errors when no row matched, the SetChannel
+// contract: the pass must notice a row that vanished under it rather than
+// silently doing nothing.
+//
+// This flag is an authority transfer, not a label: it is the single
+// condition the reconcile pass's removal phase keys on. Only that pass's
+// adoption step (provision.Reconcile, design §4.2) should ever set it, and
+// it does so only after matching the row's shape against the record.
+func (r *Registry) SetProvisioned(host string, on bool) error {
+ v := 0
+ if on {
+ v = 1
+ }
+ res, err := r.sql.Exec(`UPDATE routes SET provisioned = ? WHERE host = ?`, v, 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
+}
+
// DueInstances returns hibernatable instance routes whose next_due has
// arrived (0 means no schedule and never matches).
func (r *Registry) DueInstances(now int64) ([]Route, error) {
diff --git a/internal/registry/registry_test.go b/internal/registry/registry_test.go
index b571d12de..e3927f364 100644
--- a/internal/registry/registry_test.go
+++ b/internal/registry/registry_test.go
@@ -1639,3 +1639,23 @@ func TestValidTunnelNameLengthBoundary(t *testing.T) {
t.Errorf("ValidTunnelName(33 chars) = true, want false")
}
}
+
+func TestSetProvisioned(t *testing.T) {
+ r := openTestRegistry(t)
+ if err := r.Upsert(Route{Host: "a.example.com", Kind: "instance", App: "x", Status: "ready"}); err != nil {
+ t.Fatal(err)
+ }
+ if err := r.SetProvisioned("a.example.com", true); err != nil {
+ t.Fatal(err)
+ }
+ rt, err := r.Get("a.example.com")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !rt.Provisioned {
+ t.Error("SetProvisioned(true) did not stamp the column")
+ }
+ if err := r.SetProvisioned("missing.example.com", true); err == nil {
+ t.Error("SetProvisioned on an unknown host returned nil; want an error (the SetChannel contract)")
+ }
+}