mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-26 15:00:48 +01:00
* filer: enable per-path commit records only when a peer has its own store Since seaweedfs#11698 every metadata change writes a per-path commit record into the filer store (an MpTs key, plus an MpDl key on deletes), and the records stay after the path is deleted. They order the events a filer replays from peer filers that keep their own store, since replay only runs when peerSignature != f.Signature. Filers that all share one store (one Redis, MySQL, TiKV, ...) never replay each other's events, so the records are never read, yet each change still costs one or two extra store writes and the key count grows with every path ever changed — unbounded growth for in-memory stores. This turns the records off by default and latches them on the first time a peer reports a different store signature — no option needed. The latch is persisted in the store (MpRep) so it survives restarts and converges filers sharing a store even before they discover the peer themselves. While off, a filer keeps an MpOff marker so enabling later bumps the records' generation (MpGen, folded into each record key) past the unrecorded window: a record older than a change made while off cannot let an older replayed peer event win. Ordering for unrecorded changes falls back to the entry's mtime, the same as entries written before the records existed. Supersedes the manual option approach in seaweedfs#12060. * filer: keep delete tombstones while records off, harden enable path Review on the first commit: - A delete committed while the records were off left no entry and no usable tombstone: after enabling, an older replayed create could resurrect the path. Deletes and rename sources are now always recorded under the generation-0 MpDl key — they are never fenced by a generation bump, and they cannot go stale because every delete is recorded. Creates/updates still write nothing while off. - bumpPathCommitGen could regress MpGen under concurrent bumpers on a shared store. It now loops: each pass writes only a generation higher than any it has seen, so concurrent bumps converge upward. - doSubscribeToOneFiler proceeded into replay when enablePathCommitRecords failed, running the whole session without ordering. It now aborts the subscribe so the reconnect retries with the records on. * filer: bind pending commit-record repairs to their generation A failed MpTs write kept in metaTsPending could land after a shared-store generation bump, writing a pre-bump timestamp as a current-generation record and overriding newer unrecorded state. Each pending change record now carries the generation it failed under: repairs write it only while that generation is current, and ordering reads ignore it otherwise. Pending delete records need no binding — MpDl keys are never fenced. * filer: never move the local records' generation backward The converge loop in bumpPathCommitGen seeded from zero and both it and syncPathCommitGen stored unconditionally: a stale writer could leave the stored generation below what this filer already adopted, and the next sync or bump then walked the local value back — orphaning records it wrote under the higher generation. The bump now seeds from the local generation and every store only moves it forward. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: name a records generation after the off marker it consumes Two filers consuming the same off marker each counted one past the stored generation, so the second could store N+2 after the first stored N+1 and write records under a key the other could not read until the next check. The bump now takes the marker's own timestamp — filers racing on one marker land on one value — a marker older than the stored generation is cleaned as a leftover, and a stored value that already passed the bump is adopted instead of counted past. A newly enabling filer whose marker was already consumed names its own off window with the current time, so its unrecorded period is still fenced. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: tell a fresh off marker from one the generation already consumed A marker whose timestamp reads at or below the stored generation was dropped as leftover, but a new off period can also read that old — the off filer's clock lagging, or a host clock moving back — and dropping it kept records from before that period live, so stale replay could win. The stored generation now names the marker it was bumped for: a marker the store still shows is consumed unless it is that one, however old it reads, and only the consumed marker itself is cleared without a bump. The generation trails in the value, so a whole-slice read still yields it on stores written before the pair was stored. Enabling fences the filer's own off window with the current time instead of a marker a sharer may already have consumed. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: cover racing off-marker consumers settling on one generation * filer: adopt the stored generation, not the consumed marker it names A filer syncing with no off marker present read the value's first field — the consumed marker's timestamp — so a restart after a lagging-clock off period under-counted the generation and kept records the bump dropped. * filer: re-check the off marker before consuming it A sync whose marker read ran behind a later off period's consume cycle still bumped for its stale read: the stored generation already covered a different marker, so the bump overwrote the consumed name, counted the generation past records a newer consumption meant to keep, and cleared whatever marker was live by then. Each pass now re-reads the marker and only bumps or clears while it is still the one the pass saw. * filer: keep a failed generation read from resetting the records' generation The final generation read ignored errors and then stored whatever gen held — possibly zero — so a transient store failure could hide records the current generation wrote. Unexpected read errors now return before the local generation changes. Also cover a sync whose marker read runs behind another consumer's delete: it must adopt the stored generation, not bump for the stale read. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: fail a bump that cannot verify the stored generation A generation read that failed mid-bump was treated as absent: the write could land below a generation a racing bumper had stored, or re-store a marker that bumper had already consumed. Read failures now abort the pass like every other store error on the path. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: cover generation reads that fail instead of returning a value Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: carry the commit timestamp on the entry itself The per-path MpTs record only exists because a commit timestamp had nowhere else to live. Stamping commit_ts_ns on the entry before its store write - and reusing that stamp as the emitted event's TsNs - makes the entry its own commit record: replay and bootstrap traversal order on it with zero extra writes for creates and updates. Deletes still need a durable key since a removed entry cannot carry a timestamp, so MpDl tombstones stay (one write per delete, growth bounded by delete history). Entries predating the field fall back to mtime, matching what peers and old records already assume. This replaces the MpTs/MpOff/MpGen/MpRep machinery entirely: shared- store filers write nothing extra, and dedicated-store replication keeps last-writer-wins ordering plus tombstone coverage for deletes and ancestor drops. * filer: keep a directory update replay from tombstoning its subtree A same-path update wrote a delete record before the delete-and-reinsert, so a directory metadata change read as a subtree delete: a later peer event for a child older than the update was then skipped even though nobody deleted the children, leaving files missing on this filer. Only file updates take the tombstone now — the re-inserted directory entry's own commit stamp orders the path itself. * filer: order unstamped entries by their recorded commit before mtime Entries written before stamping existed have no CommitTsNs, so the ordering check fell back to the client-settable mtime and dropped the MpTs record those versions wrote: an entry committed at event time 300 with mtime 100 lost to a delayed peer event at 200 after upgrade. Read the legacy record for unstamped entries in localCommitTsNs, CommitTsNs, and the traverse stamp (which also stopped falling back to mtime, regressing entry_ts_ns to 0). * filer: release queued rename event stamps when the rename fails Each moved entry carried an in-flight stamp its queued event would adopt at emit time. When the move succeeded but a later child move or the commit failed, the handler rolled back without emitting the queued events or releasing their stamps, pinning the delivered and flushed watermarks below the aborted rename forever. Release every queued stamp on both failure exits in both rename handlers. * filer: apply a directory update replay with an in-place upsert Deleting the stored entry before inserting its replacement left a window where the directory had no entry and no ordering record — a tombstone cannot stand in for it without reading as a subtree delete that suppresses the children's own records. A stale peer event accepted in that gap could replay an older update or delete and sweep the directory's children. Upsert the replacement instead: the entry keeps its recorded commit order across the whole update, and a directory has no chunks or hard links for a delete to release. * filer: update an existing directory entry on replay instead of upserting InsertEntry is not an upsert on SQL stores configured without upsert support — PostgreSQL with enableUpsert=false issues a plain INSERT and its duplicate-key error does not match the MySQL-only fallback, so a replay updating a present directory failed on every retry. Pick UpdateEntry when the entry exists and InsertEntry when it is absent; UpdateEntry touches no row that is not there. * filer: detect a shared-store delete racing a replayed directory update UpdateEntry reports success even when the row was deleted between the find and the update — the UPDATE changes zero rows and the replayed change is dropped while the subscription offset advances. Re-read the entry after the update and retry the write, re-checking the recorded commit order each pass so a newer peer change wins instead of being resurrected over. * filer: declare commit_ts_ns in field-number order Field 6 sits between extended and hard_link_id; the declaration now reads in the same order. Wire format unchanged. Also syncs the Java client's checked-in copy of filer.proto, which the pb Makefile copies on gen. * filer: option to skip delete tombstones entirely filer.options.deleteTombstones=false stops writing MpDl records on deletes - a single filer or shared-store cluster never replays peer events, so the tombstones are dead weight. The tradeoff is explicit: if a dedicated-store peer does replicate, a stale event can resurrect a deleted path. * filer: drop delete tombstones entirely Deletes no longer write MpDl records, and the pending-repair machinery they needed is gone with them: per-mutation metadata overhead is now zero for every store topology. Ordering information lives only on the entry itself via commit_ts_ns. The accepted tradeoff: a dedicated-store peer replaying an event older than a delete can resurrect the path, and a kept directory no longer fences stale child creates. Recorded tombstones and MpTs records left by older builds are still read, so upgrades keep the protections they already have. * filer: stop reading legacy per-path records too Drops the MpTs/MpDl read path along with the writes: meta_path_ts.go keeps only the entry-field readers (CommitTsNs, EntryCommitTsNs, localCommitTsNs). Entries written before stamping existed fall back to mtime, and tombstones or commit records older builds left behind no longer fence replayed events. * filer: trim commit timestamp comments --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
397 lines
14 KiB
Go
397 lines
14 KiB
Go
package filer
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
newEntry := FromPbEntry(dir, message.NewEntry)
|
|
newEntry.CommitTsNs = resp.TsNs
|
|
if message.OldEntry.IsDirectory {
|
|
glog.V(4).Infof("updating %v", newPath)
|
|
return f.updateExistingEntry(ctx, oldPath, newEntry)
|
|
}
|
|
glog.V(4).Infof("deleting %v", oldPath)
|
|
if err := f.Store.DeleteEntry(ctx, oldPath); err != nil {
|
|
return err
|
|
}
|
|
glog.V(4).Infof("creating %v", newPath)
|
|
if err := f.Store.InsertEntry(ctx, newEntry); err != nil {
|
|
return err
|
|
}
|
|
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, 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
newEntry.CommitTsNs = resp.TsNs
|
|
if err := f.Store.InsertEntry(ctx, newEntry); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// updateExistingEntry applies a same-path update without deleting the row in
|
|
// between: the path never sits absent, so a failed retry cannot leave it
|
|
// unordered — a tombstone would instead read as a subtree delete that
|
|
// suppresses a directory's children. UpdateEntry goes on a present row and
|
|
// InsertEntry on an absent one (InsertEntry is not an upsert on stores
|
|
// configured without upsert support, and UpdateEntry touches no row that is
|
|
// not there).
|
|
//
|
|
// A peer filer sharing the store can still delete the row between the find
|
|
// and the update — the UPDATE then changes zero rows yet reports success,
|
|
// silently dropping the replay while its subscription offset advances. The
|
|
// write is verified by re-reading the entry, and each pass re-checks the
|
|
// recorded commit order so a newer peer change wins instead of being
|
|
// resurrected over.
|
|
func (f *Filer) updateExistingEntry(ctx context.Context, path util.FullPath, newEntry *Entry) error {
|
|
var lastErr error
|
|
for attempt := 0; attempt < 4; attempt++ {
|
|
if attempt > 0 {
|
|
newer, err := f.replayBlockedByLocal(ctx, path, newEntry.CommitTsNs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if newer {
|
|
return nil
|
|
}
|
|
}
|
|
if _, findErr := f.Store.FindEntry(ctx, path); findErr == nil {
|
|
if err := f.Store.UpdateEntry(ctx, newEntry); err != nil {
|
|
return err
|
|
}
|
|
if _, err := f.Store.FindEntry(ctx, path); err == nil {
|
|
return nil
|
|
} else if !errors.Is(err, filer_pb.ErrNotFound) {
|
|
return err
|
|
} else {
|
|
lastErr = err
|
|
}
|
|
} else if errors.Is(findErr, filer_pb.ErrNotFound) {
|
|
// An insert colliding with a racing peer create retries as an
|
|
// update on the next pass.
|
|
if err := f.Store.InsertEntry(ctx, newEntry); err != nil {
|
|
lastErr = err
|
|
} else {
|
|
return nil
|
|
}
|
|
} else {
|
|
return findErr
|
|
}
|
|
}
|
|
return fmt.Errorf("update %s: %w", path, lastErr)
|
|
}
|
|
|
|
// 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, message *filer_pb.EventNotification, tsNs int64) (bool, error) {
|
|
sweep := &folderSweep{chunksByPath: make(map[util.FullPath][]*filer_pb.FileChunk)}
|
|
kept, err := f.sweepReplayedFolder(ctx, dir, message, 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, message *filer_pb.EventNotification, tsNs int64, sweep *folderSweep) (kept bool, err error) {
|
|
keepRemote := IsMetadataOnlyDelete(message.OldEntry)
|
|
deleteRemote := !keepRemote && !message.GetIsFromOtherCluster()
|
|
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, message, tsNs, sweep)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
if childKept {
|
|
kept = true
|
|
continue
|
|
}
|
|
childKept, err = f.deleteSweptChild(ctx, sub, message, tsNs, sweep, keepRemote, deleteRemote)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if childKept {
|
|
kept = true
|
|
}
|
|
}
|
|
if len(entries) < PaginationSize {
|
|
break
|
|
}
|
|
}
|
|
return kept, nil
|
|
}
|
|
|
|
// deleteSweptChild removes sub under its entry lock and reports whether the
|
|
// child was kept instead. The listing and the first commit-time check run
|
|
// without the lock, so a local write can land in between: the check runs
|
|
// again under the lock before anything is removed. A remote-only entry that
|
|
// has taken the path since — one the commit record may not cover, such as a
|
|
// lazy fetch materialized from the remote — reads the object at the key as
|
|
// its only copy: the write-back daemon's processDeleteEvent keeps such an
|
|
// object, and the sweep does too.
|
|
func (f *Filer) deleteSweptChild(ctx context.Context, sub *Entry, message *filer_pb.EventNotification, tsNs int64, sweep *folderSweep, keepRemote, deleteRemote bool) (bool, error) {
|
|
var childLock *util.ActiveLock
|
|
if lt := f.EntryLockTable; lt != nil {
|
|
// The sweep already holds locks on the event's paths; waiting for a
|
|
// descendant here can deadlock against a local rename that holds the
|
|
// child and waits for one of them. ErrLockContended is transient, so
|
|
// the replication retry picks the event up once the write settles.
|
|
var acquired bool
|
|
childLock, acquired = lt.TryAcquireLock("MetaReplay", sub.FullPath, util.ExclusiveLock)
|
|
if !acquired {
|
|
return false, fmt.Errorf("sweep %s: %w", sub.FullPath, util.ErrLockContended)
|
|
}
|
|
defer lt.ReleaseLock(sub.FullPath, childLock)
|
|
}
|
|
if tsNs > 0 {
|
|
localTsNs, tsErr := f.localCommitTsNs(ctx, sub.FullPath)
|
|
if tsErr != nil {
|
|
return false, tsErr
|
|
}
|
|
if localTsNs > tsNs {
|
|
return true, nil
|
|
}
|
|
}
|
|
current, findErr := f.Store.FindEntry(ctx, sub.FullPath)
|
|
if findErr != nil && !errors.Is(findErr, filer_pb.ErrNotFound) {
|
|
return false, fmt.Errorf("find %s: %w", sub.FullPath, findErr)
|
|
}
|
|
if current != nil && len(current.Content) == 0 && current.IsInRemoteOnly() &&
|
|
!proto.Equal(current.ToProtoEntry(), sub.ToProtoEntry()) {
|
|
glog.V(0).InfofCtx(ctx, "keep %s: a remote-only entry has taken the path since the listing", sub.FullPath)
|
|
return true, nil
|
|
}
|
|
if !keepRemote && f.RemoteStorage != nil && f.remoteTombstones != nil {
|
|
if _, remoteLoc := f.RemoteStorage.FindMountDirectory(sub.FullPath); remoteLoc != nil {
|
|
f.remoteTombstones.addFromEvent(string(sub.FullPath), sub.IsDirectory(), tsNs)
|
|
}
|
|
}
|
|
if deleteRemote && !sub.IsDirectory() {
|
|
if remoteErr := f.deleteSweptEntryRemote(ctx, sub); remoteErr != nil {
|
|
return false, fmt.Errorf("remote delete %s: %w", sub.FullPath, remoteErr)
|
|
}
|
|
}
|
|
if derr := f.Store.DeleteEntry(ctx, sub.FullPath); derr != nil {
|
|
return false, fmt.Errorf("delete %s: %w", sub.FullPath, derr)
|
|
}
|
|
// chunks of a hard-linked child belong to the shared inode record; free
|
|
// them only once that record is gone. 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 message.DeleteChunks && len(sub.GetChunks()) > 0 && (len(sub.HardLinkId) == 0 || f.hardLinkIsOrphaned(ctx, sub.HardLinkId)) {
|
|
sweep.add(sub.FullPath, sub.GetChunks())
|
|
if sweep.count >= folderSweepChunkBatch {
|
|
sweep.flush(ctx, f)
|
|
}
|
|
}
|
|
return false, 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])
|
|
}
|
|
}
|
|
}
|