mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
A NewXWithY alongside NewX is two names for one thing, and the one that takes the argument is the only one that ever needed to exist. Callers pass the default explicitly.
163 lines
4.9 KiB
Go
163 lines
4.9 KiB
Go
package meta_cache
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
// TestApplyLoopInvalidateDoesNotDeadlockWithLockHoldingEnqueuer reproduces the
|
|
// mount hang seen in production (all readdirs stuck, FUSE waiting>0):
|
|
//
|
|
// - A FUSE flush handler (flushMetadataToFiler) holds the file handle's
|
|
// exclusive lock in wfs.fhLockTable and then enqueues a local metadata
|
|
// event, waiting for the apply loop to process it.
|
|
// - Concurrently the apply loop processes a subscriber event whose
|
|
// invalidateFunc (weedfs.go) acquires the same file handle lock.
|
|
//
|
|
// The single-goroutine apply loop blocks on the fh lock while the fh lock
|
|
// holder blocks on the apply loop: a permanent ABBA deadlock. Every later
|
|
// EnsureVisited/BeginDirectoryBuild then backs up behind the dead loop, FUSE
|
|
// handler goroutines exhaust, and the kernel queue (waiting) grows.
|
|
//
|
|
// The contract under test: the apply loop must never block on locks that can
|
|
// be held by goroutines waiting on the apply loop.
|
|
func TestApplyLoopInvalidateDoesNotDeadlockWithLockHoldingEnqueuer(t *testing.T) {
|
|
mapper, err := NewUidGidMapper("", "")
|
|
if err != nil {
|
|
t.Fatalf("uid/gid mapper: %v", err)
|
|
}
|
|
|
|
// Stand-in for wfs.fhLockTable, keyed like FileHandleId.
|
|
fhLockTable := util.NewLockTable[uint64]()
|
|
const fhKey uint64 = 42
|
|
|
|
invalidateEntered := make(chan struct{})
|
|
var enteredOnce sync.Once
|
|
|
|
var cachedMu sync.Mutex
|
|
cached := map[util.FullPath]bool{"/": true, "/dir": true}
|
|
|
|
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(inv EntryInvalidation) {
|
|
// Mirrors the wfs invalidateFunc: it takes the open file
|
|
// handle's exclusive lock before refreshing the handle entry.
|
|
enteredOnce.Do(func() { close(invalidateEntered) })
|
|
lock := fhLockTable.AcquireLock("invalidateFunc", fhKey, util.ExclusiveLock)
|
|
fhLockTable.ReleaseLock(fhKey, lock)
|
|
},
|
|
func(dir util.FullPath) {},
|
|
DefaultListingCacheEntries,
|
|
)
|
|
defer func() {
|
|
// Only safe to shut down once the apply loop is unwedged.
|
|
mc.Shutdown()
|
|
}()
|
|
|
|
subscriberEvent := &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,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
localEvent := &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,
|
|
},
|
|
},
|
|
NewParentPath: "/dir",
|
|
},
|
|
}
|
|
|
|
// Step 1: the "flush handler" takes the fh exclusive lock
|
|
// (flushMetadataToFiler does this before talking to the filer).
|
|
flushLock := fhLockTable.AcquireLock("doFlush", fhKey, util.ExclusiveLock)
|
|
flushLockReleased := false
|
|
releaseFlushLock := func() {
|
|
if !flushLockReleased {
|
|
flushLockReleased = true
|
|
fhLockTable.ReleaseLock(fhKey, flushLock)
|
|
}
|
|
}
|
|
defer releaseFlushLock()
|
|
|
|
// Step 2: a subscriber event arrives; the apply loop will run
|
|
// invalidateFunc, which wants the fh lock held above.
|
|
subscriberDone := make(chan error, 1)
|
|
go func() {
|
|
subscriberDone <- mc.ApplyMetadataResponse(context.Background(), subscriberEvent, SubscriberMetadataResponseApplyOptions)
|
|
}()
|
|
|
|
select {
|
|
case <-invalidateEntered:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("invalidateFunc was never invoked for the subscriber event")
|
|
}
|
|
// Give the invalidate goroutine a moment to actually block on AcquireLock.
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Step 3: still holding the fh lock, the flush handler applies its local
|
|
// metadata event and waits for the apply loop — exactly what
|
|
// flushMetadataToFiler does after streamCreateEntry succeeds.
|
|
flushApplyDone := make(chan error, 1)
|
|
go func() {
|
|
flushApplyDone <- mc.ApplyMetadataResponseOwned(context.Background(), localEvent, LocalMetadataResponseApplyOptions)
|
|
}()
|
|
|
|
select {
|
|
case err := <-flushApplyDone:
|
|
if err != nil {
|
|
t.Fatalf("local metadata apply: %v", err)
|
|
}
|
|
case <-time.After(3 * time.Second):
|
|
// Unwedge the apply loop so Shutdown in the deferred cleanup
|
|
// doesn't hang the whole test binary.
|
|
releaseFlushLock()
|
|
t.Fatal("deadlock: apply loop blocked on fh lock while fh lock holder waits on apply loop")
|
|
}
|
|
|
|
releaseFlushLock()
|
|
|
|
select {
|
|
case err := <-subscriberDone:
|
|
if err != nil {
|
|
t.Fatalf("subscriber apply: %v", err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("subscriber apply never completed")
|
|
}
|
|
}
|