@@ -32,7 +32,9 @@ type RangeReader interface {
3232// is likewise reported as an error, so a partially written dst must be discarded
3333// by the caller on failure. An object with no ETag to pin to (e.g. one stored
3434// before ETags were recorded) cannot be kept revision-safe across chunks, so it
35- // falls back to a single full read instead of parallelising.
35+ // falls back to a single full read instead of parallelising. A concurrency of
36+ // 1 likewise reads the whole object in one request, since chunking a single
37+ // worker would only serialise ranged GETs for no benefit.
3638//
3739// dst is written via concurrent WriteAt calls at non-overlapping offsets; the
3840// caller owns dst's lifecycle (open, close, cleanup) and need not pre-size it,
@@ -43,6 +45,13 @@ func ParallelGet(ctx context.Context, c RangeReader, key Key, dst io.WriterAt, c
4345 }
4446 concurrency = max (concurrency , 1 )
4547
48+ // A single worker gains nothing from chunking — it would only serialise
49+ // ranged GETs — so skip discovery entirely and read the object in one
50+ // revision-consistent request.
51+ if concurrency == 1 {
52+ return fullRead (ctx , c , key , dst )
53+ }
54+
4655 // Discovery: the first ranged Open delivers chunk zero and reveals the total
4756 // size and ETag used to pin the rest.
4857 rc , headers , err := c .Open (ctx , key , Range (0 , chunkSize ))
@@ -77,14 +86,7 @@ func ParallelGet(ctx context.Context, c RangeReader, key Key, dst io.WriterAt, c
7786 if err := rc .Close (); err != nil {
7887 return errors .Wrap (err , "parallel get: close discovery reader" )
7988 }
80- full , _ , err := c .Open (ctx , key )
81- if err != nil {
82- return errors .Wrap (err , "parallel get: full read" )
83- }
84- // The full read is a fresh request whose body may be a different
85- // revision than discovery, so the discovery `total` cannot validate its
86- // length; -1 skips the check and relies on transport-level EOF detection.
87- return errors .Wrap (writeChunkAt (dst , 0 , - 1 , full ), "parallel get" )
89+ return fullRead (ctx , c , key , dst )
8890 }
8991
9092 // Multiple chunks: copy the already-open first chunk concurrently with the
@@ -106,6 +108,19 @@ func ParallelGet(ctx context.Context, c RangeReader, key Key, dst io.WriterAt, c
106108 return errors .Wrap (eg .Wait (), "parallel get" )
107109}
108110
111+ // fullRead downloads the entire object in a single request and writes it at
112+ // offset zero. It is used when chunking would add no value (a single worker) or
113+ // cannot be made revision-safe (no ETag to pin). The body is a single
114+ // consistent revision, but its length is unknown up front, so writeChunkAt's
115+ // length check is skipped (-1).
116+ func fullRead (ctx context.Context , c RangeReader , key Key , dst io.WriterAt ) error {
117+ rc , _ , err := c .Open (ctx , key )
118+ if err != nil {
119+ return errors .Wrap (err , "parallel get: full read" )
120+ }
121+ return errors .Wrap (writeChunkAt (dst , 0 , - 1 , rc ), "parallel get" )
122+ }
123+
109124// fetchChunk opens the [start, end) range pinned to etag and writes it at start.
110125// An ETag change (the object was rewritten mid-download) or a short read is
111126// reported as an error.
0 commit comments