e7bd392a69
This fixes an important bug with listing that affects users with more than 500 objects in a listing operation.
445 lines
13 KiB
Go
445 lines
13 KiB
Go
// Copyright (C) 2019 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package ecclient
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"io/ioutil"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/spacemonkeygo/monkit/v3"
|
|
"github.com/zeebo/errs"
|
|
"go.uber.org/zap"
|
|
|
|
"storj.io/common/encryption"
|
|
"storj.io/common/errs2"
|
|
"storj.io/common/identity"
|
|
"storj.io/common/pb"
|
|
"storj.io/common/ranger"
|
|
"storj.io/common/rpc"
|
|
"storj.io/common/storj"
|
|
"storj.io/uplink/private/eestream"
|
|
"storj.io/uplink/private/piecestore"
|
|
)
|
|
|
|
var mon = monkit.Package()
|
|
|
|
// Client defines an interface for storing erasure coded data to piece store nodes.
|
|
type Client interface {
|
|
Put(ctx context.Context, limits []*pb.AddressedOrderLimit, privateKey storj.PiecePrivateKey, rs eestream.RedundancyStrategy, data io.Reader, expiration time.Time) (successfulNodes []*pb.Node, successfulHashes []*pb.PieceHash, err error)
|
|
PutSingleResult(ctx context.Context, limits []*pb.AddressedOrderLimit, privateKey storj.PiecePrivateKey, rs eestream.RedundancyStrategy, data io.Reader, expiration time.Time) (results []*pb.SegmentPieceUploadResult, err error)
|
|
Get(ctx context.Context, limits []*pb.AddressedOrderLimit, privateKey storj.PiecePrivateKey, es eestream.ErasureScheme, size int64) (ranger.Ranger, error)
|
|
WithForceErrorDetection(force bool) Client
|
|
// PutPiece is not intended to be used by normal uplinks directly, but is exported to support storagenode graceful exit transfers.
|
|
PutPiece(ctx, parent context.Context, limit *pb.AddressedOrderLimit, privateKey storj.PiecePrivateKey, data io.ReadCloser) (hash *pb.PieceHash, id *identity.PeerIdentity, err error)
|
|
}
|
|
|
|
type dialPiecestoreFunc func(context.Context, storj.NodeURL) (*piecestore.Client, error)
|
|
|
|
type ecClient struct {
|
|
log *zap.Logger
|
|
dialer rpc.Dialer
|
|
memoryLimit int
|
|
forceErrorDetection bool
|
|
}
|
|
|
|
// NewClient from the given identity and max buffer memory.
|
|
func NewClient(log *zap.Logger, dialer rpc.Dialer, memoryLimit int) Client {
|
|
return &ecClient{
|
|
log: log,
|
|
dialer: dialer,
|
|
memoryLimit: memoryLimit,
|
|
}
|
|
}
|
|
|
|
func (ec *ecClient) WithForceErrorDetection(force bool) Client {
|
|
ec.forceErrorDetection = force
|
|
return ec
|
|
}
|
|
|
|
func (ec *ecClient) dialPiecestore(ctx context.Context, n storj.NodeURL) (*piecestore.Client, error) {
|
|
logger := ec.log.Named(n.ID.String())
|
|
return piecestore.DialNodeURL(ctx, ec.dialer, n, logger, piecestore.DefaultConfig)
|
|
}
|
|
|
|
func (ec *ecClient) Put(ctx context.Context, limits []*pb.AddressedOrderLimit, privateKey storj.PiecePrivateKey, rs eestream.RedundancyStrategy, data io.Reader, expiration time.Time) (successfulNodes []*pb.Node, successfulHashes []*pb.PieceHash, err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
pieceCount := len(limits)
|
|
if pieceCount != rs.TotalCount() {
|
|
return nil, nil, Error.New("size of limits slice (%d) does not match total count (%d) of erasure scheme", pieceCount, rs.TotalCount())
|
|
}
|
|
|
|
nonNilLimits := nonNilCount(limits)
|
|
if nonNilLimits <= rs.RepairThreshold() && nonNilLimits < rs.OptimalThreshold() {
|
|
return nil, nil, Error.New("number of non-nil limits (%d) is less than or equal to the repair threshold (%d) of erasure scheme", nonNilLimits, rs.RepairThreshold())
|
|
}
|
|
|
|
if !unique(limits) {
|
|
return nil, nil, Error.New("duplicated nodes are not allowed")
|
|
}
|
|
|
|
ec.log.Debug("Uploading to storage nodes",
|
|
zap.Int("Erasure Share Size", rs.ErasureShareSize()),
|
|
zap.Int("Stripe Size", rs.StripeSize()),
|
|
zap.Int("Repair Threshold", rs.RepairThreshold()),
|
|
zap.Int("Optimal Threshold", rs.OptimalThreshold()),
|
|
)
|
|
|
|
padded := encryption.PadReader(ioutil.NopCloser(data), rs.StripeSize())
|
|
readers, err := eestream.EncodeReader(ctx, ec.log, padded, rs)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
type info struct {
|
|
i int
|
|
err error
|
|
hash *pb.PieceHash
|
|
}
|
|
infos := make(chan info, pieceCount)
|
|
|
|
psCtx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
for i, addressedLimit := range limits {
|
|
go func(i int, addressedLimit *pb.AddressedOrderLimit) {
|
|
hash, _, err := ec.PutPiece(psCtx, ctx, addressedLimit, privateKey, readers[i])
|
|
infos <- info{i: i, err: err, hash: hash}
|
|
}(i, addressedLimit)
|
|
}
|
|
|
|
successfulNodes = make([]*pb.Node, pieceCount)
|
|
successfulHashes = make([]*pb.PieceHash, pieceCount)
|
|
var successfulCount, failureCount, cancellationCount int32
|
|
for range limits {
|
|
info := <-infos
|
|
|
|
if limits[info.i] == nil {
|
|
continue
|
|
}
|
|
|
|
if info.err != nil {
|
|
if !errs2.IsCanceled(info.err) {
|
|
failureCount++
|
|
} else {
|
|
cancellationCount++
|
|
}
|
|
ec.log.Debug("Upload to storage node failed",
|
|
zap.Stringer("Node ID", limits[info.i].GetLimit().StorageNodeId),
|
|
zap.Error(info.err),
|
|
)
|
|
continue
|
|
}
|
|
|
|
successfulNodes[info.i] = &pb.Node{
|
|
Id: limits[info.i].GetLimit().StorageNodeId,
|
|
Address: limits[info.i].GetStorageNodeAddress(),
|
|
}
|
|
successfulHashes[info.i] = info.hash
|
|
|
|
successfulCount++
|
|
if int(successfulCount) >= rs.OptimalThreshold() {
|
|
ec.log.Debug("Success threshold reached. Cancelling remaining uploads.",
|
|
zap.Int("Optimal Threshold", rs.OptimalThreshold()),
|
|
)
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
defer func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
err = Error.New("upload cancelled by user")
|
|
default:
|
|
}
|
|
}()
|
|
|
|
mon.IntVal("put_segment_pieces_total").Observe(int64(pieceCount))
|
|
mon.IntVal("put_segment_pieces_optimal").Observe(int64(rs.OptimalThreshold()))
|
|
mon.IntVal("put_segment_pieces_successful").Observe(int64(successfulCount))
|
|
mon.IntVal("put_segment_pieces_failed").Observe(int64(failureCount))
|
|
mon.IntVal("put_segment_pieces_canceled").Observe(int64(cancellationCount))
|
|
|
|
if int(successfulCount) <= rs.RepairThreshold() && int(successfulCount) < rs.OptimalThreshold() {
|
|
return nil, nil, Error.New("successful puts (%d) less than or equal to repair threshold (%d)", successfulCount, rs.RepairThreshold())
|
|
}
|
|
|
|
if int(successfulCount) < rs.OptimalThreshold() {
|
|
return nil, nil, Error.New("successful puts (%d) less than success threshold (%d)", successfulCount, rs.OptimalThreshold())
|
|
}
|
|
|
|
return successfulNodes, successfulHashes, nil
|
|
}
|
|
|
|
func (ec *ecClient) PutSingleResult(ctx context.Context, limits []*pb.AddressedOrderLimit, privateKey storj.PiecePrivateKey, rs eestream.RedundancyStrategy, data io.Reader, expiration time.Time) (results []*pb.SegmentPieceUploadResult, err error) {
|
|
successfulNodes, successfulHashes, err := ec.Put(ctx, limits, privateKey, rs, data, expiration)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
uploadResults := make([]*pb.SegmentPieceUploadResult, 0, len(successfulNodes))
|
|
for i := range successfulNodes {
|
|
if successfulNodes[i] == nil {
|
|
continue
|
|
}
|
|
|
|
uploadResults = append(uploadResults, &pb.SegmentPieceUploadResult{
|
|
PieceNum: int32(i),
|
|
NodeId: successfulNodes[i].Id,
|
|
Hash: successfulHashes[i],
|
|
})
|
|
}
|
|
|
|
if l := len(uploadResults); l < rs.OptimalThreshold() {
|
|
return nil, Error.New("uploaded results (%d) are below the optimal threshold (%d)", l, rs.OptimalThreshold())
|
|
}
|
|
|
|
return uploadResults, nil
|
|
}
|
|
|
|
func (ec *ecClient) PutPiece(ctx, parent context.Context, limit *pb.AddressedOrderLimit, privateKey storj.PiecePrivateKey, data io.ReadCloser) (hash *pb.PieceHash, peerID *identity.PeerIdentity, err error) {
|
|
nodeName := "nil"
|
|
if limit != nil {
|
|
nodeName = limit.GetLimit().StorageNodeId.String()[0:8]
|
|
}
|
|
defer mon.Task()(&ctx, "node: "+nodeName)(&err)
|
|
defer func() { err = errs.Combine(err, data.Close()) }()
|
|
|
|
if limit == nil {
|
|
_, _ = io.Copy(ioutil.Discard, data)
|
|
return nil, nil, nil
|
|
}
|
|
|
|
storageNodeID := limit.GetLimit().StorageNodeId
|
|
pieceID := limit.GetLimit().PieceId
|
|
ps, err := ec.dialPiecestore(ctx, storj.NodeURL{
|
|
ID: storageNodeID,
|
|
Address: limit.GetStorageNodeAddress().Address,
|
|
})
|
|
if err != nil {
|
|
ec.log.Debug("Failed dialing for putting piece to node",
|
|
zap.Stringer("Piece ID", pieceID),
|
|
zap.Stringer("Node ID", storageNodeID),
|
|
zap.Error(err),
|
|
)
|
|
return nil, nil, err
|
|
}
|
|
defer func() { err = errs.Combine(err, ps.Close()) }()
|
|
|
|
peerID, err = ps.GetPeerIdentity()
|
|
if err != nil {
|
|
ec.log.Debug("Failed getting peer identity from node connection",
|
|
zap.Stringer("Node ID", storageNodeID),
|
|
zap.Error(err),
|
|
)
|
|
return nil, nil, err
|
|
}
|
|
|
|
hash, err = ps.UploadReader(ctx, limit.GetLimit(), privateKey, data)
|
|
if err != nil {
|
|
if ctx.Err() == context.Canceled {
|
|
// Canceled context means the piece upload was interrupted by user or due
|
|
// to slow connection. No error logging for this case.
|
|
if parent.Err() == context.Canceled {
|
|
ec.log.Info("Upload to node canceled by user", zap.Stringer("Node ID", storageNodeID))
|
|
} else {
|
|
ec.log.Debug("Node cut from upload due to slow connection", zap.Stringer("Node ID", storageNodeID))
|
|
}
|
|
|
|
// make sure context.Canceled is the primary error in the error chain
|
|
// for later errors.Is/errs2.IsCanceled checking
|
|
err = errs.Combine(context.Canceled, err)
|
|
|
|
} else {
|
|
nodeAddress := ""
|
|
if limit.GetStorageNodeAddress() != nil {
|
|
nodeAddress = limit.GetStorageNodeAddress().GetAddress()
|
|
}
|
|
|
|
ec.log.Debug("Failed uploading piece to node",
|
|
zap.Stringer("Piece ID", pieceID),
|
|
zap.Stringer("Node ID", storageNodeID),
|
|
zap.String("Node Address", nodeAddress),
|
|
zap.Error(err),
|
|
)
|
|
}
|
|
|
|
return nil, nil, err
|
|
}
|
|
|
|
return hash, peerID, nil
|
|
}
|
|
|
|
func (ec *ecClient) Get(ctx context.Context, limits []*pb.AddressedOrderLimit, privateKey storj.PiecePrivateKey, es eestream.ErasureScheme, size int64) (rr ranger.Ranger, err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
if len(limits) != es.TotalCount() {
|
|
return nil, Error.New("size of limits slice (%d) does not match total count (%d) of erasure scheme", len(limits), es.TotalCount())
|
|
}
|
|
|
|
if nonNilCount(limits) < es.RequiredCount() {
|
|
return nil, Error.New("number of non-nil limits (%d) is less than required count (%d) of erasure scheme", nonNilCount(limits), es.RequiredCount())
|
|
}
|
|
|
|
paddedSize := calcPadded(size, es.StripeSize())
|
|
pieceSize := paddedSize / int64(es.RequiredCount())
|
|
|
|
rrs := map[int]ranger.Ranger{}
|
|
for i, addressedLimit := range limits {
|
|
if addressedLimit == nil {
|
|
continue
|
|
}
|
|
|
|
rrs[i] = &lazyPieceRanger{
|
|
dialPiecestore: ec.dialPiecestore,
|
|
limit: addressedLimit,
|
|
privateKey: privateKey,
|
|
size: pieceSize,
|
|
}
|
|
}
|
|
|
|
rr, err = eestream.Decode(ec.log, rrs, es, ec.memoryLimit, ec.forceErrorDetection)
|
|
if err != nil {
|
|
return nil, Error.Wrap(err)
|
|
}
|
|
|
|
ranger, err := encryption.Unpad(rr, int(paddedSize-size))
|
|
return ranger, Error.Wrap(err)
|
|
}
|
|
|
|
func unique(limits []*pb.AddressedOrderLimit) bool {
|
|
if len(limits) < 2 {
|
|
return true
|
|
}
|
|
ids := make(storj.NodeIDList, len(limits))
|
|
for i, addressedLimit := range limits {
|
|
if addressedLimit != nil {
|
|
ids[i] = addressedLimit.GetLimit().StorageNodeId
|
|
}
|
|
}
|
|
|
|
// sort the ids and check for identical neighbors
|
|
sort.Sort(ids)
|
|
// sort.Slice(ids, func(i, k int) bool { return ids[i].Less(ids[k]) })
|
|
for i := 1; i < len(ids); i++ {
|
|
if ids[i] != (storj.NodeID{}) && ids[i] == ids[i-1] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func calcPadded(size int64, blockSize int) int64 {
|
|
mod := size % int64(blockSize)
|
|
if mod == 0 {
|
|
return size
|
|
}
|
|
return size + int64(blockSize) - mod
|
|
}
|
|
|
|
type lazyPieceRanger struct {
|
|
dialPiecestore dialPiecestoreFunc
|
|
limit *pb.AddressedOrderLimit
|
|
privateKey storj.PiecePrivateKey
|
|
size int64
|
|
}
|
|
|
|
// Size implements Ranger.Size.
|
|
func (lr *lazyPieceRanger) Size() int64 {
|
|
return lr.size
|
|
}
|
|
|
|
// Range implements Ranger.Range to be lazily connected.
|
|
func (lr *lazyPieceRanger) Range(ctx context.Context, offset, length int64) (_ io.ReadCloser, err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
return &lazyPieceReader{
|
|
ranger: lr,
|
|
ctx: ctx,
|
|
offset: offset,
|
|
length: length,
|
|
}, nil
|
|
}
|
|
|
|
type lazyPieceReader struct {
|
|
ranger *lazyPieceRanger
|
|
ctx context.Context
|
|
offset int64
|
|
length int64
|
|
|
|
mu sync.Mutex
|
|
|
|
isClosed bool
|
|
piecestore.Downloader
|
|
client *piecestore.Client
|
|
}
|
|
|
|
func (lr *lazyPieceReader) Read(data []byte) (_ int, err error) {
|
|
lr.mu.Lock()
|
|
defer lr.mu.Unlock()
|
|
|
|
if lr.isClosed {
|
|
return 0, io.EOF
|
|
}
|
|
if lr.Downloader == nil {
|
|
client, downloader, err := lr.ranger.dial(lr.ctx, lr.offset, lr.length)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
lr.Downloader = downloader
|
|
lr.client = client
|
|
}
|
|
|
|
return lr.Downloader.Read(data)
|
|
}
|
|
|
|
func (lr *lazyPieceRanger) dial(ctx context.Context, offset, length int64) (_ *piecestore.Client, _ piecestore.Downloader, err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
ps, err := lr.dialPiecestore(ctx, storj.NodeURL{
|
|
ID: lr.limit.GetLimit().StorageNodeId,
|
|
Address: lr.limit.GetStorageNodeAddress().Address,
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
download, err := ps.Download(ctx, lr.limit.GetLimit(), lr.privateKey, offset, length)
|
|
if err != nil {
|
|
return nil, nil, errs.Combine(err, ps.Close())
|
|
}
|
|
return ps, download, nil
|
|
}
|
|
|
|
func (lr *lazyPieceReader) Close() (err error) {
|
|
lr.mu.Lock()
|
|
defer lr.mu.Unlock()
|
|
|
|
if lr.isClosed {
|
|
return nil
|
|
}
|
|
lr.isClosed = true
|
|
|
|
if lr.Downloader != nil {
|
|
err = errs.Combine(err, lr.Downloader.Close())
|
|
}
|
|
if lr.client != nil {
|
|
err = errs.Combine(err, lr.client.Close())
|
|
}
|
|
return err
|
|
}
|
|
|
|
func nonNilCount(limits []*pb.AddressedOrderLimit) int {
|
|
total := 0
|
|
for _, limit := range limits {
|
|
if limit != nil {
|
|
total++
|
|
}
|
|
}
|
|
return total
|
|
}
|