fix: retain maintenance read compatibility with write safety fixes

Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-10-25 18:39:31 +08:00
parent 9449e6a4a8
commit 2a04d5f485
18 changed files with 639 additions and 360 deletions
+8 -62
View File
@@ -493,10 +493,6 @@ type poolErrs struct {
}
func (z *erasureServerPools) getPoolInfoExistingWithOpts(ctx context.Context, bucket, object string, opts ObjectOptions) (PoolObjInfo, []poolErrs, error) {
return z.getPoolInfoExisting(ctx, bucket, object, opts, false)
}
func (z *erasureServerPools) getPoolInfoExisting(ctx context.Context, bucket, object string, opts ObjectOptions, forWrite bool) (PoolObjInfo, []poolErrs, error) {
var noReadQuorumPools []poolErrs
poolObjInfos := make([]PoolObjInfo, len(z.serverPools))
poolOpts := make([]ObjectOptions, len(z.serverPools))
@@ -524,27 +520,6 @@ func (z *erasureServerPools) getPoolInfoExisting(ctx context.Context, bucket, ob
}
wg.Wait()
if forWrite && !z.SinglePool() {
// Reuse the metadata already read for allocation. A healthy duplicate
// must not hide an unreadable pool that will veto the next GET.
recovery := -1
for i, info := range poolObjInfos {
if info.Err == nil || isErrObjectNotFound(info.Err) || isErrVersionNotFound(info.Err) {
continue
}
rq, inconsistent := info.Err.(InsufficientReadQuorum)
if !inconsistent || rq.Type != RQInconsistentMeta || recovery >= 0 || z.IsSuspended(i) || z.IsPoolRebalancing(i) {
return info, nil, info.Err
}
recovery = i
}
if recovery >= 0 {
// Repair the sole unreadable copy in place. One write cannot repair
// several pools, and draining pools cannot accept recovery writes.
return poolObjInfos[recovery], nil, nil
}
}
// Sort the objInfos such that we always serve latest
// this is a defensive change to handle any duplicate
// content that may have been created, we always serve
@@ -661,21 +636,20 @@ func (z *erasureServerPools) getPoolIdxNoLock(ctx context.Context, bucket, objec
// if none are found falls back to most available space pool, this function is
// designed to be only used by PutObject, CopyObject (newObject creation) and NewMultipartUpload.
func (z *erasureServerPools) getPoolIdx(ctx context.Context, bucket, object string, size int64) (idx int, err error) {
idx, _, err = z.getWritePoolIdx(ctx, bucket, object, size, false, false)
return idx, err
return z.getWritePoolIdx(ctx, bucket, object, size, false)
}
// getWritePoolIdx keeps the write-allocation policy when the caller already
// holds the pools-layer object lock and must not reacquire a set read lock.
func (z *erasureServerPools) getWritePoolIdx(ctx context.Context, bucket, object string, size int64, noLock, recoverWrite bool) (idx int, recovery bool, err error) {
pinfo, _, err := z.getPoolInfoExisting(ctx, bucket, object, ObjectOptions{
func (z *erasureServerPools) getWritePoolIdx(ctx context.Context, bucket, object string, size int64, noLock bool) (idx int, err error) {
pinfo, _, err := z.getPoolInfoExistingWithOpts(ctx, bucket, object, ObjectOptions{
NoLock: noLock,
SkipDecommissioned: true,
SkipRebalancing: true,
}, recoverWrite)
})
if err != nil && !isErrObjectNotFound(err) {
return -1, false, err
return -1, err
}
idx = pinfo.Index
@@ -683,11 +657,11 @@ func (z *erasureServerPools) getWritePoolIdx(ctx context.Context, bucket, object
// will generate a temp object
idx = z.getAvailablePoolIdx(ctx, bucket, object, size)
if idx < 0 {
return -1, false, toObjectErr(errDiskFull)
return -1, toObjectErr(errDiskFull)
}
}
return idx, recoverWrite && isErrReadQuorum(pinfo.Err), nil
return idx, nil
}
func (z *erasureServerPools) Shutdown(ctx context.Context) error {
@@ -1074,17 +1048,6 @@ func (z *erasureServerPools) getLatestObjectInfoWithIdx(ctx context.Context, buc
}
wg.Wait()
// An unreadable pool may contain a newer generation. Check every pool
// before choosing a readable copy; sorting first would hide errors behind
// an older object's nonzero ModTime. Delete markers are resolved metadata
// and still participate in the ordering below.
for _, res := range results {
if res.err != nil && !isErrObjectNotFound(res.err) && !isErrVersionNotFound(res.err) &&
(!res.oi.DeleteMarker || !isErrMethodNotAllowed(res.err)) {
return res.oi, res.zIdx, res.err
}
}
// Sort the objInfos such that we always serve latest
// this is a defensive change to handle any duplicate
// content that may have been created, we always serve
@@ -1229,8 +1192,7 @@ func (z *erasureServerPools) PutObject(ctx context.Context, bucket string, objec
opts.replicaObjectInfo = resolve
}
recoverWrite := !opts.ReplicationRequest && !opts.ReplicaLockReconcile && !opts.DataMovement
idx, recovery, err := z.getWritePoolIdx(ctx, bucket, object, data.Size(), true, recoverWrite)
idx, err := z.getWritePoolIdx(ctx, bucket, object, data.Size(), true)
if err != nil {
return ObjectInfo{}, err
}
@@ -1253,22 +1215,6 @@ func (z *erasureServerPools) PutObject(ctx context.Context, bucket string, objec
opts.replicaObjectInfo = z.replicaObjectInfo
}
oi, err := z.serverPools[idx].PutObject(ctx, bucket, object, data, opts)
if err == nil && recovery {
// Only recovery pays for a second read. A partial repair or a new
// unreadable pool must not be acknowledged as recovered. Keep any
// committed write on failure; a read failure is not permission to undo.
var latest ObjectInfo
var latestIdx int
latest, latestIdx, err = z.getLatestObjectInfoWithIdx(ctx, bucket, object, ObjectOptions{
NoLock: true,
NoAuditLog: true,
Versioned: opts.Versioned,
VersionSuspended: opts.VersionSuspended,
})
if err == nil && (latestIdx != idx || latest.VersionID != oi.VersionID || latest.ETag != oi.ETag || !latest.ModTime.Equal(oi.ModTime)) {
err = InsufficientReadQuorum{Bucket: bucket, Object: decodeDirObject(object), Err: errErasureReadQuorum, Type: RQInconsistentMeta}
}
}
if err == nil && opts.ReplicaLockReconcile && !opts.DataMovement {
err = z.retireReplicaCopies(ctx, bucket, object, idx, oi)
}