Срез 0 (issue #1): раздел «Соглашения шаблона» (что оставлено от шаблона команды, что вырезается, что добавляется); change openspec/changes/slice-0-skeleton — proposal, design с таблицей «Decisions made without discussion», спеки и tasks; в openspec/config.yaml строки с двоеточием взяты в кавычки (YAML разбирал их как словари). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
6.5 KiB
Spec Delta: recognition-job-exchange
Purpose
Единственная точка обмена ядра с сервисами распознавания — таблица recognition_job (DECISION-4): outbox публикует
запросы в NATS, inbox принимает результаты идемпотентно, таймауты и DLQ отражаются в статусе задания, файл результата
регистрируется в s3_object.
ADDED Requirements
Requirement: Outbox публикует задания из queued
Фоновый публикатор SHALL с периодом OUTBOX_POLL_SECONDS (по умолчанию 5) выбирать задания status = queued
(с блокировкой FOR UPDATE SKIP LOCKED), формировать сообщение по recognition-request.schema.json v1.0 из
recognition_job ⋈ stored_file ⋈ s3_object и публиковать в subject по pipeline с заголовком
Nats-Msg-Id = <job_id>:<attempt>. После подтверждения JetStream задание SHALL получить status = published,
published_at, request_msg_id, request_subject; при ошибке публикации задание SHALL остаться queued.
Scenario: Публикация запроса парсеру
- WHEN в
recognition_jobвставлена строкаqueuedсpipeline = tabular, связанная соstored_file(XLSX) и егоs3_object - THEN в течение 10 с в
recognition.tabular.requestпоявляется сообщение, проходящее схему запроса, сjob_id,file_id,s3.keyисходника,reply_deadline,attempt = 1, а строка переходит вpublishedс заполненнымrequest_msg_id
Scenario: NATS недоступен
- WHEN публикация завершается ошибкой соединения
- THEN задание остаётся
queued, ошибка залогирована, следующий цикл повторяет публикацию; дубликат после восстановления отбрасывается JetStream поNats-Msg-Id
Requirement: Inbox принимает результат идемпотентно
Durable consumer arm-core-results на recognition.result SHALL валидировать конверт по
recognition-result-message.schema.json, найти задание по job_id и: если result_msg_id задания уже равен
Nats-Msg-Id сообщения — подтвердить без изменений; иначе зарегистрировать файл результата в s3_object
(bucket_role = results, created_by = <service.name>), обновить задание (status ∈ {done, needs_check, error},
service_name, service_version, finished_at, metrics, error_code, error_message, result_schema_version,
result_s3_object_id, result_msg_id), зафиксировать транзакцию и только затем подтвердить сообщение. Неизвестный
job_id SHALL быть залогирован и подтверждён. Сообщение с num_delivered ≥ max_deliver SHALL перевести задание в dlq.
Scenario: Успешный результат
- WHEN сервис публикует конверт
status = doneсresult_s3 = {arm-ka, results/<file_id>/<job_id>/result.json}иNats-Msg-Id = result:<job_id>:1 - THEN задание получает
status = done,result_msg_id = result:<job_id>:1,result_s3_object_idуказывает на новую строкуs3_objectс этим ключом,finished_atзаполнен
Scenario: Повторная доставка
- WHEN то же сообщение доставляется второй раз
- THEN строка
recognition_jobиs3_objectне меняются, сообщение подтверждено
Scenario: Ошибка сервиса
- WHEN конверт содержит
status = error,error = {NOT_IMPLEMENTED, …}безresult_s3 - THEN задание получает
status = error,error_code = NOT_IMPLEMENTED,error_messageиз конверта,result_s3_object_idпуст
Scenario: Отравленное сообщение
- WHEN обработка одного и того же сообщения падает ошибкой БД пять раз подряд (
max_deliver = 5) - THEN на пятой доставке задание переводится в
dlq, сообщение подтверждено, повторов больше нет
Requirement: Таймаут ожидания результата
Фоновый вахтёр SHALL с периодом 30 с переводить задания published или processing с reply_deadline < now() в
status = timeout, error_code = TIMEOUT, finished_at = now(). Replay задания (новая попытка) — вне Среза 0.
Scenario: Сервис не ответил
- WHEN задание опубликовано с
reply_deadlineв прошлом и результата нет - THEN не позднее чем через 30 с задание имеет
status = timeout
Requirement: Соединение с NATS по шаблону, stream — по флагу
Ядро SHALL подключаться к NATS с nkeys-seed и JWT, если заданы OPENBAO__NATS__NATS_SEED/NATS_JWT, и без учётных данных,
если они пусты; при NATS_MANAGE_STREAM=true SHALL создать stream RECOGNITION (subjects recognition.>,
окно дубликатов 2 мин) и свой consumer, если их нет. Фоновые задачи SHALL запускаться при NATS_ENABLED=true.
Scenario: Dev-окружение compose
- WHEN ядро стартует с
DEBUG=true,NATS_ENABLED=true,NATS_MANAGE_STREAM=trueи пустыми учётными данными - THEN stream
RECOGNITIONи consumerarm-core-resultsсуществуют, в логах нет ошибок подключения
Scenario: NATS выключен
- WHEN
NATS_ENABLED=false - THEN приложение стартует, фоновые задачи не запускаются,
/healthcheckотвечает 200