Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 41 additions & 7 deletions blob/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -723,11 +723,15 @@ type Bucket struct {
// from the io/fs package.
ioFSCallback func() (context.Context, *ReaderOptions)

// mu protects the closed variable.
// mu protects closed and defaultReaderOptions.
// Read locks are kept to allow holding a read lock for long-running calls,
// and thereby prevent closing until a call finishes.
mu sync.RWMutex
closed bool
// defaultReaderOptions, if non-nil, is used in place of an empty
// ReaderOptions when a read method is called with nil opts. Set via
// SetDefaultReaderOptions.
defaultReaderOptions *ReaderOptions
}

const pkgName = "gocloud.dev/blob"
Expand Down Expand Up @@ -799,6 +803,40 @@ func (b *Bucket) ReadAll(ctx context.Context, key string) (_ []byte, err error)
return io.ReadAll(r)
}

// SetDefaultReaderOptions sets default ReaderOptions used by NewReader,
// NewRangeReader, and Download when they are called with a nil opts. A copy of
// opts is stored, and a copy is substituted for a nil opts on each call.
//
// Only performance-related fields may be defaulted. Setting any other field
// (currently BeforeRead) returns an error, since those are per-call concerns.
//
// It is intended to be called once after opening a Bucket, before concurrent
// use, but it is safe to call concurrently.
func (b *Bucket) SetDefaultReaderOptions(opts ReaderOptions) error {
if opts.BeforeRead != nil {
return gcerr.Newf(gcerr.InvalidArgument, nil, "blob: SetDefaultReaderOptions does not allow setting BeforeRead")
}
b.mu.Lock()
defer b.mu.Unlock()
// opts is a value parameter, so it is already a copy owned by this call.
b.defaultReaderOptions = &opts
return nil
}

// readerOptionsOrDefault returns opts if it is non-nil, otherwise a copy of the
// bucket's default ReaderOptions (or an empty one if none is set). Callers must
// hold b.mu (read or write).
func (b *Bucket) readerOptionsOrDefault(opts *ReaderOptions) *ReaderOptions {
if opts != nil {
return opts
}
if b.defaultReaderOptions != nil {
cp := *b.defaultReaderOptions
return &cp
}
return &ReaderOptions{}
}

// Download writes the content of a blob into an io.Writer w.
//
// If w is an io.WriterAt and the driver implements driver.DownloaderAt, the
Expand All @@ -809,9 +847,7 @@ func (b *Bucket) Download(ctx context.Context, key string, w io.Writer, opts *Re
if b.closed {
return errClosed
}
if opts == nil {
opts = &ReaderOptions{}
}
opts = b.readerOptionsOrDefault(opts)
// If the caller opted in to parallel downloads, the destination supports
// random-access writes, and the driver can download directly into an
// io.WriterAt, use that and skip opening a Reader entirely. This avoids the
Expand Down Expand Up @@ -1068,9 +1104,7 @@ func (b *Bucket) newRangeReader(ctx context.Context, key string, offset, length
if !utf8.ValidString(key) {
return nil, gcerr.Newf(gcerr.InvalidArgument, nil, "blob: NewRangeReader key must be a valid UTF-8 string: %q", key)
}
if opts == nil {
opts = &ReaderOptions{}
}
opts = b.readerOptionsOrDefault(opts)
dopts := &driver.ReaderOptions{
BeforeRead: opts.BeforeRead,
}
Expand Down
67 changes: 67 additions & 0 deletions blob/blob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,73 @@ func TestDownloaderAtOptions(t *testing.T) {
}
}

func TestSetDefaultReaderOptions(t *testing.T) {
ctx := context.Background()

// BeforeRead is not allowlisted.
if err := NewBucket(&downloaderAtBucket{}).SetDefaultReaderOptions(ReaderOptions{
BeforeRead: func(func(any) bool) error { return nil },
}); err == nil {
t.Error("SetDefaultReaderOptions with BeforeRead should return an error")
}

// A default of AllowParallelDownload=true makes a nil-opts Download use the
// parallel path, and the part size/concurrency defaults flow to the driver.
b := &downloaderAtBucket{}
bucket := NewBucket(b)
defer closeWithErrorCheck(t, bucket)
if err := bucket.SetDefaultReaderOptions(ReaderOptions{
AllowParallelDownload: true,
BufferSize: 16 << 20,
MaxConcurrency: 8,
}); err != nil {
t.Fatalf("SetDefaultReaderOptions failed: %v", err)
}
if err := bucket.Download(ctx, "key", &memWriterAt{}, nil); err != nil {
t.Fatalf("Download failed: %v", err)
}
if !b.downloadedAt {
t.Error("DownloadAt wasn't called with a nil opts and a default of AllowParallelDownload=true")
}
if b.gotOpts == nil {
t.Fatal("DownloadAt wasn't called")
}
if got, want := b.gotOpts.BufferSize, 16<<20; got != want {
t.Errorf("BufferSize = %d, want %d", got, want)
}
if got, want := b.gotOpts.MaxConcurrency, 8; got != want {
t.Errorf("MaxConcurrency = %d, want %d", got, want)
}

// Without a default, a nil-opts Download falls back to the reader.
b2 := &downloaderAtBucket{}
bucket2 := NewBucket(b2)
defer closeWithErrorCheck(t, bucket2)
if err := bucket2.Download(ctx, "key", &memWriterAt{}, nil); err != nil {
t.Fatalf("Download failed: %v", err)
}
if b2.downloadedAt {
t.Error("DownloadAt was called without a default or per-call opt-in")
}
if !b2.r.downloaded {
t.Error("reader Download wasn't called")
}

// An explicit non-nil opts is used as-is; the default is not applied.
b3 := &downloaderAtBucket{}
bucket3 := NewBucket(b3)
defer closeWithErrorCheck(t, bucket3)
if err := bucket3.SetDefaultReaderOptions(ReaderOptions{AllowParallelDownload: true}); err != nil {
t.Fatalf("SetDefaultReaderOptions failed: %v", err)
}
if err := bucket3.Download(ctx, "key", &memWriterAt{}, &ReaderOptions{}); err != nil {
t.Fatalf("Download failed: %v", err)
}
if b3.downloadedAt {
t.Error("DownloadAt was called; an explicit opts should not inherit the default")
}
}

func TestSeekAfterReadFailure(t *testing.T) {
const filename = "f.txt"

Expand Down
Loading