package weed_server import ( "context" "errors" "fmt" "sort" "strconv" "strings" "sync" "github.com/seaweedfs/seaweedfs/weed/filer" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/storage/needle" "github.com/seaweedfs/seaweedfs/weed/util" "github.com/seaweedfs/seaweedfs/weed/wdclient" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) const ( maxFilerBatchLookupRequests = 4096 maxFilerBatchLookupWorkers = 32 ) type filerBatchEntryFinder func(context.Context, util.FullPath) (*filer.Entry, int64, error) type filerBatchVolumeLookup func(context.Context, []string) (map[string][]wdclient.Location, error) // LookupDirectoryEntries performs exact entry lookups concurrently while // preserving request order, then resolves every referenced volume in one // deduplicated master lookup. func (fs *FilerServer) LookupDirectoryEntries(ctx context.Context, req *filer_pb.LookupDirectoryEntriesRequest) (*filer_pb.LookupDirectoryEntriesResponse, error) { lookupVolumes := selectFilerBatchVolumeLookup(req, fs.filer.MasterClient.LookupVolumeIdsWithFallback, fs.filer.MasterClient.LookupVolumeIdsAuthoritative) response, err := lookupDirectoryEntries(ctx, req, fs.filer.Signature, fs.fencedFindEntry, lookupVolumes) if err != nil { return nil, err } if fs.volumeGuard != nil { populateBatchReadAuth(response, fs.maybeGetVolumeReadJwtAuthorizationToken) } return response, nil } func selectFilerBatchVolumeLookup(req *filer_pb.LookupDirectoryEntriesRequest, cached, authoritative filerBatchVolumeLookup) filerBatchVolumeLookup { if req != nil && req.GetUnavailableVolumeIsMiss() { // DeletedVids is an asynchronous cache invalidation hint and its delivery // is deliberately best-effort. Cache reads must therefore confirm the // unique Volume IDs with Master before treating a location as live. return authoritative } return cached } func populateBatchReadAuth(response *filer_pb.LookupDirectoryEntriesResponse, mint func(string) string) { response.ReadAuth = make(map[string]string) for _, result := range response.Results { if result == nil || result.Entry == nil { continue } for _, chunk := range result.Entry.Chunks { fid := chunk.GetFileIdString() if fid == "" { continue } if token := mint(fid); token != "" { response.ReadAuth[fid] = token } } } } func lookupDirectoryEntries( ctx context.Context, req *filer_pb.LookupDirectoryEntriesRequest, logSignature int32, findEntry filerBatchEntryFinder, lookupVolumes filerBatchVolumeLookup, ) (*filer_pb.LookupDirectoryEntriesResponse, error) { if err := validateFilerBatchLookupRequest(req); err != nil { return nil, err } results := make([]*filer_pb.LookupDirectoryEntryResult, len(req.Requests)) resultVolumeIDs := make([]map[string]struct{}, len(req.Requests)) jobs := make(chan int) workerCount := min(len(req.Requests), maxFilerBatchLookupWorkers) var workers sync.WaitGroup workers.Add(workerCount) for range workerCount { go func() { defer workers.Done() for index := range jobs { request := req.Requests[index] entry, logTsNs, err := findEntry(ctx, util.JoinPath(request.Directory, request.Name)) result := &filer_pb.LookupDirectoryEntryResult{ LogTsNs: logTsNs, LogSignature: logSignature, } switch { case errors.Is(err, filer_pb.ErrNotFound): results[index] = result case err != nil: result.Error = err.Error() results[index] = result case entry == nil: result.Error = "entry lookup returned no entry" results[index] = result default: pbEntry := entry.ToProtoEntry() volumeIDs, volumeErr := filerBatchEntryVolumeIDs(pbEntry) result.Found = true result.Entry = pbEntry if volumeErr != nil { result.Error = volumeErr.Error() } results[index] = result resultVolumeIDs[index] = volumeIDs } } }() } for index := range req.Requests { select { case jobs <- index: case <-ctx.Done(): close(jobs) workers.Wait() return nil, status.FromContextError(ctx.Err()).Err() } } close(jobs) workers.Wait() if err := ctx.Err(); err != nil { return nil, status.FromContextError(err).Err() } allVolumeIDs := make(map[string]struct{}) for _, volumeIDs := range resultVolumeIDs { for volumeID := range volumeIDs { allVolumeIDs[volumeID] = struct{}{} } } volumeIDs := make([]string, 0, len(allVolumeIDs)) for volumeID := range allVolumeIDs { volumeIDs = append(volumeIDs, volumeID) } sort.Strings(volumeIDs) response := &filer_pb.LookupDirectoryEntriesResponse{ Results: results, LocationsMap: make(map[string]*filer_pb.Locations, len(volumeIDs)), } if len(volumeIDs) == 0 { return response, nil } locationsByVolume, lookupErr := lookupVolumes(ctx, volumeIDs) // The provider returns a nil map when it got no answer, and a populated map // with the volumes the master does not serve reported as errors when it did, // so a nil map is the only sign of an unanswered lookup. Only a volume the // master itself left out may become a miss; anything else stays an error. missIsAuthoritative := req.UnavailableVolumeIsMiss && locationsByVolume != nil for _, volumeID := range volumeIDs { locations := locationsByVolume[volumeID] if len(locations) != 0 { response.LocationsMap[volumeID] = &filer_pb.Locations{ Locations: wdclientLocationsToPb(locations), } continue } if !missIsAuthoritative { response.LocationsMap[volumeID] = &filer_pb.Locations{} } for index, entryVolumeIDs := range resultVolumeIDs { if _, affected := entryVolumeIDs[volumeID]; !affected { continue } if missIsAuthoritative && results[index].Error == "" { results[index].Found = false results[index].Entry = nil resultVolumeIDs[index] = nil continue } message := fmt.Sprintf("volume %s has no locations", volumeID) if lookupErr != nil { message += ": " + lookupErr.Error() } results[index].Error = appendFilerBatchLookupError(results[index].Error, message) } } return response, nil } func validateFilerBatchLookupRequest(req *filer_pb.LookupDirectoryEntriesRequest) error { if req == nil || len(req.Requests) == 0 { return status.Error(codes.InvalidArgument, "batch lookup requires at least one request") } if len(req.Requests) > maxFilerBatchLookupRequests { return status.Errorf(codes.ResourceExhausted, "batch lookup has %d requests; maximum is %d", len(req.Requests), maxFilerBatchLookupRequests) } for index, request := range req.Requests { if request == nil { return status.Errorf(codes.InvalidArgument, "batch lookup request %d is missing", index) } if request.Directory == "" || !strings.HasPrefix(request.Directory, "/") || strings.ContainsRune(request.Directory, '\x00') { return status.Errorf(codes.InvalidArgument, "batch lookup request %d has invalid directory", index) } if request.Name == "" || request.Name == "." || request.Name == ".." || strings.ContainsAny(request.Name, "/\x00") { return status.Errorf(codes.InvalidArgument, "batch lookup request %d has invalid name", index) } } return nil } func filerBatchEntryVolumeIDs(entry *filer_pb.Entry) (map[string]struct{}, error) { volumeIDs := make(map[string]struct{}) for index, chunk := range entry.GetChunks() { if chunk == nil { return volumeIDs, fmt.Errorf("entry chunk %d is missing", index) } if chunk.Fid != nil { volumeIDs[strconv.FormatUint(uint64(chunk.Fid.VolumeId), 10)] = struct{}{} continue } if chunk.FileId == "" { return volumeIDs, fmt.Errorf("entry chunk %d has no file id", index) } fid, err := needle.ParseFileIdFromString(chunk.FileId) if err != nil { return volumeIDs, fmt.Errorf("entry chunk %d has invalid file id: %w", index, err) } volumeIDs[strconv.FormatUint(uint64(fid.VolumeId), 10)] = struct{}{} } return volumeIDs, nil } func appendFilerBatchLookupError(existing, next string) string { if existing == "" { return next } return existing + "; " + next }