Skip to content

Commit e6abd15

Browse files
fix: broken get and replace for s3 client (#21163)
1 parent a5cce91 commit e6abd15

1 file changed

Lines changed: 44 additions & 0 deletions

File tree

‎pkg/storage/bucket/client.go‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package bucket
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
67
"flag"
78
"fmt"
9+
"io"
810
"net/http"
911
"regexp"
1012
"strconv"
@@ -236,6 +238,8 @@ func NewClient(ctx context.Context, backend string, cfg Config, name string, log
236238
client = NewPrefixedBucketClient(client, cfg.StoragePrefix)
237239
}
238240

241+
client = newSizedGetAndReplaceBucket(client)
242+
239243
instrumentedClient := objstoretracing.WrapWithTraces(objstore.WrapWith(client, metrics))
240244

241245
// Wrap the client with any provided middleware
@@ -274,3 +278,43 @@ func (i *instrumentedRoundTripper) RoundTrip(req *http.Request) (*http.Response,
274278

275279
return resp, nil
276280
}
281+
282+
// sizedGetAndReplaceBucket wraps a Bucket and guarantees that the io.ReadCloser
283+
// returned by GetAndReplace callbacks always has a size known to
284+
// objstore.TryToGetSize. Without this, callers that return opaque ReadClosers
285+
// (such as io.NopCloser-wrapped readers) cause the S3 backend to fall into the
286+
// putObjectMultipartStreamNoLength code path in minio-go, which reconstructs
287+
// PutObjectOptions before CompleteMultipartUpload and silently drops
288+
// customHeaders — including the If-Match conditional write header set by
289+
// GetAndReplace. The result is that stale writes are accepted by S3 even when
290+
// the object's ETag has already changed, breaking the optimistic-concurrency
291+
// guarantee that GetAndReplace is supposed to provide.
292+
//
293+
// The fix buffers the reader when its size cannot be determined upfront.
294+
// GetAndReplace is only used for small metadata objects (e.g. ToC files), so
295+
// the buffering cost is negligible.
296+
type sizedGetAndReplaceBucket struct {
297+
objstore.Bucket
298+
}
299+
300+
func newSizedGetAndReplaceBucket(bkt objstore.Bucket) objstore.Bucket {
301+
return &sizedGetAndReplaceBucket{Bucket: bkt}
302+
}
303+
304+
func (b *sizedGetAndReplaceBucket) GetAndReplace(ctx context.Context, name string, fn func(io.ReadCloser) (io.ReadCloser, error)) error {
305+
return b.Bucket.GetAndReplace(ctx, name, func(existing io.ReadCloser) (io.ReadCloser, error) {
306+
rc, err := fn(existing)
307+
if err != nil || rc == nil {
308+
return rc, err
309+
}
310+
if _, sizeErr := objstore.TryToGetSize(rc); sizeErr == nil {
311+
return rc, nil
312+
}
313+
data, readErr := io.ReadAll(rc)
314+
_ = rc.Close()
315+
if readErr != nil {
316+
return nil, readErr
317+
}
318+
return objstore.NopCloserWithSize(bytes.NewReader(data)), nil
319+
})
320+
}

0 commit comments

Comments
 (0)