|
1 | 1 | package transfermanager |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "bufio" |
4 | 5 | "bytes" |
5 | 6 | "context" |
6 | 7 | "errors" |
@@ -424,3 +425,92 @@ func TestConcurrentReaderPartMemoryThrottleNoDeadlock(t *testing.T) { |
424 | 425 | t.Fatalf("expect %d bytes equal to assembled parts, got %d", len(expect), len(got)) |
425 | 426 | } |
426 | 427 | } |
| 428 | + |
| 429 | +// TestConcurrentReaderSmallReadsNoDeadlock guards against the parts-mode |
| 430 | +// deadlock (#3552) where partRead's replay loop returned as soon as the |
| 431 | +// caller's buffer was full, without draining r.ch. When every Read call is |
| 432 | +// satisfied entirely from previously buffered parts (small, part-size-aligned |
| 433 | +// reads via bufio.Reader are the common trigger), the receive loop that drains |
| 434 | +// r.ch never runs, so once enough already-downloaded parts pile up to fill |
| 435 | +// r.ch, a download producer blocks forever on its send while Read's |
| 436 | +// r.wg.Wait() blocks forever waiting for it. It reproduces the trigger |
| 437 | +// conditions from the issue: GetObjectParts, partsCount comfortably exceeding |
| 438 | +// sectionParts+Concurrency+1, and reads smaller than and evenly dividing the |
| 439 | +// part size. |
| 440 | +func TestConcurrentReaderSmallReadsNoDeadlock(t *testing.T) { |
| 441 | + const ( |
| 442 | + partSize = 1 << 10 // 1KiB parts, mirrors default 8MiB in proportion |
| 443 | + bufferThreshold = 6 * partSize |
| 444 | + sectionParts = 6 // bufferThreshold / partSize, matches real GetObject wiring |
| 445 | + concurrency = 5 |
| 446 | + partsCount = sectionParts + concurrency + 4 // > sectionParts+Concurrency+1 |
| 447 | + readSize = 128 // divides partSize evenly, smaller than partSize |
| 448 | + ) |
| 449 | + |
| 450 | + s3Client := &s3testing.TransferManagerLoggingClient{} |
| 451 | + s3Client.GetObjectFn = s3testing.ReaderPartGetObjectFn |
| 452 | + s3Client.PartsCount = partsCount |
| 453 | + |
| 454 | + var expect []byte |
| 455 | + partsData := make([][]byte, partsCount) |
| 456 | + for i := int32(0); i < partsCount; i++ { |
| 457 | + b := bytes.Repeat([]byte{byte('A' + i%26)}, partSize) |
| 458 | + expect = append(expect, b...) |
| 459 | + partsData[i] = b |
| 460 | + } |
| 461 | + s3Client.Data = expect |
| 462 | + s3Client.PartsData = partsData |
| 463 | + |
| 464 | + r := &concurrentReader{ |
| 465 | + partSize: partSize, |
| 466 | + partsCount: partsCount, |
| 467 | + sectionParts: sectionParts, |
| 468 | + getType: types.GetObjectParts, |
| 469 | + bufferThreshold: bufferThreshold, |
| 470 | + options: Options{ |
| 471 | + GetObjectType: types.GetObjectParts, |
| 472 | + Concurrency: concurrency, |
| 473 | + S3: s3Client, |
| 474 | + }, |
| 475 | + in: &GetObjectInput{Bucket: aws.String("bucket"), Key: aws.String("key")}, |
| 476 | + capacity: sectionParts, |
| 477 | + buf: make(map[int32]*outChunk), |
| 478 | + ctx: context.Background(), |
| 479 | + ch: make(chan outChunk, concurrency), |
| 480 | + totalBytes: int64(len(expect)), |
| 481 | + } |
| 482 | + |
| 483 | + done := make(chan error, 1) |
| 484 | + var got []byte |
| 485 | + go func() { |
| 486 | + br := bufio.NewReaderSize(r, readSize) |
| 487 | + buf := make([]byte, readSize) |
| 488 | + for { |
| 489 | + n, err := br.Read(buf) |
| 490 | + got = append(got, buf[:n]...) |
| 491 | + if err != nil { |
| 492 | + if err == io.EOF { |
| 493 | + err = nil |
| 494 | + } |
| 495 | + done <- err |
| 496 | + return |
| 497 | + } |
| 498 | + } |
| 499 | + }() |
| 500 | + |
| 501 | + select { |
| 502 | + case err := <-done: |
| 503 | + if err != nil { |
| 504 | + t.Fatalf("read error: %v", err) |
| 505 | + } |
| 506 | + case <-time.After(15 * time.Second): |
| 507 | + t.Fatal("DEADLOCK: Read did not complete within 15s (GH #3552)") |
| 508 | + } |
| 509 | + |
| 510 | + if e, a := len(expect), len(got); e != a { |
| 511 | + t.Fatalf("expect %d bytes, got %d", e, a) |
| 512 | + } |
| 513 | + if !bytes.Equal(expect, got) { |
| 514 | + t.Fatalf("expect downloaded stream to equal assembled parts") |
| 515 | + } |
| 516 | +} |
0 commit comments