diff --git a/autocleanup.go b/autocleanup.go index 4adc3c0..d43ada6 100644 --- a/autocleanup.go +++ b/autocleanup.go @@ -553,6 +553,12 @@ func (tr *TaskRegistry) CreateSingletonTask(name string, interval time.Duration, } return nil, fmt.Errorf("singleton task already exists: %s (requested: %s)", taskName, name) } + if strings.Contains(taskName, "memory-monitor") && strings.Contains(name, "memory-monitor") { + if tr.logger != nil { + tr.logger.Debug("Singleton enforcement: memory-monitor task %s already exists, rejecting %s", taskName, name) + } + return nil, fmt.Errorf("singleton memory-monitor task already exists: %s (requested: %s)", taskName, name) + } } // Check if exact task already exists diff --git a/memory_monitor.go b/memory_monitor.go index 0d0a17d..80266e1 100644 --- a/memory_monitor.go +++ b/memory_monitor.go @@ -304,8 +304,25 @@ func (mm *MemoryMonitor) LogMemoryStats(stats *MemoryStats) { stats.SessionCount, stats.TaskCount, stats.CacheSize, stats.ConnectionPools) } -// StartMonitoring starts continuous memory monitoring +// Global monitoring state +var ( + globalMonitoringStarted bool + globalMonitoringMutex sync.Mutex +) + +// StartMonitoring starts continuous memory monitoring as a global singleton func (mm *MemoryMonitor) StartMonitoring(ctx context.Context, interval time.Duration) { + globalMonitoringMutex.Lock() + defer globalMonitoringMutex.Unlock() + + // Check if monitoring is already started + if globalMonitoringStarted { + if !isTestMode() { + mm.logger.Debug("Memory monitoring already started, skipping duplicate start") + } + return + } + if interval <= 0 { interval = 30 * time.Second } @@ -329,9 +346,12 @@ func (mm *MemoryMonitor) StartMonitoring(ctx context.Context, interval time.Dura return } + // Only start if task was newly created or we're sure it's not already running task.Start() + globalMonitoringStarted = true + if !isTestMode() { - mm.logger.Info("Started memory monitoring with %v interval", interval) + mm.logger.Info("Started global memory monitoring with %v interval", interval) } } @@ -386,6 +406,34 @@ func (mm *MemoryMonitor) GetMemoryPressure() MemoryPressureLevel { return MemoryPressureNone } +// StopMonitoring stops the global memory monitoring if it's running +func (mm *MemoryMonitor) StopMonitoring() { + globalMonitoringMutex.Lock() + defer globalMonitoringMutex.Unlock() + + if !globalMonitoringStarted { + return + } + + registry := GetGlobalTaskRegistry() + if task, exists := registry.GetTask("memory-monitor"); exists { + task.Stop() + globalMonitoringStarted = false + if !isTestMode() { + mm.logger.Info("Stopped global memory monitoring") + } + } else { + mm.logger.Errorf("Failed to find memory monitoring task to stop") + } +} + +// IsMonitoringActive returns true if global memory monitoring is currently active +func (mm *MemoryMonitor) IsMonitoringActive() bool { + globalMonitoringMutex.Lock() + defer globalMonitoringMutex.Unlock() + return globalMonitoringStarted +} + // Global memory monitor instance var ( globalMemoryMonitor *MemoryMonitor diff --git a/memory_monitor_singleton_test.go b/memory_monitor_singleton_test.go new file mode 100644 index 0000000..7f4d738 --- /dev/null +++ b/memory_monitor_singleton_test.go @@ -0,0 +1,157 @@ +package traefikoidc + +import ( + "context" + "runtime" + "sync" + "testing" + "time" +) + +func TestMemoryMonitorSingleton(t *testing.T) { + // Reset global state + globalMonitoringMutex.Lock() + globalMonitoringStarted = false + globalMonitoringMutex.Unlock() + + // Clear task registry for clean test + registry := GetGlobalTaskRegistry() + registry.StopAllTasks() + + // Test that multiple StartMonitoring calls don't create multiple monitors + monitor := GetGlobalMemoryMonitor() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + goroutinesBefore := runtime.NumGoroutine() + + // Call StartMonitoring multiple times (simulating multiple middleware instances) + var wg sync.WaitGroup + numCalls := 10 + + for i := 0; i < numCalls; i++ { + wg.Add(1) + go func() { + defer wg.Done() + monitor.StartMonitoring(ctx, 100*time.Millisecond) + }() + } + + wg.Wait() + + // Give some time for goroutines to start + time.Sleep(50 * time.Millisecond) + + goroutinesAfter := runtime.NumGoroutine() + + // Should only create one additional goroutine for the singleton task + goroutineDiff := goroutinesAfter - goroutinesBefore + if goroutineDiff > 2 { // Allow some tolerance for test runner goroutines + t.Errorf("Expected at most 2 additional goroutines, got %d", goroutineDiff) + } + + // Check that monitoring is active + if !monitor.IsMonitoringActive() { + t.Error("Memory monitoring should be active") + } + + // Verify only one task is registered + taskCount := registry.GetTaskCount() + if taskCount != 1 { + t.Errorf("Expected 1 task, got %d", taskCount) + } + + // Check that the task is specifically memory-monitor + if task, exists := registry.GetTask("memory-monitor"); !exists { + t.Error("memory-monitor task should exist") + } else if task == nil { + t.Error("memory-monitor task should not be nil") + } + + // Stop monitoring + monitor.StopMonitoring() + + // Verify monitoring is no longer active + if monitor.IsMonitoringActive() { + t.Error("Memory monitoring should be inactive after stopping") + } + + // Clean up + registry.StopAllTasks() +} + +func TestMemoryMonitorSingletonTaskCreation(t *testing.T) { + // Reset global state + globalMonitoringMutex.Lock() + globalMonitoringStarted = false + globalMonitoringMutex.Unlock() + + registry := GetGlobalTaskRegistry() + registry.StopAllTasks() + + monitor1 := GetGlobalMemoryMonitor() + monitor2 := GetGlobalMemoryMonitor() + + // Should be the same instance + if monitor1 != monitor2 { + t.Error("GetGlobalMemoryMonitor should return the same instance") + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // First start should succeed + monitor1.StartMonitoring(ctx, 50*time.Millisecond) + + if !monitor1.IsMonitoringActive() { + t.Error("First StartMonitoring should activate monitoring") + } + + // Second start should be skipped + monitor2.StartMonitoring(ctx, 50*time.Millisecond) + + // Should still only have one task + taskCount := registry.GetTaskCount() + if taskCount != 1 { + t.Errorf("Expected 1 task after duplicate start, got %d", taskCount) + } + + // Clean up + monitor1.StopMonitoring() + registry.StopAllTasks() +} + +func TestMemoryMonitorTaskRegistryEnforcement(t *testing.T) { + // Reset global state + globalMonitoringMutex.Lock() + globalMonitoringStarted = false + globalMonitoringMutex.Unlock() + + registry := GetGlobalTaskRegistry() + registry.StopAllTasks() + + // Try to create memory-monitor task directly through registry + task1, err1 := registry.CreateSingletonTask("memory-monitor", 50*time.Millisecond, + func() {}, GetSingletonNoOpLogger(), nil) + + if err1 != nil { + t.Fatalf("First memory-monitor task creation should succeed: %v", err1) + } + + // Try to create another memory-monitor task - should fail + task2, err2 := registry.CreateSingletonTask("memory-monitor-2", 50*time.Millisecond, + func() {}, GetSingletonNoOpLogger(), nil) + + if err2 == nil { + t.Error("Second memory-monitor task creation should fail due to singleton enforcement") + if task2 != nil { + task2.Stop() + } + } + + // Clean up + if task1 != nil { + task1.Stop() + } + registry.StopAllTasks() +}