Go: Один бюджет времени на попытки — задача с проверкой
Практическая задача на Go по теме «Один бюджет времени на попытки». Условие содержит контракт функций, предоставленные типы и требования к результату. Используйте выданный шаблон и сохраняйте его публичные объявления. Перед проверкой разберите успешный путь, обработку ошибок и отмену операции, когда она предусмотрена контрактом. Здесь оценивается конкретный компонент сервиса; в полном курсе микросервисов на Go эти компоненты связываются с HTTP, PostgreSQL, gRPC и Kafka.
В этом задании вы реализуете функцию, которая запускает до трёх кооперативных вызовов одной сохранённой операции Label, не выходя за рамки единственного входящего контекста. Никакого нового контекста, никакого реального сна, никакого HTTP внутри оцениваемой функции.
Контекст как единственный бюджет
Входящий запрос принёс ctx с дедлайном или возможностью отмены. Этот контекст — общий бюджет для всей серии. Функция принимает его и передаёт каждому вызову attempt без изменений: один и тот же объект, проверяемый по идентичности.
Если создать context.WithTimeout(context.Background(), d) внутри исполнителя, новый контекст потеряет связь с исходным запросом: клиент ушёл — сервер продолжает попытки. Здесь эта ошибка исключена по конструкции.
Важный предел: ctx.Done() — кооперативный сигнал. Если attempt игнорирует переданный контекст и не возвращается, исполнитель не может прервать его силой. После возврата attempt повторная проверка ctx.Err() обнаружит завершение бюджета, даже если сам callback вернул решение.
Предоставленные типы
Эти объявления зафиксированы и не подлежат изменению. Ваш код использует их как есть.
import (
"context"
"errors"
"time"
)
type RetryAction string
const (
ActionStop RetryAction = "stop"
ActionLookup RetryAction = "lookup"
ActionRetry RetryAction = "retry"
)
type RetryDecision struct {
Action RetryAction
WaitSeconds int
}
type RetryTimer struct {
C <-chan time.Time
Stop func()
}
var ErrAttemptsExhausted = errors.New("label attempts exhausted")
Сигнатура оцениваемой функции
func RunLabelRetries(
ctx context.Context,
attempt func(context.Context) RetryDecision,
newTimer func(time.Duration) RetryTimer,
jitter func(int) time.Duration,
) (RetryDecision, error)
Валидные входные данные
- ctx — ненулевой контекст входящего запроса, возможно уже отменённый или с истёкшим дедлайном. Передаётся по идентичности каждому
attempt. Никакого новогоBackground,TODO,WithoutCancelили отдельного таймаута на попытку. - attempt — ненулевой синхронный кооперативный callback, замыкающий сохранённое намерение Label (один
request_id, одинparcel_id, одинIdempotency-Key). Возвращает одно из трёх решений. Использует переданный контекст и возвращается после его отмены, не блокируя вечно. Функция не создаёт HTTP-запросы и не разбирает заголовки — это сделала 6.3. - Решение stop/lookup:
WaitSeconds=0. Решение retry:WaitSecondsв диапазоне 0–60 в пределах этой задачи. Production-интеграция должна отдельно обработать бо́льшие значения. - newTimer — ненулевая фабрика, вызывается только для планируемого ожидания с неотрицательной
time.Duration. ВозвращаетRetryTimerс ненулевым каналомCи ненулевымStop.Stopбезопасно вызывать ровно один раз для каждого созданного таймера. - jitter — ненулевой детерминированный чистый callback без побочных эффектов. Вызывается ровно один раз для каждого планируемого ожидания: с индексом 1 для первого и с индексом 2 для второго. Возвращает
time.Durationв диапазоне [0, 50 ms].
Nil-зависимости, некорректные решения и паники фабрики находятся за пределами допустимых входов этой задачи.
Правила выполнения
- Проверяйте
ctx.Err()перед каждым вызовомattempt, после каждого возвратаattemptи после каждого пробуждения таймера. Если контекст завершён — возвращайтеRetryDecision{}и самctx.Err(). - Передавайте один и тот же
ctxв каждый вызовattempt. - Максимум три вызова
attempt: начальный и не более двух повторов. - Базовый backoff: 100 ms после первой попытки, 200 ms после второй.
- Формула ожидания:
max(base + jitter(retryIndex), time.Duration(decision.WaitSeconds) * time.Second). - Индексы jitter: 1 для первого ожидания, 2 для второго.
- В каждом ожидании используйте
selectпоctx.Done()иtimer.C. - Вызывайте
timer.Stop()ровно один раз для каждого созданного таймера — в любой ветке выхода из ожидания. - После
selectпроверяйтеctx.Err()независимо от выбранной ветки: если оба канала готовы, это делает поведение детерминированным. ActionStopиActionLookup— терминальные: возвращаются немедленно сnil, без jitter, таймера и следующего вызова.- Если третий вызов вернул
ActionRetryпри живом контексте — возвращайте это решение иErrAttemptsExhausted. Четвёртого вызова и третьего ожидания нет. - Отмена контекста имеет приоритет над решением callback и над
ErrAttemptsExhausted: при завершённом контексте возвращайтеRetryDecision{}иctx.Err(). - Никакого
time.After, реальногоtime.Sleep, глобального источника случайности, добавленных горутин, изменяемого глобального состояния и HTTP-вызовов.
Что контекст не гарантирует
Контекст не прерывает принудительно кооперативный callback, если тот игнорирует его. Истечение контекста не доказывает, что сервер отклонил уже начатый POST /labels до его принятия. Нулевое решение при отмене сообщает об отсутствии финального решения серии — не о судьбе уже отправленного запроса. Именно поэтому все попытки в серии несут одно и то же сохранённое намерение: тот же request_id, тот же parcel_id, тот же Idempotency-Key. Менять их между попытками запрещено — это превратило бы повтор в новую операцию.
Таблица исходов
- Контекст завершён до входа — 0 вызовов, 0 таймеров, нулевое решение,
ctx.Err(). - Первый stop или lookup — 1 вызов, 0 таймеров, исходное решение,
nil. - retry → stop — 2 вызова, 1 таймер,
jitter(1), Stop таймера, исходное решение,nil. - retry → retry → stop/lookup — 3 вызова, 2 таймера,
jitter(1)иjitter(2), оба Stop, терминальное решение,nil. - retry → retry → retry (живой контекст) — 3 вызова, 2 таймера, последнее решение,
ErrAttemptsExhausted. - Отмена во время ожидания — следующего вызова нет, текущий таймер Stop, нулевое решение,
ctx.Err(). - Отмена внутри кооперативного callback — после его возврата нет нового вызова, нет нового таймера, нулевое решение,
ctx.Err().
Арифметический пример
t=0 attempt(ctx) → retry/0
jitter(1)=25ms, base=100ms, WaitSeconds=0
wait = max(125ms, 0) = 125ms → newTimer(125ms)
t=125 attempt(ctx) → retry/0
jitter(2)=40ms, base=200ms, WaitSeconds=0
wait = max(240ms, 0) = 240ms → newTimer(240ms)
t=365 attempt(ctx) → lookup/0
return lookup/0, nil
Если дедлайн контекста наступает в t=300 ms, второй таймер (сработал бы в t=365 ms) прерывается по ctx.Done(). timer.Stop() вызывается, третьей попытки нет, возвращается RetryDecision{} и context.DeadlineExceeded.
Если первая попытка вернула retry/WaitSeconds=2 при том же дедлайне: max(100+jitter(1), 2000ms) = 2000 ms. Таймер создаётся на 2000 ms, дедлайн в t=300 ms прерывает первое ожидание. Второй попытки нет.
Эти числа — арифметические трассы, не измеренное время планировщика Go. Значения 100/200 ms и лимит трёх попыток — учебные редакционные решения, не универсальные параметры сервиса.
Стартовый код
func RunLabelRetries(
ctx context.Context,
attempt func(context.Context) RetryDecision,
newTimer func(time.Duration) RetryTimer,
jitter func(int) time.Duration,
) (RetryDecision, error) {
// TODO: preserve ctx, schedule no more than three attempts and stop each timer.
return RetryDecision{}, nil
}
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Связанные задачи
- Go: Принять только полный ответ о последней статье — задача с проверкой
- HTTP-клиент поиска на Go: обработка ответа и ошибок — задача
- Go: Декодирование артефакта из JSON — задача с проверкой
- Go: Обязательная и необязательная зависимость при отказе — задача с проверкой
- Go: HTTP-обработчик поиска задания через JobStore — задача с проверкой
- Go: HTTP-обработчик расчёта калорийности состава — задача с проверкой
