Skip to content

Commit 126fe2d

Browse files
authored
fix GetObject deadlock (#3554)
1 parent 5126885 commit 126fe2d

3 files changed

Lines changed: 102 additions & 1 deletion

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "000726b8-03cf-4e08-bece-acae34e7768d",
3+
"type": "bugfix",
4+
"description": "Fix potential deadlock in GetObject calls.",
5+
"modules": [
6+
"feature/s3/transfermanager"
7+
]
8+
}

‎feature/s3/transfermanager/concurrent_reader.go‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,10 @@ func (r *concurrentReader) partRead(p []byte) (int, error) {
225225
}
226226

227227
if written >= cap(p) {
228-
return written, nil
228+
// don't return yet, parts already dispatched up to capacity may
229+
// still be downloading or sitting in r.ch, and the receive loop
230+
// below is what drains them
231+
break
229232
}
230233

231234
c, ok := r.buf[i]

‎feature/s3/transfermanager/concurrent_reader_test.go‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package transfermanager
22

33
import (
4+
"bufio"
45
"bytes"
56
"context"
67
"errors"
@@ -424,3 +425,92 @@ func TestConcurrentReaderPartMemoryThrottleNoDeadlock(t *testing.T) {
424425
t.Fatalf("expect %d bytes equal to assembled parts, got %d", len(expect), len(got))
425426
}
426427
}
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

Comments
 (0)