mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
* log buffer: wake the interval loop on shutdown instead of sleeping through it loopInterval parked in time.Sleep(flushInterval) and only re-checked IsStopping when it woke, so a buffer shut down early kept both loop goroutines - and the PreviousBufferCount+1 slabs of BufferSize they reach - alive for up to a full interval afterwards. Select on shutdownCh against a ticker instead, and give the loops a WaitGroup so a test can observe that they exit. * test: release the filers the server tests build Every helper here left its filer's meta log buffer running, so each test pinned PreviousBufferCount+1 buffers of BufferSize for the rest of the run: ~3.5GB of live heap across the package, which overruns the address space on linux/386 and kills the 32-bit job with an out-of-memory throw. Thread the test through the helpers so the buffer is shut down on cleanup, and shut the subscribe harness's filer down outright - its deletion loop keeps the whole filer reachable otherwise. That harness quiesces its flush path first, since Filer.Shutdown closes the store a flush still in flight would write through.
485 lines
14 KiB
Go
485 lines
14 KiB
Go
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
|
|
findDelay time.Duration // optional: widen check-then-act windows in tests
|
|
}
|
|
|
|
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) {
|
|
if s.findDelay > 0 {
|
|
time.Sleep(s.findDelay)
|
|
}
|
|
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()
|
|
})
|
|
}
|
|
|
|
// newRenameTestFiler builds a filer whose meta log buffer is released when the
|
|
// test ends. A live LogBuffer holds PreviousBufferCount+1 buffers of BufferSize
|
|
// each, and its loop goroutines keep the whole filer reachable, so the tens of
|
|
// filers this package builds would otherwise pin gigabytes for the rest of the
|
|
// run - fatal on 32-bit, where that exhausts the address space.
|
|
func newRenameTestFiler(t *testing.T, 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{}),
|
|
)
|
|
|
|
logBuffer := log_buffer.NewLogBuffer(
|
|
"test",
|
|
time.Minute,
|
|
func(*log_buffer.LogBuffer, time.Time, time.Time, []byte, int64, int64) {},
|
|
nil,
|
|
func() {},
|
|
)
|
|
t.Cleanup(logBuffer.ShutdownLogBuffer)
|
|
|
|
return &filer.Filer{
|
|
Store: filer.NewFilerStoreWrapper(store),
|
|
MasterClient: masterClient,
|
|
FilerConf: filer.NewFilerConf(),
|
|
RemoteStorage: filer.NewFilerRemoteStorage(),
|
|
MaxFilenameLength: 255,
|
|
LocalMetaLogBuffer: logBuffer,
|
|
}
|
|
}
|
|
|
|
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(t, store), entryLockTable: util.NewLockTable[util.FullPath]()}
|
|
_, 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(t, store), entryLockTable: util.NewLockTable[util.FullPath]()}
|
|
_, 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(t, store), entryLockTable: util.NewLockTable[util.FullPath]()}
|
|
_, 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(t, store), entryLockTable: util.NewLockTable[util.FullPath]()}
|
|
_, 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(t, store), entryLockTable: util.NewLockTable[util.FullPath]()}
|
|
_, 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)
|
|
}
|
|
}
|