package trace_test import ( "context" "fmt" "runtime" "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/yaoapp/yao/trace" "github.com/yaoapp/yao/trace/types" ) // TestReleaseWhileWriting verifies that calling Release while multiple // goroutines are actively writing to the trace does not panic or deadlock. func TestReleaseWhileWriting(t *testing.T) { drivers := trace.GetTestDrivers() for _, d := range drivers { t.Run(d.Name, func(t *testing.T) { ctx := context.Background() traceID, manager, err := trace.New(ctx, d.DriverType, nil, d.DriverOptions...) assert.NoError(t, err) defer trace.Remove(ctx, d.DriverType, traceID, d.DriverOptions...) // Add a root node so logging operations have a target _, err = manager.Add("root", types.TraceNodeOption{Label: "Root"}) assert.NoError(t, err) // Start 20 goroutines continuously writing var wg sync.WaitGroup const numWriters = 20 stop := make(chan struct{}) for i := 0; i < numWriters; i++ { wg.Add(1) go func(idx int) { defer wg.Done() for { select { case <-stop: return default: manager.Info("writer %d tick", idx) } } }(i) } // Let writers run briefly, then Release while they are still writing time.Sleep(10 * time.Millisecond) err = trace.Release(traceID) assert.NoError(t, err) // Signal writers to stop and wait close(stop) wg.Wait() }) } } // TestReleaseDuringSpaceOp verifies that calling Release while space // key-value operations are in flight does not panic. func TestReleaseDuringSpaceOp(t *testing.T) { drivers := trace.GetTestDrivers() for _, d := range drivers { t.Run(d.Name, func(t *testing.T) { ctx := context.Background() traceID, manager, err := trace.New(ctx, d.DriverType, nil, d.DriverOptions...) assert.NoError(t, err) defer trace.Remove(ctx, d.DriverType, traceID, d.DriverOptions...) space, err := manager.CreateSpace(types.TraceSpaceOption{Label: "Test Space"}) assert.NoError(t, err) var wg sync.WaitGroup const numOps = 10 stop := make(chan struct{}) for i := 0; i < numOps; i++ { wg.Add(1) go func(idx int) { defer wg.Done() for { select { case <-stop: return default: key := fmt.Sprintf("key_%d", idx) // Errors are expected after Release; we only care about no panic. _ = manager.SetSpaceValue(space.ID, key, idx) _, _ = manager.GetSpaceValue(space.ID, key) } } }(i) } time.Sleep(10 * time.Millisecond) err = trace.Release(traceID) assert.NoError(t, err) close(stop) wg.Wait() }) } } // TestReleaseAfterMarkComplete verifies that MarkComplete followed by // immediate Release and further operations does not panic. func TestReleaseAfterMarkComplete(t *testing.T) { drivers := trace.GetTestDrivers() for _, d := range drivers { t.Run(d.Name, func(t *testing.T) { ctx := context.Background() traceID, manager, err := trace.New(ctx, d.DriverType, nil, d.DriverOptions...) assert.NoError(t, err) defer trace.Remove(ctx, d.DriverType, traceID, d.DriverOptions...) _, err = manager.Add("root", types.TraceNodeOption{Label: "Root"}) assert.NoError(t, err) err = manager.MarkComplete() assert.NoError(t, err) err = trace.Release(traceID) assert.NoError(t, err) // Post-release operations should not panic. // They may return errors or be silently dropped. manager.Info("after release") _ = manager.SetOutput("stale output") _, _ = manager.Add("late", types.TraceNodeOption{Label: "Late"}) }) } } // TestConcurrentReleaseAndMarkCancelled verifies that calling MarkCancelled // and Release concurrently does not panic or deadlock. func TestConcurrentReleaseAndMarkCancelled(t *testing.T) { drivers := trace.GetTestDrivers() for _, d := range drivers { t.Run(d.Name, func(t *testing.T) { ctx := context.Background() traceID, manager, err := trace.New(ctx, d.DriverType, nil, d.DriverOptions...) assert.NoError(t, err) defer trace.Remove(ctx, d.DriverType, traceID, d.DriverOptions...) _, err = manager.Add("root", types.TraceNodeOption{Label: "Root"}) assert.NoError(t, err) done := make(chan struct{}) go func() { defer close(done) var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() _ = trace.MarkCancelled(traceID, "test cancel") }() go func() { defer wg.Done() _ = trace.Release(traceID) }() wg.Wait() }() select { case <-done: // success — no deadlock case <-time.After(5 * time.Second): t.Fatal("deadlock: concurrent MarkCancelled + Release did not finish in 5s") } }) } } // TestOperationsAfterRelease verifies that using a manager reference after // Release does not panic. The manager is removed from registry but its // in-memory state remains valid (no channel close or context cancel). func TestOperationsAfterRelease(t *testing.T) { drivers := trace.GetTestDrivers() for _, d := range drivers { t.Run(d.Name, func(t *testing.T) { ctx := context.Background() traceID, manager, err := trace.New(ctx, d.DriverType, nil, d.DriverOptions...) assert.NoError(t, err) defer trace.Remove(ctx, d.DriverType, traceID, d.DriverOptions...) _, err = manager.Add("root", types.TraceNodeOption{Label: "Root"}) assert.NoError(t, err) err = trace.Release(traceID) assert.NoError(t, err) // After Release, the manager object is still usable (state in memory). // These calls should not panic. manager.Info("post-release info") manager.Debug("post-release debug") root, _ := manager.GetRootNode() assert.NotNil(t, root) }) } } // TestRapidCreateReleaseLoop stress-tests the create/release cycle to ensure // no goroutine accumulation or panics over many iterations. func TestRapidCreateReleaseLoop(t *testing.T) { drivers := trace.GetTestDrivers() for _, d := range drivers { t.Run(d.Name, func(t *testing.T) { ctx := context.Background() // Warm up and let baseline stabilize runtime.GC() time.Sleep(50 * time.Millisecond) baseGoroutines := runtime.NumGoroutine() const iterations = 100 for i := 0; i < iterations; i++ { traceID, manager, err := trace.New(ctx, d.DriverType, nil, d.DriverOptions...) assert.NoError(t, err) _, err = manager.Add("input", types.TraceNodeOption{Label: "Node"}) assert.NoError(t, err) manager.Info("iteration %d", i) err = manager.MarkComplete() assert.NoError(t, err) err = trace.Release(traceID) assert.NoError(t, err) err = trace.Remove(ctx, d.DriverType, traceID, d.DriverOptions...) assert.NoError(t, err) } // Allow goroutines to wind down runtime.GC() time.Sleep(200 * time.Millisecond) finalGoroutines := runtime.NumGoroutine() delta := finalGoroutines - baseGoroutines // Allow a small margin for runtime goroutines; flag severe leaks assert.LessOrEqual(t, delta, 20, "goroutine leak: base=%d final=%d delta=%d", baseGoroutines, finalGoroutines, delta) }) } } // TestConcurrentAllPattern simulates the real All() orchestrator pattern: // parent creates a trace, forks N goroutines that each write to the shared // trace manager, then parent calls MarkComplete + Release while children // may still be writing. func TestConcurrentAllPattern(t *testing.T) { drivers := trace.GetTestDrivers() for _, d := range drivers { t.Run(d.Name, func(t *testing.T) { ctx := context.Background() traceID, manager, err := trace.New(ctx, d.DriverType, nil, d.DriverOptions...) assert.NoError(t, err) defer trace.Remove(ctx, d.DriverType, traceID, d.DriverOptions...) // Root node _, err = manager.Add("orchestrator", types.TraceNodeOption{Label: "Orchestrator"}) assert.NoError(t, err) // Create a shared space space, err := manager.CreateSpace(types.TraceSpaceOption{Label: "Shared"}) assert.NoError(t, err) // Fork N "child" goroutines, each doing work on the shared trace const numChildren = 10 childStarted := make(chan struct{}) var wg sync.WaitGroup for i := 0; i < numChildren; i++ { wg.Add(1) go func(idx int) { defer wg.Done() // Signal that this child has started select { case childStarted <- struct{}{}: default: } // Simulate work: log, set space values, complete for j := 0; j < 20; j++ { manager.Info("child %d step %d", idx, j) _ = manager.SetSpaceValue(space.ID, fmt.Sprintf("child_%d_%d", idx, j), j) } }(i) } // Wait for at least a few children to start, then trigger shutdown time.Sleep(5 * time.Millisecond) // Parent completes and releases — some children are still writing _ = manager.MarkComplete() _ = trace.Release(traceID) // Wait for all children to finish (they should not panic) done := make(chan struct{}) go func() { wg.Wait() close(done) }() select { case <-done: // success case <-time.After(10 * time.Second): t.Fatal("deadlock: child goroutines did not finish in 10s") } }) } }