carlos / platform Public

registry: route_paths table, accessors, and fingerprint section

A hostname's routes table row is its identity — cert, home box, DNS state,
aliases — so per-prefix routing lives in its own pass-owned table keyed on
(host, prefix), mirroring route_aliases.

The fingerprint section is the load-bearing half: a path flip's entire
observable effect is in this table, so without it flips would reconcile
correctly and never be served.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 16e43f8bfac904adf7c6e2cd774b668119186edc parent 8fab91c
2 files changed, +244 −0
  • internal/registry/paths_test.go +132 −0
  • internal/registry/registry.go +112 −0
diff --git a/internal/registry/paths_test.go b/internal/registry/paths_test.go
new file mode 100644
index 000000000..0e4c474db
--- /dev/null
+++ b/internal/registry/paths_test.go
@@ -0,0 +1,132 @@
+package registry
+
+import "testing"
+
+// TestPathRoundTrip: a written prefix row comes back, and a repoint of the
+// same (host, prefix) replaces rather than duplicates.
+func TestPathRoundTrip(t *testing.T) {
+ reg := openTestRegistry(t)
+ if err := reg.UpsertPath(RoutePath{
+ Host: "ti.to", Prefix: "paulca", Target: "go.tito.io",
+ Account: "tito", App: "classic",
+ }); err != nil {
+ t.Fatal(err)
+ }
+ got, err := reg.ListPaths()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(got) != 1 {
+ t.Fatalf("ListPaths returned %d rows, want 1", len(got))
+ }
+ if got[0].Target != "go.tito.io" {
+ t.Fatalf("target = %q, want go.tito.io", got[0].Target)
+ }
+
+ if err := reg.UpsertPath(RoutePath{
+ Host: "ti.to", Prefix: "paulca", Target: "beta.tito.io",
+ Account: "tito", App: "classic",
+ }); err != nil {
+ t.Fatal(err)
+ }
+ got, err = reg.ListPaths()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(got) != 1 {
+ t.Fatalf("repoint produced %d rows, want 1 — (host, prefix) is the key", len(got))
+ }
+ if got[0].Target != "beta.tito.io" {
+ t.Fatalf("target = %q, want beta.tito.io after repoint", got[0].Target)
+ }
+}
+
+// TestDeletePathIsIdempotent: deleting a row that is already gone is a
+// success, the same contract DeleteAlias carries. The reconcile pass deletes
+// unconditionally, so this must not error.
+func TestDeletePathIsIdempotent(t *testing.T) {
+ reg := openTestRegistry(t)
+ if err := reg.DeletePath("ti.to", "nobody"); err != nil {
+ t.Fatalf("deleting an absent path row: %v", err)
+ }
+}
+
+// TestPathsAreScopedToTheirHost: two hosts may carry the same prefix without
+// colliding — the key is the pair, not the prefix.
+func TestPathsAreScopedToTheirHost(t *testing.T) {
+ reg := openTestRegistry(t)
+ for _, h := range []string{"ti.to", "other.example"} {
+ if err := reg.UpsertPath(RoutePath{
+ Host: h, Prefix: "paulca", Target: "go.tito.io",
+ Account: "tito", App: "classic",
+ }); err != nil {
+ t.Fatal(err)
+ }
+ }
+ got, err := reg.ListPaths()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(got) != 2 {
+ t.Fatalf("got %d rows, want 2 — (host, prefix) is the key, not prefix alone", len(got))
+ }
+}
+
+// TestFingerprintChangesOnPathMutation is the load-bearing test of this
+// task. Fingerprint() is the 2s content hash the edge polls; a route_paths
+// change it cannot see is a flip that reconciles perfectly and never reaches
+// the live map — a silent false negative, the failure class Fingerprint's own
+// header names as the one to engineer against.
+func TestFingerprintChangesOnPathMutation(t *testing.T) {
+ reg := openTestRegistry(t)
+ if err := reg.UpsertPath(RoutePath{
+ Host: "ti.to", Prefix: "paulca", Target: "go.tito.io",
+ Account: "tito", App: "classic",
+ }); err != nil {
+ t.Fatal(err)
+ }
+ before, err := reg.Fingerprint()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := reg.UpsertPath(RoutePath{
+ Host: "ti.to", Prefix: "paulca", Target: "beta.tito.io",
+ Account: "tito", App: "classic",
+ }); err != nil {
+ t.Fatal(err)
+ }
+ after, err := reg.Fingerprint()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if before == after {
+ t.Fatalf("fingerprint unchanged across a route_paths repoint: %q", after)
+ }
+}
+
+// TestFingerprintChangesOnPathRemoval: the un-flip direction. Rollback is a
+// row disappearing, and it must reach the edge on the same 2s bound as the
+// flip did.
+func TestFingerprintChangesOnPathRemoval(t *testing.T) {
+ reg := openTestRegistry(t)
+ if err := reg.UpsertPath(RoutePath{
+ Host: "ti.to", Prefix: "paulca", Target: "go.tito.io",
+ Account: "tito", App: "classic",
+ }); err != nil {
+ t.Fatal(err)
+ }
+ before, err := reg.Fingerprint()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := reg.DeletePath("ti.to", "paulca"); err != nil {
+ t.Fatal(err)
+ }
+ after, err := reg.Fingerprint()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if before == after {
+ t.Fatalf("fingerprint unchanged across a route_paths removal: %q", after)
+ }
+}
diff --git a/internal/registry/registry.go b/internal/registry/registry.go
index 130626a37..fb026b591 100644
--- a/internal/registry/registry.go
+++ b/internal/registry/registry.go
@@ -415,6 +415,27 @@ CREATE TABLE IF NOT EXISTS wildcard_domains (
account TEXT NOT NULL DEFAULT '',
app TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL DEFAULT 0
+);`); err != nil {
+ return err
+ }
+ // route_paths is a derived table (the domains reconcile pass owns every
+ // row, materializing a claim's Paths map into it) — a plain CREATE TABLE
+ // IF NOT EXISTS, the same additive shape as route_aliases and
+ // wildcard_domains above, so existing DBs migrate on open with zero rows
+ // and no behaviour change.
+ //
+ // The key is the PAIR: `routes` has host as its primary key and must
+ // stay one row per hostname (that row owns the cert, the home box, the
+ // DNS state and the aliases), so a hostname's prefixes live here instead.
+ // prefix is a BARE segment — "paulca", never "/paulca".
+ if _, err := r.sql.Exec(`
+CREATE TABLE IF NOT EXISTS route_paths (
+ host TEXT NOT NULL,
+ prefix TEXT NOT NULL,
+ target TEXT NOT NULL,
+ account TEXT NOT NULL DEFAULT '',
+ app TEXT NOT NULL DEFAULT '',
+ PRIMARY KEY (host, prefix)
);`); err != nil {
return err
}
@@ -1594,6 +1615,66 @@ func (r *Registry) DeleteWildcardDomain(host string) error {
return err
}
+// RoutePath is one derived row of the route_paths table: a leading URL path
+// segment on Host that is served by the route at Target instead of by Host's
+// own route. Wholly pass-owned, like route_aliases and wildcard_domains:
+// every row traces to a claim's Paths entry, so removal needs no ownership
+// flag.
+//
+// A path row deliberately carries NO upstream, channel or environment of its
+// own. It is a pointer; everything about how the traffic is served comes from
+// Target's ordinary route row. That is what makes pointing a subset of
+// prefixes at a second route a working canary rather than a special case.
+//
+// Prefix is a bare segment — "paulca", never "/paulca".
+type RoutePath struct {
+ Host string
+ Prefix string
+ Target string
+ Account string
+ App string
+}
+
+// ListPaths returns every path route, ordered by host then prefix.
+func (r *Registry) ListPaths() ([]RoutePath, error) {
+ rows, err := r.sql.Query(`SELECT host, prefix, target, account, app
+FROM route_paths ORDER BY host, prefix`)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var out []RoutePath
+ for rows.Next() {
+ var p RoutePath
+ if err := rows.Scan(&p.Host, &p.Prefix, &p.Target, &p.Account, &p.App); err != nil {
+ return nil, err
+ }
+ out = append(out, p)
+ }
+ return out, rows.Err()
+}
+
+// UpsertPath inserts or repoints one path route by its (host, prefix) key.
+// Unlike UpsertAlias there are no stamp columns to preserve — nothing on this
+// table is maintained on a separate cadence — so every column is overwritten.
+func (r *Registry) UpsertPath(p RoutePath) error {
+ _, err := r.sql.Exec(`
+INSERT INTO route_paths (host, prefix, target, account, app)
+VALUES (?, ?, ?, ?, ?)
+ON CONFLICT(host, prefix) DO UPDATE SET
+ target=excluded.target, account=excluded.account, app=excluded.app`,
+ p.Host, p.Prefix, p.Target, p.Account, p.App)
+ return err
+}
+
+// DeletePath removes one path route. Idempotent: a row already gone is a
+// success, the same contract as DeleteAlias — the reconcile pass deletes
+// unconditionally from its own view of what is no longer desired.
+func (r *Registry) DeletePath(host, prefix string) error {
+ _, err := r.sql.Exec(`DELETE FROM route_paths WHERE host = ? AND prefix = ?`, host, prefix)
+ return err
+}
+
// Fingerprint summarizes the routes, fleets, fleet_boxes, route_aliases and
// wildcard_domains tables over a fresh connection, for the edge's cheap
// change poll. It is content-accurate: it hashes the actual VALUES of the
@@ -1669,6 +1750,13 @@ func (r *Registry) DeleteWildcardDomain(host string) error {
// columns are hashed — there is no cadence-stamped column on this table to
// exclude, unlike route_aliases' dns_status/cert_expires_at.
//
+// route_paths is hashed for the same reload-latency reason, and it is the
+// section most worth stating explicitly: a path flip is the ONLY routing
+// change in the system whose whole observable effect is in this table. A
+// missing section here would not degrade a flip, it would make flips do
+// nothing at all — reconciled correctly, stored correctly, never served —
+// until some unrelated route write happened to move the digest.
+//
// The ORDER BY clauses are what make the digest deterministic across polls;
// a hypothetical reordering would only cost a redundant reload (idempotent),
// never a missed change — the failure mode worth engineering against is the
@@ -1792,6 +1880,30 @@ FROM fleet_boxes ORDER BY fleet, label`)
return "", err
}
+ // route_paths: which prefix on which hostname reaches which route is a
+ // routing decision in exactly the sense route_aliases' alias/host is, so
+ // it hashes here or a flip never reaches the live map. All five columns
+ // hash — unlike route_aliases there is no box-stamped, cadence-driven
+ // column on this table to exclude, so there is nothing here that could
+ // force a pointless fleet-wide reload.
+ fmt.Fprint(h, "\x1droute_paths\x1e")
+ prows, err := db.Query(`SELECT host, prefix, target, account, app
+FROM route_paths ORDER BY host, prefix`)
+ if err != nil {
+ return "", err
+ }
+ defer prows.Close()
+ for prows.Next() {
+ var host, prefix, target, account, app string
+ if err := prows.Scan(&host, &prefix, &target, &account, &app); err != nil {
+ return "", err
+ }
+ fmt.Fprintf(h, "%s\x1f%s\x1f%s\x1f%s\x1f%s\x1e", host, prefix, target, account, app)
+ }
+ if err := prows.Err(); err != nil {
+ return "", err
+ }
+
return fmt.Sprintf("%d:%x", n, h.Sum(nil)), nil
}