storj.io/uplink@v1.13.0/private/storage/streams/streamupload/upload_test.go (about)

     1  // Copyright (C) 2023 Storj Labs, Inc.
     2  // See LICENSE for copying information.
     3  
     4  package streamupload
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  	"github.com/zeebo/errs"
    19  
    20  	"storj.io/common/pb"
    21  	"storj.io/common/storj"
    22  	"storj.io/uplink/private/metaclient"
    23  	"storj.io/uplink/private/storage/streams/splitter"
    24  )
    25  
    26  var (
    27  	streamID            = mustNewStreamID()
    28  	bucket              = []byte("BUCKET")
    29  	encryptedObjectKey  = []byte("ENCRYPTED-OBJECT-KEY")
    30  	beginObject         = &metaclient.BeginObjectParams{Bucket: bucket, EncryptedObjectKey: encryptedObjectKey}
    31  	encMetadataKey      = storj.EncryptedPrivateKey("METADATA-KEY")
    32  	encMetadataKeyNonce = storj.Nonce{0: 0, 1: 1, 2: 2, 3: 3}
    33  	creationDate        = time.Now().UTC()
    34  	eTag                = []byte("ETAG")
    35  )
    36  
    37  func TestUploadObject(t *testing.T) {
    38  	for _, tc := range []struct {
    39  		desc                    string
    40  		segments                []splitter.Segment
    41  		blockNext               bool
    42  		sourceErr               error
    43  		sourceErrAfter          int
    44  		expectBeginObject       bool
    45  		expectCommitObject      bool
    46  		expectBeginDeleteObject bool
    47  		expectErr               string
    48  	}{
    49  		{
    50  			desc:      "source must provide at least one segment",
    51  			expectErr: "programmer error: there should always be at least one segment",
    52  		},
    53  		{
    54  			desc:     "inline",
    55  			segments: makeSegments(goodInline),
    56  		},
    57  		{
    58  			desc:     "remote",
    59  			segments: makeSegments(goodRemote),
    60  		},
    61  		{
    62  			desc:     "remote+inline",
    63  			segments: makeSegments(goodRemote, goodInline),
    64  		},
    65  		{
    66  			desc:     "remote+remote",
    67  			segments: makeSegments(goodRemote, goodRemote),
    68  		},
    69  		{
    70  			desc:           "source fails on first segment",
    71  			segments:       makeSegments(blockWaitRemote, blockWaitRemote),
    72  			sourceErr:      errs.New("source failed on first"),
    73  			sourceErrAfter: 0,
    74  			expectErr:      "source failed on first",
    75  		},
    76  		{
    77  			desc:                    "source fails on second segment",
    78  			segments:                makeSegments(blockWaitRemote, blockWaitRemote),
    79  			sourceErr:               errs.New("source failed on second"),
    80  			sourceErrAfter:          1,
    81  			expectBeginObject:       true,
    82  			expectBeginDeleteObject: true,
    83  			expectErr:               "source failed on second",
    84  		},
    85  		{
    86  			desc:                    "failed segment upload begin",
    87  			segments:                makeSegments(goodRemote, badBeginRemote),
    88  			blockNext:               true,
    89  			expectBeginObject:       true,
    90  			expectBeginDeleteObject: true,
    91  			expectErr:               "begin failed",
    92  		},
    93  		{
    94  			desc:                    "failed segment upload wait",
    95  			segments:                makeSegments(goodRemote, badWaitRemote),
    96  			blockNext:               true,
    97  			expectBeginObject:       true,
    98  			expectBeginDeleteObject: true,
    99  			expectErr:               "wait failed",
   100  		},
   101  	} {
   102  		t.Run(tc.desc, func(t *testing.T) {
   103  			var (
   104  				encMeta         = encryptedMetadata{}
   105  				segmentSource   = &segmentSource{segments: tc.segments, blockNext: tc.blockNext, err: tc.sourceErr, errAfter: tc.sourceErrAfter}
   106  				segmentUploader = segmentUploader{}
   107  				miBatcher       = newMetainfoBatcher(t, false, len(tc.segments))
   108  			)
   109  			info, err := UploadObject(context.Background(), segmentSource, segmentUploader, miBatcher, beginObject, encMeta)
   110  			if tc.expectErr != "" {
   111  				require.NoError(t, miBatcher.CheckObject(tc.expectBeginObject, tc.expectCommitObject, tc.expectBeginDeleteObject))
   112  				require.EqualError(t, err, tc.expectErr)
   113  				return
   114  			}
   115  			require.NoError(t, err)
   116  			require.Equal(t, Info{
   117  				PlainSize: segmentsPlainSize(len(tc.segments)),
   118  			}, info)
   119  
   120  			// Assert that the expected operations took place on the metainfo store
   121  			require.NoError(t, miBatcher.CheckObject(true, true, false))
   122  			require.NoError(t, miBatcher.CheckSegments(tc.segments))
   123  
   124  			// Assert that all segments were marked as "done" reading with the result error
   125  			for i, segment := range tc.segments {
   126  				require.NoError(t, segment.(interface{ Check(error) error }).Check(err), "segment %d failed check", i)
   127  			}
   128  		})
   129  	}
   130  }
   131  
   132  func TestUploadPart(t *testing.T) {
   133  	for _, tc := range []struct {
   134  		desc           string
   135  		segments       []splitter.Segment
   136  		blockNext      bool
   137  		sourceErr      error
   138  		sourceErrAfter int
   139  		expectErr      string
   140  	}{
   141  		{
   142  			desc:      "source must provide at least one segment",
   143  			expectErr: "programmer error: there should always be at least one segment",
   144  		},
   145  		{
   146  			desc:     "inline",
   147  			segments: makeSegments(goodInline),
   148  		},
   149  		{
   150  			desc:     "remote",
   151  			segments: makeSegments(goodRemote),
   152  		},
   153  		{
   154  			desc:     "remote+inline",
   155  			segments: makeSegments(goodRemote, goodInline),
   156  		},
   157  		{
   158  			desc:     "remote+remote",
   159  			segments: makeSegments(goodRemote, goodRemote),
   160  		},
   161  		{
   162  			desc:           "source fails on first segment",
   163  			segments:       makeSegments(blockWaitRemote, blockWaitRemote),
   164  			sourceErr:      errs.New("source failed on first"),
   165  			sourceErrAfter: 0,
   166  			expectErr:      "source failed on first",
   167  		},
   168  		{
   169  			desc:           "source fails on second segment",
   170  			segments:       makeSegments(blockWaitRemote, blockWaitRemote),
   171  			sourceErr:      errs.New("source failed on second"),
   172  			sourceErrAfter: 1,
   173  			expectErr:      "source failed on second",
   174  		},
   175  		{
   176  			desc:      "failed segment upload begin",
   177  			segments:  makeSegments(goodRemote, badBeginRemote),
   178  			blockNext: true,
   179  			expectErr: "begin failed",
   180  		},
   181  		{
   182  			desc:      "failed segment upload wait",
   183  			segments:  makeSegments(goodRemote, badWaitRemote),
   184  			blockNext: true,
   185  			expectErr: "wait failed",
   186  		},
   187  	} {
   188  		t.Run(tc.desc, func(t *testing.T) {
   189  			var (
   190  				segmentSource   = &segmentSource{segments: tc.segments, blockNext: tc.blockNext, err: tc.sourceErr, errAfter: tc.sourceErrAfter}
   191  				segmentUploader = segmentUploader{}
   192  				miBatcher       = newMetainfoBatcher(t, true, len(tc.segments))
   193  			)
   194  
   195  			eTagCh := make(chan []byte, 1)
   196  			eTagCh <- eTag
   197  
   198  			info, err := UploadPart(context.Background(), segmentSource, segmentUploader, miBatcher, streamID, eTagCh)
   199  			if tc.expectErr != "" {
   200  				require.NoError(t, miBatcher.CheckObject(false, false, false))
   201  				require.EqualError(t, err, tc.expectErr)
   202  				return
   203  			}
   204  			require.NoError(t, err)
   205  			require.Equal(t, Info{
   206  				PlainSize: segmentsPlainSize(len(tc.segments)),
   207  			}, info)
   208  
   209  			// Assert that the expected operations took place on the metainfo store
   210  			require.NoError(t, miBatcher.CheckObject(false, false, false))
   211  			require.NoError(t, miBatcher.CheckSegments(tc.segments))
   212  
   213  			// Assert that all segments were marked as "done" reading with the result error
   214  			for i, segment := range tc.segments {
   215  				require.NoError(t, segment.(interface{ Check(error) error }).Check(err), "segment %d failed check", i)
   216  			}
   217  		})
   218  	}
   219  }
   220  
   221  type segmentSource struct {
   222  	next      int
   223  	segments  []splitter.Segment
   224  	blockNext bool
   225  	err       error
   226  	errAfter  int
   227  }
   228  
   229  func (src *segmentSource) Next(ctx context.Context) (segment splitter.Segment, err error) {
   230  	switch {
   231  	case src.err != nil && src.next == src.errAfter:
   232  		return nil, src.err
   233  	case src.next < len(src.segments):
   234  		segment := src.segments[src.next]
   235  		src.next++
   236  		return segment, nil
   237  	case src.blockNext:
   238  		<-ctx.Done()
   239  		return nil, ctx.Err()
   240  	default:
   241  		return nil, nil
   242  	}
   243  }
   244  
   245  type segmentType int
   246  
   247  const (
   248  	goodInline segmentType = iota
   249  	goodRemote
   250  	badBeginRemote
   251  	badWaitRemote
   252  	blockWaitRemote
   253  )
   254  
   255  func makeSegments(types ...segmentType) []splitter.Segment {
   256  	var ss []splitter.Segment
   257  	for i, typ := range types {
   258  		switch typ {
   259  		case goodInline:
   260  			ss = append(ss, &segment{inline: true, index: i})
   261  		case goodRemote:
   262  			ss = append(ss, &segment{inline: false, index: i})
   263  		case badBeginRemote:
   264  			ss = append(ss, &segment{inline: false, index: i, beginErr: errs.New("begin failed")})
   265  		case badWaitRemote:
   266  			ss = append(ss, &segment{inline: false, index: i, waitErr: errs.New("wait failed")})
   267  		case blockWaitRemote:
   268  			ss = append(ss, &segment{inline: false, index: i, blockWait: true})
   269  		}
   270  	}
   271  	return ss
   272  }
   273  
   274  type segment struct {
   275  	inline    bool
   276  	index     int
   277  	done      *error
   278  	beginErr  error
   279  	waitErr   error
   280  	blockWait bool
   281  	uploadCtx context.Context
   282  	canceled  bool
   283  }
   284  
   285  func (s *segment) Begin() metaclient.BatchItem {
   286  	if s.inline {
   287  		return &metaclient.MakeInlineSegmentParams{
   288  			Position:  s.Position(),
   289  			PlainSize: int64(s.index),
   290  		}
   291  	}
   292  	return &metaclient.BeginSegmentParams{
   293  		Position: s.Position(),
   294  	}
   295  }
   296  
   297  func (s *segment) Position() metaclient.SegmentPosition {
   298  	return metaclient.SegmentPosition{Index: int32(s.index)}
   299  }
   300  func (s *segment) Inline() bool      { return s.inline }
   301  func (s *segment) Reader() io.Reader { return strings.NewReader("HELLO") }
   302  
   303  func (s *segment) Finalize() *splitter.SegmentInfo {
   304  	return &splitter.SegmentInfo{
   305  		PlainSize: int64(s.index),
   306  	}
   307  }
   308  
   309  func (s *segment) DoneReading(err error) { s.done = &err }
   310  
   311  func (s *segment) Check(uploadErr error) error {
   312  	switch {
   313  	case s.done == nil:
   314  		return errs.New("expected DoneReading(%v) but never called", uploadErr)
   315  	case !errors.Is(*s.done, uploadErr):
   316  		return errs.New("expected DoneReading(%v) but got DoneReading(%v)", uploadErr, *s.done)
   317  	case s.blockWait && !s.canceled:
   318  		return errs.New("expected upload to be canceled")
   319  	}
   320  	return nil
   321  }
   322  
   323  func (s *segment) EncryptETag(eTagIn []byte) ([]byte, error) {
   324  	if !bytes.Equal(eTagIn, eTag) {
   325  		return nil, errs.New("expected eTag %q but got %q", string(eTag), string(eTagIn))
   326  	}
   327  	return encryptETag(s.index), nil
   328  }
   329  
   330  func (s *segment) beginUpload(ctx context.Context) (SegmentUpload, error) {
   331  	if s.inline {
   332  		return nil, errs.New("programmer error: inline segments should not be uploaded")
   333  	}
   334  	if s.beginErr != nil {
   335  		return nil, s.beginErr
   336  	}
   337  	if s.uploadCtx != nil {
   338  		return nil, errs.New("programmer error: upload already began")
   339  	}
   340  	s.uploadCtx = ctx
   341  	return s, nil
   342  }
   343  
   344  func (s *segment) Wait() (*metaclient.CommitSegmentParams, error) {
   345  	if s.inline {
   346  		return nil, errs.New("programmer error: Wait should not be called on an inline segment")
   347  	}
   348  	if s.uploadCtx == nil {
   349  		return nil, errs.New("programmer error: Wait called before upload was began")
   350  	}
   351  	if s.waitErr != nil {
   352  		return nil, s.waitErr
   353  	}
   354  	if s.blockWait {
   355  		<-s.uploadCtx.Done()
   356  		s.canceled = true
   357  		return nil, s.uploadCtx.Err()
   358  	}
   359  	return &metaclient.CommitSegmentParams{
   360  		PlainSize: int64(s.index),
   361  	}, nil
   362  }
   363  
   364  type metainfoBatcher struct {
   365  	t                 *testing.T
   366  	partUpload        bool
   367  	lastSegmentIndex  int32
   368  	batchCount        int
   369  	beginObject       int
   370  	commitObject      int
   371  	beginDeleteObject int
   372  	beginSegment      map[int32]struct{}
   373  	commitSegment     map[int32]struct{}
   374  	makeInlineSegment map[int32]struct{}
   375  }
   376  
   377  func newMetainfoBatcher(t *testing.T, partUpload bool, segmentCount int) *metainfoBatcher {
   378  	return &metainfoBatcher{
   379  		t:                 t,
   380  		partUpload:        partUpload,
   381  		lastSegmentIndex:  int32(segmentCount - 1),
   382  		beginSegment:      make(map[int32]struct{}),
   383  		commitSegment:     make(map[int32]struct{}),
   384  		makeInlineSegment: make(map[int32]struct{}),
   385  	}
   386  }
   387  
   388  func (m *metainfoBatcher) Batch(ctx context.Context, items ...metaclient.BatchItem) ([]metaclient.BatchResponse, error) {
   389  	expectedStreamID := streamID
   390  
   391  	m.batchCount++
   392  	if !m.partUpload && m.batchCount == 1 {
   393  		// object uploads won't include the stream ID on the first batch since
   394  		// that batch contains the BeginObject request.
   395  		expectedStreamID = nil
   396  	}
   397  
   398  	var responses []metaclient.BatchResponse
   399  	for _, item := range items {
   400  		req := item.BatchItem()
   401  		resp := &pb.BatchResponseItem{}
   402  		switch req := req.Request.(type) {
   403  		case *pb.BatchRequestItem_ObjectBegin:
   404  			assert.Equal(m.t, &pb.BeginObjectRequest{
   405  				Bucket:               bucket,
   406  				EncryptedObjectKey:   encryptedObjectKey,
   407  				RedundancyScheme:     &pb.RedundancyScheme{},
   408  				EncryptionParameters: &pb.EncryptionParameters{},
   409  			}, req.ObjectBegin)
   410  			m.beginObject++
   411  			resp.Response = &pb.BatchResponseItem_ObjectBegin{ObjectBegin: &pb.BeginObjectResponse{StreamId: streamID}}
   412  
   413  		case *pb.BatchRequestItem_ObjectCommit:
   414  			assert.Equal(m.t, &pb.CommitObjectRequest{
   415  				StreamId:                      expectedStreamID,
   416  				EncryptedMetadataNonce:        encMetadataKeyNonce,
   417  				EncryptedMetadataEncryptedKey: encMetadataKey,
   418  				EncryptedMetadata:             encryptMetadata(int64(m.lastSegmentIndex)),
   419  			}, req.ObjectCommit)
   420  			m.commitObject++
   421  			resp.Response = &pb.BatchResponseItem_ObjectCommit{ObjectCommit: &pb.CommitObjectResponse{
   422  				Object: &pb.Object{
   423  					PlainSize: segmentsPlainSize(len(m.makeInlineSegment) + len(m.commitSegment)),
   424  				},
   425  			}}
   426  
   427  		case *pb.BatchRequestItem_ObjectBeginDelete:
   428  			assert.Equal(m.t, &pb.BeginDeleteObjectRequest{
   429  				Bucket:             bucket,
   430  				EncryptedObjectKey: encryptedObjectKey,
   431  				StreamId:           &streamID,
   432  				Status:             int32(pb.Object_UPLOADING),
   433  			}, req.ObjectBeginDelete)
   434  			m.beginDeleteObject++
   435  			resp.Response = &pb.BatchResponseItem_ObjectBeginDelete{ObjectBeginDelete: &pb.BeginDeleteObjectResponse{}}
   436  
   437  		case *pb.BatchRequestItem_SegmentBegin:
   438  			segmentIndex := req.SegmentBegin.Position.Index
   439  			assert.Equal(m.t, req.SegmentBegin.StreamId, expectedStreamID, "unexpected stream ID on segment %d", segmentIndex)
   440  			m.beginSegment[segmentIndex] = struct{}{}
   441  			resp.Response = &pb.BatchResponseItem_SegmentBegin{SegmentBegin: &pb.BeginSegmentResponse{}}
   442  
   443  		case *pb.BatchRequestItem_SegmentCommit:
   444  			segmentIndex := int32(req.SegmentCommit.PlainSize)
   445  			m.assertEncryptedETag(segmentIndex, req.SegmentCommit.EncryptedETag)
   446  			m.commitSegment[segmentIndex] = struct{}{}
   447  			resp.Response = &pb.BatchResponseItem_SegmentCommit{SegmentCommit: &pb.CommitSegmentResponse{}}
   448  
   449  		case *pb.BatchRequestItem_SegmentMakeInline:
   450  			segmentIndex := req.SegmentMakeInline.Position.Index
   451  			m.assertEncryptedETag(segmentIndex, req.SegmentMakeInline.EncryptedETag)
   452  			m.makeInlineSegment[segmentIndex] = struct{}{}
   453  			resp.Response = &pb.BatchResponseItem_SegmentMakeInline{SegmentMakeInline: &pb.MakeInlineSegmentResponse{}}
   454  		default:
   455  			return nil, errs.New("unexpected batch request type %T", req)
   456  		}
   457  		responses = append(responses, metaclient.MakeBatchResponse(req, resp))
   458  	}
   459  	return responses, nil
   460  }
   461  
   462  func (m *metainfoBatcher) CheckObject(expectBeginObject, expectCommitObject, expectBeginDeleteObject bool) error {
   463  	var eg errs.Group
   464  
   465  	checkCall := func(rpc string, callCount int, expectCalled bool) {
   466  		expectedCallCount := 0
   467  		if expectCalled {
   468  			expectedCallCount = 1
   469  		}
   470  		if callCount != expectedCallCount {
   471  			eg.Add(errs.New("expected %d %s(s) but got %d", expectedCallCount, rpc, callCount))
   472  		}
   473  	}
   474  
   475  	checkCall("BeginObject", m.beginObject, expectBeginObject)
   476  	checkCall("CommitObject", m.commitObject, expectCommitObject)
   477  	checkCall("BeginDeleteObject", m.beginDeleteObject, expectBeginDeleteObject)
   478  
   479  	return eg.Err()
   480  }
   481  
   482  func (m *metainfoBatcher) CheckSegments(segments []splitter.Segment) error {
   483  	var eg errs.Group
   484  
   485  	if len(segments) != len(m.commitSegment)+len(m.makeInlineSegment) {
   486  		eg.Add(errs.New("mismatched number of segments made/committed: expected %d but got %d remote and %d inline", len(segments), len(m.commitSegment), len(m.makeInlineSegment)))
   487  	}
   488  
   489  	for i, segment := range segments {
   490  		index := segment.Position().Index
   491  		if i != int(index) {
   492  			eg.Add(errs.New("segment %d had unexpected index %d", i, index))
   493  		}
   494  		if segment.Inline() {
   495  			if _, ok := m.makeInlineSegment[index]; !ok {
   496  				eg.Add(errs.New("segment %d (inline) was never made", i))
   497  			}
   498  		} else {
   499  			if _, ok := m.beginSegment[index]; !ok {
   500  				eg.Add(errs.New("segment %d (remote) was never began", i))
   501  			}
   502  			if _, ok := m.commitSegment[index]; !ok {
   503  				eg.Add(errs.New("segment %d (remote) was never committed", i))
   504  			}
   505  		}
   506  	}
   507  
   508  	return eg.Err()
   509  }
   510  
   511  func (m *metainfoBatcher) assertEncryptedETag(segmentIndex int32, encryptedETag []byte) {
   512  	// Last segment in a part upload needs to include the encrypted ETag
   513  	if m.partUpload && segmentIndex == m.lastSegmentIndex {
   514  		assert.Equal(m.t, encryptETag(int(segmentIndex)), encryptedETag, "unexpected encrypted eTag on segment %d", segmentIndex)
   515  	} else {
   516  		assert.Nil(m.t, encryptedETag, "unexpected encrypted eTag on segment %d", segmentIndex)
   517  	}
   518  }
   519  
   520  type segmentUploader struct{}
   521  
   522  func (segmentUploader) Begin(ctx context.Context, resp *metaclient.BeginSegmentResponse, opaque splitter.Segment) (SegmentUpload, error) {
   523  	seg := opaque.(*segment)
   524  	return seg.beginUpload(ctx)
   525  }
   526  
   527  type encryptedMetadata struct{}
   528  
   529  func (encryptedMetadata) EncryptedMetadata(lastSegmentSize int64) (data []byte, encKey *storj.EncryptedPrivateKey, nonce *storj.Nonce, err error) {
   530  	return encryptMetadata(lastSegmentSize), &encMetadataKey, &encMetadataKeyNonce, nil
   531  }
   532  
   533  func mustNewStreamID() storj.StreamID {
   534  	streamID, err := pb.Marshal(&pb.SatStreamID{
   535  		CreationDate: creationDate,
   536  	})
   537  	if err != nil {
   538  		panic(err)
   539  	}
   540  	return storj.StreamID(streamID)
   541  }
   542  
   543  func segmentsPlainSize(numSegments int) int64 {
   544  	var plainSize int64
   545  	for i := 0; i < numSegments; i++ {
   546  		plainSize += segmentPlainSize(i)
   547  	}
   548  	return plainSize
   549  }
   550  
   551  func segmentPlainSize(index int) int64 {
   552  	return int64(index)
   553  }
   554  
   555  func encryptETag(index int) []byte {
   556  	return []byte(fmt.Sprintf("%s-%d", string(eTag), index))
   557  }
   558  
   559  func encryptMetadata(lastSegmentSize int64) []byte {
   560  	return []byte(fmt.Sprintf("ENCRYPTED-METADATA-%d", lastSegmentSize))
   561  }