carlos / platform Public

#162: routes.replica_of, the mark both passes read

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 237920b9d6356caffc41cac8e80420469c4286eb parent ed49ec3
2 files changed, +74 −5
  • internal/registry/registry.go +45 −5
  • internal/registry/registry_test.go +29 −0
diff --git a/internal/registry/registry.go b/internal/registry/registry.go
index 2d1f935aa..109d9756c 100644
--- a/internal/registry/registry.go
+++ b/internal/registry/registry.go
@@ -261,6 +261,21 @@ type Route struct {
// different questions (SnapshotFingerprint's own doc comment).
Steering string
+ // ReplicaOf is the public host this route is a replica OF (#162): the
+ // home record's host, carried from Record.Home by routeFor's mint and
+ // held steady by convergeOwned. "" on every row that predates the
+ // column and on every ordinary route = today's routing, byte for byte.
+ //
+ // It exists because two passes must know this row is an account's
+ // replica and neither can read bucket records:
+ // domains.ReconcileAliases derives the alias `ReplicaOf -> Host` from
+ // it (internal/provision imports internal/domains, so the reverse is a
+ // cycle), and the edges-file writer marks that alias role "replica".
+ // Not in Fingerprint(), in Steering's shadow: it changes what the
+ // ALIAS derivation produces, not what this box dials — and the alias
+ // row it produces IS in the fingerprint.
+ ReplicaOf string
+
CreatedAt int64
}
@@ -471,6 +486,7 @@ CREATE TABLE IF NOT EXISTS routes (
`ALTER TABLE routes ADD COLUMN args TEXT NOT NULL DEFAULT ''`,
`ALTER TABLE routes ADD COLUMN idle_seconds INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE routes ADD COLUMN max_run_seconds INTEGER NOT NULL DEFAULT 0`,
+ `ALTER TABLE routes ADD COLUMN replica_of TEXT NOT NULL DEFAULT ''`,
} {
if _, err := r.sql.Exec(stmt); err != nil && !strings.Contains(err.Error(), "duplicate column") {
return err
@@ -714,7 +730,7 @@ func (r *Registry) Accounts() ([]Account, error) {
}
// routeCols is the column list every route read and the one route write
-// share — 40 names, in this exact order. scanRoute's destinations, Upsert's
+// share — 41 names, in this exact order. scanRoute's destinations, Upsert's
// placeholder row and Upsert's argument list all count from here: adding a
// column means adding it in FOUR places, and a miscount is a silent column
// shift rather than an error (TestUpsertRoundTripsAllColumns is what catches
@@ -724,7 +740,7 @@ version, prev_version, app, pool, region, channel, hibernate, canary,
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`
+args, idle_seconds, max_run_seconds, replica_of`
func scanRoute(scan func(...any) error) (Route, error) {
var t Route
@@ -735,7 +751,7 @@ func scanRoute(scan func(...any) error) (Route, error) {
&sn, &plat, &t.DNSStatus, &t.DNSCheckedAt, &t.CertExpiresAt, &t.Account,
&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.Args, &t.IdleSeconds, &t.MaxRunSeconds, &t.ReplicaOf)
t.Hibernate = hib != 0
t.Canary = can != 0
t.StatusNamed = sn != 0
@@ -941,7 +957,7 @@ func (r *Registry) Upsert(t Route) error {
}
_, 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, status=excluded.status, last_error=excluded.last_error,
@@ -963,6 +979,7 @@ ON CONFLICT(host) DO UPDATE SET
unit=CASE WHEN excluded.unit = '' THEN routes.unit ELSE excluded.unit END,
fleet_box=CASE WHEN excluded.fleet_box = '' THEN routes.fleet_box ELSE excluded.fleet_box END,
addrs=excluded.addrs, health=excluded.health, steering=excluded.steering,
+ replica_of=excluded.replica_of,
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,
@@ -973,7 +990,8 @@ ON CONFLICT(host) DO UPDATE SET
b2i(t.StatusNamed), b2i(t.Platform), t.DNSStatus, t.DNSCheckedAt,
t.CertExpiresAt, t.Account, t.Environment, t.Caps, t.Tunnel,
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.Steering, t.Gate, t.CreatedAt, t.Args, t.IdleSeconds, t.MaxRunSeconds,
+ t.ReplicaOf)
return err
}
@@ -1313,6 +1331,28 @@ func (r *Registry) SetSteering(host, steering string) error {
return nil
}
+// SetReplicaOf repoints a route's replica link (#162) as a narrow UPDATE.
+// The record's value is authoritative in BOTH directions, including down
+// to "": a row that stopped being a replica must lose the link, or the
+// domains pass goes on deriving an alias that serves an account from a
+// box its record no longer places it in. Errors on an unmatched host,
+// SetChannel's contract: the reconciler must notice a row vanishing
+// underneath rather than silently do nothing.
+func (r *Registry) SetReplicaOf(host, replicaOf string) error {
+ res, err := r.sql.Exec(`UPDATE routes SET replica_of = ? WHERE host = ?`, replicaOf, 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
+}
+
// SetGate stamps or clears a route's password gate — the narrow UPDATE the
// provision converge uses (never a full-row Upsert, which preserves a set
// gate by design and so could not clear one). Errors on an unmatched host,
diff --git a/internal/registry/registry_test.go b/internal/registry/registry_test.go
index 3bdb2fee1..4b4f5378d 100644
--- a/internal/registry/registry_test.go
+++ b/internal/registry/registry_test.go
@@ -243,6 +243,35 @@ func TestSetSteeringErrorsOnUnmatchedHost(t *testing.T) {
}
}
+func TestReplicaOfRoundTrips(t *testing.T) {
+ reg, err := Open(filepath.Join(t.TempDir(), "registry.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer reg.Close()
+ if err := reg.Upsert(Route{Host: "r7x2.go.tito.io", Kind: "instance", Status: "ready", ReplicaOf: "acme.go.tito.io"}); err != nil {
+ t.Fatal(err)
+ }
+ got, err := reg.Get("r7x2.go.tito.io")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got.ReplicaOf != "acme.go.tito.io" {
+ t.Fatalf("ReplicaOf = %q, want acme.go.tito.io", got.ReplicaOf)
+ }
+ if err := reg.SetReplicaOf("r7x2.go.tito.io", ""); err != nil {
+ t.Fatal(err)
+ }
+ if got, err := reg.Get("r7x2.go.tito.io"); err != nil {
+ t.Fatal(err)
+ } else if got.ReplicaOf != "" {
+ t.Fatalf("SetReplicaOf did not clear the column: %q", got.ReplicaOf)
+ }
+ if err := reg.SetReplicaOf("nosuch.go.tito.io", "x"); err == nil {
+ t.Error("SetReplicaOf on an unknown host returned nil, want an error")
+ }
+}
+
// TestFingerprintIgnoresSteering is the re-ruled pin (plan amendment
// b485ec5, review finding 3): steering is deliberately OUT of Fingerprint,
// the same idiom as caps/unit (Route.Steering's own doc comment) — it