Реализуйте RunService. Она запускает набор фоновых задач (метрики, брокер сообщений, очистка кэша). Каждая задача получает один и тот же ctx и обязана слушать ctx.Done().
func RunService(ctx context.Context, grace time.Duration, tasks ...func(ctx context.Context)) error
Как это работает:
- Все задачи запускаются в отдельных горутинах с переданным
ctx.
RunService ждёт отмены ctx. Когда её вызвал внешний код, у задач есть время grace, чтобы завершиться.
- Если все задачи вернулись в пределах
grace, функция возвращает nil.
- Если хотя бы одна не уложилась, функция всё равно возвращается по истечении
grace и отдаёт ошибку. Зависшая задача продолжает работать: убить чужую горутину в Go нельзя.
Пример:
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(time.Second)
cancel() // например, пришёл SIGTERM
}()
err := RunService(ctx, 2*time.Second, metrics, broker, cleanup)
// все три вернулись быстро: err == nil
// если одна из них игнорирует ctx и спит 10 секунд: через ~2 с после cancel err != nil
Требования:
- Отсчёт
grace начинается с момента отмены ctx, а не с момента запуска сервиса.
- Функция не зависает на задаче, которая игнорирует
ctx.
- Задачи запускаются до того, как функция начинает ждать отмену.
- Нет гонок под
-race.
Тесты: три задачи, которые ждут ctx.Done(), все завершаются и возвращается nil; одна задача спит 2 секунды, игнорируя ctx, при grace = 300ms функция возвращает ошибку и не дожидается её. Ещё проверяется, что grace отсчитывается от отмены (сервис проработал дольше grace, но задачи вышли сразу после cancel, ответ nil), что при зависшей задаче возврат происходит не раньше конца grace после отмены, и что при уже отменённом ctx все задачи всё равно запускаются.