package jobs import ( "context" "sync" "testing" "time" ) type tickRunner struct { mu sync.Mutex calls, active, max int block chan struct{} } func (r *tickRunner) Tick(ctx context.Context, _ string) (TickResult, error) { r.mu.Lock() r.calls++ r.active++ if r.active > r.max { r.max = r.active } r.mu.Unlock() select { case <-r.block: case <-ctx.Done(): } r.mu.Lock() r.active-- r.mu.Unlock() return TickResult{}, nil } func TestWorkerLoopStartStopAndPreventsOverlap(t *testing.T) { runner := &tickRunner{block: make(chan struct{})} loop := NewWorkerLoop(runner, LoopConfig{Interval: time.Millisecond, WorkerID: "loop"}) ctx, cancel := context.WithCancel(context.Background()) defer cancel() loop.Start(ctx) deadline := time.After(time.Second) for { runner.mu.Lock() calls := runner.calls runner.mu.Unlock() if calls > 0 { break } select { case <-deadline: t.Fatal("loop did not tick") default: time.Sleep(time.Millisecond) } } time.Sleep(10 * time.Millisecond) runner.mu.Lock() if runner.max != 1 || runner.calls != 1 { t.Fatalf("calls=%d max=%d", runner.calls, runner.max) } runner.mu.Unlock() close(runner.block) loop.Stop() loop.Stop() } func TestWorkerLoopTickNowSkipsOverlap(t *testing.T) { r := &tickRunner{block: make(chan struct{})} l := NewWorkerLoop(r, LoopConfig{WorkerID: "w"}) done := make(chan struct{}) go func() { _, _ = l.Tick(context.Background()); close(done) }() time.Sleep(time.Millisecond) if _, ok := l.Tick(context.Background()); ok { t.Fatal("overlap was not skipped") } close(r.block) <-done }