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) {