Files
traefikoidc/review_r88_test.go
T
lukaszraczylo 866e57380d R88: make StopAllTasks cover ResourceManager singleton tasks
TaskRegistry.CreateSingletonTask delegates to ResourceManager.RegisterBackgroundTask
and stores the task in the ResourceManager's registry, not TaskRegistry.tasks.
TaskRegistry.StopAllTasks previously iterated its own (always-empty) registry,
silently leaking every process-global singleton task's ticker goroutine past the
last Close (memory-monitor, replay-cache-cleanup) — tasks kept running real
periodic work with no instance to serve.

Add ResourceManager.StopAllTasks (stops all rm-owned tasks) and have
TaskRegistry.StopAllTasks stop both its own RegisterTask-direct tasks and the
ResourceManager-owned singletons.

Regression TestR88StopAllTasksStopsResourceManagerOwnedTasks (review_r88_test.go):
fails on old code (task still running after StopAllTasks), passes on new.
2026-08-13 19:25:03 +01:00

65 lines
2.3 KiB
Go

package traefikoidc
import (
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
)
// R88 regression: process-global singleton tasks are created via
// TaskRegistry.CreateSingletonTask but stored in the ResourceManager's
// registry, not in TaskRegistry.tasks. TaskRegistry.StopAllTasks previously
// iterated its own (always-empty) registry, silently leaking every singleton
// task's ticker goroutine past the last Close (memory-monitor,
// replay-cache-cleanup). It must now delegate to the ResourceManager that
// actually owns the tasks so a last-instance shutdown stops them.
func TestR88StopAllTasksStopsResourceManagerOwnedTasks(t *testing.T) {
rm := GetResourceManager()
reg := GetGlobalTaskRegistry()
// Snapshot the currently-running singleton tasks so we can re-create them
// after the global teardown below and leave the shared suite un-polluted.
type restore struct {
name string
interval time.Duration
fn func()
logger *Logger
wg *sync.WaitGroup
}
var toRestore []restore
rm.tasksMu.RLock()
for name, task := range rm.tasks {
if atomic.LoadInt32(&task.started) == 1 && atomic.LoadInt32(&task.stopped) == 0 {
toRestore = append(toRestore, restore{name, task.interval, task.taskFunc, task.logger, task.externalWG})
}
}
rm.tasksMu.RUnlock()
var wg sync.WaitGroup
name := fmt.Sprintf("shutdown-probe-%d", time.Now().UnixNano())
if _, err := reg.CreateSingletonTask(name, time.Hour, func() {}, newNoOpLogger(), &wg); err != nil {
t.Fatalf("CreateSingletonTask: %v", err)
}
if !rm.IsTaskRunning(name) {
t.Fatalf("expected singleton task %q to be running after CreateSingletonTask", name)
}
// This is the exact shutdown path Close() relies on
// (utilities.go: lastInstance -> GetGlobalTaskRegistry().StopAllTasks()).
reg.StopAllTasks()
if rm.IsTaskRunning(name) {
t.Fatalf("singleton task %q still running after StopAllTasks: global task teardown is a no-op (task lived in ResourceManager, registry iterated its own empty map)", name)
}
// Restore the pre-existing running singletons so this global teardown does
// not destabilize sibling tests that still hold live instances.
for _, r := range toRestore {
if _, err := reg.CreateSingletonTask(r.name, r.interval, r.fn, r.logger, r.wg); err != nil {
t.Errorf("restore task %q: %v", r.name, err)
}
}
}