Browse Source
Adjust rename events metadata format (#8854)
Adjust rename events metadata format (#8854)
* rename metadata events * fix subscription filter to use NewEntry.Name for rename path matching The server-side subscription filter constructed the new path using OldEntry.Name instead of NewEntry.Name when checking if a rename event's destination matches the subscriber's path prefix. This could cause events to be incorrectly filtered when a rename changes the file name. * fix bucket events to handle rename of bucket directories onBucketEvents only checked IsCreate and IsDelete. A bucket directory rename via AtomicRenameEntry now emits a single rename event (both OldEntry and NewEntry non-nil), which matched neither check. Handle IsRename by deleting the old bucket and creating the new one. * fix replicator to handle rename events across directory boundaries Two issues fixed: 1. The replicator filtered events by checking if the key (old path) was under the source directory. Rename events now use the old path as key, so renames from outside into the watched directory were silently dropped. Now both old and new paths are checked, and cross-boundary renames are converted to create or delete. 2. NewParentPath was passed to the sink without remapping to the sink's target directory structure, causing the sink to write entries at the wrong location. Now NewParentPath is remapped alongside the key. * fix filer sync to handle rename events crossing directory boundaries The early directory-prefix filter only checked resp.Directory (old parent). Rename events now carry the old parent as Directory, so renames from outside the source path into it were dropped before reaching the existing cross-boundary handling logic. Check both old and new directories against sourcePath and excludePaths so the downstream old-key/new-key logic can properly convert these to create or delete operations. * fix metadata event path matching * fix metadata event consumers for rename targets * Fix replication rename target keys Logical rename events now reach replication sinks with distinct source and target paths.\n\nHandle non-filer sinks as delete-plus-create on the translated target key, and make the rename fallback path create at the translated target key too.\n\nAdd focused tests covering non-filer renames, filer rename updates, and the fallback path.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix filer sync rename path scoping Use directory-boundary matching instead of raw prefix checks when classifying source and target paths during filer sync.\n\nAlso apply excludePaths per side so renames across excluded boundaries downgrade cleanly to create/delete instead of being misclassified as in-scope updates.\n\nAdd focused tests for boundary matching and rename classification.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix replicator directory boundary checks Use directory-boundary matching instead of raw prefix checks when deciding whether a source or target path is inside the watched tree or an excluded subtree.\n\nThis prevents sibling paths such as /foo and /foobar from being misclassified during rename handling, and preserves the earlier rename-target-key fix.\n\nAdd focused tests for boundary matching and rename classification across sibling/excluded directories.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix etc-remote rename-out handling Use boundary-safe source/target directory membership when classifying metadata events under DirectoryEtcRemote.\n\nThis prevents rename-out events from being processed as config updates, while still treating them as removals where appropriate for the remote sync and remote gateway command paths.\n\nAdd focused tests for update/removal classification and sibling-prefix handling.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Defer rename events until commit Queue logical rename metadata events during atomic and streaming renames and publish them only after the transaction commits successfully.\n\nThis prevents subscribers from seeing delete or logical rename events for operations that later fail during delete or commit.\n\nAlso serialize notification.Queue swaps in rename tests and add failure-path coverage.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Skip descendant rename target lookups Avoid redundant target lookups during recursive directory renames once the destination subtree is known absent.\n\nThe recursive move path now inserts known-absent descendants directly, and the test harness exercises prefixed directory listing so the optimization is covered by a directory rename regression test.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Tighten rename review tests Return filer_pb.ErrNotFound from the bucket tracking store test stub so it follows the FilerStore contract, and add a webhook filter case for same-name renames across parent directories.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix HardLinkId format verb in InsertEntryKnownAbsent error HardLinkId is a byte slice. %d prints each byte as a decimal number which is not useful for an identifier. Use %x to match the log line two lines above. * only skip descendant target lookup when source and dest use same store moveFolderSubEntries unconditionally passed skipTargetLookup=true for every descendant. This is safe when all paths resolve to the same underlying store, but with path-specific store configuration a child's destination may map to a different backend that already holds an entry at that path. Use FilerStoreWrapper.SameActualStore to check per-child and fall back to the full CreateEntry path when stores differ. * add nil and create edge-case tests for metadata event scope helpers * extract pathIsEqualOrUnder into util.IsEqualOrUnder Identical implementations existed in both replication/replicator.go and command/filer_sync.go. Move to util.IsEqualOrUnder (alongside the existing FullPath.IsUnder) and remove the duplicates. * use MetadataEventTargetDirectory for new-side directory in filer sync The new-side directory checks and sourceNewKey computation used message.NewParentPath directly. If NewParentPath were empty (legacy events, older filer versions during rolling upgrades), sourceNewKey would be wrong (/filename instead of /dir/filename) and the UpdateEntry parent path rewrite would panic on slice bounds. Derive targetDir once from MetadataEventTargetDirectory, which falls back to resp.Directory when NewParentPath is empty, and use it consistently for all new-side checks and the sink parent path.pull/8859/head
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1845 additions and 247 deletions
-
10weed/command/filer_remote_gateway_buckets.go
-
55weed/command/filer_remote_sync_dir.go
-
110weed/command/filer_sync.go
-
121weed/command/filer_sync_process_test.go
-
36weed/command/metadata_event_scope.go
-
116weed/command/metadata_event_scope_test.go
-
4weed/filer/filer_notify.go
-
32weed/filer/filer_on_meta_event.go
-
70weed/filer/filer_on_meta_event_test.go
-
34weed/filer/filerstore_wrapper.go
-
20weed/filer/metadata_event_context.go
-
20weed/mount/filer_conf.go
-
23weed/mount/filer_conf_test.go
-
21weed/notification/webhook/filter.go
-
63weed/notification/webhook/filter_test.go
-
12weed/notification/webhook/types.go
-
114weed/pb/filer_pb/filer_pb_helper.go
-
70weed/pb/filer_pb/filer_pb_helper_test.go
-
51weed/pb/filer_pb_direct_read.go
-
124weed/replication/replicator.go
-
284weed/replication/replicator_test.go
-
74weed/server/filer_grpc_server_rename.go
-
472weed/server/filer_grpc_server_rename_test.go
-
54weed/server/filer_grpc_server_sub_meta.go
-
77weed/server/filer_grpc_server_sub_meta_test.go
-
25weed/util/fullpath.go
@ -0,0 +1,121 @@ |
|||||
|
package command |
||||
|
|
||||
|
import ( |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/replication/sink" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/replication/source" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
) |
||||
|
|
||||
|
var _ sink.ReplicationSink = (*recordingSyncSink)(nil) |
||||
|
|
||||
|
type recordingSyncSink struct { |
||||
|
deleteKeys []string |
||||
|
createKeys []string |
||||
|
updateKeys []string |
||||
|
} |
||||
|
|
||||
|
func (s *recordingSyncSink) GetName() string { return "recording" } |
||||
|
func (s *recordingSyncSink) Initialize(util.Configuration, string) error { |
||||
|
return nil |
||||
|
} |
||||
|
func (s *recordingSyncSink) DeleteEntry(key string, isDirectory, deleteIncludeChunks bool, signatures []int32) error { |
||||
|
s.deleteKeys = append(s.deleteKeys, key) |
||||
|
return nil |
||||
|
} |
||||
|
func (s *recordingSyncSink) CreateEntry(key string, entry *filer_pb.Entry, signatures []int32) error { |
||||
|
s.createKeys = append(s.createKeys, key) |
||||
|
return nil |
||||
|
} |
||||
|
func (s *recordingSyncSink) UpdateEntry(key string, oldEntry *filer_pb.Entry, newParentPath string, newEntry *filer_pb.Entry, deleteIncludeChunks bool, signatures []int32) (bool, error) { |
||||
|
s.updateKeys = append(s.updateKeys, key) |
||||
|
return true, nil |
||||
|
} |
||||
|
func (s *recordingSyncSink) GetSinkToDirectory() string { return "/dest" } |
||||
|
func (s *recordingSyncSink) SetSourceFiler(*source.FilerSource) {} |
||||
|
func (s *recordingSyncSink) IsIncremental() bool { return false } |
||||
|
|
||||
|
func TestPathIsEqualOrUnderUsesDirectoryBoundaries(t *testing.T) { |
||||
|
tests := []struct { |
||||
|
name string |
||||
|
candidate string |
||||
|
other string |
||||
|
expected bool |
||||
|
}{ |
||||
|
{name: "equal", candidate: "/foo", other: "/foo", expected: true}, |
||||
|
{name: "descendant", candidate: "/foo/bar", other: "/foo", expected: true}, |
||||
|
{name: "sibling prefix", candidate: "/foobar/bar", other: "/foo", expected: false}, |
||||
|
{name: "root", candidate: "/foo/bar", other: "/", expected: true}, |
||||
|
{name: "empty", candidate: "", other: "/foo", expected: false}, |
||||
|
} |
||||
|
|
||||
|
for _, tt := range tests { |
||||
|
t.Run(tt.name, func(t *testing.T) { |
||||
|
if got := util.IsEqualOrUnder(tt.candidate, tt.other); got != tt.expected { |
||||
|
t.Fatalf("IsEqualOrUnder(%q, %q) = %v, want %v", tt.candidate, tt.other, got, tt.expected) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestMatchesExcludePathUsesDirectoryBoundaries(t *testing.T) { |
||||
|
if !matchesExcludePath("/tmp", []string{"/tmp"}) { |
||||
|
t.Fatal("expected exact directory match to be excluded") |
||||
|
} |
||||
|
if !matchesExcludePath("/tmp/sub", []string{"/tmp"}) { |
||||
|
t.Fatal("expected descendant directory to be excluded") |
||||
|
} |
||||
|
if matchesExcludePath("/tmp2/sub", []string{"/tmp"}) { |
||||
|
t.Fatal("did not expect sibling directory to be excluded") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestGenProcessFunctionRenameToSiblingPrefixBecomesDelete(t *testing.T) { |
||||
|
dataSink := &recordingSyncSink{} |
||||
|
processFn := genProcessFunction("/foo", "/dest", nil, nil, nil, nil, dataSink, true, false) |
||||
|
|
||||
|
err := processFn(&filer_pb.SubscribeMetadataResponse{ |
||||
|
Directory: "/foo/dir", |
||||
|
EventNotification: &filer_pb.EventNotification{ |
||||
|
OldEntry: &filer_pb.Entry{Name: "file.txt"}, |
||||
|
NewEntry: &filer_pb.Entry{Name: "file.txt"}, |
||||
|
NewParentPath: "/foobar/dir", |
||||
|
}, |
||||
|
}) |
||||
|
if err != nil { |
||||
|
t.Fatalf("processFn rename to sibling prefix: %v", err) |
||||
|
} |
||||
|
|
||||
|
if len(dataSink.deleteKeys) != 1 || dataSink.deleteKeys[0] != "/dest/dir/file.txt" { |
||||
|
t.Fatalf("delete keys = %v, want [/dest/dir/file.txt]", dataSink.deleteKeys) |
||||
|
} |
||||
|
if len(dataSink.createKeys) != 0 || len(dataSink.updateKeys) != 0 { |
||||
|
t.Fatalf("unexpected create/update calls: creates=%v updates=%v", dataSink.createKeys, dataSink.updateKeys) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestGenProcessFunctionRenameFromExcludedDirBecomesCreate(t *testing.T) { |
||||
|
dataSink := &recordingSyncSink{} |
||||
|
processFn := genProcessFunction("/foo", "/dest", []string{"/foo/excluded"}, nil, nil, nil, dataSink, true, false) |
||||
|
|
||||
|
err := processFn(&filer_pb.SubscribeMetadataResponse{ |
||||
|
Directory: "/foo/excluded", |
||||
|
EventNotification: &filer_pb.EventNotification{ |
||||
|
OldEntry: &filer_pb.Entry{Name: "file.txt"}, |
||||
|
NewEntry: &filer_pb.Entry{Name: "file.txt"}, |
||||
|
NewParentPath: "/foo/live", |
||||
|
}, |
||||
|
}) |
||||
|
if err != nil { |
||||
|
t.Fatalf("processFn rename from excluded dir: %v", err) |
||||
|
} |
||||
|
|
||||
|
if len(dataSink.createKeys) != 1 || dataSink.createKeys[0] != "/dest/live/file.txt" { |
||||
|
t.Fatalf("create keys = %v, want [/dest/live/file.txt]", dataSink.createKeys) |
||||
|
} |
||||
|
if len(dataSink.deleteKeys) != 0 || len(dataSink.updateKeys) != 0 { |
||||
|
t.Fatalf("unexpected delete/update calls: deletes=%v updates=%v", dataSink.deleteKeys, dataSink.updateKeys) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
package command |
||||
|
|
||||
|
import ( |
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
) |
||||
|
|
||||
|
func metadataEventDirectoryMembership(resp *filer_pb.SubscribeMetadataResponse, dir string) (sourceInDir, targetInDir bool) { |
||||
|
if resp == nil || resp.EventNotification == nil { |
||||
|
return false, false |
||||
|
} |
||||
|
|
||||
|
sourceInDir = util.IsEqualOrUnder(resp.Directory, dir) |
||||
|
targetInDir = resp.EventNotification.NewEntry != nil && |
||||
|
util.IsEqualOrUnder(filer_pb.MetadataEventTargetDirectory(resp), dir) |
||||
|
|
||||
|
return sourceInDir, targetInDir |
||||
|
} |
||||
|
|
||||
|
func metadataEventUpdatesDirectory(resp *filer_pb.SubscribeMetadataResponse, dir string) bool { |
||||
|
if resp == nil || resp.EventNotification == nil || resp.EventNotification.NewEntry == nil { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
_, targetInDir := metadataEventDirectoryMembership(resp, dir) |
||||
|
return targetInDir |
||||
|
} |
||||
|
|
||||
|
func metadataEventRemovesFromDirectory(resp *filer_pb.SubscribeMetadataResponse, dir string) bool { |
||||
|
if resp == nil || resp.EventNotification == nil || resp.EventNotification.OldEntry == nil { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
sourceInDir, targetInDir := metadataEventDirectoryMembership(resp, dir) |
||||
|
return sourceInDir && !targetInDir |
||||
|
} |
||||
@ -0,0 +1,116 @@ |
|||||
|
package command |
||||
|
|
||||
|
import ( |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/filer" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
||||
|
) |
||||
|
|
||||
|
func TestMetadataEventDirectoryMembershipUsesDirectoryBoundaries(t *testing.T) { |
||||
|
resp := &filer_pb.SubscribeMetadataResponse{ |
||||
|
Directory: filer.DirectoryEtcRemote, |
||||
|
EventNotification: &filer_pb.EventNotification{ |
||||
|
OldEntry: &filer_pb.Entry{Name: "remote.conf"}, |
||||
|
NewEntry: &filer_pb.Entry{Name: "remote.conf"}, |
||||
|
NewParentPath: "/etc/remote-sibling", |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
sourceInDir, targetInDir := metadataEventDirectoryMembership(resp, filer.DirectoryEtcRemote) |
||||
|
if !sourceInDir { |
||||
|
t.Fatal("expected source directory to match") |
||||
|
} |
||||
|
if targetInDir { |
||||
|
t.Fatal("did not expect sibling target directory to match") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestMetadataEventUpdatesAndRemovesDirectory(t *testing.T) { |
||||
|
tests := []struct { |
||||
|
name string |
||||
|
resp *filer_pb.SubscribeMetadataResponse |
||||
|
wantUpdate bool |
||||
|
wantRemoval bool |
||||
|
}{ |
||||
|
{ |
||||
|
name: "nil response", |
||||
|
resp: nil, |
||||
|
wantUpdate: false, |
||||
|
wantRemoval: false, |
||||
|
}, |
||||
|
{ |
||||
|
name: "create event", |
||||
|
resp: &filer_pb.SubscribeMetadataResponse{ |
||||
|
Directory: filer.DirectoryEtcRemote, |
||||
|
EventNotification: &filer_pb.EventNotification{ |
||||
|
NewEntry: &filer_pb.Entry{Name: "new.conf"}, |
||||
|
NewParentPath: filer.DirectoryEtcRemote, |
||||
|
}, |
||||
|
}, |
||||
|
wantUpdate: true, |
||||
|
wantRemoval: false, |
||||
|
}, |
||||
|
{ |
||||
|
name: "rename out", |
||||
|
resp: &filer_pb.SubscribeMetadataResponse{ |
||||
|
Directory: filer.DirectoryEtcRemote, |
||||
|
EventNotification: &filer_pb.EventNotification{ |
||||
|
OldEntry: &filer_pb.Entry{Name: "remote.conf"}, |
||||
|
NewEntry: &filer_pb.Entry{Name: "remote.conf"}, |
||||
|
NewParentPath: "/tmp", |
||||
|
}, |
||||
|
}, |
||||
|
wantUpdate: false, |
||||
|
wantRemoval: true, |
||||
|
}, |
||||
|
{ |
||||
|
name: "rename into", |
||||
|
resp: &filer_pb.SubscribeMetadataResponse{ |
||||
|
Directory: "/tmp", |
||||
|
EventNotification: &filer_pb.EventNotification{ |
||||
|
OldEntry: &filer_pb.Entry{Name: "remote.conf"}, |
||||
|
NewEntry: &filer_pb.Entry{Name: "remote.conf"}, |
||||
|
NewParentPath: filer.DirectoryEtcRemote, |
||||
|
}, |
||||
|
}, |
||||
|
wantUpdate: true, |
||||
|
wantRemoval: false, |
||||
|
}, |
||||
|
{ |
||||
|
name: "rename within", |
||||
|
resp: &filer_pb.SubscribeMetadataResponse{ |
||||
|
Directory: filer.DirectoryEtcRemote, |
||||
|
EventNotification: &filer_pb.EventNotification{ |
||||
|
OldEntry: &filer_pb.Entry{Name: "remote.conf"}, |
||||
|
NewEntry: &filer_pb.Entry{Name: "renamed.conf"}, |
||||
|
NewParentPath: filer.DirectoryEtcRemote, |
||||
|
}, |
||||
|
}, |
||||
|
wantUpdate: true, |
||||
|
wantRemoval: false, |
||||
|
}, |
||||
|
{ |
||||
|
name: "delete", |
||||
|
resp: &filer_pb.SubscribeMetadataResponse{ |
||||
|
Directory: filer.DirectoryEtcRemote, |
||||
|
EventNotification: &filer_pb.EventNotification{ |
||||
|
OldEntry: &filer_pb.Entry{Name: "remote.conf"}, |
||||
|
}, |
||||
|
}, |
||||
|
wantUpdate: false, |
||||
|
wantRemoval: true, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
for _, tt := range tests { |
||||
|
t.Run(tt.name, func(t *testing.T) { |
||||
|
if got := metadataEventUpdatesDirectory(tt.resp, filer.DirectoryEtcRemote); got != tt.wantUpdate { |
||||
|
t.Fatalf("metadataEventUpdatesDirectory() = %v, want %v", got, tt.wantUpdate) |
||||
|
} |
||||
|
if got := metadataEventRemovesFromDirectory(tt.resp, filer.DirectoryEtcRemote); got != tt.wantRemoval { |
||||
|
t.Fatalf("metadataEventRemovesFromDirectory() = %v, want %v", got, tt.wantRemoval) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,70 @@ |
|||||
|
package filer |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
) |
||||
|
|
||||
|
type bucketTrackingStore struct { |
||||
|
created []string |
||||
|
deleted []string |
||||
|
} |
||||
|
|
||||
|
func (s *bucketTrackingStore) GetName() string { return "bucket-tracking" } |
||||
|
func (s *bucketTrackingStore) Initialize(configuration util.Configuration, prefix string) error { |
||||
|
return nil |
||||
|
} |
||||
|
func (s *bucketTrackingStore) InsertEntry(context.Context, *Entry) error { return nil } |
||||
|
func (s *bucketTrackingStore) UpdateEntry(context.Context, *Entry) error { return nil } |
||||
|
func (s *bucketTrackingStore) FindEntry(context.Context, util.FullPath) (*Entry, error) { |
||||
|
return nil, filer_pb.ErrNotFound |
||||
|
} |
||||
|
func (s *bucketTrackingStore) DeleteEntry(context.Context, util.FullPath) error { return nil } |
||||
|
func (s *bucketTrackingStore) DeleteFolderChildren(context.Context, util.FullPath) error { return nil } |
||||
|
func (s *bucketTrackingStore) ListDirectoryEntries(context.Context, util.FullPath, string, bool, int64, ListEachEntryFunc) (string, error) { |
||||
|
return "", nil |
||||
|
} |
||||
|
func (s *bucketTrackingStore) ListDirectoryPrefixedEntries(context.Context, util.FullPath, string, bool, int64, string, ListEachEntryFunc) (string, error) { |
||||
|
return "", nil |
||||
|
} |
||||
|
func (s *bucketTrackingStore) BeginTransaction(ctx context.Context) (context.Context, error) { |
||||
|
return ctx, nil |
||||
|
} |
||||
|
func (s *bucketTrackingStore) CommitTransaction(context.Context) error { return nil } |
||||
|
func (s *bucketTrackingStore) RollbackTransaction(context.Context) error { return nil } |
||||
|
func (s *bucketTrackingStore) KvPut(context.Context, []byte, []byte) error { return nil } |
||||
|
func (s *bucketTrackingStore) KvGet(context.Context, []byte) ([]byte, error) { |
||||
|
return nil, ErrKvNotFound |
||||
|
} |
||||
|
func (s *bucketTrackingStore) KvDelete(context.Context, []byte) error { return nil } |
||||
|
func (s *bucketTrackingStore) Shutdown() {} |
||||
|
func (s *bucketTrackingStore) OnBucketCreation(bucket string) { s.created = append(s.created, bucket) } |
||||
|
func (s *bucketTrackingStore) OnBucketDeletion(bucket string) { s.deleted = append(s.deleted, bucket) } |
||||
|
func (s *bucketTrackingStore) CanDropWholeBucket() bool { return false } |
||||
|
|
||||
|
func TestOnBucketEventsRenameIntoBucketsRootCreatesBucket(t *testing.T) { |
||||
|
store := &bucketTrackingStore{} |
||||
|
f := &Filer{ |
||||
|
DirBucketsPath: "/buckets", |
||||
|
Store: NewFilerStoreWrapper(store), |
||||
|
} |
||||
|
|
||||
|
f.onBucketEvents(&filer_pb.SubscribeMetadataResponse{ |
||||
|
Directory: "/tmp", |
||||
|
EventNotification: &filer_pb.EventNotification{ |
||||
|
OldEntry: &filer_pb.Entry{Name: "migrated", IsDirectory: true}, |
||||
|
NewEntry: &filer_pb.Entry{Name: "migrated", IsDirectory: true}, |
||||
|
NewParentPath: "/buckets", |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
if len(store.created) != 1 || store.created[0] != "migrated" { |
||||
|
t.Fatalf("created buckets = %v, want [migrated]", store.created) |
||||
|
} |
||||
|
if len(store.deleted) != 0 { |
||||
|
t.Fatalf("deleted buckets = %v, want []", store.deleted) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
package filer |
||||
|
|
||||
|
import "context" |
||||
|
|
||||
|
type suppressMetadataEventsKey struct{} |
||||
|
|
||||
|
// WithSuppressedMetadataEvents disables automatic metadata event emission for
|
||||
|
// nested filer operations that are part of a larger logical change, such as a
|
||||
|
// rename implemented via create+delete.
|
||||
|
func WithSuppressedMetadataEvents(ctx context.Context) context.Context { |
||||
|
return context.WithValue(ctx, suppressMetadataEventsKey{}, true) |
||||
|
} |
||||
|
|
||||
|
func metadataEventsSuppressed(ctx context.Context) bool { |
||||
|
if ctx == nil { |
||||
|
return false |
||||
|
} |
||||
|
suppressed, _ := ctx.Value(suppressMetadataEventsKey{}).(bool) |
||||
|
return suppressed |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
package mount |
||||
|
|
||||
|
import ( |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/filer" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
||||
|
) |
||||
|
|
||||
|
func TestIsFilerConfUpdateEventMatchesRenameTarget(t *testing.T) { |
||||
|
event := &filer_pb.SubscribeMetadataResponse{ |
||||
|
Directory: "/tmp", |
||||
|
EventNotification: &filer_pb.EventNotification{ |
||||
|
OldEntry: &filer_pb.Entry{Name: filer.FilerConfName}, |
||||
|
NewEntry: &filer_pb.Entry{Name: filer.FilerConfName}, |
||||
|
NewParentPath: filer.DirectoryEtcSeaweedFS, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
if !isFilerConfUpdateEvent(event, filer.DirectoryEtcSeaweedFS, filer.FilerConfName) { |
||||
|
t.Fatalf("expected rename target to match filer.conf watcher") |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,284 @@ |
|||||
|
package replication |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/replication/sink" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/replication/source" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
) |
||||
|
|
||||
|
var _ sink.ReplicationSink = (*recordingSink)(nil) |
||||
|
|
||||
|
type deleteCall struct { |
||||
|
key string |
||||
|
isDirectory bool |
||||
|
} |
||||
|
|
||||
|
type createCall struct { |
||||
|
key string |
||||
|
} |
||||
|
|
||||
|
type updateCall struct { |
||||
|
key string |
||||
|
newParentPath string |
||||
|
} |
||||
|
|
||||
|
type recordingSink struct { |
||||
|
name string |
||||
|
sinkToDirectory string |
||||
|
incremental bool |
||||
|
updateFoundExisting bool |
||||
|
|
||||
|
deleteCalls []deleteCall |
||||
|
createCalls []createCall |
||||
|
updateCalls []updateCall |
||||
|
} |
||||
|
|
||||
|
func (s *recordingSink) GetName() string { |
||||
|
return s.name |
||||
|
} |
||||
|
|
||||
|
func (s *recordingSink) Initialize(util.Configuration, string) error { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (s *recordingSink) DeleteEntry(key string, isDirectory, deleteIncludeChunks bool, signatures []int32) error { |
||||
|
s.deleteCalls = append(s.deleteCalls, deleteCall{key: key, isDirectory: isDirectory}) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (s *recordingSink) CreateEntry(key string, entry *filer_pb.Entry, signatures []int32) error { |
||||
|
s.createCalls = append(s.createCalls, createCall{key: key}) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (s *recordingSink) UpdateEntry(key string, oldEntry *filer_pb.Entry, newParentPath string, newEntry *filer_pb.Entry, deleteIncludeChunks bool, signatures []int32) (bool, error) { |
||||
|
s.updateCalls = append(s.updateCalls, updateCall{key: key, newParentPath: newParentPath}) |
||||
|
return s.updateFoundExisting, nil |
||||
|
} |
||||
|
|
||||
|
func (s *recordingSink) GetSinkToDirectory() string { |
||||
|
return s.sinkToDirectory |
||||
|
} |
||||
|
|
||||
|
func (s *recordingSink) SetSourceFiler(*source.FilerSource) {} |
||||
|
|
||||
|
func (s *recordingSink) IsIncremental() bool { |
||||
|
return s.incremental |
||||
|
} |
||||
|
|
||||
|
func TestReplicateRenameUsesTargetKeyForNonFilerSink(t *testing.T) { |
||||
|
s := &recordingSink{name: "local", sinkToDirectory: "/dest"} |
||||
|
r := &Replicator{ |
||||
|
sink: s, |
||||
|
source: &source.FilerSource{Dir: "/source"}, |
||||
|
} |
||||
|
|
||||
|
err := r.Replicate(context.Background(), "/source/old/file.txt", &filer_pb.EventNotification{ |
||||
|
OldEntry: &filer_pb.Entry{ |
||||
|
Name: "file.txt", |
||||
|
Attributes: &filer_pb.FuseAttributes{ |
||||
|
Mtime: 123, |
||||
|
}, |
||||
|
}, |
||||
|
NewEntry: &filer_pb.Entry{ |
||||
|
Name: "renamed.txt", |
||||
|
Attributes: &filer_pb.FuseAttributes{ |
||||
|
Mtime: 123, |
||||
|
}, |
||||
|
}, |
||||
|
NewParentPath: "/source/new", |
||||
|
}) |
||||
|
if err != nil { |
||||
|
t.Fatalf("Replicate rename: %v", err) |
||||
|
} |
||||
|
|
||||
|
if len(s.updateCalls) != 0 { |
||||
|
t.Fatalf("expected non-filer rename to bypass UpdateEntry, got %d calls", len(s.updateCalls)) |
||||
|
} |
||||
|
if len(s.deleteCalls) != 1 || s.deleteCalls[0].key != "/dest/old/file.txt" { |
||||
|
t.Fatalf("delete calls = %+v, want old sink key", s.deleteCalls) |
||||
|
} |
||||
|
if len(s.createCalls) != 1 || s.createCalls[0].key != "/dest/new/renamed.txt" { |
||||
|
t.Fatalf("create calls = %+v, want target sink key", s.createCalls) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestReplicateRenameUsesUpdateForFilerSink(t *testing.T) { |
||||
|
s := &recordingSink{ |
||||
|
name: "filer", |
||||
|
sinkToDirectory: "/dest", |
||||
|
updateFoundExisting: true, |
||||
|
} |
||||
|
r := &Replicator{ |
||||
|
sink: s, |
||||
|
source: &source.FilerSource{Dir: "/source"}, |
||||
|
} |
||||
|
|
||||
|
err := r.Replicate(context.Background(), "/source/old/file.txt", &filer_pb.EventNotification{ |
||||
|
OldEntry: &filer_pb.Entry{ |
||||
|
Name: "file.txt", |
||||
|
Attributes: &filer_pb.FuseAttributes{ |
||||
|
Mtime: 123, |
||||
|
}, |
||||
|
}, |
||||
|
NewEntry: &filer_pb.Entry{ |
||||
|
Name: "renamed.txt", |
||||
|
Attributes: &filer_pb.FuseAttributes{ |
||||
|
Mtime: 123, |
||||
|
}, |
||||
|
}, |
||||
|
NewParentPath: "/source/new", |
||||
|
}) |
||||
|
if err != nil { |
||||
|
t.Fatalf("Replicate rename: %v", err) |
||||
|
} |
||||
|
|
||||
|
if len(s.updateCalls) != 1 { |
||||
|
t.Fatalf("update calls = %d, want 1", len(s.updateCalls)) |
||||
|
} |
||||
|
if s.updateCalls[0].key != "/dest/old/file.txt" { |
||||
|
t.Fatalf("update key = %q, want /dest/old/file.txt", s.updateCalls[0].key) |
||||
|
} |
||||
|
if s.updateCalls[0].newParentPath != "/dest/new" { |
||||
|
t.Fatalf("update newParentPath = %q, want /dest/new", s.updateCalls[0].newParentPath) |
||||
|
} |
||||
|
if len(s.deleteCalls) != 0 || len(s.createCalls) != 0 { |
||||
|
t.Fatalf("unexpected delete/create calls: deletes=%+v creates=%+v", s.deleteCalls, s.createCalls) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestReplicateRenameFallbackCreatesTargetKey(t *testing.T) { |
||||
|
s := &recordingSink{ |
||||
|
name: "filer", |
||||
|
sinkToDirectory: "/dest", |
||||
|
updateFoundExisting: false, |
||||
|
} |
||||
|
r := &Replicator{ |
||||
|
sink: s, |
||||
|
source: &source.FilerSource{Dir: "/source"}, |
||||
|
} |
||||
|
|
||||
|
err := r.Replicate(context.Background(), "/source/old/file.txt", &filer_pb.EventNotification{ |
||||
|
OldEntry: &filer_pb.Entry{ |
||||
|
Name: "file.txt", |
||||
|
Attributes: &filer_pb.FuseAttributes{ |
||||
|
Mtime: 123, |
||||
|
}, |
||||
|
}, |
||||
|
NewEntry: &filer_pb.Entry{ |
||||
|
Name: "renamed.txt", |
||||
|
Attributes: &filer_pb.FuseAttributes{ |
||||
|
Mtime: 123, |
||||
|
}, |
||||
|
}, |
||||
|
NewParentPath: "/source/new", |
||||
|
}) |
||||
|
if err != nil { |
||||
|
t.Fatalf("Replicate rename fallback: %v", err) |
||||
|
} |
||||
|
|
||||
|
if len(s.updateCalls) != 1 { |
||||
|
t.Fatalf("update calls = %d, want 1", len(s.updateCalls)) |
||||
|
} |
||||
|
if len(s.deleteCalls) != 1 || s.deleteCalls[0].key != "/dest/old/file.txt" { |
||||
|
t.Fatalf("delete calls = %+v, want old sink key", s.deleteCalls) |
||||
|
} |
||||
|
if len(s.createCalls) != 1 || s.createCalls[0].key != "/dest/new/renamed.txt" { |
||||
|
t.Fatalf("create calls = %+v, want target sink key", s.createCalls) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestPathIsEqualOrUnderUsesDirectoryBoundaries(t *testing.T) { |
||||
|
tests := []struct { |
||||
|
name string |
||||
|
candidate string |
||||
|
other string |
||||
|
expected bool |
||||
|
}{ |
||||
|
{name: "equal", candidate: "/foo", other: "/foo", expected: true}, |
||||
|
{name: "descendant", candidate: "/foo/bar", other: "/foo", expected: true}, |
||||
|
{name: "sibling prefix", candidate: "/foobar/bar", other: "/foo", expected: false}, |
||||
|
{name: "root", candidate: "/foo/bar", other: "/", expected: true}, |
||||
|
} |
||||
|
|
||||
|
for _, tt := range tests { |
||||
|
t.Run(tt.name, func(t *testing.T) { |
||||
|
if got := util.IsEqualOrUnder(tt.candidate, tt.other); got != tt.expected { |
||||
|
t.Fatalf("IsEqualOrUnder(%q, %q) = %v, want %v", tt.candidate, tt.other, got, tt.expected) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestReplicateRenameOutToSiblingPrefixBecomesDelete(t *testing.T) { |
||||
|
s := &recordingSink{name: "local", sinkToDirectory: "/dest"} |
||||
|
r := &Replicator{ |
||||
|
sink: s, |
||||
|
source: &source.FilerSource{Dir: "/foo"}, |
||||
|
} |
||||
|
|
||||
|
err := r.Replicate(context.Background(), "/foo/old/file.txt", &filer_pb.EventNotification{ |
||||
|
OldEntry: &filer_pb.Entry{ |
||||
|
Name: "file.txt", |
||||
|
Attributes: &filer_pb.FuseAttributes{ |
||||
|
Mtime: 123, |
||||
|
}, |
||||
|
}, |
||||
|
NewEntry: &filer_pb.Entry{ |
||||
|
Name: "file.txt", |
||||
|
Attributes: &filer_pb.FuseAttributes{ |
||||
|
Mtime: 123, |
||||
|
}, |
||||
|
}, |
||||
|
NewParentPath: "/foobar/new", |
||||
|
}) |
||||
|
if err != nil { |
||||
|
t.Fatalf("Replicate rename out to sibling prefix: %v", err) |
||||
|
} |
||||
|
|
||||
|
if len(s.deleteCalls) != 1 || s.deleteCalls[0].key != "/dest/old/file.txt" { |
||||
|
t.Fatalf("delete calls = %+v, want old sink key", s.deleteCalls) |
||||
|
} |
||||
|
if len(s.createCalls) != 0 || len(s.updateCalls) != 0 { |
||||
|
t.Fatalf("unexpected create/update calls: creates=%+v updates=%+v", s.createCalls, s.updateCalls) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestReplicateRenameFromExcludedDirBecomesCreate(t *testing.T) { |
||||
|
s := &recordingSink{name: "local", sinkToDirectory: "/dest"} |
||||
|
r := &Replicator{ |
||||
|
sink: s, |
||||
|
source: &source.FilerSource{Dir: "/foo"}, |
||||
|
excludeDirs: []string{"/foo/excluded"}, |
||||
|
} |
||||
|
|
||||
|
err := r.Replicate(context.Background(), "/foo/excluded/file.txt", &filer_pb.EventNotification{ |
||||
|
OldEntry: &filer_pb.Entry{ |
||||
|
Name: "file.txt", |
||||
|
Attributes: &filer_pb.FuseAttributes{ |
||||
|
Mtime: 123, |
||||
|
}, |
||||
|
}, |
||||
|
NewEntry: &filer_pb.Entry{ |
||||
|
Name: "file.txt", |
||||
|
Attributes: &filer_pb.FuseAttributes{ |
||||
|
Mtime: 123, |
||||
|
}, |
||||
|
}, |
||||
|
NewParentPath: "/foo/live", |
||||
|
}) |
||||
|
if err != nil { |
||||
|
t.Fatalf("Replicate rename from excluded dir: %v", err) |
||||
|
} |
||||
|
|
||||
|
if len(s.createCalls) != 1 || s.createCalls[0].key != "/dest/live/file.txt" { |
||||
|
t.Fatalf("create calls = %+v, want target sink key", s.createCalls) |
||||
|
} |
||||
|
if len(s.deleteCalls) != 0 || len(s.updateCalls) != 0 { |
||||
|
t.Fatalf("unexpected delete/update calls: deletes=%+v updates=%+v", s.deleteCalls, s.updateCalls) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,472 @@ |
|||||
|
package weed_server |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"errors" |
||||
|
"os" |
||||
|
"sort" |
||||
|
"strings" |
||||
|
"sync" |
||||
|
"testing" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/cluster" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/filer" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/notification" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util/log_buffer" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/wdclient" |
||||
|
"google.golang.org/grpc" |
||||
|
"google.golang.org/grpc/credentials/insecure" |
||||
|
"google.golang.org/protobuf/proto" |
||||
|
) |
||||
|
|
||||
|
type renameTestStore struct { |
||||
|
mu sync.Mutex |
||||
|
entries map[string]*filer.Entry |
||||
|
findCalls map[string]int |
||||
|
commitErr error |
||||
|
deleteErr error |
||||
|
} |
||||
|
|
||||
|
func newRenameTestStore() *renameTestStore { |
||||
|
return &renameTestStore{ |
||||
|
entries: make(map[string]*filer.Entry), |
||||
|
findCalls: make(map[string]int), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (s *renameTestStore) GetName() string { return "rename_test" } |
||||
|
func (s *renameTestStore) Initialize(util.Configuration, string) error { return nil } |
||||
|
func (s *renameTestStore) Shutdown() {} |
||||
|
func (s *renameTestStore) BeginTransaction(ctx context.Context) (context.Context, error) { |
||||
|
return ctx, nil |
||||
|
} |
||||
|
func (s *renameTestStore) CommitTransaction(context.Context) error { return s.commitErr } |
||||
|
func (s *renameTestStore) RollbackTransaction(context.Context) error { return nil } |
||||
|
func (s *renameTestStore) KvPut(context.Context, []byte, []byte) error { |
||||
|
return nil |
||||
|
} |
||||
|
func (s *renameTestStore) KvGet(context.Context, []byte) ([]byte, error) { |
||||
|
return nil, filer.ErrKvNotFound |
||||
|
} |
||||
|
func (s *renameTestStore) KvDelete(context.Context, []byte) error { return nil } |
||||
|
|
||||
|
func (s *renameTestStore) InsertEntry(_ context.Context, entry *filer.Entry) error { |
||||
|
s.mu.Lock() |
||||
|
defer s.mu.Unlock() |
||||
|
s.entries[string(entry.FullPath)] = entry.ShallowClone() |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (s *renameTestStore) UpdateEntry(_ context.Context, entry *filer.Entry) error { |
||||
|
s.mu.Lock() |
||||
|
defer s.mu.Unlock() |
||||
|
s.entries[string(entry.FullPath)] = entry.ShallowClone() |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (s *renameTestStore) FindEntry(_ context.Context, p util.FullPath) (*filer.Entry, error) { |
||||
|
s.mu.Lock() |
||||
|
defer s.mu.Unlock() |
||||
|
s.findCalls[string(p)]++ |
||||
|
entry, found := s.entries[string(p)] |
||||
|
if !found { |
||||
|
return nil, filer_pb.ErrNotFound |
||||
|
} |
||||
|
return entry.ShallowClone(), nil |
||||
|
} |
||||
|
|
||||
|
func (s *renameTestStore) DeleteEntry(_ context.Context, p util.FullPath) error { |
||||
|
if s.deleteErr != nil { |
||||
|
return s.deleteErr |
||||
|
} |
||||
|
s.mu.Lock() |
||||
|
defer s.mu.Unlock() |
||||
|
delete(s.entries, string(p)) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (s *renameTestStore) DeleteFolderChildren(_ context.Context, p util.FullPath) error { |
||||
|
s.mu.Lock() |
||||
|
defer s.mu.Unlock() |
||||
|
prefix := string(p) + "/" |
||||
|
for path := range s.entries { |
||||
|
if len(path) > len(prefix) && path[:len(prefix)] == prefix { |
||||
|
delete(s.entries, path) |
||||
|
} |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (s *renameTestStore) listDirectoryEntries(dirPath util.FullPath, startFileName string, includeStartFile bool, limit int64, prefix string, eachEntryFunc filer.ListEachEntryFunc) (string, error) { |
||||
|
s.mu.Lock() |
||||
|
var entries []*filer.Entry |
||||
|
for path, entry := range s.entries { |
||||
|
if path == string(dirPath) { |
||||
|
continue |
||||
|
} |
||||
|
parent, _ := util.FullPath(path).DirAndName() |
||||
|
if parent != string(dirPath) { |
||||
|
continue |
||||
|
} |
||||
|
if prefix != "" && !strings.HasPrefix(entry.Name(), prefix) { |
||||
|
continue |
||||
|
} |
||||
|
entries = append(entries, entry.ShallowClone()) |
||||
|
} |
||||
|
s.mu.Unlock() |
||||
|
|
||||
|
sort.Slice(entries, func(i, j int) bool { |
||||
|
return entries[i].Name() < entries[j].Name() |
||||
|
}) |
||||
|
|
||||
|
count := int64(0) |
||||
|
lastFileName := "" |
||||
|
for _, entry := range entries { |
||||
|
name := entry.Name() |
||||
|
if startFileName != "" { |
||||
|
if includeStartFile { |
||||
|
if name < startFileName { |
||||
|
continue |
||||
|
} |
||||
|
} else if name <= startFileName { |
||||
|
continue |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
lastFileName = name |
||||
|
if eachEntryFunc != nil { |
||||
|
includeMore, err := eachEntryFunc(entry) |
||||
|
if err != nil { |
||||
|
return lastFileName, err |
||||
|
} |
||||
|
if !includeMore { |
||||
|
return lastFileName, nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
count++ |
||||
|
if limit > 0 && count >= limit { |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return lastFileName, nil |
||||
|
} |
||||
|
|
||||
|
func (s *renameTestStore) ListDirectoryEntries(_ context.Context, dirPath util.FullPath, startFileName string, includeStartFile bool, limit int64, eachEntryFunc filer.ListEachEntryFunc) (string, error) { |
||||
|
return s.listDirectoryEntries(dirPath, startFileName, includeStartFile, limit, "", eachEntryFunc) |
||||
|
} |
||||
|
|
||||
|
func (s *renameTestStore) ListDirectoryPrefixedEntries(_ context.Context, dirPath util.FullPath, startFileName string, includeStartFile bool, limit int64, prefix string, eachEntryFunc filer.ListEachEntryFunc) (string, error) { |
||||
|
return s.listDirectoryEntries(dirPath, startFileName, includeStartFile, limit, prefix, eachEntryFunc) |
||||
|
} |
||||
|
|
||||
|
func (s *renameTestStore) findEntryCallCount(path string) int { |
||||
|
s.mu.Lock() |
||||
|
defer s.mu.Unlock() |
||||
|
return s.findCalls[path] |
||||
|
} |
||||
|
|
||||
|
type capturedEvent struct { |
||||
|
key string |
||||
|
notification *filer_pb.EventNotification |
||||
|
} |
||||
|
|
||||
|
type captureQueue struct { |
||||
|
mu sync.Mutex |
||||
|
events []capturedEvent |
||||
|
} |
||||
|
|
||||
|
var notificationQueueSwapMu sync.Mutex |
||||
|
|
||||
|
func (q *captureQueue) GetName() string { return "capture" } |
||||
|
func (q *captureQueue) Initialize(util.Configuration, string) error { return nil } |
||||
|
func (q *captureQueue) SendMessage(key string, message proto.Message) error { |
||||
|
notification, ok := message.(*filer_pb.EventNotification) |
||||
|
if !ok { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
q.mu.Lock() |
||||
|
defer q.mu.Unlock() |
||||
|
q.events = append(q.events, capturedEvent{ |
||||
|
key: key, |
||||
|
notification: proto.Clone(notification).(*filer_pb.EventNotification), |
||||
|
}) |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (q *captureQueue) snapshot() []capturedEvent { |
||||
|
q.mu.Lock() |
||||
|
defer q.mu.Unlock() |
||||
|
events := make([]capturedEvent, len(q.events)) |
||||
|
copy(events, q.events) |
||||
|
return events |
||||
|
} |
||||
|
|
||||
|
func swapNotificationQueue(t *testing.T, q notification.MessageQueue) { |
||||
|
t.Helper() |
||||
|
notificationQueueSwapMu.Lock() |
||||
|
prevQueue := notification.Queue |
||||
|
notification.Queue = q |
||||
|
t.Cleanup(func() { |
||||
|
notification.Queue = prevQueue |
||||
|
notificationQueueSwapMu.Unlock() |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
func newRenameTestFiler(store *renameTestStore) *filer.Filer { |
||||
|
dialOption := grpc.WithTransportCredentials(insecure.NewCredentials()) |
||||
|
masterClient := wdclient.NewMasterClient( |
||||
|
dialOption, |
||||
|
"test", |
||||
|
cluster.FilerType, |
||||
|
pb.ServerAddress("localhost:0"), |
||||
|
"", |
||||
|
"", |
||||
|
*pb.NewServiceDiscoveryFromMap(map[string]pb.ServerAddress{}), |
||||
|
) |
||||
|
|
||||
|
return &filer.Filer{ |
||||
|
Store: filer.NewFilerStoreWrapper(store), |
||||
|
MasterClient: masterClient, |
||||
|
FilerConf: filer.NewFilerConf(), |
||||
|
RemoteStorage: filer.NewFilerRemoteStorage(), |
||||
|
MaxFilenameLength: 255, |
||||
|
LocalMetaLogBuffer: log_buffer.NewLogBuffer( |
||||
|
"test", |
||||
|
time.Minute, |
||||
|
func(*log_buffer.LogBuffer, time.Time, time.Time, []byte, int64, int64) {}, |
||||
|
nil, |
||||
|
func() {}, |
||||
|
), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func newFileEntry(path string, inode uint64) *filer.Entry { |
||||
|
now := time.Unix(1700000000, 0) |
||||
|
return &filer.Entry{ |
||||
|
FullPath: util.FullPath(path), |
||||
|
Attr: filer.Attr{ |
||||
|
Mtime: now, |
||||
|
Crtime: now, |
||||
|
Mode: 0644, |
||||
|
Inode: inode, |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func newDirectoryEntry(path string, inode uint64) *filer.Entry { |
||||
|
now := time.Unix(1700000000, 0) |
||||
|
return &filer.Entry{ |
||||
|
FullPath: util.FullPath(path), |
||||
|
Attr: filer.Attr{ |
||||
|
Mtime: now, |
||||
|
Crtime: now, |
||||
|
Mode: os.ModeDir | 0755, |
||||
|
Inode: inode, |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestAtomicRenameEntryEmitsLogicalRenameEvent(t *testing.T) { |
||||
|
store := newRenameTestStore() |
||||
|
store.entries["/src.txt"] = newFileEntry("/src.txt", 101) |
||||
|
|
||||
|
queue := &captureQueue{} |
||||
|
swapNotificationQueue(t, queue) |
||||
|
|
||||
|
server := &FilerServer{filer: newRenameTestFiler(store)} |
||||
|
_, err := server.AtomicRenameEntry(context.Background(), &filer_pb.AtomicRenameEntryRequest{ |
||||
|
OldDirectory: "/", |
||||
|
OldName: "src.txt", |
||||
|
NewDirectory: "/", |
||||
|
NewName: "dst.txt", |
||||
|
}) |
||||
|
if err != nil { |
||||
|
t.Fatalf("AtomicRenameEntry: %v", err) |
||||
|
} |
||||
|
|
||||
|
events := queue.snapshot() |
||||
|
if len(events) != 1 { |
||||
|
t.Fatalf("event count = %d, want 1", len(events)) |
||||
|
} |
||||
|
|
||||
|
event := events[0] |
||||
|
if event.key != "/src.txt" { |
||||
|
t.Fatalf("event key = %q, want /src.txt", event.key) |
||||
|
} |
||||
|
if event.notification.OldEntry == nil || event.notification.OldEntry.Name != "src.txt" { |
||||
|
t.Fatalf("old entry = %+v, want src.txt", event.notification.OldEntry) |
||||
|
} |
||||
|
if event.notification.NewEntry == nil || event.notification.NewEntry.Name != "dst.txt" { |
||||
|
t.Fatalf("new entry = %+v, want dst.txt", event.notification.NewEntry) |
||||
|
} |
||||
|
if event.notification.NewParentPath != "/" { |
||||
|
t.Fatalf("new parent path = %q, want /", event.notification.NewParentPath) |
||||
|
} |
||||
|
|
||||
|
if _, err := store.FindEntry(context.Background(), "/src.txt"); err != filer_pb.ErrNotFound { |
||||
|
t.Fatalf("source entry error = %v, want %v", err, filer_pb.ErrNotFound) |
||||
|
} |
||||
|
dst, err := store.FindEntry(context.Background(), "/dst.txt") |
||||
|
if err != nil { |
||||
|
t.Fatalf("find destination: %v", err) |
||||
|
} |
||||
|
if dst.Attr.Inode != 101 { |
||||
|
t.Fatalf("destination inode = %d, want 101", dst.Attr.Inode) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestAtomicRenameEntryOverwriteEmitsDeleteThenRename(t *testing.T) { |
||||
|
store := newRenameTestStore() |
||||
|
store.entries["/src.txt"] = newFileEntry("/src.txt", 101) |
||||
|
store.entries["/dst.txt"] = newFileEntry("/dst.txt", 202) |
||||
|
|
||||
|
queue := &captureQueue{} |
||||
|
swapNotificationQueue(t, queue) |
||||
|
|
||||
|
server := &FilerServer{filer: newRenameTestFiler(store)} |
||||
|
_, err := server.AtomicRenameEntry(context.Background(), &filer_pb.AtomicRenameEntryRequest{ |
||||
|
OldDirectory: "/", |
||||
|
OldName: "src.txt", |
||||
|
NewDirectory: "/", |
||||
|
NewName: "dst.txt", |
||||
|
}) |
||||
|
if err != nil { |
||||
|
t.Fatalf("AtomicRenameEntry: %v", err) |
||||
|
} |
||||
|
|
||||
|
events := queue.snapshot() |
||||
|
if len(events) != 2 { |
||||
|
t.Fatalf("event count = %d, want 2", len(events)) |
||||
|
} |
||||
|
|
||||
|
deleteEvent := events[0] |
||||
|
if deleteEvent.key != "/dst.txt" { |
||||
|
t.Fatalf("delete event key = %q, want /dst.txt", deleteEvent.key) |
||||
|
} |
||||
|
if deleteEvent.notification.OldEntry == nil || deleteEvent.notification.OldEntry.Name != "dst.txt" { |
||||
|
t.Fatalf("delete old entry = %+v, want dst.txt", deleteEvent.notification.OldEntry) |
||||
|
} |
||||
|
if deleteEvent.notification.NewEntry != nil { |
||||
|
t.Fatalf("delete new entry = %+v, want nil", deleteEvent.notification.NewEntry) |
||||
|
} |
||||
|
if !deleteEvent.notification.DeleteChunks { |
||||
|
t.Fatal("delete event should delete chunks") |
||||
|
} |
||||
|
|
||||
|
renameEvent := events[1] |
||||
|
if renameEvent.key != "/src.txt" { |
||||
|
t.Fatalf("rename event key = %q, want /src.txt", renameEvent.key) |
||||
|
} |
||||
|
if renameEvent.notification.OldEntry == nil || renameEvent.notification.OldEntry.Name != "src.txt" { |
||||
|
t.Fatalf("rename old entry = %+v, want src.txt", renameEvent.notification.OldEntry) |
||||
|
} |
||||
|
if renameEvent.notification.NewEntry == nil || renameEvent.notification.NewEntry.Name != "dst.txt" { |
||||
|
t.Fatalf("rename new entry = %+v, want dst.txt", renameEvent.notification.NewEntry) |
||||
|
} |
||||
|
if renameEvent.notification.NewParentPath != "/" { |
||||
|
t.Fatalf("rename new parent path = %q, want /", renameEvent.notification.NewParentPath) |
||||
|
} |
||||
|
|
||||
|
if _, err := store.FindEntry(context.Background(), "/src.txt"); err != filer_pb.ErrNotFound { |
||||
|
t.Fatalf("source entry error = %v, want %v", err, filer_pb.ErrNotFound) |
||||
|
} |
||||
|
dst, err := store.FindEntry(context.Background(), "/dst.txt") |
||||
|
if err != nil { |
||||
|
t.Fatalf("find destination: %v", err) |
||||
|
} |
||||
|
if dst.Attr.Inode != 101 { |
||||
|
t.Fatalf("destination inode = %d, want 101", dst.Attr.Inode) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestAtomicRenameEntryDoesNotEmitEventOnDeleteFailure(t *testing.T) { |
||||
|
store := newRenameTestStore() |
||||
|
store.entries["/src.txt"] = newFileEntry("/src.txt", 101) |
||||
|
store.deleteErr = errors.New("delete failed") |
||||
|
|
||||
|
queue := &captureQueue{} |
||||
|
swapNotificationQueue(t, queue) |
||||
|
|
||||
|
server := &FilerServer{filer: newRenameTestFiler(store)} |
||||
|
_, err := server.AtomicRenameEntry(context.Background(), &filer_pb.AtomicRenameEntryRequest{ |
||||
|
OldDirectory: "/", |
||||
|
OldName: "src.txt", |
||||
|
NewDirectory: "/", |
||||
|
NewName: "dst.txt", |
||||
|
}) |
||||
|
if err == nil { |
||||
|
t.Fatal("expected delete failure") |
||||
|
} |
||||
|
|
||||
|
if events := queue.snapshot(); len(events) != 0 { |
||||
|
t.Fatalf("event count = %d, want 0", len(events)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestAtomicRenameEntryDoesNotEmitEventOnCommitFailure(t *testing.T) { |
||||
|
store := newRenameTestStore() |
||||
|
store.entries["/src.txt"] = newFileEntry("/src.txt", 101) |
||||
|
store.commitErr = errors.New("commit failed") |
||||
|
|
||||
|
queue := &captureQueue{} |
||||
|
swapNotificationQueue(t, queue) |
||||
|
|
||||
|
server := &FilerServer{filer: newRenameTestFiler(store)} |
||||
|
_, err := server.AtomicRenameEntry(context.Background(), &filer_pb.AtomicRenameEntryRequest{ |
||||
|
OldDirectory: "/", |
||||
|
OldName: "src.txt", |
||||
|
NewDirectory: "/", |
||||
|
NewName: "dst.txt", |
||||
|
}) |
||||
|
if err == nil { |
||||
|
t.Fatal("expected commit failure") |
||||
|
} |
||||
|
|
||||
|
if events := queue.snapshot(); len(events) != 0 { |
||||
|
t.Fatalf("event count = %d, want 0", len(events)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestAtomicRenameEntrySkipsDescendantTargetLookups(t *testing.T) { |
||||
|
store := newRenameTestStore() |
||||
|
store.entries["/srcdir"] = newDirectoryEntry("/srcdir", 100) |
||||
|
store.entries["/srcdir/subdir"] = newDirectoryEntry("/srcdir/subdir", 101) |
||||
|
store.entries["/srcdir/subdir/file.txt"] = newFileEntry("/srcdir/subdir/file.txt", 102) |
||||
|
|
||||
|
queue := &captureQueue{} |
||||
|
swapNotificationQueue(t, queue) |
||||
|
|
||||
|
server := &FilerServer{filer: newRenameTestFiler(store)} |
||||
|
_, err := server.AtomicRenameEntry(context.Background(), &filer_pb.AtomicRenameEntryRequest{ |
||||
|
OldDirectory: "/", |
||||
|
OldName: "srcdir", |
||||
|
NewDirectory: "/", |
||||
|
NewName: "dstdir", |
||||
|
}) |
||||
|
if err != nil { |
||||
|
t.Fatalf("AtomicRenameEntry: %v", err) |
||||
|
} |
||||
|
|
||||
|
for _, target := range []string{"/dstdir/subdir", "/dstdir/subdir/file.txt"} { |
||||
|
if calls := store.findEntryCallCount(target); calls != 0 { |
||||
|
t.Fatalf("FindEntry(%q) called %d times, want 0", target, calls) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for _, target := range []string{"/dstdir", "/dstdir/subdir", "/dstdir/subdir/file.txt"} { |
||||
|
if _, err := store.FindEntry(context.Background(), util.FullPath(target)); err != nil { |
||||
|
t.Fatalf("find renamed target %q: %v", target, err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if got := len(queue.snapshot()); got != 3 { |
||||
|
t.Fatalf("event count = %d, want 3", got) |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue