blob: 4d4e15d9aa239224dbc1143ef2ca51281e749a22 [file]
// Copyright 2023 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 (
"cmp"
"context"
"fmt"
"io"
bpb "google.golang.org/genproto/googleapis/bytestream"
"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/reapi/bytestreamio"
)
// CacheStore provides a thin wrapper around REAPI client that gets and uploads blobs and action results.
type CacheStore struct {
client *Client
}
// CacheStore returns cache store of the client.
func (c *Client) CacheStore() CacheStore {
return CacheStore{
client: c,
}
}
func (c CacheStore) String() string {
return fmt.Sprintf("cachestore:reapi addr:%s instance:%s", c.client.opt.Address, c.client.opt.Instance)
}
// GetActionResult gets action result for the action identified by the digest.
func (c CacheStore) GetActionResult(ctx context.Context, d digest.Digest) (*rpb.ActionResult, error) {
return c.client.GetActionResult(ctx, d)
}
// SetActionResult sets action result for the action identified by the digest.
// If a failing action is provided, caching will be skipped.
func (c CacheStore) SetActionResult(ctx context.Context, d digest.Digest, ar *rpb.ActionResult) error {
// Intentionally no-op. Setting the action result could allow a malicious
// actor to perform arbitrary code execution.
// May reconsider in the future for trusted workers, but for now, do nothing.
// Intentionally return no error, as it is intended for this function to be
// called.
return nil
}
// GetContent gets contents for the fname identified by the digest.
func (c CacheStore) GetContent(ctx context.Context, d digest.Digest, fname string) ([]byte, error) {
return c.client.Get(ctx, d, fname)
}
// SetContent sets contents for the fname identified by the digest.
func (c CacheStore) SetContent(ctx context.Context, d digest.Digest, fname string, content []byte) error {
data := blob.FromBytes(c.client.digestFn, fname, content)
if d != data.Digest() {
return fmt.Errorf("digest mismatch: d=%s content=%s", d, data.Digest())
}
ds := blob.NewStore()
ds.Set(data)
_, err := c.client.UploadAll(ctx, ds)
return err
}
// HasContent returns whether digest is in cache store.
func (c CacheStore) HasContent(ctx context.Context, d digest.Digest) bool {
missing, err := c.client.Missing(ctx, []digest.Digest{d})
if err != nil {
clog.Warningf(ctx, "failed to call missing [%s]: %v", d, err)
return false
}
return len(missing) != 1
}
// Source returns digest source for fname identified by the digest.
func (c CacheStore) Source(_ context.Context, d digest.Digest, fname string) blob.Source {
return digestSource{
c: c.client,
d: d,
fname: fname,
}
}
type digestSourceReader struct {
r io.ReadCloser
n int
size int64
c *Client
cancel context.CancelFunc
}
func (r *digestSourceReader) Read(buf []byte) (int, error) {
n, err := r.r.Read(buf)
r.n += n
if err == io.EOF && int64(r.n) != r.size {
err = io.ErrUnexpectedEOF
}
return n, err
}
func (r *digestSourceReader) Close() error {
// On a partial read the consumer gave up early, so skip the EOF check
// and cancel directly instead of draining a possibly large residual.
var eofErr error
if int64(r.n) == r.size {
eofErr = expectEOF(r.r)
}
closeErr := r.r.Close()
r.c.m.ReadDone(r.n, cmp.Or(closeErr, eofErr))
if r.cancel != nil {
r.cancel()
}
return closeErr
}
type digestSource struct {
c *Client
d digest.Digest
fname string
}
func (s digestSource) Open(ctx context.Context) (io.ReadCloser, error) {
// The context passed to bytestreamio.Open is used for the gRPC stream.
// We need to cancel this context to close the stream when we are done reading
// or if an error occurs during setup, otherwise we leak the stream until the
// parent context (step context) is canceled.
ctx, cancel := context.WithCancel(ctx)
r, err := bytestreamio.Open(ctx, bpb.NewByteStreamClient(s.c.casDataConn), s.c.resourceName(s.d))
if err != nil {
cancel()
s.c.m.ReadDone(0, err)
return nil, err
}
rd, err := s.c.newDecoder(r, s.d)
if err != nil {
cancel()
s.c.m.ReadDone(0, err)
return nil, err
}
return &digestSourceReader{r: rd, size: s.d.SizeBytes, c: s.c, cancel: cancel}, err
}
// ReadAll returns the full blob content in a single CAS fetch, retrying on
// retriable errors. blob.DataToBytes uses this instead of Open+read.
func (s digestSource) ReadAll(ctx context.Context) ([]byte, error) {
return s.c.Get(ctx, s.d, s.fname)
}
func (s digestSource) String() string {
return fmt.Sprintf("digest-data-in-cas:%s for %s", s.d, s.fname)
}