mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-18 12:08:32 +02:00
* filer: sweep the local subtree on a replayed directory delete A recursive delete emits only the directory's own key, so peers applied it with a single DeleteEntry. Children the peer held that the origin had not listed stayed behind unlisted but still readable, and their needles were never deleted. Sweep the local children on a replayed directory delete and drop their chunks when the origin deleted chunks. * filer: walk the subtree on a replayed directory delete DeleteFolderChildren removes only direct children in stores keyed by immediate parent, so the sweep now lists and deletes each descendant entry itself. Entries the local filer committed after the event survive — with the directories that hold them — and hard-linked files keep their chunks for the origin's own events to settle. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: key replayed subtree chunk deletes to each entry's path The sweep queued every descendant's chunks under the deleted directory's storage rule and skipped directory entries entirely. Chunks now collect keyed by their owning path, so per-path rules apply to each file — and a directory carrying its own chunks (a file promoted by an S3 child) frees them too. * filer: fence kept replayed dirs and flush collected chunks on error A directory kept because a newer descendant survived still needs its delete timestamp recorded, or a stale replayed create can resurrect another child under it. And chunks collected for deleted children must be flushed even when the sweep fails partway — the entries are gone and a retry cannot list them again. Collection now also runs only after the entry delete succeeds. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: flush swept chunk references in batches --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
287 lines
9.9 KiB
Go
287 lines
9.9 KiB
Go
package filer
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
// Replay applies the delete and the insert as separate, non-transactional
|
|
// store calls, so retrying it after a partial failure is not atomic; both
|
|
// operations are idempotent and the timestamp check tolerates re-delivery.
|
|
//
|
|
// A replayed event can be older than local state: a peer resuming from a
|
|
// stale offset replays everything since the offset, and two writes through
|
|
// different filers race within the replication lag. Apply each path's change
|
|
// only when no newer local change is recorded, so a late event cannot revert
|
|
// or resurrect entries.
|
|
//
|
|
// Ordering uses the per-path commit timestamp recorded for every emitted
|
|
// mutation (meta_path_ts.go), which survives deletes as a tombstone and is
|
|
// not client-controllable the way entry mtime is — a mounted client can set
|
|
// mtime into the future, and a rename keeps the source file's mtime on the
|
|
// destination entry. Entries written before recording existed fall back to
|
|
// their mtime.
|
|
//
|
|
// The check and the mutation run under the same per-path entry lock the
|
|
// filer server's local writes hold, so they cannot be split by a concurrent
|
|
// local update or by another peer's replay.
|
|
func (f *Filer) Replay(resp *filer_pb.SubscribeMetadataResponse) error {
|
|
message := resp.EventNotification
|
|
dir := resp.Directory
|
|
if message.NewParentPath != "" {
|
|
dir = message.NewParentPath
|
|
}
|
|
var oldPath, newPath util.FullPath
|
|
if message.OldEntry != nil {
|
|
oldPath = util.NewFullPath(resp.Directory, message.OldEntry.Name)
|
|
}
|
|
if message.NewEntry != nil {
|
|
newPath = util.NewFullPath(dir, message.NewEntry.Name)
|
|
}
|
|
ctx := context.Background()
|
|
|
|
unlock := f.lockReplayPaths(oldPath, newPath)
|
|
defer unlock()
|
|
|
|
samePath := oldPath != "" && oldPath == newPath
|
|
if samePath {
|
|
newer, err := f.replayBlockedByLocal(ctx, oldPath, resp.TsNs)
|
|
if err != nil || newer {
|
|
return err
|
|
}
|
|
glog.V(4).Infof("deleting %v", oldPath)
|
|
if err := f.Store.DeleteEntry(ctx, oldPath); err != nil {
|
|
return err
|
|
}
|
|
// Record the commit before the insert too: if the insert fails and
|
|
// the event is retried later, the path sits deleted in between —
|
|
// without the record a stale peer create could resurrect it in the
|
|
// gap. The entry still exists afterward, so no delete record.
|
|
f.notePathChange(oldPath, resp.TsNs, false)
|
|
glog.V(4).Infof("creating %v", newPath)
|
|
newEntry := FromPbEntry(dir, message.NewEntry)
|
|
if err := f.Store.InsertEntry(ctx, newEntry); err != nil {
|
|
return err
|
|
}
|
|
f.notePathChange(oldPath, resp.TsNs, false)
|
|
return nil
|
|
}
|
|
|
|
if message.OldEntry != nil {
|
|
newer, err := f.replayBlockedByLocal(ctx, oldPath, resp.TsNs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !newer {
|
|
// A directory delete on the origin removed the whole subtree, but
|
|
// only the directory's own key replicates. Sweep local children the
|
|
// origin never listed, or they orphan under a missing parent.
|
|
keepDir := false
|
|
if message.OldEntry.IsDirectory {
|
|
var err error
|
|
keepDir, err = f.deleteReplayedFolderChildren(ctx, oldPath, message.DeleteChunks, resp.TsNs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !keepDir {
|
|
glog.V(4).Infof("deleting %v", oldPath)
|
|
if err := f.Store.DeleteEntry(ctx, oldPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// The origin deleted the directory even when a newer descendant
|
|
// keeps its entry here — recording the delete fences stale child
|
|
// creates that would otherwise resurrect under it.
|
|
f.notePathChange(oldPath, resp.TsNs, true)
|
|
}
|
|
}
|
|
|
|
if message.NewEntry != nil {
|
|
newer, err := f.replayBlockedByLocal(ctx, newPath, resp.TsNs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !newer {
|
|
glog.V(4).Infof("creating %v", newPath)
|
|
newEntry := FromPbEntry(dir, message.NewEntry)
|
|
if err := f.Store.InsertEntry(ctx, newEntry); err != nil {
|
|
return err
|
|
}
|
|
f.notePathChange(newPath, resp.TsNs, false)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// deleteReplayedFolderChildren removes local entries under dir, and queues
|
|
// the chunks of removed files for deletion. Children this filer committed
|
|
// after the event stay — a newer local write wins over a delayed delete —
|
|
// along with the directories that hold them.
|
|
func (f *Filer) deleteReplayedFolderChildren(ctx context.Context, dir util.FullPath, deleteChunks bool, tsNs int64) (bool, error) {
|
|
sweep := &folderSweep{chunksByPath: make(map[util.FullPath][]*filer_pb.FileChunk)}
|
|
kept, err := f.sweepReplayedFolder(ctx, dir, deleteChunks, tsNs, sweep)
|
|
// Flush what was collected even on error: the entries already deleted
|
|
// cannot be listed again on retry, so their chunks must be queued now.
|
|
sweep.flush(ctx, f)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return kept, nil
|
|
}
|
|
|
|
// folderSweep accumulates the chunks of removed children for deletion. It
|
|
// flushes in batches so a large subtree cannot hold every chunk reference in
|
|
// memory at once, and so chunk queueing overlaps the listing work.
|
|
type folderSweep struct {
|
|
chunksByPath map[util.FullPath][]*filer_pb.FileChunk
|
|
count int
|
|
}
|
|
|
|
const folderSweepChunkBatch = 4096
|
|
|
|
func (s *folderSweep) add(path util.FullPath, chunks []*filer_pb.FileChunk) {
|
|
s.chunksByPath[path] = append(s.chunksByPath[path], chunks...)
|
|
s.count += len(chunks)
|
|
}
|
|
|
|
func (s *folderSweep) flush(ctx context.Context, f *Filer) {
|
|
for path, chunks := range s.chunksByPath {
|
|
f.DeleteChunks(ctx, path, chunks)
|
|
}
|
|
s.chunksByPath = make(map[util.FullPath][]*filer_pb.FileChunk)
|
|
s.count = 0
|
|
}
|
|
|
|
// sweepReplayedFolder deletes every child of dir whose local commit is not
|
|
// newer than the event, working subtree-first so nested entries go too.
|
|
// It reports whether a newer descendant keeps dir alive — the caller then
|
|
// keeps the dir entry itself.
|
|
func (f *Filer) sweepReplayedFolder(ctx context.Context, dir util.FullPath, deleteChunks bool, tsNs int64, sweep *folderSweep) (kept bool, err error) {
|
|
lastFileName := ""
|
|
for {
|
|
entries, _, lerr := f.ListDirectoryEntries(ctx, dir, lastFileName, false, PaginationSize, "", "", "")
|
|
if lerr != nil {
|
|
return false, fmt.Errorf("list folder %s: %w", dir, lerr)
|
|
}
|
|
for _, sub := range entries {
|
|
lastFileName = sub.Name()
|
|
childKept := false
|
|
if tsNs > 0 {
|
|
localTsNs, tsErr := f.localCommitTsNs(ctx, sub.FullPath)
|
|
if tsErr != nil {
|
|
return false, tsErr
|
|
}
|
|
childKept = localTsNs > tsNs
|
|
}
|
|
if !childKept && sub.IsDirectory() {
|
|
childKept, err = f.sweepReplayedFolder(ctx, sub.FullPath, deleteChunks, tsNs, sweep)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
if childKept {
|
|
kept = true
|
|
continue
|
|
}
|
|
if derr := f.Store.DeleteEntry(ctx, sub.FullPath); derr != nil {
|
|
return false, fmt.Errorf("delete %s: %w", sub.FullPath, derr)
|
|
}
|
|
f.notePathChange(sub.FullPath, tsNs, true)
|
|
// hard-linked chunks are owned by the shared inode record; the
|
|
// origin's own events decide their deletion. A directory entry
|
|
// can still carry chunks of its own — an S3 child promotes an
|
|
// existing file to a directory without dropping them — so the
|
|
// entry's chunks are keyed to its own path whatever it is.
|
|
if deleteChunks && len(sub.HardLinkId) == 0 && len(sub.GetChunks()) > 0 {
|
|
sweep.add(sub.FullPath, sub.GetChunks())
|
|
if sweep.count >= folderSweepChunkBatch {
|
|
sweep.flush(ctx, f)
|
|
}
|
|
}
|
|
}
|
|
if len(entries) < PaginationSize {
|
|
break
|
|
}
|
|
}
|
|
return kept, nil
|
|
}
|
|
|
|
// replayBlockedByLocal reports whether the local store already holds a
|
|
// version of path committed after the event's timestamp. Lookup failures are
|
|
// propagated: a replayed change must not be applied on the guess that the
|
|
// path was simply absent, or a transient read error could let a stale event
|
|
// overwrite a newer entry past the point of no return.
|
|
func (f *Filer) replayBlockedByLocal(ctx context.Context, path util.FullPath, tsNs int64) (bool, error) {
|
|
if tsNs <= 0 {
|
|
return false, nil
|
|
}
|
|
localTsNs, err := f.localCommitTsNs(ctx, path)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return localTsNs > tsNs, nil
|
|
}
|
|
|
|
// lockReplayPaths holds the shared entry locks for every path the event
|
|
// touches, in the same sorted order the rename handler takes them.
|
|
// lockReplayPaths takes the per-path entry locks for the event's paths and
|
|
// every ancestor of them, in lexicographic order — parents sort before
|
|
// children, the same ordering the local rename handler's two-path lock uses,
|
|
// so lock acquisition is deadlock-free across operations. Ancestors are
|
|
// included because the replay check consults ancestor delete records: a
|
|
// local recursive delete or directory rename holds only the ancestor's lock,
|
|
// and without contending on it a replay could reinsert a stale child after
|
|
// its check. Ancestors are locked shared — they only need to exclude an
|
|
// exclusive holder, not each other, or every replay under a busy parent
|
|
// would serialize — while a path the event actually changes stays exclusive
|
|
// even when it is also the other path's ancestor. The root is never locked:
|
|
// it cannot be deleted, and a shared root lock would still be excluded by
|
|
// every top-level delete.
|
|
func (f *Filer) lockReplayPaths(oldPath, newPath util.FullPath) func() {
|
|
lt := f.EntryLockTable
|
|
if lt == nil {
|
|
return func() {}
|
|
}
|
|
var paths []util.FullPath
|
|
addAncestors := func(p util.FullPath) {
|
|
for i := strings.LastIndexByte(string(p), '/'); i > 0; i = strings.LastIndexByte(string(p), '/') {
|
|
p = p[:i]
|
|
paths = append(paths, p)
|
|
}
|
|
}
|
|
addAncestors(oldPath)
|
|
addAncestors(newPath)
|
|
if oldPath != "" {
|
|
paths = append(paths, oldPath)
|
|
}
|
|
if newPath != "" {
|
|
paths = append(paths, newPath)
|
|
}
|
|
if len(paths) == 0 {
|
|
return func() {}
|
|
}
|
|
slices.Sort(paths)
|
|
paths = slices.Compact(paths)
|
|
locks := make([]*util.ActiveLock, len(paths))
|
|
for i, p := range paths {
|
|
lockType := util.SharedLock
|
|
if p == oldPath || p == newPath {
|
|
lockType = util.ExclusiveLock
|
|
}
|
|
locks[i] = lt.AcquireLock("MetaReplay", p, lockType)
|
|
}
|
|
return func() {
|
|
for i := len(paths) - 1; i >= 0; i-- {
|
|
lt.ReleaseLock(paths[i], locks[i])
|
|
}
|
|
}
|
|
}
|