diff --git a/internal/bazel/bazel.go b/internal/bazel/bazel.go index 51dd07b..fe8637e 100644 --- a/internal/bazel/bazel.go +++ b/internal/bazel/bazel.go @@ -82,6 +82,7 @@ var ErrDigestMismatch = errors.New("body does not match its digest") type store interface { Get(ctx context.Context, a ids.ActionID) (cache.Result, error) Has(ctx context.Context, a ids.ActionID) bool + HasRemote(ctx context.Context, a ids.ActionID) bool PutStaged(ctx context.Context, a ids.ActionID, o ids.OutputID, stagedPath string, size int64) (string, error) } @@ -187,6 +188,11 @@ func (s *Store) Has(ctx context.Context, k Kind, d Digest) bool { return s.cache.Has(ctx, k.actionID(d)) } +// HasRemote reports whether a digest is available from the shared cache tier. +func (s *Store) HasRemote(ctx context.Context, k Kind, d Digest) bool { + return s.cache.HasRemote(ctx, k.actionID(d)) +} + // Put stores a body under a digest, streaming it and hashing it on the way // past. It never holds the body in memory, and publishes by hardlink rather // than by copy, so the bytes cross the disk once however large they are. diff --git a/internal/bazel/http_test.go b/internal/bazel/http_test.go index 1866445..4d6ce11 100644 --- a/internal/bazel/http_test.go +++ b/internal/bazel/http_test.go @@ -420,6 +420,9 @@ func (failingStore) Get(context.Context, ids.ActionID) (cache.Result, error) { // Has always reports absent, so an upload is always attempted. func (failingStore) Has(context.Context, ids.ActionID) bool { return false } +// HasRemote always reports absent because the test store has no shared tier. +func (failingStore) HasRemote(context.Context, ids.ActionID) bool { return false } + // PutStaged always fails. func (failingStore) PutStaged(context.Context, ids.ActionID, ids.OutputID, string, int64) (string, error) { return "", errors.New("no space left on device") diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 81adcd2..4178336 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -246,6 +246,25 @@ func (c *Cache) getRemote(ctx context.Context, a ids.ActionID) (Result, error) { return Result{OutputID: outputID, Size: size, DiskPath: path, Time: time.Unix(0, created)}, nil } +// HasRemote reports whether an action and its body are both available from the shared tier. +// +// It opens the remote body and closes it without reading, which obtains the +// storage service's presence verdict without faulting bytes into the local tier. +func (c *Cache) HasRemote(ctx context.Context, a ids.ActionID) bool { + if !c.cfg.RemoteEnabled() { + return false + } + outputID, _, err := c.rem.GetAction(ctx, a) + if err != nil { + return false + } + body, _, err := c.rem.GetObject(ctx, outputID) + if err != nil { + return false + } + return body.Close() == nil +} + // Has reports whether an action already resolves to a readable body, // refreshing its last-used time if it does. // diff --git a/internal/reapi/rrcc.go b/internal/reapi/rrcc.go index 020044d..ff9b4e8 100644 --- a/internal/reapi/rrcc.go +++ b/internal/reapi/rrcc.go @@ -51,15 +51,15 @@ func (m *rrccMetrics) Snapshot() RRCCMetricsSnapshot { func (s *Server) RRCCMetrics() RRCCMetricsSnapshot { return s.rrccMetrics.Snapshot() } // validateRRCCLocalClosure accepts ordinary action results and synthetic -// repository-cache results whose complete closure is local. A missing local body -// is a cache miss: returning the ActionResult would make Bazel fail later while -// lazily reading the injected repository. +// repository-cache results whose complete closure is available locally or from +// the shared tier. A missing body is a cache miss: returning the ActionResult +// would make Bazel fail later while lazily reading the injected repository. func (s *Server) validateRRCCLocalClosure(ctx context.Context, result *repb.ActionResult) bool { marker, tree, ok := rrccOutputs(result) if !ok { return true } - if !s.hasLocalCAS(ctx, marker) { + if !s.hasCAS(ctx, marker) { s.rrccMetrics.markerMissing.Add(1) s.logf("bazel grpc: rrcc local closure missing marker %s", marker.GetHash()) return false @@ -71,6 +71,9 @@ func (s *Server) validateRRCCLocalClosure(ctx context.Context, result *repb.Acti return false } file, size, ok := s.store.OpenLocal(ctx, bazel.KindCAS, treeDigest) + if !ok && s.store.HasRemote(ctx, bazel.KindCAS, treeDigest) { + file, size, ok = s.store.Open(ctx, bazel.KindCAS, treeDigest) + } if !ok { s.rrccMetrics.treeMissing.Add(1) s.logf("bazel grpc: rrcc local closure missing tree %s", tree.GetHash()) @@ -96,7 +99,7 @@ func (s *Server) validateRRCCLocalClosure(ctx context.Context, result *repb.Acti } for _, directory := range append([]*repb.Directory{contents.GetRoot()}, contents.GetChildren()...) { for _, node := range directory.GetFiles() { - if !s.hasLocalCAS(ctx, node.GetDigest()) { + if !s.hasCAS(ctx, node.GetDigest()) { s.rrccMetrics.fileMissing.Add(1) s.logf("bazel grpc: rrcc local closure missing file %s", node.GetDigest().GetHash()) return false @@ -107,10 +110,10 @@ func (s *Server) validateRRCCLocalClosure(ctx context.Context, result *repb.Acti return true } -// hasLocalCAS reports whether a valid digest is currently present in the local cache. -func (s *Server) hasLocalCAS(ctx context.Context, d *repb.Digest) bool { +// hasCAS reports whether a valid digest is available locally or from the shared tier. +func (s *Server) hasCAS(ctx context.Context, d *repb.Digest) bool { parsed, err := digest(d) - return err == nil && s.store.Has(ctx, bazel.KindCAS, parsed) + return err == nil && (s.store.Has(ctx, bazel.KindCAS, parsed) || s.store.HasRemote(ctx, bazel.KindCAS, parsed)) } // rrccOutputs recognizes Bazel's synthetic remote repository-contents result shape.