filer.meta.backup: resume an interrupted metadata copy and add -entriesPerSecond (#12549)

* filer_pb: add a depth-first traversal that can resume after a path

TraverseDfs walks a tree in listing order and reports each finished
path, so a caller can save one path and continue from it later. Pages
are read whole before visiting, and the entries of a page are visited
by up to five workers.

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

* filer.meta.backup: resume an interrupted metadata copy

The walk saves the last finished path every few seconds, and a walk
that continues an unfinished one starts after it. A fresh walk clears
the saved path first, and a finished walk clears it with the walk
start.

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

* filer.meta.backup: add -entriesPerSecond to throttle the metadata copy

Each entry the full copy reads waits on a rate limiter, and pages are
listed only after the previous page is copied, so the flag also bounds
how fast the copy lists the source filer. 0 keeps it unlimited.

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

* filer_pb: report each file of a depth-first walk once all before it are done

A throttled walk only reported progress after a whole listing page, so a
slow copy could go many minutes without a resumable path. Visit entries in
runs that end at a directory and report each file as soon as every entry
before it is done.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chris LuandClaude Opus 5.5 authored and GitHub committed 2026-11-11 09:08:11 -08:00
1 parent 0ef0260844
commit e9606c8ecc
4 files changed
+372 -5

No files matched your search

+49
View File
@@ -118,3 +118,52 @@ func TestCopyMetadataIgnoresAWalkStartTheStreamPassed(t *testing.T) {
t.Fatalf("offset = %v, want a fresh walk's start after %v", offset, beforeWalk)
}
}
// TestCopyMetadataResumesOnlyItsOwnWalk: the saved walk path belongs to the
// walk whose start the stream replays from; a fresh walk must not skip ahead.
func TestCopyMetadataResumesOnlyItsOwnWalk(t *testing.T) {
backup := &FilerMetaBackupOptions{store: &kvOnlyStore{kv: map[string][]byte{}}}
walkedTo := func() string {
value, err := backup.store.KvGet(context.Background(), MetaBackupWalkPathKey)
if errors.Is(err, filer.ErrKvNotFound) {
return ""
}
if err != nil {
t.Fatal(err)
}
return string(value)
}
interrupted := errors.New("killed")
if err := backup.copyMetadata(func() error {
return errors.Join(backup.store.KvPut(context.Background(), MetaBackupWalkPathKey, []byte("/a/b")), interrupted)
}); !errors.Is(err, interrupted) {
t.Fatalf("interrupted walk: err = %v", err)
}
if err := backup.copyMetadata(func() error {
if got := walkedTo(); got != "/a/b" {
t.Fatalf("resumed walk sees %q, want /a/b", got)
}
return nil
}); err != nil {
t.Fatal(err)
}
if got := walkedTo(); got != "" {
t.Fatalf("walk path after a finished walk = %q, want it cleared", got)
}
if err := backup.setTime(MetaBackupWalkKey, time.Now().Add(-time.Hour)); err != nil {
t.Fatal(err)
}
if err := backup.store.KvPut(context.Background(), MetaBackupWalkPathKey, []byte("/x")); err != nil {
t.Fatal(err)
}
if err := backup.copyMetadata(func() error {
if got := walkedTo(); got != "" {
t.Fatalf("fresh walk sees %q left by a walk the stream passed", got)
}
return nil
}); err != nil {
t.Fatal(err)
}
}