mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-12 23:09:33 +01:00
* 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>
188 lines
6.4 KiB
Go
188 lines
6.4 KiB
Go
package replication
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/replication/repl_util"
|
|
"github.com/seaweedfs/seaweedfs/weed/replication/sink"
|
|
"github.com/seaweedfs/seaweedfs/weed/replication/source"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
type Replicator struct {
|
|
sink sink.ReplicationSink
|
|
source *source.FilerSource
|
|
excludeDirs []string
|
|
}
|
|
|
|
func NewReplicator(sourceConfig util.Configuration, configPrefix string, dataSink sink.ReplicationSink) *Replicator {
|
|
|
|
source := &source.FilerSource{}
|
|
source.Initialize(sourceConfig, configPrefix)
|
|
|
|
if err := repl_util.InitializeSSEForReplication(source); err != nil {
|
|
glog.Warningf("SSE initialization failed: %v (encrypted objects may fail to replicate)", err)
|
|
}
|
|
|
|
dataSink.SetSourceFiler(source)
|
|
|
|
return &Replicator{
|
|
sink: dataSink,
|
|
source: source,
|
|
excludeDirs: sourceConfig.GetStringSlice(configPrefix + "excludeDirectories"),
|
|
}
|
|
}
|
|
|
|
func (r *Replicator) Replicate(ctx context.Context, key string, message *filer_pb.EventNotification) error {
|
|
if message.IsFromOtherCluster && r.sink.GetName() == "filer" {
|
|
return nil
|
|
}
|
|
|
|
oldEntry := message.OldEntry
|
|
newEntry := message.NewEntry
|
|
newParentPath := message.NewParentPath
|
|
|
|
oldInSource := util.IsEqualOrUnder(key, r.source.Dir) && !r.isExcluded(key)
|
|
|
|
// For rename events (both old and new entry present), check both paths
|
|
// against the source directory. Convert cross-boundary renames to
|
|
// create or delete so the sink stays consistent.
|
|
if oldEntry != nil && newEntry != nil {
|
|
newFullPath, targetParent := metadataEventTarget(key, newEntry, newParentPath)
|
|
newInSource := util.IsEqualOrUnder(newFullPath, r.source.Dir) && !r.isExcluded(newFullPath)
|
|
|
|
if !oldInSource && !newInSource {
|
|
return nil
|
|
}
|
|
if !oldInSource {
|
|
// Rename into watched directory: treat as create
|
|
oldEntry = nil
|
|
key = newFullPath
|
|
newParentPath = targetParent
|
|
} else if !newInSource {
|
|
// Rename out of watched directory: treat as delete
|
|
newEntry = nil
|
|
newParentPath = ""
|
|
}
|
|
} else if !oldInSource {
|
|
glog.V(4).Infof("skipping %v outside of %v", key, r.source.Dir)
|
|
return nil
|
|
}
|
|
|
|
var dateKey string
|
|
if r.sink.IsIncremental() {
|
|
var mTime int64
|
|
if newEntry != nil {
|
|
mTime = newEntry.Attributes.Mtime
|
|
} else if oldEntry != nil {
|
|
mTime = oldEntry.Attributes.Mtime
|
|
}
|
|
dateKey = time.Unix(mTime, 0).Format("2006-01-02")
|
|
}
|
|
oldSinkKey := r.sourceToSinkKey(key, dateKey)
|
|
glog.V(3).Infof("replicate %s => %s", key, oldSinkKey)
|
|
|
|
newSinkKey := oldSinkKey
|
|
newSinkParentPath := newParentPath
|
|
if oldEntry != nil && newEntry != nil {
|
|
targetSourceKey, targetSourceParent := metadataEventTarget(key, newEntry, newParentPath)
|
|
newSinkKey = r.sourceToSinkKey(targetSourceKey, dateKey)
|
|
newSinkParentPath = r.sourceToSinkPath(targetSourceParent, dateKey)
|
|
} else if newParentPath != "" && util.IsEqualOrUnder(newParentPath, r.source.Dir) {
|
|
newSinkParentPath = r.sourceToSinkPath(newParentPath, dateKey)
|
|
}
|
|
|
|
if oldEntry != nil && newEntry == nil {
|
|
glog.V(4).Infof("deleting %v", oldSinkKey)
|
|
return sink.DeleteEntryVersion(r.sink, oldSinkKey, oldEntry, 0, message.DeleteChunks, filer.IsMetadataOnlyDelete(oldEntry), message.Signatures)
|
|
}
|
|
if oldEntry == nil && newEntry != nil {
|
|
glog.V(4).Infof("creating %v", oldSinkKey)
|
|
return r.sink.CreateEntry(oldSinkKey, newEntry, message.Signatures)
|
|
}
|
|
if oldEntry == nil && newEntry == nil {
|
|
glog.V(0).Infof("weird message %v: no old or new entry", key)
|
|
return nil
|
|
}
|
|
|
|
if oldSinkKey != newSinkKey {
|
|
// A real move: the path changed. UpdateEntry cannot move an entry.
|
|
if mover, ok := r.sink.(sink.EntryMover); ok {
|
|
glog.V(4).Infof("moving %v => %v", oldSinkKey, newSinkKey)
|
|
return mover.MoveEntry(oldSinkKey, newSinkKey, oldEntry, newEntry, message.Signatures)
|
|
}
|
|
// Sinks without a native move: create at the new key first, then delete the
|
|
// old, so a crash between the two leaves the entry visible under both names
|
|
// rather than gone.
|
|
glog.V(4).Infof("creating renamed %v", newSinkKey)
|
|
if err := r.sink.CreateEntry(newSinkKey, newEntry, message.Signatures); err != nil {
|
|
return fmt.Errorf("create renamed entry %v: %w", newSinkKey, err)
|
|
}
|
|
return sink.DeleteEntryVersion(r.sink, oldSinkKey, oldEntry, 0, false, false, message.Signatures)
|
|
}
|
|
|
|
// oldSinkKey == newSinkKey: pure in-place update (same path, content/attrs changed).
|
|
foundExisting, err := r.sink.UpdateEntry(oldSinkKey, oldEntry, newSinkParentPath, newEntry, message.DeleteChunks, message.Signatures)
|
|
if foundExisting {
|
|
glog.V(4).Infof("updated %v", oldSinkKey)
|
|
return err
|
|
}
|
|
err = r.sink.DeleteEntry(oldSinkKey, oldEntry.IsDirectory, false, message.Signatures)
|
|
if err != nil {
|
|
return fmt.Errorf("delete old entry %v: %w", oldSinkKey, err)
|
|
}
|
|
glog.V(4).Infof("creating missing %v", newSinkKey)
|
|
return r.sink.CreateEntry(newSinkKey, newEntry, message.Signatures)
|
|
}
|
|
|
|
func (r *Replicator) isExcluded(path string) bool {
|
|
for _, excludeDir := range r.excludeDirs {
|
|
if util.IsEqualOrUnder(path, excludeDir) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (r *Replicator) sourceToSinkKey(sourceKey, dateKey string) string {
|
|
return util.Join(r.sink.GetSinkToDirectory(), dateKey, sourceKey[len(r.source.Dir):])
|
|
}
|
|
|
|
func (r *Replicator) sourceToSinkPath(sourcePath, dateKey string) string {
|
|
return util.Join(r.sink.GetSinkToDirectory(), dateKey, sourcePath[len(r.source.Dir):])
|
|
}
|
|
|
|
func metadataEventTarget(key string, newEntry *filer_pb.Entry, newParentPath string) (targetKey, targetParent string) {
|
|
if newEntry == nil {
|
|
return "", ""
|
|
}
|
|
|
|
targetParent = newParentPath
|
|
if targetParent == "" {
|
|
targetParent, _ = util.FullPath(key).DirAndName()
|
|
}
|
|
|
|
return util.Join(targetParent, newEntry.Name), targetParent
|
|
}
|
|
|
|
func ReadFilerSignature(grpcDialOption grpc.DialOption, filer pb.ServerAddress) (filerSignature int32, readErr error) {
|
|
if readErr = pb.WithFilerClient(false, 0, filer, grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
|
|
if resp, err := client.GetFilerConfiguration(context.Background(), &filer_pb.GetFilerConfigurationRequest{}); err != nil {
|
|
return fmt.Errorf("GetFilerConfiguration %s: %v", filer, err)
|
|
} else {
|
|
filerSignature = resp.Signature
|
|
}
|
|
return nil
|
|
}); readErr != nil {
|
|
return 0, readErr
|
|
}
|
|
return filerSignature, nil
|
|
}
|