Files
seaweedfs/weed/replication/sink/replication_sink.go
T
Chris LuandClaude Opus 5.5 bef1b13b3d replication: keep a local sink path the source recreated after a delete (#12581)
* replication: let the local sink check a delete against the live source

LocalSink removed the destination path for any delete it was handed. A
delete that reaches the sink after the source recreated the path (a peer
filer's event delivered late, or a historical event replayed) removed the
newer file, and no later event brings it back.

DeleteEntryVersion receives the deleted entry. Before removing an existing
destination, the local sink looks up the source path and keeps the
destination when the source holds an entry of the same kind committed
after the deleted version (commit_ts_ns, or mtime for unstamped entries).
A live entry that is the deleted version itself or older means the source
store lags the event, so the delete proceeds. A failed lookup returns the
error so the event is retried instead of deleting a file that may be live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* filer.sync, filer.replicate: hand the deleted version to the sink

Pure deletes, renames without a native move and moves out of the
watched directory now go through sink.DeleteEntryVersion with the old
entry, so a sink that checks the live source (the local sink) can keep a
path the source recreated after the deleted version. Other sinks fall
back to DeleteEntry unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* replication: order the local sink check by the delete's mutation time

A source filer can hold a version written concurrently with the delete:
newer than the deleted entry but older than the delete itself. The
source's own replay applies the delete over it, so keeping the backup
copy because it outranks the deleted entry left a file the source no
longer has.

The sink now gets the delete's mutation time (MutationTsNs, else TsNs)
and keeps the destination only when the live entry was committed after
it, the order Filer.Replay keeps. That also covers a stamped replacement
of an unstamped entry whose mtime did not move. filer.replicate events
carry no time, so they keep the deleted entry's stamp as the bound, and
mtime only when nothing is stamped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* replication: count an unstamped live entry from its mtime

A source filer that predates commit stamps stores entries with no
CommitTsNs, so comparing the live stamp with the delete's time never
kept a replacement there. An unstamped live entry now counts from its
mtime against the delete's time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* replication: keep a stamped live entry when the deleted one is unstamped and the delete has no time

filer.replicate gets no delete time from the notification queue. When the
deleted entry has no commit stamp either, a stamped live entry was written
after it, so its mtime no longer decides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 09:23:54 -08:00

88 lines
4.2 KiB
Go

package sink
import (
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/replication/source"
"github.com/seaweedfs/seaweedfs/weed/util"
)
type ReplicationSink interface {
GetName() string
Initialize(configuration util.Configuration, prefix string) error
DeleteEntry(key string, isDirectory, deleteIncludeChunks bool, signatures []int32) error
CreateEntry(key string, entry *filer_pb.Entry, signatures []int32) error
UpdateEntry(key string, oldEntry *filer_pb.Entry, newParentPath string, newEntry *filer_pb.Entry, deleteIncludeChunks bool, signatures []int32) (foundExistingEntry bool, err error)
GetSinkToDirectory() string
// GetDestinationIdentity distinguishes this sink's write destination from
// any other destination the same sink type could write to: endpoint or
// account, bucket or container, and directory. filer.backup keys its resume
// checkpoint on it, so two configurations writing to different places must
// not share a value.
GetDestinationIdentity() string
SetSourceFiler(s *source.FilerSource)
IsIncremental() bool
}
// EntryMover is an optional capability for sinks that can relocate an entry
// natively, in one atomic step, instead of create-then-delete. Drivers prefer
// it for a rename so a failed copy can never leave the source deleted with no
// committed destination, a directory move never deletes descendants before they
// are recreated, and the entry's chunks are neither re-copied nor leaked.
type EntryMover interface {
MoveEntry(oldKey, newKey string, oldEntry, newEntry *filer_pb.Entry, signatures []int32) error
}
// MetadataOnlyDeleter is an optional capability for sinks whose own delete
// events reach remote write-back daemons: the destination filer re-emits a
// replicated delete, so a source delete that kept the remote object must stay
// metadata-only there too.
type MetadataOnlyDeleter interface {
DeleteEntryKeepingRemoteObject(key string, isDirectory, deleteIncludeChunks bool, signatures []int32) error
}
// LatestVersionWriter is an optional capability for sinks whose CreateEntry
// guards a replayed write against the live source at the key's own path. A
// caller that already resolved the current source state — a flattened latest
// version lives under a different source path than the key it lands on — must
// not be skipped as stale by that check.
type LatestVersionWriter interface {
CreateEntryLatest(key string, entry *filer_pb.Entry, signatures []int32) error
}
func CreateEntryLatest(s ReplicationSink, key string, entry *filer_pb.Entry, signatures []int32) error {
if w, ok := s.(LatestVersionWriter); ok {
return w.CreateEntryLatest(key, entry, signatures)
}
return s.CreateEntry(key, entry, signatures)
}
func DeleteEntry(s ReplicationSink, key string, isDirectory, deleteIncludeChunks, keepRemoteObject bool, signatures []int32) error {
if deleter, ok := s.(MetadataOnlyDeleter); ok && keepRemoteObject {
return deleter.DeleteEntryKeepingRemoteObject(key, isDirectory, deleteIncludeChunks, signatures)
}
return s.DeleteEntry(key, isDirectory, deleteIncludeChunks, signatures)
}
// VersionedDeleter is an optional capability for sinks that check a delete
// against the live source before removing the destination: a delete that
// reaches the sink after the source recreated the path must not remove the
// newer destination, since no later event brings it back. tsNs is the
// delete's mutation time, or 0 when the event does not carry one.
type VersionedDeleter interface {
DeleteEntryVersion(key string, oldEntry *filer_pb.Entry, tsNs int64, deleteIncludeChunks bool, signatures []int32) error
}
func DeleteEntryVersion(s ReplicationSink, key string, oldEntry *filer_pb.Entry, tsNs int64, deleteIncludeChunks, keepRemoteObject bool, signatures []int32) error {
if _, ok := s.(MetadataOnlyDeleter); ok && keepRemoteObject {
return DeleteEntry(s, key, oldEntry.IsDirectory, deleteIncludeChunks, keepRemoteObject, signatures)
}
if deleter, ok := s.(VersionedDeleter); ok {
return deleter.DeleteEntryVersion(key, oldEntry, tsNs, deleteIncludeChunks, signatures)
}
return DeleteEntry(s, key, oldEntry.IsDirectory, deleteIncludeChunks, keepRemoteObject, signatures)
}
var (
Sinks []ReplicationSink
)