blob: b40d67f3fc438a06f8c826a085cf8d8482c3edca [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package reapi
import (
"context"
"errors"
"fmt"
"io"
"iter"
"path"
"time"
log "github.com/golang/glog"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"go.chromium.org/build/hashigo/digest"
rpb "go.chromium.org/build/remote-apis/build/bazel/remote/execution/v2"
"go.chromium.org/build/siso/blob"
"go.chromium.org/build/siso/o11y/clog"
"go.chromium.org/build/siso/o11y/trace"
"go.chromium.org/build/siso/reapi/retry"
)
// WalkDir walks the directory tree rooted identified by d,
// calling fn for each directory in the tree, including root.
func (c *Client) WalkDir(ctx context.Context, d digest.Digest, fn func(dname string, dir *rpb.Directory) error) error {
if c == nil {
return fmt.Errorf("reapi is not configured")
}
ctx, span := trace.NewSpan(ctx, "reapi-walkdir")
defer span.Close(nil)
dirs := make(map[string]digest.Digest) // dname -> digest
waiting := make(map[digest.Digest][]string) // digest -> dnames
waiting[d] = append(waiting[d], "")
known := make(map[digest.Digest]*rpb.Directory)
var stats walkDirStats
err := retry.Do(ctx, func() error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
for {
var handleDir func(d digest.Digest, dir *rpb.Directory) error
handleDir = func(d digest.Digest, dir *rpb.Directory) error {
if len(waiting[d]) == 0 {
return nil
}
var dnames []string
dnames, waiting[d] = waiting[d], nil
for _, dname := range dnames {
dirs[dname] = d
started := time.Now()
err := fn(dname, dir)
stats.cbDur += time.Since(started)
stats.ndirs++
if err != nil {
return err
}
for _, subdir := range dir.Directories {
sd := digest.FromProto(subdir.Digest)
subname := path.Join(dname, subdir.Name)
waiting[sd] = append(waiting[sd], subname)
if sdir := known[sd]; sdir != nil {
// check loop
for pname := dname; pname != "" && pname != "."; pname = path.Dir(pname) {
if dirs[pname] == sd {
return fmt.Errorf("directory loop %s from %s -> %s", sd, subname, pname)
}
}
err := handleDir(sd, sdir)
if err != nil {
return err
}
return nil
}
}
}
return nil
}
for dir, err := range c.walkDirIter(ctx, d, &stats) {
if err != nil {
return err
}
dd, derr := blob.FromProtoMessage(c.digestFn, dir)
if derr != nil {
return derr
}
if known[dd.Digest()] != nil {
clog.Warningf(ctx, "duplicate dir %s", dd.Digest())
continue
}
known[dd.Digest()] = dir
err := handleDir(dd.Digest(), dir)
if err != nil {
return err
}
}
break
}
return nil
})
clog.Infof(ctx, "walkdir %s %s", d, stats.String())
return err
}
type walkDirStats struct {
pageToken string
apiDur, cbDur time.Duration
nrecvs, ndirs int
}
func (s *walkDirStats) String() string {
var avgAPIDur time.Duration
if s.nrecvs > 0 {
avgAPIDur = s.apiDur / time.Duration(s.nrecvs)
}
var avgCBDur time.Duration
if s.ndirs > 0 {
avgCBDur = s.cbDur / time.Duration(s.ndirs)
}
return fmt.Sprintf("api %d %s avg:%s dir %d cb %s avg:%s",
s.nrecvs, s.apiDur, avgAPIDur, s.ndirs, s.cbDur, avgCBDur)
}
func (c *Client) walkDirIter(ctx context.Context, d digest.Digest, stats *walkDirStats) iter.Seq2[*rpb.Directory, error] {
if c.opt.WalkDirStream {
return c.getTreeIter(ctx, d, stats)
}
return c.readDirIter(ctx, d, stats)
}
func (c *Client) getTreeIter(ctx context.Context, d digest.Digest, stats *walkDirStats) iter.Seq2[*rpb.Directory, error] {
casClient := rpb.NewContentAddressableStorageClient(c.casConn)
return func(yield func(*rpb.Directory, error) bool) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
stream, err := casClient.GetTree(ctx, &rpb.GetTreeRequest{
InstanceName: c.opt.Instance,
RootDigest: d.Proto(),
PageToken: stats.pageToken,
DigestFunction: c.digestFn.Value(),
})
if err != nil {
yield(nil, err)
return
}
for {
started := time.Now()
resp, err := stream.Recv()
dur := time.Since(started)
stats.apiDur += dur
stats.nrecvs++
if log.V(1) {
clog.Infof(ctx, "recv %d %s", len(resp.GetDirectories()), err)
}
if errors.Is(err, io.EOF) {
return
}
if err != nil {
yield(nil, err)
return
}
for _, dir := range resp.Directories {
if !yield(dir, nil) {
return
}
}
stats.pageToken = resp.NextPageToken
}
}
}
func (c *Client) readDirIter(ctx context.Context, d digest.Digest, stats *walkDirStats) iter.Seq2[*rpb.Directory, error] {
casClient := rpb.NewContentAddressableStorageClient(c.casConn)
var compressors []rpb.Compressor_Value
if c.opt.BatchCompressedBlob > 0 {
compressors = []rpb.Compressor_Value{
rpb.Compressor_ZSTD,
rpb.Compressor_DEFLATE,
}
}
return func(yield func(*rpb.Directory, error) bool) {
pendings := []digest.Digest{d}
seen := make(map[digest.Digest]struct{})
seen[d] = struct{}{}
for len(pendings) > 0 {
req := &rpb.BatchReadBlobsRequest{
InstanceName: c.opt.Instance,
AcceptableCompressors: compressors,
DigestFunction: c.digestFn.Value(),
}
for _, d := range pendings {
req.Digests = append(req.Digests, d.Proto())
}
pendings = pendings[:0]
started := time.Now()
resp, err := casClient.BatchReadBlobs(ctx, req)
dur := time.Since(started)
stats.apiDur += dur
stats.nrecvs++
if err != nil {
yield(nil, err)
return
}
for _, rd := range resp.Responses {
dd := digest.FromProto(rd.Digest)
if rd.Status.GetCode() != 0 {
yield(nil, fmt.Errorf("blob %s resp: %w", dd, status.FromProto(rd.Status).Err()))
return
}
data, err := c.decodeForBatchRead(dd, rd.Data, rd.Compressor)
if err != nil {
yield(nil, fmt.Errorf("blob %s decode: %w", dd, err))
return
}
dir := &rpb.Directory{}
err = proto.Unmarshal(data, dir)
if err != nil {
yield(nil, fmt.Errorf("blob %s unmarshal: %w", dd, err))
return
}
if !yield(dir, nil) {
return
}
for _, subdir := range dir.Directories {
sd := digest.FromProto(subdir.Digest)
if _, ok := seen[sd]; ok {
continue
}
pendings = append(pendings, sd)
seen[sd] = struct{}{}
}
}
}
}
}