mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-07-20 15:02:27 +00:00
18868e5204
* 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>
124 lines
3.1 KiB
Go
124 lines
3.1 KiB
Go
package util
|
|
|
|
import (
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestAsyncBatchWorkerProcessesInOrder(t *testing.T) {
|
|
var mu sync.Mutex
|
|
var got []int
|
|
w := NewAsyncBatchWorker(func(batch []int) {
|
|
mu.Lock()
|
|
got = append(got, batch...)
|
|
mu.Unlock()
|
|
})
|
|
for i := 0; i < 100; i++ {
|
|
w.Enqueue(i)
|
|
}
|
|
w.Drain()
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if len(got) != 100 {
|
|
t.Fatalf("processed %d items, want 100", len(got))
|
|
}
|
|
for i, v := range got {
|
|
if v != i {
|
|
t.Fatalf("item %d = %d, want %d (FIFO order broken)", i, v, i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAsyncBatchWorkerDrainWaitsForEnqueued(t *testing.T) {
|
|
var processed int64
|
|
w := NewAsyncBatchWorker(func(batch []int) {
|
|
time.Sleep(time.Millisecond)
|
|
atomic.AddInt64(&processed, int64(len(batch)))
|
|
})
|
|
w.Enqueue(1, 2, 3)
|
|
w.Enqueue(4, 5)
|
|
w.Drain()
|
|
if n := atomic.LoadInt64(&processed); n != 5 {
|
|
t.Fatalf("processed %d, want 5 after Drain", n)
|
|
}
|
|
}
|
|
|
|
// Enqueue must not block even while the worker is busy processing under a lock
|
|
// the producer also holds — the unbounded queue is what guarantees this.
|
|
func TestAsyncBatchWorkerEnqueueNeverBlocks(t *testing.T) {
|
|
release := make(chan struct{})
|
|
entered := make(chan struct{})
|
|
var once sync.Once
|
|
w := NewAsyncBatchWorker(func(batch []int) {
|
|
once.Do(func() { close(entered) })
|
|
<-release
|
|
})
|
|
w.Enqueue(0) // worker picks this up and blocks in process
|
|
<-entered
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
for i := 1; i < 1000; i++ {
|
|
w.Enqueue(i)
|
|
}
|
|
close(done)
|
|
}()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Enqueue blocked while worker was busy")
|
|
}
|
|
close(release)
|
|
w.Shutdown()
|
|
}
|
|
|
|
// Drain must honor its contract even when Shutdown runs concurrently: it may
|
|
// not return until the in-flight batch has actually been processed.
|
|
func TestAsyncBatchWorkerDrainCompletesDuringShutdown(t *testing.T) {
|
|
var processed int64
|
|
release := make(chan struct{})
|
|
entered := make(chan struct{})
|
|
var once sync.Once
|
|
w := NewAsyncBatchWorker(func(batch []int) {
|
|
once.Do(func() { close(entered) })
|
|
<-release
|
|
atomic.AddInt64(&processed, int64(len(batch)))
|
|
})
|
|
w.Enqueue(1, 2, 3)
|
|
<-entered // worker has picked up the batch and is blocked in process
|
|
|
|
drained := make(chan struct{})
|
|
go func() { w.Drain(); close(drained) }()
|
|
go w.Shutdown() // sets closed and broadcasts while the batch is still in flight
|
|
|
|
select {
|
|
case <-drained:
|
|
t.Fatal("Drain returned before the in-flight batch finished")
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
close(release)
|
|
<-drained
|
|
if n := atomic.LoadInt64(&processed); n != 3 {
|
|
t.Fatalf("processed %d after Drain, want 3", n)
|
|
}
|
|
}
|
|
|
|
func TestAsyncBatchWorkerShutdownDrainsThenStops(t *testing.T) {
|
|
var processed int64
|
|
w := NewAsyncBatchWorker(func(batch []int) {
|
|
atomic.AddInt64(&processed, int64(len(batch)))
|
|
})
|
|
w.Enqueue(1, 2, 3, 4)
|
|
w.Shutdown()
|
|
if n := atomic.LoadInt64(&processed); n != 4 {
|
|
t.Fatalf("processed %d, want 4 (Shutdown should drain)", n)
|
|
}
|
|
// Enqueue after Shutdown is dropped, not processed.
|
|
w.Enqueue(5, 6)
|
|
if n := atomic.LoadInt64(&processed); n != 4 {
|
|
t.Fatalf("processed %d after post-shutdown Enqueue, want 4", n)
|
|
}
|
|
}
|