carlos / platform Public

lease: hibernating instances keep 7 days of snapshots, like the box-wide replicator (#263)

The activator's embedded litestream store set no snapshot retention, so it
took litestream's 24h default, and every wake pruned snapshots older than a
day. carlos db pull --at (which reads only sleeping instances) reached back
about a day. The window now lives in lease (SnapshotInterval 24h,
SnapshotRetention 7d) and internal/ops uses the same constants for
/etc/litestream.yml. A bound generation (a move) still has retention off.
No extra S3 requests: the snapshot monitor's cadence is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Human

Prompt

Yes.

From this commit's own message. The prompt ledger has no entries for it — which is what a squashed, rebased or amended commit looks like, and what every commit looks like in a repo without a ledger.

Summary

Pins 7-day snapshot retention on hibernating instances' replicas, shared with ops.
Agent notes
Mutation-checked: dropping store.SnapshotRetention fails TestNewStorePinsTheRecoveryWindow with 24h.
claude agent, operated by paul@keymail.dev 128dcf2c26c25bdc1dc7a6937f67b1b400043d0d parent 1621d27
3 files changed, +77 −17
  • internal/lease/lease.go +27 −0
  • internal/lease/retention_window_test.go +44 −0
  • internal/ops/litestream.go +6 −17

CI cancelled — superseded by a newer push — run details

diff --git a/internal/lease/lease.go b/internal/lease/lease.go
index d4ae8b158..a9ab03ba5 100644
--- a/internal/lease/lease.go
+++ b/internal/lease/lease.go
@@ -59,6 +59,31 @@ var compactionLevels = litestream.CompactionLevels{
{Level: 3, Interval: 24 * time.Hour},
}
+// SnapshotInterval and SnapshotRetention are the fleet's point-in-time
+// recovery window, for every replica the platform writes: this store (a
+// hibernating instance's) and the box-wide /etc/litestream.yml, which
+// internal/ops writes from these same two constants so they cannot drift.
+//
+// litestream 0.5's DefaultSnapshotRetention is 24h. That covers a dead box;
+// it does not cover LOGICAL corruption, where integrity_check says "ok" and
+// the data is simply wrong. A bad migration or an errant DELETE is
+// typically noticed days later, and by then the only copy is the
+// faithfully-replicated bad one. Here the default was worse than a stated
+// day: retention runs in the snapshot monitor, which runs only while the
+// instance is awake, so each wake pruned every snapshot older than a day
+// (#263), and `carlos db pull --at` reached back about a day.
+//
+// A week is the deliberate choice: seven daily snapshots per database, of
+// files measured in megabytes, against S3 storage at ~$0.023/GB-mo. It adds
+// no requests: the snapshot monitor's cadence is SnapshotInterval either
+// way, and only how far back its deletes reach changes. Lengthen it if a
+// restore is ever wanted from further back than "we noticed within the
+// week".
+const (
+ SnapshotInterval = 24 * time.Hour
+ SnapshotRetention = 7 * 24 * time.Hour
+)
+
// dbMonitorInterval slows litestream's DB monitor (default 1s) — the local
// WAL poll that shovels new frames into shadow LTX. It makes no S3 calls,
// but 1s polling per awake instance is pointless churn when the sleep-time
@@ -299,6 +324,8 @@ func (h *Held) newStore(dbPath string, syncInterval time.Duration) (*litestream.
db.Replica.SyncInterval = syncInterval
}
store := litestream.NewStore([]*litestream.DB{db}, compactionLevels)
+ store.SnapshotInterval = SnapshotInterval
+ store.SnapshotRetention = SnapshotRetention
if h.generationPrefix != "" {
// NewStore resets the DB flag. Set all three store/DB/compactor flags
// afterward, before Open starts any monitor or compaction work.
diff --git a/internal/lease/retention_window_test.go b/internal/lease/retention_window_test.go
new file mode 100644
index 000000000..c5235c0e1
--- /dev/null
+++ b/internal/lease/retention_window_test.go
@@ -0,0 +1,44 @@
+package lease
+
+import (
+ "context"
+ "path/filepath"
+ "testing"
+ "time"
+
+ lss3 "github.com/benbjohnson/litestream/s3"
+)
+
+// A hibernating instance's replica keeps the same recovery window the
+// box-wide replicator does (#263). litestream's own default is 24h, which
+// a wake would enforce, deleting every snapshot older than a day.
+func TestNewStorePinsTheRecoveryWindow(t *testing.T) {
+ l := lss3.NewLeaser()
+ l.Path = "instances/x.example/inc"
+ h := &Held{leaser: l, replicaClient: &generationFake{}}
+ store, db := h.newStore(filepath.Join(t.TempDir(), "db"), time.Second)
+ if store.SnapshotRetention != SnapshotRetention || SnapshotRetention != 7*24*time.Hour {
+ t.Errorf("SnapshotRetention = %v (constant %v), want 7 days", store.SnapshotRetention, SnapshotRetention)
+ }
+ if store.SnapshotInterval != SnapshotInterval || SnapshotInterval != 24*time.Hour {
+ t.Errorf("SnapshotInterval = %v (constant %v), want 24h", store.SnapshotInterval, SnapshotInterval)
+ }
+ if !store.RetentionEnabled || !db.RetentionEnabled {
+ t.Error("retention disabled on an unbound store; the window must be enforced, not only stated")
+ }
+}
+
+// A bound generation (an instance move) keeps retention off whatever the
+// window says: the new setting must not turn it back on.
+func TestNewStoreWindowLeavesAGenerationsRetentionOff(t *testing.T) {
+ l := lss3.NewLeaser()
+ l.Path = "instances/x.example/inc"
+ h := &Held{leaser: l, replicaClient: &generationFake{}}
+ if err := h.BindGeneration(l.Path+"/g-one/", func(context.Context) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ store, db := h.newStore(filepath.Join(t.TempDir(), "db"), time.Second)
+ if store.RetentionEnabled || db.RetentionEnabled {
+ t.Fatal("a bound generation's store has retention on")
+ }
+}
diff --git a/internal/ops/litestream.go b/internal/ops/litestream.go
index 312aaddcd..de6307f40 100644
--- a/internal/ops/litestream.go
+++ b/internal/ops/litestream.go
@@ -47,24 +47,13 @@ const litestreamVersion = "0.5.15"
var retryDelay = 10 * time.Second
// SnapshotInterval and SnapshotRetention pin the fleet's point-in-time
-// recovery window instead of inheriting it.
-//
-// This is the one setting whose DEFAULT was a real hole: litestream 0.5's
-// DefaultSnapshotRetention is 24h, so a database this replicator owns could
-// only ever be restored to roughly the last day. That covers a dead box; it
-// does not cover the failure mode replication is otherwise useless against —
-// LOGICAL corruption, where integrity_check says "ok" and the data is simply
-// wrong. A bad migration or an errant DELETE is typically noticed days later,
-// and by then the only copy is the faithfully-replicated bad one.
-//
-// A week is the deliberate choice: seven daily snapshots per database, of
-// files measured in megabytes, against S3 storage at ~$0.023/GB-mo — noise
-// next to the request bill this fleet actually watches (see internal/lease's
-// LeaseTTL for that story). Lengthen it if a restore is ever wanted from
-// further back than "we noticed within the week".
+// recovery window instead of inheriting litestream's 24h default. They are
+// lease's constants, which hibernating instances' replicas use too, so the
+// box-wide window and the per-instance one are one decision; lease.go
+// records why a week.
const (
- SnapshotInterval = 24 * time.Hour
- SnapshotRetention = 7 * 24 * time.Hour
+ SnapshotInterval = lease.SnapshotInterval
+ SnapshotRetention = lease.SnapshotRetention
)
// SnapshotPolicySupported reports whether `litestream version` output names a