fix: inline data upon overwrites should be readable (#12369)

This PR fixes two bugs

- Remove fi.Data upon overwrite of objects from inlined-data to non-inlined-data
- Workaround for an existing bug on disk with latest releases to ignore fi.Data
  and instead read from the disk for non-inlined-data
- Addtionally add a reserved metadata header to indicate data is inlined for
  a given version.
This commit is contained in:
Harshavardhana 2021-05-25 16:33:06 -07:00 committed by GitHub
parent 4fd1378242
commit 4840974d7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 60 additions and 56 deletions

View file

@ -520,6 +520,7 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
if partsMetadata[i].IsRemote() {
partsMetadata[i].DataDir = ""
}
// Attempt a rename now from healed data to final location.
if err = disk.RenameData(ctx, minioMetaTmpBucket, tmpID, partsMetadata[i], bucket, object); err != nil {
logger.LogIf(ctx, err)
@ -895,7 +896,6 @@ func (er erasureObjects) HealObject(ctx context.Context, bucket, object, version
// Read metadata files from all the disks
partsMetadata, errs := readAllFileInfo(healCtx, storageDisks, bucket, object, versionID, false)
if isAllNotFound(errs) {
err = toObjectErr(errFileNotFound, bucket, object)
if versionID != "" {

View file

@ -214,37 +214,6 @@ func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object stri
return fn(pr, h, opts.CheckPrecondFn, pipeCloser, nsUnlocker)
}
// GetObject - reads an object erasured coded across multiple
// disks. Supports additional parameters like offset and length
// which are synonymous with HTTP Range requests.
//
// startOffset indicates the starting read location of the object.
// length indicates the total length of the object.
func (er erasureObjects) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string, opts ObjectOptions) (err error) {
// Lock the object before reading.
lk := er.NewNSLock(bucket, object)
lkctx, err := lk.GetRLock(ctx, globalOperationTimeout)
if err != nil {
return err
}
ctx = lkctx.Context()
defer lk.RUnlock(lkctx.Cancel)
// Start offset cannot be negative.
if startOffset < 0 {
logger.LogIf(ctx, errUnexpected, logger.Application)
return errUnexpected
}
// Writer cannot be nil.
if writer == nil {
logger.LogIf(ctx, errUnexpected)
return errUnexpected
}
return er.getObject(ctx, bucket, object, startOffset, length, writer, opts)
}
func (er erasureObjects) getObjectWithFileInfo(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, fi FileInfo, metaArr []FileInfo, onlineDisks []StorageAPI) error {
// Reorder online disks based on erasure distribution order.
// Reorder parts metadata based on erasure distribution order.
@ -284,6 +253,27 @@ func (er erasureObjects) getObjectWithFileInfo(ctx context.Context, bucket, obje
if err != nil {
return toObjectErr(err, bucket, object)
}
// This hack is needed to avoid a bug found when overwriting
// the inline data object with a un-inlined version, for the
// time being we need this as we have released inline-data
// version already and this bug is already present in newer
// releases.
//
// This mainly happens with objects < smallFileThreshold when
// they are overwritten with un-inlined objects >= smallFileThreshold,
// due to a bug in RenameData() the fi.Data is not niled leading to
// GetObject thinking that fi.Data is valid while fi.Size has
// changed already.
if _, ok := fi.Metadata[ReservedMetadataPrefixLower+"inline-data"]; ok {
shardFileSize := erasure.ShardFileSize(fi.Size)
if shardFileSize >= 0 && shardFileSize >= smallFileThreshold {
for i := range metaArr {
metaArr[i].Data = nil
}
}
}
var healOnce sync.Once
// once we have obtained a common FileInfo i.e latest, we should stick
@ -373,23 +363,6 @@ func (er erasureObjects) getObjectWithFileInfo(ctx context.Context, bucket, obje
return nil
}
// getObject wrapper for erasure GetObject
func (er erasureObjects) getObject(ctx context.Context, bucket, object string, startOffset, length int64, writer io.Writer, opts ObjectOptions) error {
fi, metaArr, onlineDisks, err := er.getObjectFileInfo(ctx, bucket, object, opts, true)
if err != nil {
return toObjectErr(err, bucket, object)
}
if fi.Deleted {
if opts.VersionID == "" {
return toObjectErr(errFileNotFound, bucket, object)
}
// Make sure to return object info to provide extra information.
return toObjectErr(errMethodNotAllowed, bucket, object)
}
return er.getObjectWithFileInfo(ctx, bucket, object, startOffset, length, writer, fi, metaArr, onlineDisks)
}
// GetObjectInfo - reads object metadata and replies back ObjectInfo.
func (er erasureObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (info ObjectInfo, err error) {
if !opts.NoLock {
@ -797,6 +770,13 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
partsMetadata[index].ModTime = modTime
}
if len(inlineBuffers) > 0 {
// Set an additional header when data is inlined.
for index := range partsMetadata {
partsMetadata[index].Metadata[ReservedMetadataPrefixLower+"inline-data"] = "true"
}
}
// Rename the successfully written temporary object to final location.
if onlineDisks, err = renameData(ctx, onlineDisks, minioMetaTmpBucket, tempObj, partsMetadata, bucket, object, writeQuorum); err != nil {
logger.LogIf(ctx, err)

View file

@ -323,9 +323,18 @@ func TestGetObjectNoQuorum(t *testing.T) {
}
}
err = xl.GetObject(ctx, bucket, object, 0, int64(len(buf)), ioutil.Discard, "", opts)
if err != toObjectErr(errErasureReadQuorum, bucket, object) {
t.Errorf("Expected GetObject to fail with %v, but failed with %v", toObjectErr(errErasureReadQuorum, bucket, object), err)
gr, err := xl.GetObjectNInfo(ctx, bucket, object, nil, nil, readLock, opts)
if err != nil {
if err != toObjectErr(errErasureReadQuorum, bucket, object) {
t.Errorf("Expected GetObject to fail with %v, but failed with %v", toObjectErr(errErasureReadQuorum, bucket, object), err)
}
}
if gr != nil {
_, err = io.Copy(ioutil.Discard, gr)
if err != toObjectErr(errErasureReadQuorum, bucket, object) {
t.Errorf("Expected GetObject to fail with %v, but failed with %v", toObjectErr(errErasureReadQuorum, bucket, object), err)
}
gr.Close()
}
// Test use case 2: Make 9 disks offline, which leaves less than quorum number of disks
@ -359,9 +368,18 @@ func TestGetObjectNoQuorum(t *testing.T) {
}
z.serverPools[0].erasureDisksMu.Unlock()
// Fetch object from store.
err = xl.GetObject(ctx, bucket, object, 0, int64(len("abcd")), ioutil.Discard, "", opts)
if err != toObjectErr(errErasureReadQuorum, bucket, object) {
t.Errorf("Expected GetObject to fail with %v, but failed with %v", toObjectErr(errErasureWriteQuorum, bucket, object), err)
gr, err := xl.GetObjectNInfo(ctx, bucket, object, nil, nil, readLock, opts)
if err != nil {
if err != toObjectErr(errErasureReadQuorum, bucket, object) {
t.Errorf("Expected GetObject to fail with %v, but failed with %v", toObjectErr(errErasureReadQuorum, bucket, object), err)
}
}
if gr != nil {
_, err = io.Copy(ioutil.Discard, gr)
if err != toObjectErr(errErasureReadQuorum, bucket, object) {
t.Errorf("Expected GetObject to fail with %v, but failed with %v", toObjectErr(errErasureReadQuorum, bucket, object), err)
}
gr.Close()
}
}

View file

@ -18,7 +18,7 @@
package cmd
const (
storageRESTVersion = "v34" // Streaming Usage Updates
storageRESTVersion = "v35" // Inline bugfix needs all servers to be updated
storageRESTVersionPrefix = SlashSeparator + storageRESTVersion
storageRESTPrefix = minioReservedBucketPath + "/storage"
)

View file

@ -1076,6 +1076,11 @@ func (s *xlStorage) ReadVersion(ctx context.Context, volume, path, versionID str
if readData {
if len(fi.Data) > 0 || fi.Size == 0 {
if len(fi.Data) > 0 {
if _, ok := fi.Metadata[ReservedMetadataPrefixLower+"inline-data"]; !ok {
fi.Metadata[ReservedMetadataPrefixLower+"inline-data"] = "true"
}
}
return fi, nil
}
@ -2023,6 +2028,7 @@ func (s *xlStorage) RenameData(ctx context.Context, srcVolume, srcPath string, f
// Purge the destination path as we are not preserving anything
// versioned object was not requested.
oldDstDataPath = pathJoin(dstVolumeDir, dstPath, ofi.DataDir)
xlMeta.data.remove(nullVersionID, ofi.DataDir)
}
}
}