Files
silo/cmd/object-mutation.go
Feng Ruohang 39278e068e fix: isolate storage mutation guards by exact resource
Replace process-wide hash-slot mutexes with reference-counted locks keyed
by drive and path. Retain entries before waiting, acquire unique paths in
order, and reclaim entries only after all holders and waiters finish.

Add real storage API regressions for unrelated colliding objects and drives,
plus coverage for cancellation, multi-path order and waiter lifetimes.

Signed-off-by: Feng Ruohang <rh@vonng.com>
2026-10-26 21:11:49 +08:00

219 lines
6.5 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"
type storageMutationKey struct {
drive, path string
}
type storageMutationLock struct {
mu sync.Mutex
refs int // Holders and waiters; protected by the registry mutex.
}
// Keep exact resource locks only while they have holders or waiters. The
// registry mutex protects references, never storage I/O or resource waits.
var storageMutationLocks = struct {
sync.Mutex
locks map[storageMutationKey]*storageMutationLock
}{locks: make(map[storageMutationKey]*storageMutationLock)}
func (s *xlStorage) lockMutations(paths ...string) func() {
// Each call uses one drive. Sorting its unique paths gives all overlapping
// calls the same acquisition order without changing the caller's slice.
paths = slices.Clone(paths)
slices.Sort(paths)
paths = slices.Compact(paths)
drive := s.drivePath
locks := make([]*storageMutationLock, len(paths))
// Retain every entry before waiting so unlock cannot remove a mutex that
// a queued operation still needs.
storageMutationLocks.Lock()
for i, path := range paths {
key := storageMutationKey{drive, path}
lock := storageMutationLocks.locks[key]
if lock == nil {
lock = &storageMutationLock{}
storageMutationLocks.locks[key] = lock
}
lock.refs++
locks[i] = lock
}
storageMutationLocks.Unlock()
for _, lock := range locks {
lock.mu.Lock()
}
return func() {
for i := len(locks) - 1; i >= 0; i-- {
locks[i].mu.Unlock()
}
storageMutationLocks.Lock()
for i, path := range paths {
locks[i].refs--
if locks[i].refs == 0 {
delete(storageMutationLocks.locks, storageMutationKey{drive, path})
}
}
storageMutationLocks.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
}