Паттерн start stop на golang
Есть ряд компонентов у которых общий функционал запуск и остановка сервиса (с возможностью перезапуска). Надо этот общий функционал выделить в отдельный компонент. Поддержка конкурентности. Описание требований к интерфейсу.
start_stop_pattern
import (
"context"
"github.com/pkg/errors"
)
var (
ErrAlreadyRunning = errors.New("already running")
ErrNothingStopped = errors.New("nothing stopped")
)
// New
// f функция пользователя с контекстом, через который
// функция может узнать, что был вызван Stop.
// Пользовательская функция не обязана использовать ctx.
func New(f func(ctx context.Context) error) *Service {
return &Service{}
}
type Service struct{}
// Start
// Запускает переданную f функцию пользователя и блокируется до тех пор, пока f не будет выполнена.
// Если другой поток вызывает Start и она уже запущена, то возвращает ErrAlreadyRunning.
func (s *Service) Start() error {}
// Stop
// Останавливает выполнение f пользовательской функции, если Start запущен и ждет ее завершения.
// Если Start не работает, то возвращает ошибку ErrNothingStopped.
// После вызова Stop можно снова запускать Start.
func (s *Service) Stop() error {}
Тесты
package start_stop_pattern
import (
"context"
"log"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestService(t *testing.T) {
t.Run("race test", func(t *testing.T) {
srv := New(func(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return nil
case <-time.NewTicker(1 * time.Millisecond).C:
}
}
}, time.Millisecond*10)
wg := sync.WaitGroup{}
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
_ = srv.Start()
wg.Done()
}()
wg.Add(1)
go func() {
srv.Stop()
wg.Done()
}()
}
wg.Wait()
})
t.Run("running with done", func(t *testing.T) {
srv := New(func(ctx context.Context) error {
for {
select {
case <-ctx.Done():
log.Println("service stop")
return nil
case <-time.NewTicker(100 * time.Millisecond).C:
log.Println("some do")
}
}
}, time.Millisecond*100)
done := make(chan struct{})
go func() {
time.Sleep(1 * time.Second)
require.ErrorIs(t, srv.Start(), ErrAlreadyRunning)
require.NoError(t, srv.Stop())
time.Sleep(1 * time.Second)
require.ErrorIs(t, srv.Stop(), ErrNothingToStop)
close(done)
}()
require.NoError(t, srv.Start())
<-done
})
t.Run("running without done", func(t *testing.T) {
srv := New(func(ctx context.Context) error {
time.Sleep(1 * time.Second)
return nil
}, time.Millisecond*10)
done := make(chan struct{})
go func() {
time.Sleep(100 * time.Millisecond)
require.ErrorIs(t, srv.Start(), ErrAlreadyRunning)
require.ErrorIs(t, srv.Stop(), ErrNothingStopped)
time.Sleep(100 * time.Millisecond)
require.ErrorIs(t, srv.Stop(), ErrNothingStopped)
close(done)
}()
require.NoError(t, srv.Start())
<-done
srv.Stop()
})
}
Не получается реализовать функционал, удовлетворящий всем требованиям.
Источник: Stack Overflow на русском