From 0d4e11a6e5b0c7fb855388936d6bf79ad5001a2a Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 23 Oct 2026 18:45:09 -0700 Subject: [PATCH] filer: bootstrap a timestamp-less peer entry onto a missing local path (#11975) A zero local commit time conflated 'no local state' with 'state at the epoch', so a missing entry was skipped whenever the sender reported no commit time and the entry mtime was at or before the epoch. Only hold the copy off when a local entry actually exists. --- weed/filer/meta_aggregator.go | 17 ++++++++++- weed/filer/meta_aggregator_replay_test.go | 37 +++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/weed/filer/meta_aggregator.go b/weed/filer/meta_aggregator.go index c5d6521c89..310874a2e2 100644 --- a/weed/filer/meta_aggregator.go +++ b/weed/filer/meta_aggregator.go @@ -788,7 +788,22 @@ func (f *Filer) applyTraversedEntry(ctx context.Context, entry *Entry, peerTsNs return fmt.Errorf("check local state for %s: %w", entry.FullPath, err) } if localTsNs >= peerTsNs { - return nil + if localTsNs != 0 { + return nil + } + // Zero timestamps carry no order: keep the path only when an entry + // exists locally — a missing one takes the peer's copy, or an + // epoch-dated entry from a peer reporting no commit time is dropped. + if f.Store == nil { + return nil + } + local, findErr := f.Store.FindEntry(ctx, entry.FullPath) + if findErr != nil && !errors.Is(findErr, filer_pb.ErrNotFound) { + return fmt.Errorf("check local state for %s: %w", entry.FullPath, findErr) + } + if local != nil { + return nil + } } if insertErr := f.Store.InsertEntry(ctx, entry); insertErr != nil { // Entry already exists (root dir, or partial previous bootstrap); diff --git a/weed/filer/meta_aggregator_replay_test.go b/weed/filer/meta_aggregator_replay_test.go index 3e700a47ec..efbc9968f2 100644 --- a/weed/filer/meta_aggregator_replay_test.go +++ b/weed/filer/meta_aggregator_replay_test.go @@ -575,6 +575,43 @@ func TestBootstrapUpdatesExistingByCommitTime(t *testing.T) { } } +// A missing local entry has no commit time to order against: the peer copy +// must apply even when both sides are timestamp-less — an epoch-dated entry +// from a peer that predates EntryTsNs is still bootstrapped, while an +// existing zero-mtime local entry keeps its place. +func TestBootstrapCopiesMissingEntryWithoutCommitTime(t *testing.T) { + store := newStubFilerStore() + f := newReplayTestFiler(store) + missing := util.NewFullPath("/dir", "missing") + existing := util.NewFullPath("/dir", "existing") + + store.entries[string(existing)] = &Entry{ + FullPath: existing, + Attr: Attr{Mtime: time.Unix(0, 0)}, + Content: []byte("zero-mtime local entry"), + } + for _, path := range []util.FullPath{missing, existing} { + entry := &Entry{ + FullPath: path, + Attr: Attr{Mtime: time.Unix(0, 0)}, + Content: []byte("epoch-dated peer entry"), + } + if err := f.applyTraversedEntry(context.Background(), entry, 0); err != nil { + t.Fatalf("applyTraversedEntry %s: %v", path, err) + } + } + got, err := store.FindEntry(context.Background(), missing) + if err != nil { + t.Fatalf("missing local entry must take the peer copy: %v", err) + } + if string(got.Content) != "epoch-dated peer entry" { + t.Fatalf("content = %q, want peer entry applied", got.Content) + } + if got, _ := store.FindEntry(context.Background(), existing); string(got.Content) != "zero-mtime local entry" { + t.Fatalf("content = %q, existing local entry must be kept", got.Content) + } +} + // A delete whose commit record failed to persist still blocks a stale // replayed create: the pending record stands in until the retry lands it. func TestPendingDeleteRecordBlocksStaleReplay(t *testing.T) {