yao/event/leak_test.go
Max fd24e31912 Update Makefile for enhanced testing and add new event documentation
- Modify unit test commands in the Makefile to include additional skip patterns for memory leak tests, improving test coverage and accuracy.
- Expand benchmark and memory leak detection to include the event module, ensuring comprehensive testing across all components.
- Add new design and TODO documentation files for the event module to facilitate future development.
2026-02-23 12:18:13 +08:00

348 lines
9.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package event_test
import (
"context"
"fmt"
"runtime"
"sync"
"testing"
"time"
"github.com/yaoapp/yao/event"
"github.com/yaoapp/yao/event/types"
)
// ---------------------------------------------------------------------------
// Helper: snapshot goroutine count after GC stabilization.
// ---------------------------------------------------------------------------
func stableGoroutineCount() int {
// Let runtime settle: GC + finalizers + scheduler
for i := 0; i < 5; i++ {
runtime.GC()
runtime.Gosched()
time.Sleep(10 * time.Millisecond)
}
return runtime.NumGoroutine()
}
// leakHandler is a no-op handler for leak tests.
type leakHandler struct{}
func (h *leakHandler) Handle(ctx context.Context, ev *types.Event, resp chan<- types.Result) {
if ev.IsCall {
resp <- types.Result{Data: "ok"}
}
}
func (h *leakHandler) Shutdown(ctx context.Context) error { return nil }
// leakListener is a no-op listener for leak tests.
type leakListener struct{}
func (l *leakListener) OnEvent(ev *types.Event) {}
func (l *leakListener) Shutdown(ctx context.Context) error { return nil }
// ---------------------------------------------------------------------------
// Test: 1000 Queue create/release cycles leak no goroutines.
// ---------------------------------------------------------------------------
func TestLeak_QueueCreateRelease(t *testing.T) {
event.Reset()
defer event.Reset()
event.Register("leak", &leakHandler{}, event.QueueSize(64))
_ = event.Start()
before := stableGoroutineCount()
const cycles = 1000
for i := 0; i < cycles; i++ {
qID, err := event.QueueCreate("leak")
if err != nil {
t.Fatalf("cycle %d: QueueCreate: %v", i, err)
}
// Push a few events to exercise consumer goroutine
for j := 0; j < 3; j++ {
_, _ = event.Push(context.Background(), "leak.work", j, event.Queue(qID))
}
event.QueueRelease(qID)
}
// Let all consumer goroutines drain and exit
time.Sleep(500 * time.Millisecond)
after := stableGoroutineCount()
_ = event.Stop(context.Background())
leaked := after - before
t.Logf("goroutines: before=%d after=%d delta=%d (over %d cycles)", before, after, leaked, cycles)
// Allow a small margin for runtime jitter (GC, timers, etc.)
if leaked > 5 {
t.Errorf("goroutine leak: %d goroutines accumulated over %d queue cycles", leaked, cycles)
}
}
// ---------------------------------------------------------------------------
// Test: 1000 Queue create/abort cycles leak no goroutines.
// ---------------------------------------------------------------------------
func TestLeak_QueueCreateAbort(t *testing.T) {
event.Reset()
defer event.Reset()
event.Register("leak", &leakHandler{}, event.QueueSize(64))
_ = event.Start()
before := stableGoroutineCount()
const cycles = 1000
for i := 0; i < cycles; i++ {
qID, err := event.QueueCreate("leak")
if err != nil {
t.Fatalf("cycle %d: QueueCreate: %v", i, err)
}
for j := 0; j < 3; j++ {
_, _ = event.Push(context.Background(), "leak.work", j, event.Queue(qID))
}
event.QueueAbort(qID)
}
time.Sleep(500 * time.Millisecond)
after := stableGoroutineCount()
_ = event.Stop(context.Background())
leaked := after - before
t.Logf("goroutines: before=%d after=%d delta=%d (over %d cycles)", before, after, leaked, cycles)
if leaked > 5 {
t.Errorf("goroutine leak: %d goroutines accumulated over %d abort cycles", leaked, cycles)
}
}
// ---------------------------------------------------------------------------
// Test: Subscriber create/unsubscribe cycles leak no goroutines or memory.
// ---------------------------------------------------------------------------
func TestLeak_SubscriberLifecycle(t *testing.T) {
event.Reset()
defer event.Reset()
event.Register("leak", &leakHandler{})
_ = event.Start()
defer func() { _ = event.Stop(context.Background()) }()
before := stableGoroutineCount()
runtime.GC()
var memBefore runtime.MemStats
runtime.ReadMemStats(&memBefore)
const cycles = 1000
for i := 0; i < cycles; i++ {
ch := make(chan *types.Event, 16)
subID := event.Subscribe("leak.*", ch)
_, _ = event.Push(context.Background(), "leak.work", nil)
time.Sleep(time.Microsecond) // let notify propagate
event.Unsubscribe(subID)
}
time.Sleep(200 * time.Millisecond)
after := stableGoroutineCount()
runtime.GC()
var memAfter runtime.MemStats
runtime.ReadMemStats(&memAfter)
leaked := after - before
memDeltaMB := float64(int64(memAfter.HeapInuse)-int64(memBefore.HeapInuse)) / 1024 / 1024
t.Logf("goroutines: before=%d after=%d delta=%d", before, after, leaked)
t.Logf("heap in-use delta: %.2f MB", memDeltaMB)
if leaked > 3 {
t.Errorf("goroutine leak: %d goroutines after %d sub/unsub cycles", leaked, cycles)
}
}
// ---------------------------------------------------------------------------
// Test: Start/Stop cycles leak no goroutines.
// ---------------------------------------------------------------------------
func TestLeak_StartStopCycles(t *testing.T) {
before := stableGoroutineCount()
const cycles = 20
for i := 0; i < cycles; i++ {
event.Reset()
event.Register("leak", &leakHandler{})
event.Listen("leak.*", &leakListener{})
_ = event.Start()
ctx := context.Background()
for j := 0; j < 10; j++ {
_, _ = event.Push(ctx, "leak.work", j)
}
time.Sleep(10 * time.Millisecond)
_ = event.Stop(ctx)
}
event.Reset()
time.Sleep(300 * time.Millisecond)
after := stableGoroutineCount()
leaked := after - before
t.Logf("goroutines: before=%d after=%d delta=%d (over %d start/stop cycles)", before, after, leaked, cycles)
if leaked > 3 {
t.Errorf("goroutine leak: %d goroutines after %d start/stop cycles", leaked, cycles)
}
}
// ---------------------------------------------------------------------------
// Test: 1000 concurrent users creating/using/releasing queues, verify
// no goroutine leak when everything settles.
// ---------------------------------------------------------------------------
func TestLeak_1000Users_FullCycle(t *testing.T) {
event.Reset()
defer event.Reset()
event.Register("trace", &leakHandler{}, event.MaxWorkers(512), event.QueueSize(8192))
event.Register("job", &leakHandler{}, event.MaxWorkers(256), event.QueueSize(4096))
event.Listen("trace.*", &leakListener{})
_ = event.Start()
before := stableGoroutineCount()
const numUsers = 1000
var wg sync.WaitGroup
for u := 0; u < numUsers; u++ {
wg.Add(1)
go func(uid int) {
defer wg.Done()
ctx := event.WithSID(context.Background(), fmt.Sprintf("s-%d", uid))
tqID, err := event.QueueCreate("trace")
if err != nil {
return
}
jqID, err := event.QueueCreate("job")
if err != nil {
event.QueueRelease(tqID)
return
}
for i := 0; i < 10; i++ {
_, _ = event.Push(ctx, "trace.add", i, event.Queue(tqID))
}
for i := 0; i < 3; i++ {
_, _ = event.Push(ctx, "job.progress", i, event.Queue(jqID))
}
callCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
_, _, _ = event.Call(callCtx, "trace.get", nil, event.Queue(tqID))
cancel()
event.QueueRelease(tqID)
event.QueueRelease(jqID)
}(u)
}
wg.Wait()
time.Sleep(1 * time.Second) // let all consumers drain
after := stableGoroutineCount()
_ = event.Stop(context.Background())
// Final check after full stop
afterStop := stableGoroutineCount()
leaked := after - before
leakedAfterStop := afterStop - before
t.Logf("goroutines: before=%d after_drain=%d after_stop=%d", before, after, afterStop)
t.Logf("delta after drain: %d, delta after stop: %d", leaked, leakedAfterStop)
if leaked > 10 {
t.Errorf("goroutine leak after drain: %d (1000 users × 2 queues)", leaked)
}
if leakedAfterStop > 3 {
t.Errorf("goroutine leak after stop: %d", leakedAfterStop)
}
}
// ---------------------------------------------------------------------------
// Test: Memory stability under sustained load.
// Push 100k events through 100 queues, measure heap growth.
// ---------------------------------------------------------------------------
func TestLeak_MemoryStability(t *testing.T) {
event.Reset()
defer event.Reset()
event.Register("mem", &leakHandler{}, event.MaxWorkers(256), event.QueueSize(8192))
_ = event.Start()
defer func() { _ = event.Stop(context.Background()) }()
const (
numQueues = 100
eventsPerQueue = 1000
totalEvents = numQueues * eventsPerQueue
)
queueIDs := make([]string, numQueues)
for i := 0; i < numQueues; i++ {
qID, _ := event.QueueCreate("mem")
queueIDs[i] = qID
}
runtime.GC()
var memBefore runtime.MemStats
runtime.ReadMemStats(&memBefore)
ctx := context.Background()
var wg sync.WaitGroup
for q := 0; q < numQueues; q++ {
wg.Add(1)
go func(qIdx int) {
defer wg.Done()
qID := queueIDs[qIdx]
for i := 0; i < eventsPerQueue; i++ {
_, _ = event.Push(ctx, "mem.work", i, event.Queue(qID))
}
}(q)
}
wg.Wait()
// Release all and wait
for _, qID := range queueIDs {
event.QueueRelease(qID)
}
time.Sleep(1 * time.Second)
runtime.GC()
var memAfter runtime.MemStats
runtime.ReadMemStats(&memAfter)
// Use signed arithmetic to handle GC reclaiming memory between snapshots.
heapDeltaMB := float64(int64(memAfter.HeapInuse)-int64(memBefore.HeapInuse)) / 1024 / 1024
allocDeltaMB := float64(memAfter.TotalAlloc-memBefore.TotalAlloc) / 1024 / 1024
t.Logf("=== Memory Stability ===")
t.Logf("Events: %d (%d queues × %d events)", totalEvents, numQueues, eventsPerQueue)
t.Logf("HeapInuse delta: %.2f MB", heapDeltaMB)
t.Logf("TotalAlloc: %.2f MB", allocDeltaMB)
t.Logf("Alloc/event: %.0f bytes", allocDeltaMB*1024*1024/float64(totalEvents))
// After drain, heap should not retain significant memory.
// Allow generous 50 MB for 100k events (runtime overhead, GC timing).
if heapDeltaMB > 50 {
t.Errorf("heap grew %.2f MB after %d events, possible leak", heapDeltaMB, totalEvents)
}
}