storj/cmd/uplink/ulfs/handle_uplink.go
Andrew Harding 5efb08cd7b cmd/uplink: skip abort on committed multipart writes
The copyFile method has some safeguards to ensure that the multipart
write is aborted. This is accomplished by always calling abort on the
MultiWriteHandle when the copy is finished, whether or not there was a
failure or it was successfully committed. If the copy was committed,
then this RPC is a no-op on the metainfo server.

Regardless, the calls to abort to constitute an additional RPC to the
satellite for no benefit. This is exacerbated by the fact that the code
currently ends up calling abort twice.

This change updates the libuplink-backed MultiWriteHandle implementation
to not call abort if the write is committed and vice-versa. This
eliminates the two wasteful RPC calls.

Change-Id: I13679234f6f473e9a93179e6791fb57eac512f25
2023-01-05 18:30:23 +00:00

293 lines
5.7 KiB
Go

// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package ulfs
import (
"context"
"io"
"sync"
"github.com/zeebo/errs"
"storj.io/uplink"
)
//
// read handles
//
type uplinkMultiReadHandle struct {
project *uplink.Project
bucket string
key string
mu sync.Mutex
done bool
eof bool
off int64
info *ObjectInfo
}
func newUplinkMultiReadHandle(project *uplink.Project, bucket, key string) *uplinkMultiReadHandle {
return &uplinkMultiReadHandle{
project: project,
bucket: bucket,
key: key,
}
}
func (u *uplinkMultiReadHandle) Close() error {
u.mu.Lock()
defer u.mu.Unlock()
u.done = true
return nil
}
func (u *uplinkMultiReadHandle) SetOffset(offset int64) error {
u.mu.Lock()
defer u.mu.Unlock()
if u.done {
return errs.New("already closed")
}
u.off = offset
u.eof = false
return nil
}
func (u *uplinkMultiReadHandle) NextPart(ctx context.Context, length int64) (ReadHandle, error) {
opts, err := func() (opts *uplink.DownloadOptions, err error) {
u.mu.Lock()
defer u.mu.Unlock()
if u.done {
return nil, errs.New("already closed")
} else if u.eof {
return nil, io.EOF
} else if u.info != nil && u.off >= u.info.ContentLength {
return nil, io.EOF
}
opts = &uplink.DownloadOptions{Offset: u.off, Length: length}
if u.off < 0 {
opts.Length = -1
u.eof = u.off+length > 0
}
u.off += length
return opts, nil
}()
if err != nil {
return nil, err
}
// TODO: this can cause tearing if the object is modified during
// the download. this should be fixed when we extend the api to
// allow requesting a specific version of the object.
dl, err := u.project.DownloadObject(ctx, u.bucket, u.key, opts)
if err != nil {
return nil, err
}
u.mu.Lock()
defer u.mu.Unlock()
if u.info == nil {
info := uplinkObjectToObjectInfo(u.bucket, dl.Info())
u.info = &info
}
if u.off < 0 {
if norm := u.off + u.info.ContentLength; norm > 0 {
u.off = norm
}
}
return &uplinkReadHandle{
info: u.info,
dl: dl,
}, nil
}
func (u *uplinkMultiReadHandle) Info(ctx context.Context) (*ObjectInfo, error) {
u.mu.Lock()
if u.info != nil {
u.mu.Unlock()
return u.info, nil
}
u.mu.Unlock()
// TODO(jeff): maybe we want to dedupe concurrent requests?
obj, err := u.project.StatObject(ctx, u.bucket, u.key)
if err != nil {
return nil, err
}
u.mu.Lock()
defer u.mu.Unlock()
if u.info == nil {
info := uplinkObjectToObjectInfo(u.bucket, obj)
u.info = &info
}
info := *u.info
return &info, nil
}
// Length returns the size of the object.
func (u *uplinkMultiReadHandle) Length() int64 {
u.mu.Lock()
defer u.mu.Unlock()
// if we have not fetched the info yet, return unknown length
if u.info == nil {
return -1
}
return u.info.ContentLength
}
// uplinkReadHandle implements readHandle for *uplink.Downloads.
type uplinkReadHandle struct {
info *ObjectInfo
dl *uplink.Download
}
func (u *uplinkReadHandle) Read(p []byte) (int, error) { return u.dl.Read(p) }
func (u *uplinkReadHandle) Close() error { return u.dl.Close() }
func (u *uplinkReadHandle) Info() ObjectInfo { return *u.info }
//
// write handles
//
type uplinkMultiWriteHandle struct {
project *uplink.Project
bucket string
info uplink.UploadInfo
metadata uplink.CustomMetadata
mu sync.Mutex
tail bool
part uint32
commitErr *error
abortErr *error
}
func newUplinkMultiWriteHandle(project *uplink.Project, bucket string, info uplink.UploadInfo, metadata uplink.CustomMetadata) *uplinkMultiWriteHandle {
return &uplinkMultiWriteHandle{
project: project,
bucket: bucket,
info: info,
metadata: metadata,
}
}
func (u *uplinkMultiWriteHandle) NextPart(ctx context.Context, length int64) (WriteHandle, error) {
part, err := func() (uint32, error) {
u.mu.Lock()
defer u.mu.Unlock()
switch {
case u.abortErr != nil:
return 0, errs.New("cannot make part after multipart write has been aborted")
case u.commitErr != nil:
return 0, errs.New("cannot make part after multipart write has been committed")
}
if u.tail {
return 0, errs.New("unable to make part after tail part")
}
u.tail = length < 0
u.part++
return u.part, nil
}()
if err != nil {
return nil, err
}
ul, err := u.project.UploadPart(ctx, u.bucket, u.info.Key, u.info.UploadID, part)
if err != nil {
return nil, err
}
return &uplinkWriteHandle{
ul: ul,
tail: length < 0,
len: length,
}, nil
}
func (u *uplinkMultiWriteHandle) Commit(ctx context.Context) error {
u.mu.Lock()
defer u.mu.Unlock()
switch {
case u.abortErr != nil:
return errs.New("cannot commit an aborted multipart write")
case u.commitErr != nil:
return *u.commitErr
}
_, err := u.project.CommitUpload(ctx, u.bucket, u.info.Key, u.info.UploadID, &uplink.CommitUploadOptions{
CustomMetadata: u.metadata,
})
u.commitErr = &err
return err
}
func (u *uplinkMultiWriteHandle) Abort(ctx context.Context) error {
u.mu.Lock()
defer u.mu.Unlock()
switch {
case u.abortErr != nil:
return *u.abortErr
case u.commitErr != nil:
return errs.New("cannot abort a committed multipart write")
}
err := u.project.AbortUpload(ctx, u.bucket, u.info.Key, u.info.UploadID)
u.abortErr = &err
return err
}
// uplinkWriteHandle implements writeHandle for *uplink.Uploads.
type uplinkWriteHandle struct {
ul *uplink.PartUpload
tail bool
len int64
}
func (u *uplinkWriteHandle) Write(p []byte) (int, error) {
if !u.tail {
if u.len <= 0 {
return 0, errs.New("write past maximum length")
} else if u.len < int64(len(p)) {
p = p[:u.len]
}
}
n, err := u.ul.Write(p)
if !u.tail {
u.len -= int64(n)
}
return n, err
}
func (u *uplinkWriteHandle) Commit() error {
return u.ul.Commit()
}
func (u *uplinkWriteHandle) Abort() error {
return u.ul.Abort()
}