carlos / platform Public

lease: MirrorPrefixKeepObserved reports every decision of a mirror round

One CopyEvent per source key the round decided (copied with the ETag and
version the copy returned; Unchanged with the listed ETag, or the receipt's
ETag and version for a receipt-vouched skip; Deferred) and per destination
key deleted, then one Complete event with the round's source and
destination key sets after a round that finished without error.
MirrorPrefixKeep is the observed round with no observer: ordinary mirroring
and the receipt ledger are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 6eda4ce1e0a5e7fce006df73ef0800b7c2ee2dc0 parent 72c02bb
2 files changed, +293 −4
  • internal/lease/mirror.go +83 −4
  • internal/lease/observed_test.go +210 −0
diff --git a/internal/lease/mirror.go b/internal/lease/mirror.go
index a1aa65870..a81bcb977 100644
--- a/internal/lease/mirror.go
+++ b/internal/lease/mirror.go
@@ -183,6 +183,49 @@ func MirrorPrefix(ctx context.Context, m MirrorStore, srcBucket, srcPrefix, dstB
// LTX files and nothing else this way. A nil keep admits every key, which
// is MirrorPrefix.
func MirrorPrefixKeep(ctx context.Context, m MirrorStore, srcBucket, srcPrefix, dstBucket, dstPrefix string, rc Receipts, keep func(rel string) bool) (MirrorRound, error) {
+ return MirrorPrefixKeepObserved(ctx, m, srcBucket, srcPrefix, dstBucket, dstPrefix, rc, keep, nil)
+}
+
+// CopyEvent is one decision of a mirror round, as MirrorPrefixKeepObserved
+// reports it (self-serve moves plan 2 Task 6: the stager's book). Rel is the
+// key relative to the prefixes. For a source key the round decided:
+// SrcETag and SrcSize as listed, and either the copy's result (DstETag, and
+// DstVersionID on a versioned store), or Unchanged with the destination's
+// ETag when the round skipped it because the two ETags were equal or a
+// receipt vouched for the destination (then DstETag and DstVersionID are
+// the receipt's), or Deferred when the source changed under the copy. For a
+// destination key the source no longer has, Deleted. The one Complete event
+// ends a round that finished without error and carries the round's source
+// keys and the destination's keys after it, both relative and sorted,
+// lock.json left out of both.
+type CopyEvent struct {
+ Rel string
+ SrcETag string
+ SrcSize int64
+ DstETag string
+ DstVersionID string
+ Unchanged bool
+ Deferred bool
+ Deleted bool
+ Complete bool
+ SourceKeys []string
+ DestKeys []string
+}
+
+// MirrorPrefixKeepObserved is MirrorPrefixKeep with observe told every
+// decision the round makes (CopyEvent), one event per source key it decided
+// and per destination key it deleted, then one Complete event once the round
+// has finished without error. observe is called one event at a time, never
+// while a request is in flight on its behalf, and never under the round's
+// lock while a request is made; it must not call the store. A nil observe
+// is MirrorPrefixKeep exactly: the observer changes nothing a round copies,
+// skips, deletes or records.
+func MirrorPrefixKeepObserved(ctx context.Context, m MirrorStore, srcBucket, srcPrefix, dstBucket, dstPrefix string, rc Receipts, keep func(rel string) bool, observe func(CopyEvent)) (MirrorRound, error) {
+ tell := func(e CopyEvent) {
+ if observe != nil {
+ observe(e)
+ }
+ }
// Both prefixes are DIRECTORY boundaries, normalized here to end in
// "/": activator.HibernatePrefix carries none (path.Join), MovesPrefix
// carries one, and relative-key math over a slashless prefix would both
@@ -240,6 +283,7 @@ func MirrorPrefixKeep(ctx context.Context, m MirrorStore, srcBucket, srcPrefix,
s := src[srcPrefix+r]
d, staged := dst[dstPrefix+r]
if staged && d.ETag == s.ETag && !versioned {
+ tell(CopyEvent{Rel: r, SrcETag: s.ETag, SrcSize: s.Size, DstETag: d.ETag, Unchanged: true})
continue
}
if staged {
@@ -257,6 +301,7 @@ func MirrorPrefixKeep(ctx context.Context, m MirrorStore, srcBucket, srcPrefix,
if versioned {
round.Versions[r] = rcpt.DstVersionID
}
+ tell(CopyEvent{Rel: r, SrcETag: s.ETag, SrcSize: s.Size, DstETag: rcpt.DstETag, DstVersionID: rcpt.DstVersionID, Unchanged: true})
continue
}
}
@@ -282,6 +327,7 @@ func MirrorPrefixKeep(ctx context.Context, m MirrorStore, srcBucket, srcPrefix,
}
}
var mu sync.Mutex
+ var copied []string // the jobs whose copy landed (a deferred one wrote nothing)
err = forEachParallel(ctx, mirrorWorkers(m), len(jobs), func(cctx context.Context, i int) error {
r := jobs[i]
sk, dk := srcPrefix+r, dstPrefix+r
@@ -300,6 +346,7 @@ func MirrorPrefixKeep(ctx context.Context, m MirrorStore, srcBucket, srcPrefix,
round.Changed++
round.Deferred++
round.BytesDeferred += s.Size
+ tell(CopyEvent{Rel: r, SrcETag: s.ETag, SrcSize: s.Size, Deferred: true})
return nil
}
if err != nil {
@@ -313,11 +360,13 @@ func MirrorPrefixKeep(ctx context.Context, m MirrorStore, srcBucket, srcPrefix,
if ver != "" {
round.Versions[r] = ver
}
+ dstETag := etag
+ if dstETag == "" {
+ dstETag = s.ETag
+ }
+ tell(CopyEvent{Rel: r, SrcETag: s.ETag, SrcSize: s.Size, DstETag: dstETag, DstVersionID: ver})
+ copied = append(copied, r)
if ver != "" || (etag != "" && etag != s.ETag) {
- dstETag := etag
- if dstETag == "" {
- dstETag = s.ETag
- }
// The book was read before the copies began, so this is a
// write to memory, never a request under mu.
return book.set(ctx, r, copyReceipt{SrcETag: s.ETag, SrcSize: s.Size, DstETag: dstETag, DstVersionID: ver})
@@ -351,6 +400,7 @@ func MirrorPrefixKeep(ctx context.Context, m MirrorStore, srcBucket, srcPrefix,
defer mu.Unlock()
book.drop(r)
round.Changed++
+ tell(CopyEvent{Rel: r, Deleted: true})
return nil
})
if err != nil {
@@ -361,6 +411,35 @@ func MirrorPrefixKeep(ctx context.Context, m MirrorStore, srcBucket, srcPrefix,
if err := book.save(ctx); err != nil {
return round, err
}
+ if observe != nil {
+ // The destination after the round: what it listed, every key this
+ // round copied (a deferred copy wrote nothing), less what it
+ // deleted.
+ after := map[string]bool{}
+ for dk := range dst {
+ if r := dk[len(dstPrefix):]; r != leaseLockName {
+ after[r] = true
+ }
+ }
+ for _, r := range copied {
+ after[r] = true
+ }
+ for _, r := range gone {
+ delete(after, r)
+ }
+ destKeys := make([]string, 0, len(after))
+ for r := range after {
+ destKeys = append(destKeys, r)
+ }
+ sort.Strings(destKeys)
+ sourceKeys := make([]string, 0, len(rel))
+ for _, r := range rel {
+ if r != leaseLockName {
+ sourceKeys = append(sourceKeys, r)
+ }
+ }
+ observe(CopyEvent{Complete: true, SourceKeys: sourceKeys, DestKeys: destKeys})
+ }
return round, nil
}
diff --git a/internal/lease/observed_test.go b/internal/lease/observed_test.go
new file mode 100644
index 000000000..b5d7364ca
--- /dev/null
+++ b/internal/lease/observed_test.go
@@ -0,0 +1,210 @@
+package lease
+
+import (
+ "context"
+ "reflect"
+ "sort"
+ "strings"
+ "sync"
+ "testing"
+)
+
+// eventLog collects an observer's events, in the order the round made them,
+// keyed by what they said.
+type eventLog struct {
+ mu sync.Mutex
+ evs []CopyEvent
+}
+
+func (l *eventLog) observe(e CopyEvent) {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ l.evs = append(l.evs, e)
+}
+
+// byRel is every non-final event by key.
+func (l *eventLog) byRel(t *testing.T) map[string]CopyEvent {
+ t.Helper()
+ out := map[string]CopyEvent{}
+ for _, e := range l.evs {
+ if e.Complete {
+ continue
+ }
+ if _, dup := out[e.Rel]; dup {
+ t.Fatalf("two events for %s: %+v", e.Rel, l.evs)
+ }
+ out[e.Rel] = e
+ }
+ return out
+}
+
+func (l *eventLog) completes() []CopyEvent {
+ var out []CopyEvent
+ for _, e := range l.evs {
+ if e.Complete {
+ out = append(out, e)
+ }
+ }
+ return out
+}
+
+// mirrorState is everything a round leaves behind that a reader can see:
+// the destination's objects and the receipts object's bytes.
+func mirrorState(f *fakeMirror) map[string]string {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+ out := map[string]string{}
+ for b, objs := range f.buckets {
+ for k, o := range objs {
+ out[b+"/"+k] = o.ETag + "|" + o.body
+ }
+ }
+ return out
+}
+
+func TestObservedMirrorEmitsEveryDecision(t *testing.T) {
+ ctx := context.Background()
+ rc := Receipts{Bucket: "src", Key: "receipts/h.json"}
+ seed := func() *fakeMirror {
+ f := newFakeMirror()
+ f.workers = 1
+ f.putSized("src", "instances/h/same", "e-same", 4) // staged with the same ETag
+ f.putMultipart("src", "instances/h/multi", "b-multi", 64) // copied once, then vouched by a receipt
+ f.putSized("src", "instances/h/new", "e-new", 8) // to copy
+ f.putSized("src", "instances/h/busy", "e-busy", 16) // changes under its copy: deferred
+ f.putSized("src", "instances/h/lock.json", "lease", 1) // never decided
+ f.putSized("dst", "moves/h/same", "e-same", 4) // already there
+ f.putSized("dst", "moves/h/gone", "e-gone", 2) // the source no longer has it
+ f.putSized("dst", "moves/h/lock.json", "dst-lease", 1) // never decided
+ return f
+ }
+ // The multipart object's receipt comes from an earlier round of the
+ // same pair, so this round skips it by receipt.
+ prime := func(f *fakeMirror) {
+ if _, err := MirrorPrefixKeep(ctx, f, "src", "instances/h/", "dst", "moves/h/", rc, func(rel string) bool { return rel == "multi" || rel == "same" }); err != nil {
+ t.Fatal(err)
+ }
+ }
+ busy := func(f *fakeMirror) {
+ f.beforeCopy = func(k string) {
+ if k == "instances/h/busy" {
+ f.putSized("src", "instances/h/busy", "e-busy-2", 16)
+ }
+ }
+ }
+
+ // The observed round and the plain one leave the same destination and
+ // the same receipts: the observer changes nothing.
+ plain, observed := seed(), seed()
+ prime(plain)
+ prime(observed)
+ plain.putSized("dst", "moves/h/gone", "e-gone", 2)
+ observed.putSized("dst", "moves/h/gone", "e-gone", 2)
+ busy(plain)
+ busy(observed)
+ rPlain, err := MirrorPrefixKeep(ctx, plain, "src", "instances/h/", "dst", "moves/h/", rc, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var log eventLog
+ rObs, err := MirrorPrefixKeepObserved(ctx, observed, "src", "instances/h/", "dst", "moves/h/", rc, nil, log.observe)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(rPlain, rObs) {
+ t.Fatalf("rounds differ: plain %+v, observed %+v", rPlain, rObs)
+ }
+ if a, b := mirrorState(plain), mirrorState(observed); !reflect.DeepEqual(a, b) {
+ t.Fatalf("the observer changed what the round left:\nplain %v\nobserved %v", a, b)
+ }
+
+ evs := log.byRel(t)
+ multi, _ := observed.object("dst", "moves/h/multi")
+ newObj, _ := observed.object("dst", "moves/h/new")
+ want := map[string]CopyEvent{
+ "same": {Rel: "same", SrcETag: "e-same", SrcSize: 4, DstETag: "e-same", Unchanged: true},
+ "multi": {Rel: "multi", SrcETag: "b-multi-27", SrcSize: 64, DstETag: multi.ETag, Unchanged: true},
+ "new": {Rel: "new", SrcETag: "e-new", SrcSize: 8, DstETag: newObj.ETag},
+ "busy": {Rel: "busy", SrcETag: "e-busy", SrcSize: 16, Deferred: true},
+ "gone": {Rel: "gone", Deleted: true},
+ }
+ if !reflect.DeepEqual(evs, want) {
+ t.Fatalf("events:\n got %+v\nwant %+v", evs, want)
+ }
+ if multi.ETag == "b-multi-27" {
+ t.Fatal("the fixture's multipart copy kept the source ETag; the receipt case is not exercised")
+ }
+ done := log.completes()
+ if len(done) != 1 || !log.evs[len(log.evs)-1].Complete {
+ t.Fatalf("want exactly one Complete, and last: %+v", log.evs)
+ }
+ src, dst := append([]string(nil), done[0].SourceKeys...), append([]string(nil), done[0].DestKeys...)
+ if !sort.StringsAreSorted(src) || !sort.StringsAreSorted(dst) {
+ t.Fatalf("key sets are not sorted: %v %v", src, dst)
+ }
+ if !reflect.DeepEqual(src, []string{"busy", "multi", "new", "same"}) || !reflect.DeepEqual(dst, []string{"multi", "new", "same"}) {
+ t.Fatalf("Complete key sets: source %v, destination %v", src, dst)
+ }
+
+ // A round that fails emits no Complete, and the events it made before
+ // the failure stand.
+ f := seed()
+ f.putSized("src", "instances/h/zz-fails", "e-z", 1)
+ f.failOn = "instances/h/zz-fails"
+ var flog eventLog
+ if _, err := MirrorPrefixKeepObserved(ctx, f, "src", "instances/h/", "dst", "moves/h/", rc, nil, flog.observe); err == nil || !strings.Contains(err.Error(), "zz-fails") {
+ t.Fatalf("injected failure: %v", err)
+ }
+ if len(flog.completes()) != 0 {
+ t.Fatalf("a failed round completed: %+v", flog.evs)
+ }
+ if e, ok := flog.byRel(t)["same"]; !ok || !e.Unchanged {
+ t.Fatalf("a skip decided before the failure was not reported: %+v", flog.evs)
+ }
+
+ // A versioned store's copies and receipt skips carry the version.
+ vf := &versionedFake{fakeMirror: seed()}
+ vf.workers = 1
+ var vlog eventLog
+ if _, err := MirrorPrefixKeepObserved(ctx, vf, "src", "instances/h/", "dst", "moves/h/", rc, nil, vlog.observe); err != nil {
+ t.Fatal(err)
+ }
+ for rel, e := range vlog.byRel(t) {
+ if (e.Deleted || e.Deferred) == (e.DstVersionID != "") {
+ t.Errorf("%s: version %q on %+v", rel, e.DstVersionID, e)
+ }
+ }
+ var again eventLog
+ if _, err := MirrorPrefixKeepObserved(ctx, vf, "src", "instances/h/", "dst", "moves/h/", rc, nil, again.observe); err != nil {
+ t.Fatal(err)
+ }
+ for rel, e := range again.byRel(t) {
+ if e.Unchanged && e.DstVersionID == "" {
+ t.Errorf("%s: a receipt-vouched skip without the receipt's version: %+v", rel, e)
+ }
+ }
+
+ // MirrorPrefixKeep is the observed round with no observer.
+ if _, err := MirrorPrefixKeepObserved(ctx, seed(), "src", "instances/h/", "dst", "moves/h/", rc, nil, nil); err != nil {
+ t.Fatal(err)
+ }
+}
+
+// versionedFake is fakeMirror as a VersionedCopier, each copy a fresh
+// version.
+type versionedFake struct {
+ *fakeMirror
+ n int
+}
+
+func (v *versionedFake) CopyVersioned(ctx context.Context, srcBucket, srcKey, srcETag, dstBucket, dstKey string) (string, string, error) {
+ etag, err := v.Copy(ctx, srcBucket, srcKey, srcETag, dstBucket, dstKey)
+ if err != nil {
+ return "", "", err
+ }
+ v.mu.Lock()
+ v.n++
+ n := v.n
+ v.mu.Unlock()
+ return etag, "ver-" + string(rune('a'+n)), nil
+}