Go: Не перегружаем Label при сбое — задача с проверкой
Практическая задача на Go по теме «Не перегружаем Label при сбое». Условие содержит контракт функций, предоставленные типы и требования к результату. Используйте выданный шаблон и сохраняйте его публичные объявления. Перед проверкой разберите успешный путь, обработку ошибок и отмену операции, когда она предусмотрена контрактом. Здесь оценивается конкретный компонент сервиса; в полном курсе микросервисов на Go эти компоненты связываются с HTTP, PostgreSQL, gRPC и Kafka.
В уроке 6.4 вы уже научили сервис делать до трёх попыток одного Label-вызова и ждать между ними. Теперь представьте, что одновременно поступает двадцать таких запросов. Каждый запускает свою серию повторов. Каждая серия в свой момент делает физический POST к Label-сервису. Сервис не знает о соседних — и в худшем случае двадцать горутин одновременно атакуют один медленный upstream. Это thundering herd: каждый клиент ведёт себя разумно, но в совокупности они перегружают цель.
Решение — семафор на буферизованном канале. Один общий канал ёмкости 4 разделяется всеми двадцатью горутинами. Отправка токена в канал означает «я занял слот», чтение означает «я освободил слот». Пока в буфере четыре токена, пятая отправка блокирует горутину. Канал считает ширину параллелизма, а не порядок или время.
Что вам нужно реализовать
Реализуйте RunLabelCall в предоставленном шаблоне:
func RunLabelCall(
ctx context.Context,
slots chan struct{},
policy AdmissionPolicy,
call func(context.Context) error,
) error
Предоставленные типы (не изменяйте):
type AdmissionPolicy string
const AdmissionQueue AdmissionPolicy = "queue"
const AdmissionReject AdmissionPolicy = "reject"
var ErrLabelBusy = errors.New("label concurrency limit reached")
Гарантированные входные условия
ctxиcall— не nil.slots— один незакрытый буферизованный канал ёмкости 4, общий для всех двадцати вызовов; никто снаружи функции не отправляет в него и не читает из него.policy— ровно одно из двух значений:AdmissionQueueилиAdmissionReject.call— синхронный; получает тот же объектctx; может вернуть nil, любую ошибку или запаниковать; отмену обрабатывает кооперативно.
Контракт поведения
1. Проверка до допуска
Если ctx.Err() уже не nil при входе в функцию — вернуть эту ошибку немедленно, не трогая канал и не вызывая call.
2. AdmissionQueue — ожидать или отменить
Ожидание свободного места должно реагировать на отмену исходного контекста.
Пока все четыре слота заняты, горутина блокируется в select, не удерживая слот семафора. Порядок входа не гарантирован — Go не обещает FIFO среди ожидающих. Количество ожидающих горутин не ограничено этим семафором.
3. AdmissionReject — немедленный отказ
Если свободного места нет, возвращается ErrLabelBusy без ожидания и без callback. Если отмена уже наблюдаема при отказе, ctx.Err() имеет приоритет.
ErrLabelBusy — это локальное решение об отказе в допуске, не HTTP-статус и не информация о Label-сервисе. Что делать с этой ошибкой дальше — политика слоя выше.
4. Проверка после приобретения
После успешной отправки токена select мог выбрать именно эту ветку в тот момент, когда ctx.Done() уже закрыт — Go допускает случайный выбор среди одновременно готовых веток. Поэтому сразу после приобретения нужно обнаружить уже наблюдаемую отмену, освободить приобретённый слот и вернуть ctx.Err(), не вызывая callback.
После приобретения освобождается только свой слот. Отмена до приобретения не даёт права читать из канала и освобождать слот другого вызова.
5. Освобождение через defer
Для каждого приобретённого слота обеспечьте ровно одно освобождение при любом последующем выходе.
Этот defer выполнится при любом выходе: обычном return, return с ошибкой, и при панике. Паника не подавляется — функция не делает recover. Исходное значение паники должно пройти наружу после освобождения слота.
6. Вызов callback
Вызвать call ровно один раз, передав тот же объект ctx — не обёртку, не context.Background(), а оригинал. Вернуть его ошибку как есть, включая nil. Не интерпретировать, не подменять, не добавлять retry внутри этой функции.
Пример допуска
Пока четыре вызова работают, следующий запрос с AdmissionQueue ждёт свободного места или отмены. При отмене он завершается без callback и не меняет занятость чужих слотов. С AdmissionReject тот же запрос при живом контексте получает ErrLabelBusy сразу. Когда один допущенный вызов завершится, его место станет доступно следующему запросу; порядок ожидающих не фиксируется.
Ошибка или паника внутри допущенного вызова не должна оставлять место занятым. При этом ошибка возвращается без подмены, а паника распространяется с исходным значением. Один слот относится к одному физическому вызову, поэтому во время backoff следующая серия может использовать освободившееся место.
Что функция не делает
- Не создаёт новый канал, горутину, таймер, контекст, HTTP-запрос или retry-цикл.
- Не создаёт отдельный канал на каждый вызов — иначе общего ограничения не будет.
- Не удерживает слот во время backoff между попытками в 6.4 — слот занят только во время одного физического вызова.
- Не интерпретирует ошибку callback и не заменяет её.
- Не подавляет панику.
- Не освобождает чужие токены при отмене до приобретения.
Связь с 6.3 и 6.4
Сохранённые из 6.3 поля — идентификатор запроса, маршрут POST /labels, заголовок Idempotency-Key — остаются неизменными при повторах. Семафор к идемпотентности ничего не добавляет. Решение о следующей попытке принимает исполнитель 6.4 на основе классификатора 6.3 — RunLabelCall в этом не участвует.
Типичное место вызова внутри одной попытки из 6.4 (иллюстрация контекста интеграции, не вводимый код):
// Внутри одной попытки из 6.4; slots общий для всех серий.
var decision RetryDecision
err := RunLabelCall(ctx, slots, AdmissionQueue, func(callCtx context.Context) error {
decision = callAndClassifySavedPOST(callCtx, savedOperation)
return nil
})
// Исполнитель 6.4 далее использует err и decision.
Задание
Допишите RunLabelCall в предоставленном шаблоне. Сохраните предоставленные типы, константы, переменную ошибки и публичный JSON-адаптер; отправьте весь код шаблона.
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Связанные задачи
- Go: Принять только полный ответ о последней статье — задача с проверкой
- HTTP-клиент поиска на Go: обработка ответа и ошибок — задача
- Go: Декодирование артефакта из JSON — задача с проверкой
- Go: Обязательная и необязательная зависимость при отказе — задача с проверкой
- Go: HTTP-обработчик поиска задания через JobStore — задача с проверкой
- Go: HTTP-обработчик расчёта калорийности состава — задача с проверкой
