mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-13 17:48:29 +02:00
* filer: skip replayed peer events older than the local entry Replay deleted and re-inserted every received event unconditionally, so a restarted filer that rewound its subscription offset re-applied stale peer writes over newer local state (values reverted, deleted entries came back and pointed at already-vacuumed chunks). Compare the event's TsNs against the local entry's Mtime and skip per-path, so a move event can still remove its source while keeping a newer destination. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: add replay last-writer-wins regression tests Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: stop peer subscriptions and flush offsets on shutdown Filer.Shutdown closed the store while subscription goroutines were still mid-stream; their deferred offset save was never guaranteed to run, so a restart rewound each peer's stream back to the last periodic flush and replayed stale events. MetaAggregator.Shutdown now closes every peer's stop channel and waits for the loops so the saved offset covers what was replayed, and the reconnect sleep is interruptible by the stop channel. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: test that shutdown stops and waits for peer loops Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: order replay by persisted per-path commit timestamps Review feedback found the mtime comparison still lets stale replay win: a local delete removes the entry it would have been compared against, an entry mtime is client-controllable, and the check ran unlocked so a local write could land between it and the store mutation. - Record the event timestamp per mutated path in the store KV on every emitted mutation, plus a delete record for sources of deletes and renames. The record survives the entry, so a replayed create older than a local delete no longer resurrects it; ancestor delete records cover children swept without per-child events. - Replay compares the event timestamp against that record under the same per-path entry locks local RPC mutations hold, falling back to entry mtime only for entries written before records existed. Lookup errors now abort the event instead of being treated as an absent entry. - MetaAggregator.Shutdown marks the aggregator stopped under peerChansLock so a master update cannot start a subscription that outlives the wait, and waits for the loops without the five-second cap now that every blocking stage — signature RPC, bootstrap traversal, retry sleeps, and per-event processing — cancels on the stop channel. * filer: pin peer offset at failed replay, fix commit-record context and key size A replayed event that exhausted its retries (or was interrupted by shutdown) was still followed by advancing the peer watermark, so the persisted offset skipped the unapplied event forever. replicateMetadataChange now reports the outcome: errPeerSubscriptionStopped aborts the subscription so the deferred save keeps the last completed event, and an exhausted retry records a barrier the persisted offset and reconnect resume point cannot pass while the stream keeps flowing. Commit records also failed on two fronts: NotifyUpdateEvent runs after CommitTransaction with the same context, so SQL stores wrote MpTs/MpDl through an already-committed *sql.Tx; and metaPathKey embedded the whole path, overflowing the 766-char name column SQL KV stores use, which the KvPut fallback update then silently dropped. The writes now use a fresh context and a sha256-hashed fixed-size key. * filer: lock HTTP delete handler and replay against ancestor paths The HTTP DeleteHandler called DeleteEntryMetaAndData without the shared entry lock, so a peer replay could still interleave with a local delete and its commit record. It now holds the same lock the gRPC DeleteEntry handler does. Replay also locked only the exact event paths, but its ordering check consults ancestor delete records: a local recursive delete holds only the ancestor's lock, so a stale child create could check before the tombstone landed and then reinsert. lockReplayPaths now locks every ancestor of both event paths in one lexicographic order — parents sort before children, as the rename handler's two-path order already does — while never locking the root, which cannot be deleted and would serialize all replays. * filer: record replay commit before the replacement insert If the same-path update DeleteEntry succeeded but InsertEntry failed, the path sat deleted with no commit record until the retry arrived, and a stale peer create could resurrect it in the gap. The commit record now lands immediately after the delete — without a delete record, since an update is not a delete — and again after the successful insert. * filer: lock replay ancestors shared, not exclusive Exclusive locks on every ancestor serialized all replays under a shared parent — unrelated objects in the same bucket could no longer apply in parallel. Ancestors only need to exclude an exclusive holder (a local delete or rename of the ancestor), so they now take shared locks; a path the event changes stays exclusive even when it is also the other path's ancestor. * filer: order peer bootstrap on the sender's commit timestamp traversePeerMetadata compared localCommitTsNs — a commit-clock value — against the entry's mtime, which a client can set freely: a peer change committed recently with an old mtime was skipped, and a stale entry mtime-stamped in the future overwrote a newer local commit. TraverseBfsMetadataResponse now carries the sender's per-path commit timestamp (recorded MpTs record, entry mtime as the unrecorded fallback). The bootstrap applies an entry only when its commit is newer than the local one and records the applied timestamp, so replayed events order against it — closing the gap between bootstrap content and the event stream. A peer without the field sends 0 and keeps the mtime fallback, preserving rolling-upgrade behavior. * filer: keep failed commit records pending until they land notePathChange ran after the mutation committed and only logged a KvPut failure: a delete whose MpTs/MpDl record never landed left no tombstone, so a stale replayed create could resurrect the path while the store was unreachable. Failed records now stay in metaTsPending and a repair pass retries them until they land. localCommitTsNs, deletedAfterTsNs and CommitTsNs consult the pending record, so ordering holds in the window between the committed mutation and the durable record. A per-path flag bounds the repair to one goroutine, and respawns cover merges landing between its last pass and the flag release. * filer: keep only unwritten work in the pending commit record The pending record merged every obligation into one timestamp and flag, and a later successful write left it untouched: a repair pass could then write an older timestamp over a newer one, and a merged delete inherited the newer change's time, which the ancestor check reads as a newer delete. The record now holds a change and a delete timestamp of its own, both discharged under the record's lock alongside the store writes that cover them, so a repair writes only what is still owed. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: fail traversal on a commit-record read error CommitTsNs answered a store read failure with the entry mtime, a value clients can set arbitrarily far ahead; a peer bootstrapping on it would record the minted time and reject real updates below it. The lookup now returns the error so the traversal restarts instead. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
325 lines
11 KiB
Go
325 lines
11 KiB
Go
package filer
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
const (
|
|
// metaPathTsPrefix keys per-path commit timestamps in the filer store KV:
|
|
// the newest event timestamp this filer committed or applied for the
|
|
// path. Written for every emitted mutation event and kept after the entry
|
|
// is deleted, so a replayed peer event can still be ordered against a
|
|
// change that left no entry behind.
|
|
metaPathTsPrefix = "MpTs"
|
|
// metaPathDelPrefix keys delete-only timestamps. A replayed event checks
|
|
// the path's ancestors too, so a directory delete also covers children
|
|
// swept without per-child events (a dropped bucket's whole-store delete,
|
|
// or a walk aborted partway).
|
|
metaPathDelPrefix = "MpDl"
|
|
)
|
|
|
|
// metaPathKey hashes the path into a fixed-size key. SQL-backed KV stores
|
|
// split the key at byte 8 and store the remainder base64-encoded in a
|
|
// VARCHAR(766) name column: a raw path over ~578 bytes overflows it, the
|
|
// KvPut insert fails, and the fallback update touches no row yet returns no
|
|
// error — the commit record would never land and stale replay could restore
|
|
// the path.
|
|
func metaPathKey(prefix string, path util.FullPath) []byte {
|
|
sum := sha256.Sum256([]byte(path))
|
|
key := make([]byte, len(prefix)+16)
|
|
copy(key, prefix)
|
|
copy(key[len(prefix):], sum[:16])
|
|
return key
|
|
}
|
|
|
|
// kvReadTsNs returns the timestamp recorded for key, or found=false when the
|
|
// key is absent or malformed. Any other store read failure is an error: a
|
|
// caller deciding whether a replayed change is stale must not guess.
|
|
func (f *Filer) kvReadTsNs(ctx context.Context, key []byte) (tsNs int64, found bool, err error) {
|
|
value, err := f.Store.KvGet(ctx, key)
|
|
if err != nil {
|
|
if errors.Is(err, ErrKvNotFound) {
|
|
return 0, false, nil
|
|
}
|
|
return 0, false, err
|
|
}
|
|
if len(value) < 8 {
|
|
return 0, false, nil
|
|
}
|
|
return int64(util.BytesToUint64(value)), true, nil
|
|
}
|
|
|
|
// notePathChange persists tsNs as path's newest committed change. Local event
|
|
// timestamps are stamped monotonically (metaLogInflight.stamp), so a plain
|
|
// put cannot regress an earlier local record; a higher peer record it may
|
|
// overwrite was already newer under last-writer-wins ordering anyway. A
|
|
// delete additionally writes a delete record the ancestor lookup consults.
|
|
//
|
|
// The write uses a fresh context: callers notify after CommitTransaction,
|
|
// and SQL stores keep the already-committed *sql.Tx in the request context —
|
|
// a record written through it would fail and the path would lose ordering.
|
|
//
|
|
// A failed write is kept in metaTsPending and retried in the background: the
|
|
// mutation the record orders has already committed, so dropping it would let
|
|
// a stale replayed change overwrite or resurrect the path while the store is
|
|
// unreachable. Ordering reads consult the pending record until the retry
|
|
// lands it.
|
|
func (f *Filer) notePathChange(path util.FullPath, tsNs int64, deleted bool) {
|
|
if f.Store == nil {
|
|
return
|
|
}
|
|
ctx := context.Background()
|
|
key := string(path)
|
|
rec := f.pendingRec(key)
|
|
defer rec.mu.Unlock()
|
|
|
|
value := make([]byte, 8)
|
|
util.Uint64toBytes(value, uint64(tsNs))
|
|
if err := f.Store.KvPut(ctx, metaPathKey(metaPathTsPrefix, path), value); err != nil {
|
|
glog.WarningfCtx(ctx, "record meta change time %s: %v", path, err)
|
|
if tsNs > rec.changeTsNs.Load() {
|
|
rec.changeTsNs.Store(tsNs)
|
|
}
|
|
} else if rec.changeTsNs.Load() <= tsNs {
|
|
// This write covers every change record owed so far.
|
|
rec.changeTsNs.Store(0)
|
|
}
|
|
if deleted {
|
|
if err := f.Store.KvPut(ctx, metaPathKey(metaPathDelPrefix, path), value); err != nil {
|
|
glog.WarningfCtx(ctx, "record meta delete time %s: %v", path, err)
|
|
if tsNs > rec.deleteTsNs.Load() {
|
|
rec.deleteTsNs.Store(tsNs)
|
|
}
|
|
} else if rec.deleteTsNs.Load() <= tsNs {
|
|
rec.deleteTsNs.Store(0)
|
|
}
|
|
}
|
|
if rec.changeTsNs.Load() == 0 && rec.deleteTsNs.Load() == 0 {
|
|
f.metaTsPending.CompareAndDelete(key, rec)
|
|
return
|
|
}
|
|
if _, running := f.metaTsRepairing.LoadOrStore(key, struct{}{}); !running {
|
|
go f.repairMetaPathTs(path)
|
|
}
|
|
}
|
|
|
|
// metaTsPendingRec is a commit record still on its way to the store, holding
|
|
// only work not yet written: changeTsNs is the newest committed change the
|
|
// MpTs record lacks and deleteTsNs the newest delete the MpDl record lacks —
|
|
// the two keep their own times so a later update cannot masquerade as the
|
|
// delete. Store writes and the bookkeeping they discharge share mu, so a
|
|
// repair pass can never write an older record over a newer one that already
|
|
// landed; readers take the atomic fields directly.
|
|
type metaTsPendingRec struct {
|
|
mu sync.Mutex
|
|
changeTsNs atomic.Int64
|
|
deleteTsNs atomic.Int64
|
|
}
|
|
|
|
// pendingRec returns the live pending record for key, locked for mutation,
|
|
// recreating it when a repair already dropped the previous one.
|
|
func (f *Filer) pendingRec(key string) *metaTsPendingRec {
|
|
for {
|
|
cur, _ := f.metaTsPending.LoadOrStore(key, &metaTsPendingRec{})
|
|
rec := cur.(*metaTsPendingRec)
|
|
rec.mu.Lock()
|
|
if live, ok := f.metaTsPending.Load(key); ok && live == cur {
|
|
return rec
|
|
}
|
|
rec.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// repairMetaPathTs retries a pending commit record until it lands. Each pass
|
|
// writes only what the record still owes, so a merge by a later failure is
|
|
// picked up without another goroutine and a landed write is never rewritten.
|
|
// The retry is unbounded — the mutation is already committed and only the
|
|
// record preserves its ordering.
|
|
func (f *Filer) repairMetaPathTs(path util.FullPath) {
|
|
key := string(path)
|
|
ctx := f.flushCtx
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
defer func() {
|
|
f.metaTsRepairing.Delete(key)
|
|
// A failure landing between the last pass and the flag release
|
|
// still needs a repair pass — unless the retry context is done.
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
if _, ok := f.metaTsPending.Load(key); ok {
|
|
if _, running := f.metaTsRepairing.LoadOrStore(key, struct{}{}); !running {
|
|
go f.repairMetaPathTs(path)
|
|
}
|
|
}
|
|
}()
|
|
for {
|
|
if _, ok := f.metaTsPending.Load(key); !ok {
|
|
return
|
|
}
|
|
rec := f.pendingRec(key)
|
|
var tsErr, delErr error
|
|
if tsNs := rec.changeTsNs.Load(); tsNs > 0 {
|
|
value := make([]byte, 8)
|
|
util.Uint64toBytes(value, uint64(tsNs))
|
|
if tsErr = f.Store.KvPut(ctx, metaPathKey(metaPathTsPrefix, path), value); tsErr == nil {
|
|
rec.changeTsNs.Store(0)
|
|
}
|
|
}
|
|
if tsNs := rec.deleteTsNs.Load(); tsNs > 0 {
|
|
value := make([]byte, 8)
|
|
util.Uint64toBytes(value, uint64(tsNs))
|
|
if delErr = f.Store.KvPut(ctx, metaPathKey(metaPathDelPrefix, path), value); delErr == nil {
|
|
rec.deleteTsNs.Store(0)
|
|
}
|
|
}
|
|
done := rec.changeTsNs.Load() == 0 && rec.deleteTsNs.Load() == 0
|
|
if done {
|
|
f.metaTsPending.CompareAndDelete(key, rec)
|
|
}
|
|
rec.mu.Unlock()
|
|
if !done {
|
|
glog.V(0).InfofCtx(ctx, "retry record meta change time %s: %v %v", path, tsErr, delErr)
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(time.Second):
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// pendingPathTsNs returns the newest pending commit timestamp for path and
|
|
// the newest pending delete timestamp.
|
|
func (f *Filer) pendingPathTsNs(path util.FullPath) (changeTsNs, deleteTsNs int64) {
|
|
cur, ok := f.metaTsPending.Load(string(path))
|
|
if !ok {
|
|
return 0, 0
|
|
}
|
|
rec := cur.(*metaTsPendingRec)
|
|
return rec.changeTsNs.Load(), rec.deleteTsNs.Load()
|
|
}
|
|
|
|
// CommitTsNs returns the newest commit timestamp known for path — the
|
|
// recorded event time, or a still-pending one — and fallbackTsNs when none
|
|
// exists. Peers stamp traversal entries with it so a bootstrap subscriber
|
|
// orders each entry on the sender's commit clock rather than the entry's
|
|
// client-settable mtime. A store read failure is an error, not a fallback:
|
|
// the mtime it would mint could outdate real commits.
|
|
func (f *Filer) CommitTsNs(ctx context.Context, path util.FullPath, fallbackTsNs int64) (int64, error) {
|
|
if f.Store == nil {
|
|
return fallbackTsNs, nil
|
|
}
|
|
tsNs := fallbackTsNs
|
|
recTs, found, err := f.kvReadTsNs(ctx, metaPathKey(metaPathTsPrefix, path))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if found {
|
|
tsNs = recTs
|
|
}
|
|
if pendTs, _ := f.pendingPathTsNs(path); pendTs > tsNs {
|
|
tsNs = pendTs
|
|
}
|
|
return tsNs, nil
|
|
}
|
|
|
|
// noteCommittedEvent records the event's commit timestamp for every path it
|
|
// touches: the source of a delete or rename, and the target of a create,
|
|
// update, or rename. It runs after the local mutation commits while the
|
|
// caller still holds the path's entry lock (for filer-server mutations), so
|
|
// a peer replay ordered by the same lock sees either the whole local change
|
|
// or none of it.
|
|
func (f *Filer) noteCommittedEvent(event *filer_pb.SubscribeMetadataResponse) {
|
|
message := event.EventNotification
|
|
if message == nil {
|
|
return
|
|
}
|
|
var source, target util.FullPath
|
|
if message.OldEntry != nil {
|
|
source = util.FullPath(filer_pb.MetadataEventSourceFullPath(event))
|
|
}
|
|
if message.NewEntry != nil {
|
|
target = util.FullPath(filer_pb.MetadataEventTargetFullPath(event))
|
|
}
|
|
// A delete or a rename removes the source path; an update reports the
|
|
// same path on both sides and counts once.
|
|
if source != "" && source != target {
|
|
f.notePathChange(source, event.TsNs, true)
|
|
}
|
|
if target != "" {
|
|
f.notePathChange(target, event.TsNs, false)
|
|
}
|
|
}
|
|
|
|
// deletedAfterTsNs returns the newest recorded delete timestamp among path
|
|
// and its ancestors.
|
|
func (f *Filer) deletedAfterTsNs(ctx context.Context, path util.FullPath) (int64, error) {
|
|
var tsNs int64
|
|
for p := string(path); p != "" && p != "/"; {
|
|
ts, found, err := f.kvReadTsNs(ctx, metaPathKey(metaPathDelPrefix, util.FullPath(p)))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if found && ts > tsNs {
|
|
tsNs = ts
|
|
}
|
|
if _, pendDel := f.pendingPathTsNs(util.FullPath(p)); pendDel > tsNs {
|
|
tsNs = pendDel
|
|
}
|
|
i := strings.LastIndexByte(p, '/')
|
|
if i <= 0 {
|
|
break
|
|
}
|
|
p = p[:i]
|
|
}
|
|
return tsNs, nil
|
|
}
|
|
|
|
// localCommitTsNs returns the newest timestamp this filer is known to have
|
|
// committed for path: the recorded event time when one exists — it is not
|
|
// client-controllable the way an entry's mtime is — else the entry's mtime
|
|
// for entries written before recording existed, plus any recorded delete of
|
|
// the path or an ancestor.
|
|
func (f *Filer) localCommitTsNs(ctx context.Context, path util.FullPath) (int64, error) {
|
|
if f.Store == nil {
|
|
return 0, nil
|
|
}
|
|
var tsNs int64
|
|
recTs, found, err := f.kvReadTsNs(ctx, metaPathKey(metaPathTsPrefix, path))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if found {
|
|
tsNs = recTs
|
|
} else if entry, findErr := f.Store.FindEntry(ctx, path); findErr != nil {
|
|
if !errors.Is(findErr, filer_pb.ErrNotFound) {
|
|
return 0, findErr
|
|
}
|
|
} else if entry != nil {
|
|
tsNs = entry.Mtime.UnixNano()
|
|
}
|
|
if pendTs, _ := f.pendingPathTsNs(path); pendTs > tsNs {
|
|
tsNs = pendTs
|
|
}
|
|
delTs, err := f.deletedAfterTsNs(ctx, path)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if delTs > tsNs {
|
|
tsNs = delTs
|
|
}
|
|
return tsNs, nil
|
|
}
|