mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-07-23 16:43:23 +00:00
* fix(mount): run entry invalidations off the meta-cache apply loop The apply loop ran invalidateFunc inline, which acquires the open file handle's lock in fhLockTable. Meanwhile flushMetadataToFiler holds that same fh lock and then waits on the apply loop (applyLocalMetadataEvent). When both target the same open file concurrently, the loop blocks on the fh lock while the lock holder blocks on the loop: an ABBA deadlock that backs up every later readdir/flush and hangs the mount. Fix: dispatch entry invalidations to a dedicated FIFO worker goroutine so the apply loop never blocks on locks held by goroutines waiting on it. Adds a regression test reproducing the interleaving. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * perf(mount): update invalidate counter once per batch Run the batch's invalidateFunc calls without re-taking invalidateMu per item, then bump invalidateProcessed and broadcast once after the loop. WaitForEntryInvalidations only needs the count to reach its target and a batch always completes together, so the per-item lock + broadcast was wasted work. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * mount: extract the invalidate worker into util.AsyncBatchWorker The apply loop's off-thread entry-invalidation queue was a one-off mutex + cond + slice + counters living inside MetaCache. Pull it out as a generic unbounded FIFO worker so the deadlock-avoidance contract (never block the producer, drain on shutdown, wait-for-quiesce) lives in one place. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Chris Lu <chris.lu@gmail.com>
162 lines
4.9 KiB
Go
162 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(path util.FullPath, entry *filer_pb.Entry) {
|
|
// 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) {},
|
|
)
|
|
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")
|
|
}
|
|
}
|