carlos / platform Public

Merge pull request #361 from carlosframework/mirror-skips-the-lease

mirror: skip the lease's lock.json (#331 rehearsal catch)
Paul Campbell pushed by paul@keymail.dev ae64ea53c2cbea92f3c3652467ad16dd2b3cdb30 parent fa5ba8eparent 3cb1dab
2 files changed, +33 −0
  • internal/lease/mirror.go +12 −0
  • internal/lease/mirror_test.go +21 −0
diff --git a/internal/lease/mirror.go b/internal/lease/mirror.go
index b0cda7f7d..684bdf312 100644
--- a/internal/lease/mirror.go
+++ b/internal/lease/mirror.go
@@ -67,6 +67,15 @@ func MirrorPrefix(ctx context.Context, m MirrorStore, srcBucket, srcPrefix, dstB
sort.Strings(rel)
changed := 0
for _, r := range rel {
+ // The lease's lock.json is COORDINATION state, not data (live
+ // rehearsal catch, 2026-08-29): the renewer rewrites it on every
+ // TTL renewal, so mirroring it makes "changed==0" unreachable for
+ // any awake instance — and promoting it would hand the target a
+ // stale foreign lease. Skipped on both sides: never copied, and
+ // the destination's own copy (if any) is never deleted.
+ if r == "lock.json" {
+ continue
+ }
sk, dk := srcPrefix+r, dstPrefix+r
if et, ok := dst[dk]; ok && et == src[sk] {
continue
@@ -78,6 +87,9 @@ func MirrorPrefix(ctx context.Context, m MirrorStore, srcBucket, srcPrefix, dstB
}
for dk := range dst {
r := dk[len(dstPrefix):]
+ if r == "lock.json" {
+ continue
+ }
if _, ok := src[srcPrefix+r]; !ok {
if err := m.Delete(ctx, dstBucket, dk); err != nil {
return changed, fmt.Errorf("mirror: delete %s: %w", dk, err)
diff --git a/internal/lease/mirror_test.go b/internal/lease/mirror_test.go
index 3ab8f11b0..124b3b896 100644
--- a/internal/lease/mirror_test.go
+++ b/internal/lease/mirror_test.go
@@ -110,3 +110,24 @@ func TestMirrorPrefixNormalizesBoundaries(t *testing.T) {
t.Fatalf("staging keys = %v, want the one properly-joined key", got)
}
}
+
+// lock.json is coordination state: never copied, never deleted, and its
+// churn (the renewer rewrites it every TTL) must not keep changed above 0.
+func TestMirrorPrefixSkipsTheLease(t *testing.T) {
+ ctx := context.Background()
+ f := newFakeMirror()
+ f.put("src", "instances/h/lock.json", "renewal-1")
+ f.put("src", "instances/h/generations/a/snapshot", "e1")
+ f.put("dst", "moves/h/lock.json", "old-foreign-lease")
+ if n, err := MirrorPrefix(ctx, f, "src", "instances/h/", "dst", "moves/h/"); err != nil || n != 1 {
+ t.Fatalf("round = %d, %v; want only the snapshot to move", n, err)
+ }
+ f.put("src", "instances/h/lock.json", "renewal-2") // the churn
+ if n, err := MirrorPrefix(ctx, f, "src", "instances/h/", "dst", "moves/h/"); err != nil || n != 0 {
+ t.Fatalf("churn round = %d, %v; want caught-up despite the renewal", n, err)
+ }
+ got, _ := f.List(ctx, "dst", "moves/h/")
+ if got["moves/h/lock.json"] != "old-foreign-lease" || len(got) != 2 {
+ t.Fatalf("staging = %v; the dst lease must be untouched", got)
+ }
+}