carlos / platform Public

#162: Role, Replicas and Home on the instance record

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev cf919930d6084b2a8fdbc53e35c6e7212f9813c9 parent f24c418
2 files changed, +199 −0
  • internal/provision/provision.go +112 −0
  • internal/provision/provision_test.go +87 −0
diff --git a/internal/provision/provision.go b/internal/provision/provision.go
index 7c68ed826..234d43a1a 100644
--- a/internal/provision/provision.go
+++ b/internal/provision/provision.go
@@ -18,6 +18,7 @@ import (
"os"
"path/filepath"
"regexp"
+ "slices"
"sort"
"strconv"
"strings"
@@ -140,6 +141,32 @@ type Record struct {
// host. Converged into routes.gate by Reconcile's owned-converge arm
// (reconcile.go) and read by the edge on every request. Spec docs/superpowers/specs/2026-09-07-password-gate-design.md.
Gate string `json:"gate,omitempty"`
+
+ // --- replica placements (amadan carlos/platform #162) ---
+
+ // Role is what this instance IS within its app: "" (the default every
+ // record written before this field existed decodes to) is the serving
+ // home, the instance whose data is authoritative. "replica" is a
+ // SECOND instance of the same account holding a one-way mirror of it,
+ // placed near the buyers (spec §2). Written only at create; no PATCH
+ // flips a record into or out of the role, because which database is
+ // authoritative is a data-plane fact, not a shape edit. ValidRole is
+ // the door.
+ Role string `json:"role,omitempty"`
+
+ // Replicas names the placements an account's replicas live in — the
+ // HOME record's own opt-in and the residence gate (spec §2). Never
+ // defaulted, never inferred from Region. A record carrying any
+ // placement here must also carry Steering "latency", or the account's
+ // host resolves to one edge and the replica serves nothing (spec §5).
+ Replicas []string `json:"replicas,omitempty"`
+
+ // Home is the public host a REPLICA record mirrors: the home record's
+ // own host. Written at create and never converged — repointing a
+ // replica is delete-and-recreate, because what it serves is a mirror
+ // OF something. Carried onto the route row as replica_of, which is
+ // what the alias derivation and the edges-file writer read (spec §3).
+ Home string `json:"home,omitempty"`
}
// Health is the per-route probe config (HA spec Part II). Zero value =
@@ -367,6 +394,55 @@ func ValidSteering(s string) bool {
return false
}
+// ValidRole pins the record's role vocabulary (spec §2): "" (the serving
+// home — every record written before the field existed decodes to it,
+// which is what makes the field additive) or "replica". Case-sensitive
+// and closed, ValidSteering's discipline: a value a future reader has
+// never heard of is refused where it is typed, rather than discovered as
+// a row nothing routes for.
+func ValidRole(s string) bool {
+ switch s {
+ case "", "replica":
+ return true
+ }
+ return false
+}
+
+// MaxReplicas caps the placement set one record may name. Each entry is a
+// decision about where an account's order data lives until a drain
+// confirms (spec §2), not a cache hint, so the cap is small and deliberate.
+const MaxReplicas = 4
+
+// ValidReplicas normalizes and validates a replica placement set: trim,
+// drop empties, deduplicate, sort into the canonical order, and check each
+// element against ValidRegion — the same vocabulary a record's own Region
+// speaks. Exported because the console's door canonicalizes the request
+// body through the SAME derivation ValidateShape re-checks, so neither
+// side trusts the other about what is in the bucket.
+func ValidReplicas(regions []string) ([]string, error) {
+ if len(regions) > MaxReplicas {
+ return nil, fmt.Errorf("replicas: %d placements, at most %d — a replica holds the account's order data in the placement it names until a drain confirms, so each one is a residence decision", len(regions), MaxReplicas)
+ }
+ seen := make(map[string]bool, len(regions))
+ out := make([]string, 0, len(regions))
+ for _, r := range regions {
+ r = strings.TrimSpace(r)
+ if r == "" {
+ return nil, errors.New("replicas: empty placement code")
+ }
+ if !ValidRegion(r) {
+ return nil, fmt.Errorf("replica placement %q: want a placement code (e.g. ie, uk)", r)
+ }
+ if seen[r] {
+ continue
+ }
+ seen[r] = true
+ out = append(out, r)
+ }
+ sort.Strings(out)
+ return out, nil
+}
+
// MaxUpstreams caps the Addrs set one record may name. The prober probes a
// host's upstreams SEQUENTIALLY once per interval (internal/edge/prober.go),
// and every scale figure the multi-upstream design rests on — probe traffic,
@@ -388,6 +464,42 @@ func ValidateShape(rec Record) error {
if !ValidSteering(rec.Steering) {
return fmt.Errorf("steering %q: want \"\" or \"latency\"", rec.Steering)
}
+ if !ValidRole(rec.Role) {
+ return fmt.Errorf("role %q: want \"\" or \"replica\"", rec.Role)
+ }
+ if rec.Role == "replica" {
+ // A replica is a process holding a mirror of live data — never a
+ // file tree, never a foreign upstream the platform did not spawn.
+ if rec.KindOrDefault() != "instance" {
+ return errors.New("role replica with kind " + rec.KindOrDefault() + ": a replica is an instance of the account — drop kind or drop role")
+ }
+ if rec.Home == "" {
+ return errors.New("role replica with no home: a replica names the public host it mirrors")
+ }
+ // ValidRegion("") is true, so the placement rail is written out.
+ if rec.Region == "" {
+ return errors.New("role replica with no region: a replica is always in a named placement")
+ }
+ if len(rec.Replicas) > 0 {
+ return errors.New("role replica with replicas: a replica of a replica is not a shape the platform carries")
+ }
+ }
+ if len(rec.Replicas) > 0 {
+ norm, err := ValidReplicas(rec.Replicas)
+ if err != nil {
+ return err
+ }
+ if slices.Contains(norm, rec.Region) && rec.Region != "" {
+ return fmt.Errorf("replica placement %q is the record's own placement: a replica lives somewhere else", rec.Region)
+ }
+ // The home region normalizes to "" on a record (homeRegionIn,
+ // console apiinstancemove.go), so this rail cannot see a home in
+ // the deployment's own region naming it as a replica placement.
+ // That comparison lives at the door, which knows the code (spec §2).
+ if rec.Steering != "latency" {
+ return errors.New("replicas without steering latency: the account's host would resolve to one edge and the replica would serve nothing — set steering first")
+ }
+ }
if rec.Gate != "" {
if _, err := bcrypt.Cost([]byte(rec.Gate)); err != nil {
return errors.New("gate: want a bcrypt hash — the console hashes the password; a record never carries it in clear")
diff --git a/internal/provision/provision_test.go b/internal/provision/provision_test.go
index 1ab77449c..0b8b3fdaa 100644
--- a/internal/provision/provision_test.go
+++ b/internal/provision/provision_test.go
@@ -1,11 +1,13 @@
package provision
import (
+ "bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
+ "slices"
"strings"
"testing"
"time"
@@ -1115,3 +1117,88 @@ func TestLayoutJournalPath(t *testing.T) {
t.Errorf("LayoutJournalPath = %q", got)
}
}
+
+func TestValidRole(t *testing.T) {
+ for _, ok := range []string{"", "replica"} {
+ if !ValidRole(ok) {
+ t.Errorf("ValidRole(%q) = false, want true", ok)
+ }
+ }
+ for _, bad := range []string{"Replica", "home", "read", "replica "} {
+ if ValidRole(bad) {
+ t.Errorf("ValidRole(%q) = true, want false", bad)
+ }
+ }
+}
+
+func TestValidReplicas(t *testing.T) {
+ got, err := ValidReplicas([]string{" uk ", "ie", "uk"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := []string{"ie", "uk"}; !slices.Equal(got, want) {
+ t.Errorf("ValidReplicas = %v, want %v", got, want)
+ }
+ if _, err := ValidReplicas([]string{"ie", ""}); err == nil {
+ t.Error("ValidReplicas accepted an empty placement: an empty entry is a caller bug, not a blank to skip")
+ }
+ if _, err := ValidReplicas([]string{"UK"}); err == nil {
+ t.Error("ValidReplicas accepted an upper-case placement code")
+ }
+ if _, err := ValidReplicas([]string{"a", "b", "c", "d", "e"}); err == nil {
+ t.Errorf("ValidReplicas accepted more than %d placements", MaxReplicas)
+ }
+}
+
+func TestValidateShapeReplicaRails(t *testing.T) {
+ base := Record{Schema: 1, Host: "acme.go.tito.io", Slug: "acme", App: "tito", Account: "abc", Channel: "stable"}
+ replica := base
+ replica.Role, replica.Home, replica.Region = "replica", "acme.go.tito.io", "uk"
+ if err := ValidateShape(replica); err != nil {
+ t.Fatalf("a well-formed replica record was refused: %v", err)
+ }
+ for name, mut := range map[string]func(*Record){
+ "unknown role": func(r *Record) { r.Role = "read" },
+ "replica of a static": func(r *Record) { r.Kind = "static" },
+ "replica with no home": func(r *Record) { r.Home = "" },
+ "replica with no region": func(r *Record) { r.Region = "" },
+ "replica carrying replicas": func(r *Record) { r.Replicas = []string{"ie"} },
+ } {
+ rec := replica
+ mut(&rec)
+ if err := ValidateShape(rec); err == nil {
+ t.Errorf("%s: ValidateShape accepted it", name)
+ }
+ }
+
+ home := base
+ home.Region, home.Steering, home.Replicas = "ie", "latency", []string{"uk"}
+ if err := ValidateShape(home); err != nil {
+ t.Fatalf("a well-formed home record was refused: %v", err)
+ }
+ for name, mut := range map[string]func(*Record){
+ "replicas without steering": func(r *Record) { r.Steering = "" },
+ "replica placement is the record's own region": func(r *Record) { r.Replicas = []string{"ie"} },
+ "empty placement": func(r *Record) { r.Replicas = []string{""} },
+ "bad placement": func(r *Record) { r.Replicas = []string{"UK"} },
+ "too many placements": func(r *Record) { r.Replicas = []string{"a", "b", "c", "d", "e"} },
+ } {
+ rec := home
+ mut(&rec)
+ if err := ValidateShape(rec); err == nil {
+ t.Errorf("%s: ValidateShape accepted it", name)
+ }
+ }
+}
+
+func TestRecordReplicaFieldsAreAdditive(t *testing.T) {
+ raw, err := json.Marshal(Record{Schema: 1, Host: "acme.go.tito.io", Channel: "stable"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, key := range []string{"role", "replicas", "home"} {
+ if bytes.Contains(raw, []byte(`"`+key+`"`)) {
+ t.Errorf("a record with no replica fields marshalled %q: the field is not omitempty", key)
+ }
+ }
+}