diff --git a/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md b/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md index 219679d..fbc0f56 100644 --- a/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md +++ b/openspec/changes/slice-0-skeleton/specs/recognition-contract/spec.md @@ -21,8 +21,28 @@ заголовком `Nats-Msg-Id` = `:` #### Scenario: Сообщение не по схеме -- **WHEN** сервис получает запрос, не проходящий схему (нет `s3.key`, неизвестный `file_type`) -- **THEN** сервис публикует результат со `status = error`, `error.code = SCHEMA_MISMATCH` и не падает +- **WHEN** сервис получает запрос, не проходящий схему (нет `s3.key`, неизвестный `file_type`), но из тела извлекаются + корректные `job_id` и `file_id` (и `attempt` для `Nats-Msg-Id`) +- **THEN** сервис публикует результат со `status = error`, `error.code = SCHEMA_MISMATCH` **без предшествующего ack + `processing`** и не падает + +#### Scenario: Нечитаемый запрос +- **WHEN** сервис получает сообщение, из которого нельзя извлечь `job_id` (не JSON, не объект, `job_id` отсутствует или + не uuid), а значит нельзя собрать валидный конверт +- **THEN** сервис ответа не публикует: запрос записывается в лог и отбрасывается; ядро не получит ack, повторит + публикацию `OUTBOX_MAX_REPUBLISH` раз и переведёт задание в `dlq` (`error_code = NO_ACK`) + +#### Scenario: Невалидный повтор запроса, уже находящегося в работе +- **WHEN** сервис получает запрос, не проходящий схему, но с `job_id`, который уже в работе или обработан этим процессом +- **THEN** конверт `SCHEMA_MISMATCH` не публикуется (его `Nats-Msg-Id` `result::` совпал бы с настоящим + результатом, и ядро отбросило бы настоящий как дубликат); запрос записывается в лог и отбрасывается + +#### Scenario: Примеры контракта — иллюстрации +- **WHEN** запрос берётся из `docs/contracts/examples/request-*.json` (в `request-image.json` `reply_deadline` лежит в + прошлом) +- **THEN** он проходит схему: схема проверяет для `reply_deadline` лишь формат `date-time`, но не сравнивает срок с + текущим временем; пример — иллюстрация формы сообщения, а не готовое к исполнению задание; проверка срока — дело + сервиса (`scan-gateway` отвечает `error TIMEOUT`, если `reply_deadline` истёк до отправки файла в ИС Сканирование) ### Requirement: Подтверждение взятия в работу (ack без JetStream) Получив запрос, сервис SHALL немедленно, до чтения файла из S3, опубликовать в `recognition.result` конверт со @@ -82,8 +102,10 @@ v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id`, `f #### Scenario: Несовместимая версия запроса - **WHEN** сервис, поддерживающий `1.x`, получает запрос `schema_version = "2.0"` -- **THEN** публикуется результат `status = error`, `error.code = SCHEMA_MISMATCH`, запрос подтверждается (ack), чтобы не - зациклить доставку +- **THEN** публикуется результат `status = error`, `error.code = SCHEMA_MISMATCH` **без ack `processing`**: ответом на + запрос служит сам финальный конверт, ядро закрывает им задание (`published → error`), и повторной доставки запроса не + будет. Конверт публикуется только если из тела извлекаются корректные `job_id` и `file_id`; иначе — как для нечитаемого + запроса (ответа нет, ядро закроет задание по `NO_ACK` / `dlq`) ### Requirement: Идемпотентность результатов без брокера Публикующий SHALL ставить заголовок `Nats-Msg-Id` на каждое сообщение (запрос `:`, ack @@ -95,6 +117,23 @@ v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id`, `f - **WHEN** сервис опубликовал один и тот же результат два раза (повтор публикации после сетевой ошибки) - **THEN** задание изменено один раз, второе сообщение проигнорировано +### Requirement: Повторы публикации ответов сервисом +Сервис SHALL публиковать ack и результат с заголовком `Nats-Msg-Id` и при временной ошибке публикации (потеря соединения, +таймаут подтверждения сервера) повторять публикацию с тем же `Nats-Msg-Id`: **одна первая попытка и до +`PUBLISH_RETRIES` повторов** (значение по умолчанию 3, то есть не более 4 попыток), с паузой между попытками; пауза — +деталь реализации. Повторы исчерпаны — сервис пишет error-лог и не публикует это сообщение снова; задание закрывается +на стороне ядра (не пришёл результат — `reply_deadline` → `timeout`; не пришёл ack — повтор запроса и `dlq`, но результат, +дошедший без ack, ядро принимает и в статусе `published`). Дубликаты, появившиеся из-за повтора, +безопасны: ядро дедуплицирует их по `Nats-Msg-Id`. + +#### Scenario: Временный сбой публикации +- **WHEN** публикация результата упала с временной ошибкой, а следующая попытка успешна +- **THEN** сообщение опубликовано с прежним `Nats-Msg-Id`; ядро изменит задание один раз, даже если первая попытка тоже дошла + +#### Scenario: Повторы исчерпаны +- **WHEN** все 1 + `PUBLISH_RETRIES` попыток публикации результата неуспешны +- **THEN** сервис пишет error-лог и продолжает работу (процесс не падает), а ядро закрывает задание по `reply_deadline` + ### Requirement: Копия контракта в сервисе идентична источнику Каждый сервис, читающий или пишущий контракт, SHALL хранить копию файлов схем в `api/contracts/` и проверять в `make check` их байтовую идентичность файлам `docs/contracts/` (`../docs` или путь `ARM_KA_DOCS`; при недоступности — пропуск с предупреждением).