WIP: name the account that pays for each database the box replicates
The second piece of the platform's own replicator. Every replica write has to be metered against an account, so every database needs one: the route whose database it is, the unit tenant whose directory it lies under, or the platform for the box's registry and console. A file two accounts claim, or a route with no account, is named as unowned, never guessed and never dropped. Nothing calls it. Not gated, not reviewed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 files changed,
+200
−0
internal/ops/litestream_owners.go+102 −0internal/ops/litestream_owners_test.go+98 −0
diff --git a/internal/ops/litestream_owners.go b/internal/ops/litestream_owners.go| new file mode 100644 |
| index 000000000..630403deb |
| --- /dev/null |
| +++ b/internal/ops/litestream_owners.go |
| @@ -0,0 +1,102 @@ |
| +package ops |
| + |
| +import ( |
| + "path/filepath" |
| + "sort" |
| + "strings" |
| + |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +// OwnedDB is a database the box replicates, with who pays for its |
| +// replication writes. |
| +type OwnedDB struct { |
| + Path string |
| + // Account, App and Host name the route that owns the database. For the |
| + // box's own databases (the registry and the console) Account is the |
| + // operations account and Platform is true. |
| + Account, App, Host string |
| + Platform bool |
| +} |
| + |
| +// FleetDBOwners is FleetDBs with an owner for each database, for the |
| +// platform's own replicator (path B in |
| +// docs/superpowers/plans/2026-10-01-durability-attestation-path.md): every |
| +// replica write has to be metered against an account, so every database has |
| +// to have one. |
| +// |
| +// A database is owned by the route whose DBPath is that file, or whose |
| +// DBPath is a directory the file lies under (a unit tenant's tree). The |
| +// box's registry and console databases are the platform's. |
| +// |
| +// unowned lists the databases FleetDBs would replicate that no single |
| +// account can be shown to own: a file two routes of different accounts |
| +// both claim, or a route with no account. They are named, never dropped |
| +// and never guessed. A replicator must not stream them unmetered, and a box |
| +// that replicates one cannot say that every write was counted. |
| +func FleetDBOwners(reg *registry.Registry, dataDir string) (owned []OwnedDB, unowned []string, skipped []string, err error) { |
| + dbs, skipped, err := FleetDBsWithSkipped(reg, dataDir) |
| + if err != nil { |
| + return nil, nil, nil, err |
| + } |
| + routes, err := reg.List() |
| + if err != nil { |
| + return nil, nil, nil, err |
| + } |
| + platform := map[string]bool{ |
| + filepath.Clean(filepath.Join(dataDir, "registry.db")): true, |
| + filepath.Clean(consoleDBPath(dataDir)): true, |
| + } |
| + for _, p := range dbs { |
| + p = filepath.Clean(p) |
| + if platform[p] { |
| + owned = append(owned, OwnedDB{Path: p, Account: release.OpsAccount, Platform: true}) |
| + continue |
| + } |
| + // Every route that claims the file, exactly or by a directory above |
| + // it. The most specific claim wins only when it is the only one of |
| + // its kind; two accounts on one file is not something to choose |
| + // between. |
| + var claims []registry.Route |
| + for _, r := range routes { |
| + if r.DBPath == "" { |
| + continue |
| + } |
| + claim := filepath.Clean(r.DBPath) |
| + if claim == p || (r.Backing == registry.BackingUnit && strings.HasPrefix(p, claim+string(filepath.Separator))) { |
| + claims = append(claims, r) |
| + } |
| + } |
| + owner, ok := singleOwner(claims) |
| + if !ok { |
| + unowned = append(unowned, p) |
| + continue |
| + } |
| + owned = append(owned, OwnedDB{Path: p, Account: owner.Account, App: owner.App, Host: owner.Host, Platform: owner.Account == release.OpsAccount}) |
| + } |
| + sort.Slice(owned, func(i, j int) bool { return owned[i].Path < owned[j].Path }) |
| + sort.Strings(unowned) |
| + return owned, unowned, skipped, nil |
| +} |
| + |
| +// singleOwner picks the route that owns a database from the routes that |
| +// claim it. There must be at least one, each must name an account, and all |
| +// must name the same account and app. The host is the first claim's by |
| +// name, so the answer does not depend on the registry's order. |
| +func singleOwner(claims []registry.Route) (registry.Route, bool) { |
| + if len(claims) == 0 { |
| + return registry.Route{}, false |
| + } |
| + sort.Slice(claims, func(i, j int) bool { return claims[i].Host < claims[j].Host }) |
| + first := claims[0] |
| + if first.Account == "" { |
| + return registry.Route{}, false |
| + } |
| + for _, c := range claims[1:] { |
| + if c.Account != first.Account || c.App != first.App { |
| + return registry.Route{}, false |
| + } |
| + } |
| + return first, true |
| +} |
diff --git a/internal/ops/litestream_owners_test.go b/internal/ops/litestream_owners_test.go| new file mode 100644 |
| index 000000000..4bf886efc |
| --- /dev/null |
| +++ b/internal/ops/litestream_owners_test.go |
| @@ -0,0 +1,98 @@ |
| +package ops |
| + |
| +import ( |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +// Every database the box replicates gets the account that pays for its |
| +// replication writes, or is named as having none. Nothing is guessed. |
| +func TestFleetDBOwnersNamesAnOwnerOrSaysThereIsNone(t *testing.T) { |
| + dir := t.TempDir() |
| + reg, err := registry.Open(filepath.Join(dir, "registry.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + write := func(p string) string { |
| + t.Helper() |
| + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(p, []byte("x"), 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| + return p |
| + } |
| + exec := write(filepath.Join(dir, "a.example.com.db")) |
| + state := filepath.Join(t.TempDir(), "unit-state") |
| + unitFile := write(filepath.Join(state, "main.db")) |
| + unitNested := write(filepath.Join(state, "tenants", "t1.db")) |
| + shared := write(filepath.Join(dir, "shared.example.com.db")) |
| + orphan := write(filepath.Join(dir, "no-account.example.com.db")) |
| + asleep := write(filepath.Join(dir, "asleep.example.com.db")) |
| + leftover := write(filepath.Join(dir, "leftover.db")) |
| + for _, r := range []registry.Route{ |
| + {Host: "a.example.com", Account: "bac", App: "one", DBPath: exec}, |
| + {Host: "unit.example.com", Account: "bad", App: "unit", DBPath: state, Backing: registry.BackingUnit}, |
| + // Two accounts claim one file. |
| + {Host: "shared.example.com", Account: "bac", App: "one", DBPath: shared}, |
| + {Host: "shared2.example.com", Account: "bae", App: "two", DBPath: shared}, |
| + // A route with no account. |
| + {Host: "no-account.example.com", DBPath: orphan}, |
| + // Hibernating: the activator replicates it, metered already. |
| + {Host: "asleep.example.com", Account: "bac", App: "one", DBPath: asleep, Hibernate: true}, |
| + } { |
| + if err := reg.Upsert(r); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + owned, unowned, skipped, err := FleetDBOwners(reg, dir) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + got := map[string]OwnedDB{} |
| + for _, o := range owned { |
| + got[o.Path] = o |
| + } |
| + if o := got[filepath.Join(dir, "registry.db")]; o.Account != release.OpsAccount || !o.Platform { |
| + t.Errorf("the registry is not the platform's: %+v", o) |
| + } |
| + if o := got[exec]; o.Account != "bac" || o.App != "one" || o.Host != "a.example.com" || o.Platform { |
| + t.Errorf("an exec route's database: %+v", o) |
| + } |
| + for _, p := range []string{unitFile, unitNested} { |
| + if o := got[p]; o.Account != "bad" || o.App != "unit" || o.Host != "unit.example.com" { |
| + t.Errorf("a file under a unit tenant's directory (%s): %+v", p, o) |
| + } |
| + } |
| + want := map[string]bool{shared: true, orphan: true} |
| + if len(unowned) != len(want) { |
| + t.Fatalf("unowned = %v, want the file two accounts claim and the file of a route with no account", unowned) |
| + } |
| + for _, p := range unowned { |
| + if !want[p] { |
| + t.Errorf("unowned names %s", p) |
| + } |
| + if _, ok := got[p]; ok { |
| + t.Errorf("%s is both owned and unowned", p) |
| + } |
| + } |
| + if _, ok := got[asleep]; ok { |
| + t.Error("a hibernating route's database is listed: the activator replicates it") |
| + } |
| + if len(skipped) != 1 || skipped[0] != leftover { |
| + t.Errorf("skipped = %v, want the leftover file no route claims", skipped) |
| + } |
| + // Everything FleetDBs replicates is either owned or named as unowned. |
| + dbs, err := FleetDBs(reg, dir) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if len(dbs) != len(owned)+len(unowned) { |
| + t.Fatalf("FleetDBs lists %d databases; %d owned and %d unowned", len(dbs), len(owned), len(unowned)) |
| + } |
| +} |