mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
Three holes remained after resolving queued invalidations against the local store. A store hit is only trustworthy when the parent directory is children-cached — an uncached parent receives no store writes, so a leftover entry there is stale and would mask the event; gate the store read on the cached flag. A snapshot-covered buffered event got neither a store write nor a replay, yet its immediate invalidation may have run before the listing inserted the newer entry; build completion now re-invalidates every buffered event after publishing the directory. And in a read-through directory nothing reaches the store at all, so a queued event could still roll back a newer local flush: the filer already returns its log-stamped metadata event from CreateEntry, so the handle now keeps a watermark of its last filer-acknowledged local mutation and drops any subscription event at or before it — both sides of that comparison come from the filer clock, so it orders exactly.
536 lines
15 KiB
Go
536 lines
15 KiB
Go
package meta_cache
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
func TestApplyMetadataResponseAppliesEventsInOrder(t *testing.T) {
|
|
mc, _, notifications, invalidations := newTestMetaCache(t, map[util.FullPath]bool{
|
|
"/": true,
|
|
"/dir": true,
|
|
})
|
|
defer mc.Shutdown()
|
|
|
|
createResp := &filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/dir",
|
|
EventNotification: &filer_pb.EventNotification{
|
|
NewEntry: &filer_pb.Entry{
|
|
Name: "file.txt",
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
Crtime: 1,
|
|
Mtime: 1,
|
|
FileMode: 0100644,
|
|
FileSize: 11,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
updateResp := &filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/dir",
|
|
EventNotification: &filer_pb.EventNotification{
|
|
OldEntry: &filer_pb.Entry{
|
|
Name: "file.txt",
|
|
},
|
|
NewEntry: &filer_pb.Entry{
|
|
Name: "file.txt",
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
Crtime: 1,
|
|
Mtime: 2,
|
|
FileMode: 0100644,
|
|
FileSize: 29,
|
|
},
|
|
},
|
|
NewParentPath: "/dir",
|
|
},
|
|
}
|
|
deleteResp := &filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/dir",
|
|
EventNotification: &filer_pb.EventNotification{
|
|
OldEntry: &filer_pb.Entry{
|
|
Name: "file.txt",
|
|
},
|
|
},
|
|
}
|
|
|
|
if err := mc.ApplyMetadataResponse(context.Background(), createResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
|
t.Fatalf("apply create: %v", err)
|
|
}
|
|
|
|
entry, err := mc.FindEntry(context.Background(), util.FullPath("/dir/file.txt"))
|
|
if err != nil {
|
|
t.Fatalf("find created entry: %v", err)
|
|
}
|
|
if entry.FileSize != 11 {
|
|
t.Fatalf("created file size = %d, want 11", entry.FileSize)
|
|
}
|
|
|
|
if err := mc.ApplyMetadataResponse(context.Background(), updateResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
|
t.Fatalf("apply update: %v", err)
|
|
}
|
|
|
|
entry, err = mc.FindEntry(context.Background(), util.FullPath("/dir/file.txt"))
|
|
if err != nil {
|
|
t.Fatalf("find updated entry: %v", err)
|
|
}
|
|
if entry.FileSize != 29 {
|
|
t.Fatalf("updated file size = %d, want 29", entry.FileSize)
|
|
}
|
|
|
|
if err := mc.ApplyMetadataResponse(context.Background(), deleteResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
|
t.Fatalf("apply delete: %v", err)
|
|
}
|
|
|
|
entry, err = mc.FindEntry(context.Background(), util.FullPath("/dir/file.txt"))
|
|
if err != filer_pb.ErrNotFound {
|
|
t.Fatalf("find deleted entry error = %v, want %v", err, filer_pb.ErrNotFound)
|
|
}
|
|
if entry != nil {
|
|
t.Fatalf("deleted entry still cached: %+v", entry)
|
|
}
|
|
|
|
if got := countPath(notifications.paths(), util.FullPath("/dir")); got != 3 {
|
|
t.Fatalf("directory notifications for /dir = %d, want 3", got)
|
|
}
|
|
mc.WaitForEntryInvalidations()
|
|
if got := countPath(invalidations.paths(), util.FullPath("/dir/file.txt")); got != 3 {
|
|
t.Fatalf("invalidations for /dir/file.txt = %d, want 3 (create + update + delete)", got)
|
|
}
|
|
}
|
|
|
|
func TestApplyMetadataResponseRenamesAcrossCachedDirectories(t *testing.T) {
|
|
mc, _, notifications, invalidations := newTestMetaCache(t, map[util.FullPath]bool{
|
|
"/": true,
|
|
"/src": true,
|
|
"/dst": true,
|
|
})
|
|
defer mc.Shutdown()
|
|
|
|
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
|
FullPath: "/src/file.tmp",
|
|
Attr: filer.Attr{
|
|
Crtime: time.Unix(1, 0),
|
|
Mtime: time.Unix(1, 0),
|
|
Mode: 0100644,
|
|
FileSize: 7,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("insert source entry: %v", err)
|
|
}
|
|
|
|
renameResp := &filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/src",
|
|
EventNotification: &filer_pb.EventNotification{
|
|
OldEntry: &filer_pb.Entry{
|
|
Name: "file.tmp",
|
|
},
|
|
NewEntry: &filer_pb.Entry{
|
|
Name: "file.txt",
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
Crtime: 1,
|
|
Mtime: 2,
|
|
FileMode: 0100644,
|
|
FileSize: 41,
|
|
},
|
|
},
|
|
NewParentPath: "/dst",
|
|
},
|
|
}
|
|
|
|
if err := mc.ApplyMetadataResponse(context.Background(), renameResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
|
t.Fatalf("apply rename: %v", err)
|
|
}
|
|
|
|
oldEntry, err := mc.FindEntry(context.Background(), util.FullPath("/src/file.tmp"))
|
|
if err != filer_pb.ErrNotFound {
|
|
t.Fatalf("find old path error = %v, want %v", err, filer_pb.ErrNotFound)
|
|
}
|
|
if oldEntry != nil {
|
|
t.Fatalf("old path still cached: %+v", oldEntry)
|
|
}
|
|
|
|
newEntry, err := mc.FindEntry(context.Background(), util.FullPath("/dst/file.txt"))
|
|
if err != nil {
|
|
t.Fatalf("find new path: %v", err)
|
|
}
|
|
if newEntry.FileSize != 41 {
|
|
t.Fatalf("renamed file size = %d, want 41", newEntry.FileSize)
|
|
}
|
|
|
|
if got := countPath(notifications.paths(), util.FullPath("/src")); got != 1 {
|
|
t.Fatalf("directory notifications for /src = %d, want 1", got)
|
|
}
|
|
if got := countPath(notifications.paths(), util.FullPath("/dst")); got != 1 {
|
|
t.Fatalf("directory notifications for /dst = %d, want 1", got)
|
|
}
|
|
mc.WaitForEntryInvalidations()
|
|
if got := countPath(invalidations.paths(), util.FullPath("/src/file.tmp")); got != 1 {
|
|
t.Fatalf("invalidations for /src/file.tmp = %d, want 1", got)
|
|
}
|
|
if got := countPath(invalidations.paths(), util.FullPath("/dst/file.txt")); got != 1 {
|
|
t.Fatalf("invalidations for /dst/file.txt = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestApplyMetadataResponseLocalOptionsSkipInvalidations(t *testing.T) {
|
|
mc, _, notifications, invalidations := newTestMetaCache(t, map[util.FullPath]bool{
|
|
"/": true,
|
|
"/dir": true,
|
|
})
|
|
defer mc.Shutdown()
|
|
|
|
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
|
FullPath: "/dir/file.txt",
|
|
Attr: filer.Attr{
|
|
Crtime: time.Unix(1, 0),
|
|
Mtime: time.Unix(1, 0),
|
|
Mode: 0100644,
|
|
FileSize: 7,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("insert source entry: %v", err)
|
|
}
|
|
|
|
updateResp := &filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/dir",
|
|
EventNotification: &filer_pb.EventNotification{
|
|
OldEntry: &filer_pb.Entry{
|
|
Name: "file.txt",
|
|
},
|
|
NewEntry: &filer_pb.Entry{
|
|
Name: "file.txt",
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
Crtime: 1,
|
|
Mtime: 2,
|
|
FileMode: 0100644,
|
|
FileSize: 17,
|
|
},
|
|
},
|
|
NewParentPath: "/dir",
|
|
},
|
|
}
|
|
|
|
if err := mc.ApplyMetadataResponse(context.Background(), updateResp, LocalMetadataResponseApplyOptions); err != nil {
|
|
t.Fatalf("apply local update: %v", err)
|
|
}
|
|
|
|
entry, err := mc.FindEntry(context.Background(), util.FullPath("/dir/file.txt"))
|
|
if err != nil {
|
|
t.Fatalf("find updated entry: %v", err)
|
|
}
|
|
if entry.FileSize != 17 {
|
|
t.Fatalf("updated file size = %d, want 17", entry.FileSize)
|
|
}
|
|
if got := countPath(notifications.paths(), util.FullPath("/dir")); got != 1 {
|
|
t.Fatalf("directory notifications for /dir = %d, want 1", got)
|
|
}
|
|
mc.WaitForEntryInvalidations()
|
|
if got := len(invalidations.paths()); got != 0 {
|
|
t.Fatalf("invalidations = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestApplyMetadataResponseDeduplicatesRepeatedFilerEvent(t *testing.T) {
|
|
mc, _, notifications, invalidations := newTestMetaCache(t, map[util.FullPath]bool{
|
|
"/": true,
|
|
"/dir": true,
|
|
})
|
|
defer mc.Shutdown()
|
|
|
|
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
|
FullPath: "/dir/file.txt",
|
|
Attr: filer.Attr{
|
|
Crtime: time.Unix(1, 0),
|
|
Mtime: time.Unix(1, 0),
|
|
Mode: 0100644,
|
|
FileSize: 5,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("insert source entry: %v", err)
|
|
}
|
|
|
|
updateResp := &filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/dir",
|
|
EventNotification: &filer_pb.EventNotification{
|
|
OldEntry: &filer_pb.Entry{
|
|
Name: "file.txt",
|
|
},
|
|
NewEntry: &filer_pb.Entry{
|
|
Name: "file.txt",
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
Crtime: 1,
|
|
Mtime: 2,
|
|
FileMode: 0100644,
|
|
FileSize: 15,
|
|
},
|
|
},
|
|
NewParentPath: "/dir",
|
|
Signatures: []int32{7},
|
|
},
|
|
TsNs: 99,
|
|
}
|
|
|
|
if err := mc.ApplyMetadataResponse(context.Background(), updateResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
|
t.Fatalf("first apply: %v", err)
|
|
}
|
|
if err := mc.ApplyMetadataResponse(context.Background(), updateResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
|
t.Fatalf("second apply: %v", err)
|
|
}
|
|
|
|
entry, err := mc.FindEntry(context.Background(), util.FullPath("/dir/file.txt"))
|
|
if err != nil {
|
|
t.Fatalf("find updated entry: %v", err)
|
|
}
|
|
if entry.FileSize != 15 {
|
|
t.Fatalf("updated file size = %d, want 15", entry.FileSize)
|
|
}
|
|
if got := countPath(notifications.paths(), util.FullPath("/dir")); got != 1 {
|
|
t.Fatalf("directory notifications for /dir = %d, want 1", got)
|
|
}
|
|
mc.WaitForEntryInvalidations()
|
|
if got := countPath(invalidations.paths(), util.FullPath("/dir/file.txt")); got != 1 {
|
|
t.Fatalf("invalidations for /dir/file.txt = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestApplyMetadataResponseSkipsHiddenSystemEntryWhenDisabled(t *testing.T) {
|
|
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
|
"/": true,
|
|
})
|
|
defer mc.Shutdown()
|
|
|
|
createResp := &filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/",
|
|
EventNotification: &filer_pb.EventNotification{
|
|
NewEntry: &filer_pb.Entry{
|
|
Name: "topics",
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
Crtime: 1,
|
|
Mtime: 1,
|
|
FileMode: uint32(os.ModeDir | 0o755),
|
|
},
|
|
IsDirectory: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
if err := mc.ApplyMetadataResponse(context.Background(), createResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
|
t.Fatalf("apply create: %v", err)
|
|
}
|
|
|
|
entry, err := mc.FindEntry(context.Background(), util.FullPath("/topics"))
|
|
if err != filer_pb.ErrNotFound {
|
|
t.Fatalf("find hidden entry error = %v, want %v", err, filer_pb.ErrNotFound)
|
|
}
|
|
if entry != nil {
|
|
t.Fatalf("hidden entry still cached: %+v", entry)
|
|
}
|
|
}
|
|
|
|
func TestApplyMetadataResponsePurgesHiddenDestinationPath(t *testing.T) {
|
|
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
|
"/": true,
|
|
"/src": true,
|
|
})
|
|
defer mc.Shutdown()
|
|
|
|
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
|
FullPath: "/topics",
|
|
Attr: filer.Attr{
|
|
Crtime: time.Unix(1, 0),
|
|
Mtime: time.Unix(1, 0),
|
|
Mode: os.ModeDir | 0o755,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("insert stale hidden dir: %v", err)
|
|
}
|
|
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
|
FullPath: "/topics/leaked.txt",
|
|
Attr: filer.Attr{
|
|
Crtime: time.Unix(1, 0),
|
|
Mtime: time.Unix(1, 0),
|
|
Mode: 0o644,
|
|
FileSize: 7,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("insert leaked hidden child: %v", err)
|
|
}
|
|
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
|
FullPath: "/src/visible",
|
|
Attr: filer.Attr{
|
|
Crtime: time.Unix(1, 0),
|
|
Mtime: time.Unix(1, 0),
|
|
Mode: os.ModeDir | 0o755,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("insert source dir: %v", err)
|
|
}
|
|
|
|
renameResp := &filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/src",
|
|
EventNotification: &filer_pb.EventNotification{
|
|
OldEntry: &filer_pb.Entry{
|
|
Name: "visible",
|
|
IsDirectory: true,
|
|
},
|
|
NewEntry: &filer_pb.Entry{
|
|
Name: "topics",
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
Crtime: 2,
|
|
Mtime: 2,
|
|
FileMode: uint32(os.ModeDir | 0o755),
|
|
},
|
|
IsDirectory: true,
|
|
},
|
|
NewParentPath: "/",
|
|
},
|
|
}
|
|
|
|
if err := mc.ApplyMetadataResponse(context.Background(), renameResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
|
t.Fatalf("apply rename: %v", err)
|
|
}
|
|
|
|
if entry, err := mc.FindEntry(context.Background(), util.FullPath("/src/visible")); err != filer_pb.ErrNotFound || entry != nil {
|
|
t.Fatalf("source dir after rename = %+v, %v; want nil, %v", entry, err, filer_pb.ErrNotFound)
|
|
}
|
|
if entry, err := mc.FindEntry(context.Background(), util.FullPath("/topics")); err != filer_pb.ErrNotFound || entry != nil {
|
|
t.Fatalf("hidden destination after rename = %+v, %v; want nil, %v", entry, err, filer_pb.ErrNotFound)
|
|
}
|
|
if entry, err := mc.FindEntry(context.Background(), util.FullPath("/topics/leaked.txt")); err != filer_pb.ErrNotFound || entry != nil {
|
|
t.Fatalf("hidden child after rename = %+v, %v; want nil, %v", entry, err, filer_pb.ErrNotFound)
|
|
}
|
|
}
|
|
|
|
// The entry attached to each invalidation is what an open file handle gets
|
|
// refreshed with, so it must be the entry now at that path — or nil when the
|
|
// path was vacated and the handle should keep its last entry.
|
|
func TestCollectEntryInvalidationsCarryAuthoritativeEntries(t *testing.T) {
|
|
newEntry := &filer_pb.Entry{
|
|
Name: "file.txt",
|
|
Attributes: &filer_pb.FuseAttributes{FileSize: 42},
|
|
}
|
|
|
|
update := &filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/dir",
|
|
TsNs: 77,
|
|
EventNotification: &filer_pb.EventNotification{
|
|
OldEntry: &filer_pb.Entry{Name: "file.txt"},
|
|
NewEntry: newEntry,
|
|
NewParentPath: "/dir",
|
|
},
|
|
}
|
|
got := collectEntryInvalidations(update)
|
|
if len(got) != 1 || got[0].path != "/dir/file.txt" || got[0].entry != newEntry || got[0].tsNs != 77 {
|
|
t.Fatalf("in-place update invalidations = %+v, want [{/dir/file.txt NewEntry ts 77}]", got)
|
|
}
|
|
|
|
rename := &filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/src",
|
|
EventNotification: &filer_pb.EventNotification{
|
|
OldEntry: &filer_pb.Entry{Name: "file.tmp"},
|
|
NewEntry: newEntry,
|
|
NewParentPath: "/dst",
|
|
},
|
|
}
|
|
got = collectEntryInvalidations(rename)
|
|
if len(got) != 2 || got[0].path != "/src/file.tmp" || got[0].entry != nil ||
|
|
got[1].path != "/dst/file.txt" || got[1].entry != newEntry {
|
|
t.Fatalf("rename invalidations = %+v, want [{/src/file.tmp nil} {/dst/file.txt NewEntry}]", got)
|
|
}
|
|
|
|
create := &filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/dir",
|
|
EventNotification: &filer_pb.EventNotification{
|
|
NewEntry: newEntry,
|
|
},
|
|
}
|
|
got = collectEntryInvalidations(create)
|
|
if len(got) != 1 || got[0].path != "/dir/file.txt" || got[0].entry != newEntry {
|
|
t.Fatalf("create invalidations = %+v, want [{/dir/file.txt NewEntry}]", got)
|
|
}
|
|
|
|
deleteResp := &filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/dir",
|
|
EventNotification: &filer_pb.EventNotification{
|
|
OldEntry: &filer_pb.Entry{Name: "file.txt"},
|
|
},
|
|
}
|
|
got = collectEntryInvalidations(deleteResp)
|
|
if len(got) != 1 || got[0].path != "/dir/file.txt" || got[0].entry != nil {
|
|
t.Fatalf("delete invalidations = %+v, want [{/dir/file.txt nil}]", got)
|
|
}
|
|
}
|
|
|
|
func newTestMetaCache(t *testing.T, cached map[util.FullPath]bool) (*MetaCache, map[util.FullPath]bool, *recordedPaths, *recordedPaths) {
|
|
t.Helper()
|
|
|
|
mapper, err := NewUidGidMapper("", "")
|
|
if err != nil {
|
|
t.Fatalf("uid/gid mapper: %v", err)
|
|
}
|
|
|
|
var cachedMu sync.Mutex
|
|
notifications := &recordedPaths{}
|
|
invalidations := &recordedPaths{}
|
|
|
|
mc := NewMetaCache(
|
|
filepath.Join(t.TempDir(), "meta"),
|
|
mapper,
|
|
util.FullPath("/"),
|
|
false,
|
|
func(path util.FullPath) {
|
|
cachedMu.Lock()
|
|
defer cachedMu.Unlock()
|
|
cached[path] = true
|
|
},
|
|
func(path util.FullPath) bool {
|
|
cachedMu.Lock()
|
|
defer cachedMu.Unlock()
|
|
return cached[path]
|
|
},
|
|
func(path util.FullPath, entry *filer_pb.Entry, eventTsNs int64) {
|
|
invalidations.record(path)
|
|
},
|
|
func(dir util.FullPath) {
|
|
notifications.record(dir)
|
|
},
|
|
)
|
|
|
|
return mc, cached, notifications, invalidations
|
|
}
|
|
|
|
type recordedPaths struct {
|
|
mu sync.Mutex
|
|
items []util.FullPath
|
|
}
|
|
|
|
func (r *recordedPaths) record(path util.FullPath) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.items = append(r.items, path)
|
|
}
|
|
|
|
func (r *recordedPaths) paths() []util.FullPath {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return append([]util.FullPath(nil), r.items...)
|
|
}
|
|
|
|
func countPath(paths []util.FullPath, target util.FullPath) int {
|
|
count := 0
|
|
for _, path := range paths {
|
|
if path == target {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|