package worker import ( "context" "log/slog" "sync/atomic" "testing" "time" ) // Пул — предмет этой работы: воркеров стало сколько угодно одинаковых вместо // трёх именованных. Проверки ниже судят саму обвязку — подъём, остановку и // нулевой размер, — а не шаг, который она крутит: шаг судят проверки конвейера. // countingStep считает свои вызовы и отпускает проверку, когда их набралось // достаточно. func countingStep(t *testing.T, enough int64) (func(context.Context) error, <-chan struct{}, *atomic.Int64) { t.Helper() var calls atomic.Int64 done := make(chan struct{}) var closed atomic.Bool return func(context.Context) error { if calls.Add(1) >= enough && closed.CompareAndSwap(false, true) { close(done) } return nil }, done, &calls } // Пул поднимает столько воркеров, сколько ему назвали, и все они крутят шаг. func TestPoolRunsEveryWorker(t *testing.T) { const size = 4 step, done, calls := countingStep(t, size) pool := NewPool(size, step, slog.New(slog.DiscardHandler)) for _, w := range pool.workers { w.interval = time.Millisecond } if pool.Size() != size { t.Fatalf("в пуле %d воркеров вместо %d", pool.Size(), size) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() finished := make(chan struct{}) go func() { pool.Start(ctx) close(finished) }() select { case <-done: case <-time.After(5 * time.Second): t.Fatalf("шаг позвали %d раз вместо %d: не все воркеры поднялись", calls.Load(), size) } cancel() select { case <-finished: case <-time.After(5 * time.Second): t.Fatal("пул не дождался остановки воркеров: горутина осталась висеть") } } // Нулевой пул — законный режим, а не поломка: сервис поднимается, записи // принимаются и не двигаются. Проверка судит именно это: шаг не зовётся ни разу, // а подъём не блокируется. func TestZeroPoolRunsNothingAndReturns(t *testing.T) { var calls atomic.Int64 pool := NewPool(0, func(context.Context) error { calls.Add(1) return nil }, slog.New(slog.DiscardHandler)) if pool.Size() != 0 { t.Fatalf("пустой пул завёл %d воркеров", pool.Size()) } finished := make(chan struct{}) go func() { pool.Start(context.Background()) close(finished) }() select { case <-finished: case <-time.After(5 * time.Second): t.Fatal("пустой пул не вернул управление: подъём сервиса заблокирован") } if got := calls.Load(); got != 0 { t.Errorf("шаг позвали %d раз при нулевом пуле", got) } } // Отменённый контекст останавливает **всех** воркеров пула: забытая горутина не // падает и не пишет, а держит процесс и продолжает опрашивать базу после // остановки сервиса. func TestPoolStopsEveryWorkerOnCancel(t *testing.T) { const size = 3 step, done, _ := countingStep(t, size) pool := NewPool(size, step, slog.New(slog.DiscardHandler)) for _, w := range pool.workers { w.interval = time.Millisecond } ctx, cancel := context.WithCancel(context.Background()) finished := make(chan struct{}) go func() { pool.Start(ctx) close(finished) }() <-done cancel() select { case <-finished: case <-time.After(5 * time.Second): t.Fatal("пул не остановился по отмене контекста") } }