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>
2 files changed,
+21
−7
internal/ops/litestream.go+9 −6internal/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")) { |