carlos / platform Public

ops: unit-backed hibernating tenants stay in the fleet litestream config

The skip-hibernating rule exists because the activator's embedded
litestream owns exec children's replication; for carlos-app@ tenants the
activator only drives systemctl and can't read the DynamicUser state
dir — excluding them would leave their data replicated by nobody.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 59847e3a6853c882948ea933acd798290aa0b217 parent 4f2c257
2 files changed, +21 −7
  • internal/ops/litestream.go +9 −6
  • internal/ops/ops_test.go +12 −1
diff --git a/internal/ops/litestream.go b/internal/ops/litestream.go
index 590ef6825..b87830b08 100644
--- a/internal/ops/litestream.go
+++ b/internal/ops/litestream.go
@@ -82,11 +82,14 @@ func ParseLitestreamDBs(cfg string) []string {
}
// FleetDBs lists the non-empty *.db files under dataDir that this replicator
-// owns. Hibernating routes' databases (matched by DBPath) are excluded:
-// the activator's embedded Litestream owns their replication (to the
-// instances bucket, under the lease) — a second replicator on the same WAL
-// would fight it for checkpointing and corrupt the replica. (Eleven's
-// fleet-backup.sh lesson, carried verbatim.)
+// owns. Hibernating EXEC-backed routes' databases (matched by DBPath) are
+// excluded: the activator's embedded Litestream owns their replication (to
+// the instances bucket, under the lease) — a second replicator on the same
+// WAL would fight it for checkpointing and corrupt the replica. (Eleven's
+// fleet-backup.sh lesson, carried verbatim.) UNIT-backed hibernating routes
+// (carlos-app@ tenants) stay in: their wake/sleep is systemctl, the agent
+// never replicates them (it can't even read a DynamicUser's state dir), so
+// dropping them here would leave their data replicated by nobody.
func FleetDBs(reg *registry.Registry, dataDir string) ([]string, error) {
routes, err := reg.List()
if err != nil {
@@ -94,7 +97,7 @@ func FleetDBs(reg *registry.Registry, dataDir string) ([]string, error) {
}
hibernating := map[string]bool{}
for _, r := range routes {
- if r.Hibernate && r.DBPath != "" {
+ if r.Hibernate && r.Backing != registry.BackingUnit && r.DBPath != "" {
hibernating[filepath.Clean(r.DBPath)] = true
}
}
diff --git a/internal/ops/ops_test.go b/internal/ops/ops_test.go
index bda9689d1..18400fde4 100644
--- a/internal/ops/ops_test.go
+++ b/internal/ops/ops_test.go
@@ -24,7 +24,8 @@ func TestConfigGenSkipsHibernatingRoutes(t *testing.T) {
awake := filepath.Join(dir, "a.example.com.db")
asleep := filepath.Join(dir, "b.example.com.db")
- for _, p := range []string{awake, asleep} {
+ unitTenant := filepath.Join(dir, "c.example.com.db")
+ for _, p := range []string{awake, asleep, unitTenant} {
if err := os.WriteFile(p, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
@@ -35,6 +36,13 @@ func TestConfigGenSkipsHibernatingRoutes(t *testing.T) {
if err := reg.Upsert(registry.Route{Host: "b.example.com", DBPath: asleep, Hibernate: true}); err != nil {
t.Fatal(err)
}
+ // A unit-backed hibernating tenant (carlos-app@): its wake/sleep is
+ // systemctl and the activator never replicates it, so this replicator
+ // must keep owning its database.
+ if err := reg.Upsert(registry.Route{Host: "c.example.com", DBPath: unitTenant,
+ Hibernate: true, Backing: registry.BackingUnit}); err != nil {
+ t.Fatal(err)
+ }
dbs, err := FleetDBs(reg, dir)
if err != nil {
@@ -47,6 +55,9 @@ func TestConfigGenSkipsHibernatingRoutes(t *testing.T) {
if strings.Contains(cfg, asleep) {
t.Fatalf("hibernating route's DB must be excluded (activator owns its replication):\n%s", cfg)
}
+ if !strings.Contains(cfg, unitTenant) {
+ t.Fatalf("unit-backed hibernating route's DB missing (nobody else replicates it):\n%s", cfg)
+ }
// The registry itself lives in the data dir and is not anyone's hibernating
// instance — it must be replicated.
if !strings.Contains(cfg, filepath.Join(dir, "registry.db")) {