storj.io/uplink@v1.13.0/private/storage/streams/streambatcher/batcher_test.go (about)

     1  // Copyright (C) 2023 Storj Labs, Inc.
     2  // See LICENSE for copying information.
     3  
     4  package streambatcher
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"strconv"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  	"github.com/zeebo/errs"
    16  
    17  	"storj.io/common/pb"
    18  	"storj.io/common/storj"
    19  	"storj.io/uplink/private/metaclient"
    20  )
    21  
    22  var (
    23  	now           = time.Now().UTC().Truncate(time.Second)
    24  	creationDate1 = now.Add(1 * time.Hour)
    25  	creationDate2 = now.Add(2 * time.Hour)
    26  	streamID1     = makeStreamID(creationDate1)
    27  	streamID2     = makeStreamID(creationDate2)
    28  )
    29  
    30  func TestBatcher(t *testing.T) {
    31  	type batchRun struct {
    32  		items          []metaclient.BatchItem
    33  		responses      []*pb.BatchResponseItem
    34  		err            error
    35  		expectStreamID storj.StreamID
    36  		expectItems    []metaclient.BatchItem
    37  		expectBatchErr string
    38  		expectInfo     Info
    39  	}
    40  
    41  	for _, tc := range []struct {
    42  		desc     string
    43  		streamID storj.StreamID
    44  		runs     []batchRun
    45  	}{
    46  		{
    47  			desc: "batch fails when request missing batch request items",
    48  			runs: []batchRun{
    49  				{
    50  					expectBatchErr: "programmer error: empty batch request",
    51  				},
    52  			},
    53  		},
    54  		{
    55  			desc: "batch fails when response missing batch response items",
    56  			runs: []batchRun{
    57  				{
    58  					items: []metaclient.BatchItem{
    59  						&metaclient.BeginObjectParams{},
    60  					},
    61  					expectBatchErr: "programmer error: empty batch response",
    62  				},
    63  			},
    64  		},
    65  		{
    66  			desc: "batch fails when metainfo client fails",
    67  			runs: []batchRun{
    68  				{
    69  					err: errors.New("oh no"),
    70  					items: []metaclient.BatchItem{
    71  						&metaclient.BeginObjectParams{},
    72  					},
    73  					expectBatchErr: "oh no",
    74  				},
    75  			},
    76  		},
    77  		{
    78  			desc: "batch fails when first batch does not contain a BeginObject call for object upload",
    79  			runs: []batchRun{
    80  				{
    81  					items: []metaclient.BatchItem{
    82  						&metaclient.MakeInlineSegmentParams{PlainSize: 123},
    83  					},
    84  					responses: []*pb.BatchResponseItem{
    85  						{Response: &pb.BatchResponseItem_SegmentMakeInline{SegmentMakeInline: &pb.MakeInlineSegmentResponse{}}},
    86  					},
    87  					expectBatchErr: "programmer error: first batch must start with BeginObject: invalid response type",
    88  				},
    89  			},
    90  		},
    91  		{
    92  			desc: "batch fails when stream ID is not provided by BeginObject response for object upload",
    93  			runs: []batchRun{
    94  				{
    95  					items: []metaclient.BatchItem{
    96  						&metaclient.BeginObjectParams{},
    97  					},
    98  					responses: []*pb.BatchResponseItem{
    99  						{Response: &pb.BatchResponseItem_ObjectBegin{ObjectBegin: &pb.BeginObjectResponse{}}},
   100  					},
   101  					expectBatchErr: "stream ID missing from BeginObject response",
   102  				},
   103  			},
   104  		},
   105  		{
   106  			desc: "upload object with single inline segment",
   107  			runs: []batchRun{
   108  				{
   109  					items: []metaclient.BatchItem{
   110  						&metaclient.BeginObjectParams{},
   111  						&metaclient.MakeInlineSegmentParams{PlainSize: 123},
   112  						&metaclient.CommitObjectParams{},
   113  					},
   114  					expectItems: []metaclient.BatchItem{
   115  						&metaclient.BeginObjectParams{},
   116  						&metaclient.MakeInlineSegmentParams{PlainSize: 123},
   117  						&metaclient.CommitObjectParams{},
   118  					},
   119  					responses: []*pb.BatchResponseItem{
   120  						{Response: &pb.BatchResponseItem_ObjectBegin{ObjectBegin: &pb.BeginObjectResponse{StreamId: streamID1}}},
   121  						{Response: &pb.BatchResponseItem_SegmentMakeInline{SegmentMakeInline: &pb.MakeInlineSegmentResponse{}}},
   122  						{Response: &pb.BatchResponseItem_ObjectCommit{ObjectCommit: &pb.CommitObjectResponse{Object: &pb.Object{
   123  							CreatedAt: creationDate1,
   124  							PlainSize: 123,
   125  						}}}},
   126  					},
   127  					expectStreamID: streamID1,
   128  					expectInfo:     Info{PlainSize: 123, CreationDate: creationDate1},
   129  				},
   130  			},
   131  		},
   132  		{
   133  			desc: "upload object with a remote segment and inline segment",
   134  			runs: []batchRun{
   135  				{
   136  					items: []metaclient.BatchItem{
   137  						&metaclient.BeginObjectParams{},
   138  						&metaclient.BeginSegmentParams{},
   139  					},
   140  					expectItems: []metaclient.BatchItem{
   141  						&metaclient.BeginObjectParams{},
   142  						&metaclient.BeginSegmentParams{},
   143  					},
   144  					responses: []*pb.BatchResponseItem{
   145  						{Response: &pb.BatchResponseItem_ObjectBegin{ObjectBegin: &pb.BeginObjectResponse{StreamId: streamID1}}},
   146  						{Response: &pb.BatchResponseItem_SegmentBegin{SegmentBegin: &pb.BeginSegmentResponse{}}},
   147  					},
   148  					expectStreamID: streamID1,
   149  					expectInfo:     Info{PlainSize: 0},
   150  				},
   151  				{
   152  					items: []metaclient.BatchItem{
   153  						&metaclient.CommitSegmentParams{PlainSize: 123},
   154  					},
   155  					expectItems: []metaclient.BatchItem{
   156  						&metaclient.CommitSegmentParams{PlainSize: 123},
   157  					},
   158  					responses: []*pb.BatchResponseItem{
   159  						{Response: &pb.BatchResponseItem_SegmentCommit{SegmentCommit: &pb.CommitSegmentResponse{}}},
   160  					},
   161  					expectStreamID: streamID1,
   162  					expectInfo:     Info{PlainSize: 123},
   163  				},
   164  				{
   165  					items: []metaclient.BatchItem{
   166  						&metaclient.MakeInlineSegmentParams{PlainSize: 321},
   167  					},
   168  					expectItems: []metaclient.BatchItem{
   169  						&metaclient.MakeInlineSegmentParams{PlainSize: 321, StreamID: streamID1},
   170  					},
   171  					responses: []*pb.BatchResponseItem{
   172  						{Response: &pb.BatchResponseItem_SegmentMakeInline{SegmentMakeInline: &pb.MakeInlineSegmentResponse{}}},
   173  					},
   174  					expectStreamID: streamID1,
   175  					expectInfo:     Info{PlainSize: 444},
   176  				},
   177  			},
   178  		},
   179  		{
   180  			desc:     "upload part with single inline segment",
   181  			streamID: streamID2,
   182  			runs: []batchRun{
   183  				{
   184  					items: []metaclient.BatchItem{
   185  						&metaclient.MakeInlineSegmentParams{PlainSize: 123, StreamID: streamID2},
   186  					},
   187  					expectItems: []metaclient.BatchItem{
   188  						&metaclient.MakeInlineSegmentParams{PlainSize: 123, StreamID: streamID2},
   189  					},
   190  					responses: []*pb.BatchResponseItem{
   191  						{Response: &pb.BatchResponseItem_SegmentMakeInline{SegmentMakeInline: &pb.MakeInlineSegmentResponse{}}},
   192  					},
   193  					expectStreamID: streamID2,
   194  					expectInfo:     Info{PlainSize: 123},
   195  				},
   196  			},
   197  		},
   198  		{
   199  			desc:     "upload part with a remote segment and inline segment",
   200  			streamID: streamID2,
   201  			runs: []batchRun{
   202  				{
   203  					items: []metaclient.BatchItem{
   204  						&metaclient.BeginSegmentParams{StreamID: streamID2},
   205  					},
   206  					expectItems: []metaclient.BatchItem{
   207  						&metaclient.BeginSegmentParams{StreamID: streamID2},
   208  					},
   209  					responses: []*pb.BatchResponseItem{
   210  						{Response: &pb.BatchResponseItem_SegmentBegin{SegmentBegin: &pb.BeginSegmentResponse{}}},
   211  					},
   212  					expectStreamID: streamID2,
   213  					expectInfo:     Info{PlainSize: 0},
   214  				},
   215  				{
   216  					items: []metaclient.BatchItem{
   217  						&metaclient.CommitSegmentParams{PlainSize: 123},
   218  					},
   219  					expectItems: []metaclient.BatchItem{
   220  						&metaclient.CommitSegmentParams{PlainSize: 123},
   221  					},
   222  					responses: []*pb.BatchResponseItem{
   223  						{Response: &pb.BatchResponseItem_SegmentCommit{SegmentCommit: &pb.CommitSegmentResponse{}}},
   224  					},
   225  					expectStreamID: streamID2,
   226  					expectInfo:     Info{PlainSize: 123},
   227  				},
   228  				{
   229  					items: []metaclient.BatchItem{
   230  						&metaclient.MakeInlineSegmentParams{PlainSize: 321, StreamID: streamID2},
   231  					},
   232  					expectItems: []metaclient.BatchItem{
   233  						&metaclient.MakeInlineSegmentParams{PlainSize: 321, StreamID: streamID2},
   234  					},
   235  					responses: []*pb.BatchResponseItem{
   236  						{Response: &pb.BatchResponseItem_SegmentMakeInline{SegmentMakeInline: &pb.MakeInlineSegmentResponse{}}},
   237  					},
   238  					expectStreamID: streamID2,
   239  					expectInfo:     Info{PlainSize: 444},
   240  				},
   241  			},
   242  		},
   243  	} {
   244  		t.Run(tc.desc, func(t *testing.T) {
   245  			miBatcher := new(fakeMetainfoBatcher)
   246  			streamBatcher := New(miBatcher, tc.streamID)
   247  
   248  			require.Equal(t, tc.streamID, streamBatcher.StreamID())
   249  
   250  			for i, run := range tc.runs {
   251  				t.Run(strconv.Itoa(i), func(t *testing.T) {
   252  					miBatcher.err = run.err
   253  					miBatcher.items = nil
   254  					miBatcher.responses = run.responses
   255  
   256  					responses, err := streamBatcher.Batch(context.Background(), run.items...)
   257  					if run.expectBatchErr != "" {
   258  						require.EqualError(t, err, run.expectBatchErr)
   259  						return
   260  					}
   261  					require.NoError(t, err)
   262  
   263  					info, err := streamBatcher.Info()
   264  					require.NoError(t, err)
   265  
   266  					assert.Equal(t, run.expectStreamID, streamBatcher.StreamID(), "unexpected stream ID tracked by the stream batcher")
   267  					assert.Len(t, responses, len(run.items), "stream batcher returned unexpected response count")
   268  					assert.Equal(t, run.expectItems, miBatcher.items, "metainfo batcher received unexpected items")
   269  					assert.Equal(t, run.expectInfo, info)
   270  				})
   271  			}
   272  		})
   273  	}
   274  }
   275  
   276  func TestStreamBatcherInfoReturnsErrorIfNoStreamIDHasBeenSet(t *testing.T) {
   277  	streamBatcher := New(new(fakeMetainfoBatcher), nil)
   278  	_, err := streamBatcher.Info()
   279  	require.EqualError(t, err, "stream ID is unexpectedly nil")
   280  }
   281  
   282  type fakeMetainfoBatcher struct {
   283  	items     []metaclient.BatchItem
   284  	responses []*pb.BatchResponseItem
   285  	err       error
   286  }
   287  
   288  func (mi *fakeMetainfoBatcher) Batch(ctx context.Context, items ...metaclient.BatchItem) ([]metaclient.BatchResponse, error) {
   289  	if len(items) == 0 {
   290  		return nil, errs.New("test/programmer error: batch should never be issued with no items")
   291  	}
   292  	mi.items = items
   293  	if mi.err != nil {
   294  		return nil, mi.err
   295  	}
   296  	var responses []metaclient.BatchResponse
   297  	for i := 0; i < len(mi.responses); i++ {
   298  		item := items[i]
   299  		response := mi.responses[i]
   300  		responses = append(responses, metaclient.MakeBatchResponse(item.BatchItem(), response))
   301  	}
   302  	return responses, nil
   303  }
   304  
   305  func makeStreamID(creationDate time.Time) storj.StreamID {
   306  	streamID, _ := pb.Marshal(&pb.SatStreamID{CreationDate: creationDate})
   307  	return storj.StreamID(streamID)
   308  }