Files
minio/cmd/object-mutation.go
T
2026-10-25 13:02:23 +08:00

216 lines
6.6 KiB
Go

// Copyright (c) 2026 Feng Ruohang
// SPDX-License-Identifier: AGPL-3.0-or-later
package cmd
import (
"bytes"
"context"
"errors"
"maps"
"slices"
"strings"
"sync"
)
// This is an undo capability, not a distributed fencing token. Any subsequent
// mutation of the same version revokes it, including a no-op metadata update.
const rollbackIDKey = ReservedMetadataPrefixLower + "rollback-id"
// Bound memory independently of object count. Include the physical drive so
// shards of one object are not serialized across drives. Multiple paths are
// locked in slot order, including collisions, to avoid lock inversion.
var storageMutationLocks [65536]sync.Mutex
func (s *xlStorage) lockMutations(paths ...string) func() {
var slots [4]int
n := 0
for _, path := range paths {
h := uint64(14695981039346656037)
for _, part := range []string{s.drivePath, path} {
for i := range part {
h = (h ^ uint64(part[i])) * 1099511628211
}
h *= 1099511628211
}
slot := int(h % uint64(len(storageMutationLocks)))
if !slices.Contains(slots[:n], slot) {
slots[n] = slot
n++
}
}
slices.Sort(slots[:n])
for _, slot := range slots[:n] {
storageMutationLocks[slot].Lock()
}
return func() {
for i := n - 1; i >= 0; i-- {
storageMutationLocks[slots[i]].Unlock()
}
}
}
// Compare logical version state, excluding per-shard checksums/index, physical
// DataDir placement and properties of other versions in the same xl.meta.
// Different DataDir values remain readable for the existing rebalance recovery
// contract; rollback separately requires an exact DataDir match.
func sameFileInfoVersion(a, b FileInfo) bool {
if a.VersionID != b.VersionID || a.Deleted != b.Deleted || a.XLV1 != b.XLV1 || a.Size != b.Size ||
a.TransitionStatus != b.TransitionStatus || a.TransitionTier != b.TransitionTier ||
a.TransitionedObjName != b.TransitionedObjName || a.TransitionVersionID != b.TransitionVersionID ||
!sameVersionMetadata(a.Metadata, b.Metadata) || !bytes.Equal(a.Checksum, b.Checksum) || len(a.Parts) != len(b.Parts) {
return false
}
if !a.Deleted && a.Size != 0 && !a.Erasure.Equal(b.Erasure) {
return false
}
for i, p := range a.Parts {
q := b.Parts[i]
if p.Number != q.Number || p.Size != q.Size || p.ActualSize != q.ActualSize || p.ETag != q.ETag || !bytes.Equal(p.Index, q.Index) {
return false
}
}
return true
}
// The undo capability controls rollback ownership, not the logical contents.
// A partial no-op update can revoke it without changing a single public field.
func sameVersionMetadata(a, b map[string]string) bool {
if a[rollbackIDKey] == b[rollbackIDKey] {
return maps.Equal(a, b)
}
for k, v := range a {
if k != rollbackIDKey {
if other, ok := b[k]; !ok || other != v {
return false
}
}
}
for k := range b {
if k != rollbackIDKey {
if _, ok := a[k]; !ok {
return false
}
}
}
return true
}
// Legacy callers without an undo capability must match the persisted state,
// rather than transient coordinator-only fields in their original FileInfo.
func samePersistedVersion(current, expected FileInfo) bool {
if current.Metadata[rollbackIDKey] != expected.Metadata[rollbackIDKey] {
return false
}
var meta xlMetaV2
if err := meta.AddVersion(expected); err != nil {
return false
}
fi, err := meta.ToFileInfo(current.Volume, current.Name, expected.VersionID, true, true)
return err == nil && sameFileInfoVersion(current, fi)
}
// Caller holds the object's mutation guard. Restore just the overwritten
// version, and persist that decision before removing any external shards.
func (s *xlStorage) undoWrite(ctx context.Context, volume, path string, fi FileInfo, opts DeleteOptions, meta *xlMetaV2) error {
versionID := fi.VersionID
if versionID == "" {
versionID = nullVersionID
}
current, err := meta.ToFileInfo(volume, path, versionID, true, true)
if err != nil {
return err
}
if current.DataDir != fi.DataDir || !current.ModTime.Equal(fi.ModTime) || !samePersistedVersion(current, fi) {
return errFileVersionNotFound
}
var previous xlMetaV2
var oldVersion *xlMetaV2Version
var oldData []byte
volumeDir, err := s.getVolDir(volume)
if err != nil {
return err
}
if opts.OldDataDir != "" {
buf, err := s.readAllData(ctx, volume, volumeDir, pathJoin(volumeDir, path, opts.OldDataDir, xlStorageFormatFileBackup))
if err != nil {
return err
}
defer metaDataPoolPut(buf)
if err := previous.LoadOrConvert(buf); err != nil {
return err
}
_, oldVersion, err = previous.findVersionStr(versionID)
if err != nil {
return err
}
oldData = previous.data.find(versionID)
if oldData == nil {
oldData = previous.data.find(oldVersion.getDataDir())
}
}
dataDir, err := meta.DeleteVersion(fi)
if err != nil {
return err
}
if dataDir != "" {
meta.data.remove(versionID, dataDir)
}
if oldVersion != nil {
if err := meta.addVersion(*oldVersion); err != nil {
return err
}
if oldData != nil {
meta.data.replace(versionID, oldData)
}
if dataDir == oldVersion.getDataDir() {
dataDir = ""
}
}
if len(meta.versions) > 0 {
buf, err := meta.AppendTo(metaDataPoolGet())
defer metaDataPoolPut(buf)
if err != nil {
return err
}
if err := s.writeAllMeta(ctx, volume, pathJoin(path, xlStorageFormatFile), buf, true); err != nil {
return err
}
} else if err := s.deleteFile(volumeDir, pathJoin(volumeDir, path, xlStorageFormatFile), true, false); err != nil {
return err
}
if dataDir != "" {
if err := s.moveToTrash(pathJoin(volumeDir, path, dataDir), true, false); err != nil && !errors.Is(err, errFileNotFound) {
return err
}
}
if opts.OldDataDir != "" {
backupDir := pathJoin(volumeDir, path, opts.OldDataDir)
Remove(pathJoin(backupDir, xlStorageFormatFileBackup))
if strings.HasSuffix(opts.OldDataDir, "-rollback") {
Remove(backupDir)
}
}
return nil
}
// All drives have replied, and fileInfoFromRaw has already validated each raw
// buffer and selected its latest visible version. Reuse that decoded state:
// revalidating the raw history here adds work proportional to version count.
// Any missing response, logical disagreement, or history-property disagreement
// retains the full merge. Legacy objects and delete markers also use that path.
func uniformLatestVersion(meta []FileInfo, errs []error) bool {
if len(meta) == 0 {
return false
}
first := meta[0]
for i, fi := range meta {
if errs[i] != nil || !fi.IsValid() || fi.XLV1 || fi.Deleted || !fi.IsLatest || fi.NumVersions < 1 ||
!fi.ModTime.Equal(first.ModTime) || !sameFileInfoVersion(first, fi) ||
fi.NumVersions != first.NumVersions || !fi.SuccessorModTime.Equal(first.SuccessorModTime) {
return false
}
}
return true
}