Есть функция со случайной задержкой, которая возвращает число. Её тело менять
нельзя: условно, внутри сетевой вызов чужой библиотеки, и контекст он не
принимает.
func flakyOp() int64 {
d := rand.Int63n(4000)
time.Sleep(time.Duration(d) * time.Millisecond)
return d
}
func callWithBudget() int64 {
return flakyOp()
}
Нужна обёртка с бюджетом времени (например, 1 секунда). Если op успела,
обёртка возвращает её результат, если не успела — ошибку. Ещё обёртка замеряет,
сколько длился вызов, и отдаёт это время в observe (там логирование или
метрика). Сигнатуру можно менять, поэтому функция и логгер передаются явно:
// CallWithBudget вызывает op и ждёт её не дольше budget.
// observe (если не nil) вызывается ровно один раз перед возвратом.
func CallWithBudget(ctx context.Context, budget time.Duration, op func() int64,
observe func(d time.Duration, err error)) (int64, error)
Требования
- Если
op вернулась за budget, функция возвращает (результат, nil) сразу,
не дожидаясь конца бюджета.
- Если не успела, функция возвращает
(0, err), где
errors.Is(err, context.DeadlineExceeded), и делает это сразу по истечении
budget, а не когда op всё-таки закончится.
- Если внешний
ctx отменили или его дедлайн наступил раньше budget,
функция сразу возвращает (0, ctx.Err()).
op прервать нельзя, она доработает в фоне. Но когда она закончится, от
CallWithBudget не должно остаться ни одной горутины.
observe вызывается ровно один раз, до возврата. Ему передаётся время от
начала вызова до возврата и та же ошибка, которую возвращает функция
(nil при успехе).
- Код проходит
go test -race.
Пример
v, err := CallWithBudget(ctx, time.Second, flakyOp, func(d time.Duration, err error) {
log.Printf("flakyOp: %v, err=%v", d, err)
})
// flakyOp за 300 мс → v == 300, err == nil, в логе ~300ms
// flakyOp за 3 с → через ~1 с err == context.DeadlineExceeded
Такой же замер хочется навесить на любой долгий вызов, не копируя код.
// Timed возвращает функцию с той же сигнатурой, что fn: она вызывает fn с тем же ctx
// и после завершения отдаёт в observe длительность и ошибку.
func Timed[T any](fn func(ctx context.Context) (T, error),
observe func(d time.Duration, err error)) func(ctx context.Context) (T, error)
- Результат и ошибка
fn возвращаются без изменений, ctx передаётся как есть.
observe вызывается ровно один раз на каждый вызов обёртки, после
завершения fn.
Устно: что происходит с op после тайм-аута, и как сделать, чтобы она
действительно прекращала работу?