mirror of
https://github.com/pgsty/silo.git
synced 2026-10-26 13:50:26 +00:00
39278e068e
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>
219 lines
6.5 KiB
Go
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
|
|
}
|