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 }