Два разных повтора: запрос клиента и попытка обработки
Ответ не дошёл
Клиент отправил POST, сервер создал запись, но ответ потерялся. Если клиент повторит запрос, обычное создание даст второе задание. Добавьте необязательный заголовок Idempotency-Key: клиент выбирает ключ операции, а сервер связывает его с заданием и исходным текстом.
Повтор с тем же ключом и тем же text возвращает прежнюю запись. Тот же ключ с другим текстом получает 409: клиент пытается использовать идентификатор одной операции для другой. Сравнивайте исходное поле text до удаления пробелов, а не результат Analyze. В записи задания текст хранится с удалёнными краевыми пробелами; исходный вход для проверки ключа сохраняется отдельно. Поэтому "hello" и " hello " с одним ключом конфликтуют, хотя результат преобразования совпадёт. Без ключа каждый POST остаётся отдельным созданием.
Что прочитать
Прочитайте разделы о неопределённом результате и повторах в HTTP, об обработке сообщений в очередях и событиях, о таймаутах и повторах в надёжности. Повторите errors.Is в ошибках Go и отменяемое ожидание в context.
Реализуйте оба механизма
Проверка ключа и создание задания должны быть одной согласованной операцией: два одновременных запроса с одним ключом не должны пройти проверку «ещё нет» и создать две записи. Связь ключа с записью должна восстанавливаться из файла после перезапуска. Ключ не обещает один запуск Process: восстановление из прошлого этапа допускает повторную обработку одного задания.
Другой повтор происходит внутри обработчика. Process может вернуть временную ошибку, узнаваемую через errors.Is(err, ErrRetryable). После неё разрешена новая попытка с ожиданием RetryDelay. Прочие ошибки сразу завершают задание состоянием failed. Считайте первую попытку в пределе одного цикла обработки: при MaxAttempts: 3 допускаются максимум три вызова в этом цикле, а не первый плюс три повтора. Поле attempts накопительное между перезапусками. После сбоя незавершённая запись может начать новый цикл и получить суммарно больше трёх попыток: это следствие принятой обработки как минимум один раз.
Не используйте голый time.Sleep для ожидания повтора: сервис должен реагировать на отмену. Ожидание заканчивается либо по таймеру, либо по ctx.Done(). Если обработка отменена при остановке, не расходуйте новую попытку просто из-за восстановления записи. Разделяйте число реально начатых попыток, сохранённое состояние и окончательную ошибку.
Проверьте без случайности
go test ./... -run '^TestStage0[1-5]' -count=1
go test -race ./... -run '^TestStage0[1-5]' -count=1Подмените Process: в первом тесте она дважды возвращает ErrRetryable, а затем результат; во втором всегда возвращает временную ошибку; в третьем — обычную ошибку. На новом задании без перезапуска проверьте done и три попытки, failed и три попытки, failed и одну попытку соответственно. Сделайте отдельный тест отмены во время задержки.
Для ключа отправьте несколько конкурентных одинаковых запросов, сравните id, затем измените текст и проверьте 409. Перезапустите приложение с тем же файлом и повторите запрос с прежним ключом. Последовательный тест сам по себе не проверяет гонку двух первых запросов.
На что посмотреть в ошибках
Бесконечные повторы удерживают обработчики и скрывают неисправность. Повтор всех ошибок заставляет заново выполнять работу с неверными данными. Увеличение лимита без причины может усилить нагрузку на неисправную зависимость. В этом проекте задержка постоянная для простоты; перед работой с внешними зависимостями отдельно изучают backoff, случайный разброс и общий бюджет времени.
Когда идти дальше
Объясните разницу между двумя id от повторных запросов без ключа, одним id с ключом и несколькими попытками одного id. Тесты проверяют заданные сценарии; они не доказывают отсутствие любых дублей внешних эффектов. Дальше сделаем работу сервиса видимой.
Самопроверка этапа
Отмечайте результат после своей проверки. Открытие главы ничего не завершает. Отметки сохраняются в этом браузере и не являются независимой аттестацией.
Для завершения отметьте все критерии и добавьте объяснение.