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 }