Files
seaweedfs/weed/pb/filer_pb/filer_client_bfs.go
T
Chris Lu b8049bc633 shell: keep fs.mergeVolumes from spinning past the finished moves (#11050)
* filer_pb: walk a re-delivered directory only once in TraverseBfs

A directory handed back twice by a listing (a page-boundary race with
concurrent renames, or a store whose ordering misbehaves) was enqueued
twice; the second walk re-lists the same subtree and can keep the
traversal from ever terminating.

Claude-Session: https://claude.ai/code/session_01XH7iM88ZqWMEvsLB8tkWPQ

* filer_pb: fail a directory listing whose pagination stops advancing

A full page ending on the very name the cursor started from re-fetches
the same page forever; a store whose listing order does not advance past
the cursor turns any full-directory read into a silent infinite loop.
Return an error naming the stuck cursor instead.

Claude-Session: https://claude.ai/code/session_01XH7iM88ZqWMEvsLB8tkWPQ

* shell: skip foreign-collection manifests in fs.mergeVolumes

Every manifest chunk in the namespace was resolved, downloading its
manifest needle, even when the merge plan only touches one collection.
Sub-chunks live in the manifest's own collection, so a manifest on a
volume outside the plan's collections cannot reference a source volume;
skip it and spare a cluster-wide download pass that looks like a hang
after the real moves finish.

Claude-Session: https://claude.ai/code/session_01XH7iM88ZqWMEvsLB8tkWPQ
2026-08-31 10:23:35 -07:00

142 lines
3.2 KiB
Go

package filer_pb
import (
"context"
"fmt"
"io"
"sync"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/util"
)
func TraverseBfs(ctx context.Context, filerClient FilerClient, parentPath util.FullPath, fn func(parentPath util.FullPath, entry *Entry) error) (err error) {
K := 5
ctx, cancel := context.WithCancel(ctx)
defer cancel()
queue := util.NewQueue[util.FullPath]()
var pending sync.WaitGroup
pending.Add(1)
queue.Enqueue(parentPath)
var once sync.Once
var firstErr error
// A directory delivered twice (a page-boundary race with concurrent
// renames, or a store whose listing order misbehaves) must not be walked
// twice: the second walk re-lists the same subtree and can keep the
// traversal from ever terminating.
var visited sync.Map
visited.Store(string(parentPath), struct{}{})
enqueue := func(p util.FullPath) bool {
// Stop expanding traversal once canceled (e.g. first error encountered).
if ctx.Err() != nil {
return false
}
if _, seen := visited.LoadOrStore(string(p), struct{}{}); seen {
return true
}
pending.Add(1)
queue.Enqueue(p)
return true
}
done := make(chan struct{})
var workers sync.WaitGroup
for i := 0; i < K; i++ {
workers.Add(1)
go func() {
defer workers.Done()
for {
select {
case <-done:
return
default:
}
dir := queue.Dequeue()
if dir == "" {
// queue is empty for now
select {
case <-done:
return
case <-time.After(50 * time.Millisecond):
continue
}
}
// Always mark the directory as done so the closer can finish.
if ctx.Err() == nil {
processErr := processOneDirectory(ctx, filerClient, dir, enqueue, fn)
if processErr != nil {
once.Do(func() {
firstErr = processErr
cancel()
})
}
}
pending.Done()
}
}()
}
pending.Wait()
close(done)
workers.Wait()
return firstErr
}
func processOneDirectory(ctx context.Context, filerClient FilerClient, parentPath util.FullPath, enqueue func(p util.FullPath) bool, fn func(parentPath util.FullPath, entry *Entry) error) (err error) {
return ReadDirAllEntries(ctx, filerClient, parentPath, "", func(entry *Entry, isLast bool) error {
if err := fn(parentPath, entry); err != nil {
return err
}
if entry.IsDirectory {
subDir := fmt.Sprintf("%s/%s", parentPath, entry.Name)
if parentPath == "/" {
subDir = "/" + entry.Name
}
if !enqueue(util.FullPath(subDir)) {
return ctx.Err()
}
}
return nil
})
}
func StreamBfs(client SeaweedFilerClient, dir util.FullPath, olderThanTsNs int64, fn func(parentPath util.FullPath, entry *Entry) error) (err error) {
glog.V(0).Infof("TraverseBfsMetadata %v if before %v", dir, time.Unix(0, olderThanTsNs))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
stream, err := client.TraverseBfsMetadata(ctx, &TraverseBfsMetadataRequest{
Directory: string(dir),
})
if err != nil {
return fmt.Errorf("traverse bfs metadata: %w", err)
}
for {
resp, err := stream.Recv()
if err != nil {
if err == io.EOF {
break
}
return fmt.Errorf("traverse bfs metadata: %w", err)
}
if err := fn(util.FullPath(resp.Directory), resp.Entry); err != nil {
return err
}
}
return nil
}