|
1 | 1 | package bucket |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "bytes" |
4 | 5 | "context" |
5 | 6 | "errors" |
6 | 7 | "flag" |
7 | 8 | "fmt" |
| 9 | + "io" |
8 | 10 | "net/http" |
9 | 11 | "regexp" |
10 | 12 | "strconv" |
@@ -236,6 +238,8 @@ func NewClient(ctx context.Context, backend string, cfg Config, name string, log |
236 | 238 | client = NewPrefixedBucketClient(client, cfg.StoragePrefix) |
237 | 239 | } |
238 | 240 |
|
| 241 | + client = newSizedGetAndReplaceBucket(client) |
| 242 | + |
239 | 243 | instrumentedClient := objstoretracing.WrapWithTraces(objstore.WrapWith(client, metrics)) |
240 | 244 |
|
241 | 245 | // Wrap the client with any provided middleware |
@@ -274,3 +278,43 @@ func (i *instrumentedRoundTripper) RoundTrip(req *http.Request) (*http.Response, |
274 | 278 |
|
275 | 279 | return resp, nil |
276 | 280 | } |
| 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