12 KiB
Raw Blame History

tabular-recognition-service Specification

Purpose

Stateless-сервис разбора табличных файлов (XLS/XLSX/XLSB): принимает задания из NATS без JetStream, подтверждает их, читает исходник из S3, отдаёт результат в S3 и конверт в NATS по контракту v1.1, не имея доступа к БД и фронта. С Среза 2a реализация по умолчанию разбирает ОСВ (спека osv-parsing); заглушка NullParser — только для тестов воркера.

Requirements

Requirement: Сервис не зависит от БД и фронта

Репозиторий MUST NOT содержать каталоги web/, alembic/, src/db/, src/models/, src/repositories/; зависимости SQLAlchemy, asyncpg, alembic, smbprotocol, raisa_fastapi_protected_api MUST NOT присутствовать в requirements.txt; настройки OPENBAO.DATABASE и OPENBAO.SMB MUST NOT требоваться для старта.

Scenario: Старт с минимальным окружением

  • WHEN сервис запускается с .env, содержащим только OPENBAO__MINIO__*, OPENBAO__NATS__NATS_URL, DEBUG=true
  • THEN GET /healthcheck отвечает 200 с полями uptime, version, nats

Requirement: Подтверждение запроса до обработки

Сервис SHALL быть подписан на recognition.tabular.request в queue group tabular-parser. Получив запрос, проходящий recognition-request.schema.json, сервис SHALL до чтения файла из S3 опубликовать в recognition.result конверт status = processing с job_id, file_id, service {name: "tabular-parser", version} и заголовком Nats-Msg-Id = ack:<job_id>:<attempt>. Повторный запрос с job_id, который уже обрабатывается или обработан этим процессом, SHALL получить повторный ack и MUST NOT обрабатываться второй раз.

Scenario: Ack раньше результата

  • WHEN опубликован запрос из docs/contracts/examples/request-tabular.json
  • THEN первым сообщением сервиса в recognition.result является конверт processing, и только затем — результат

Scenario: Повторный запрос во время обработки

  • WHEN тот же запрос доставлен второй раз, пока первый обрабатывается
  • THEN публикуется второй ack processing, файл из S3 повторно не читается, результат публикуется один раз

Requirement: Результат по контракту

После подтверждения сервис SHALL прочитать объект s3.key из бакета s3.bucket, вызвать парсер, записать файл результата (по recognition-result.schema.json) в results/{file_id}/{job_id}/result.json и опубликовать конверт результата (по recognition-result-message.schema.json) с заголовком Nats-Msg-Id = result:<job_id>:<attempt>. При неудаче публикации сервис SHALL повторить её до трёх раз с паузой, затем залогировать ошибку.

Scenario: Запрос с заглушкой парсера (Срез 0)

  • WHEN воркеру явно передана заглушка NullParser (только тесты воркера; реализация по умолчанию — реальный парсер), запрос подтверждён и объект существует в uploads/
  • THEN в recognition.result появляется конверт status = error, error.code = NOT_IMPLEMENTED, job_id/file_id из запроса, metrics.duration_ms ≥ 0; файл результата может отсутствовать

Scenario: Запрос с реальным парсером

  • WHEN запрос подтверждён и объект в uploads/ существует, но не является книгой Excel (сигнатура не XLS/XLSX/XLSB)
  • THEN в recognition.result появляется конверт status = error, error.code = UNSUPPORTED_FORMAT (повреждённая книга — UNREADABLE), job_id/file_id из запроса, metrics.duration_ms ≥ 0; файл результата может отсутствовать; корректная ОСВ в том же потоке даёт done (см. «Парсер — заменяемый интерфейс»)

Scenario: Повтор публикации

  • WHEN первая публикация конверта результата завершается ошибкой соединения, вторая — успешно
  • THEN конверт опубликован один раз с тем же Nats-Msg-Id, обработка не повторяется

Requirement: Ошибки транспорта превращаются в результат

Сервис SHALL отвечать конвертом status = error в случаях: тело не проходит схему или MAJOR версии ≠ 1 → SCHEMA_MISMATCH; объект S3 не читается → FILE_READ_ERROR; исключение парсера → UNKNOWN_ERROR (сообщение без трассировки). Сервис MUST NOT падать по этим причинам. SCHEMA_MISMATCH SHALL публиковаться (без ack), когда из тела читаемы job_id и file_id (uuid), а attempt — из тела или из заголовка запроса Nats-Msg-Id = <job_id>:<attempt>, и этот job_id сервису ещё не известен (не в работе и не среди обработанных). Иначе валидный конверт собрать нельзя либо он ушёл бы под тем же Nats-Msg-Id, что и настоящий результат: сервис SHALL записать в лог ошибку (error_category=schema_mismatch, причина) — уровнем error, если идентификаторы не читаемы, уровнем warning, если job_id уже известен, — и MUST NOT публиковать ни ack, ни результат; ядро обнаружит такое задание по dlq/timeout (явная запись в контракт — открытый вопрос «невалидный запрос без читаемых job_id/file_id/attempt» в open-questions-proposal.md). FILE_READ_ERROR и UNKNOWN_ERROR публикуются всегда: сервис MUST NOT молчать по ним.

Scenario: Нет объекта в S3

  • WHEN s3.key запроса указывает на несуществующий объект
  • THEN публикуется конверт error.code = FILE_READ_ERROR, процесс продолжает работу

Scenario: Несовместимая версия

  • WHEN запрос содержит schema_version = "2.0"
  • THEN публикуется конверт error.code = SCHEMA_MISMATCH, ack не публикуется

Scenario: Невалидный запрос без читаемых идентификаторов

  • WHEN тело запроса не JSON либо в нём не читаются job_id или file_id, либо attempt нет ни в теле, ни в заголовке Nats-Msg-Id
  • THEN в лог пишется ошибка error_category=schema_mismatch, ни ack, ни результат не публикуются, процесс продолжает работу

Scenario: Невалидный запрос с уже известным job_id

  • WHEN запрос не проходит схему, а его job_id уже в работе или среди обработанных
  • THEN в лог пишется предупреждение error_category=schema_mismatch, ни ack, ни результат не публикуются; обработка или результат задания с этим job_id не меняются

Requirement: Парсер — заменяемый интерфейс

Разбор файла SHALL выполняться через единый интерфейс «байты + тип файла → документы/метрики/ошибка», не знающий о NATS и S3; воркер MUST NOT зависеть от конкретной реализации. Реализация по умолчанию (get_parser()) SHALL разбирать ОСВ по спеке osv-parsing и писать файл результата с schema_version = "1.1"; листы с признаками ББ/ОФР SHALL пропускаться с предупреждением до подсреза 2b. Заглушка NullParser (error NOT_IMPLEMENTED) SHALL использоваться только в тестах воркера и MUST NOT быть реализацией по умолчанию. Реализация по умолчанию SHALL создаваться один раз при старте приложения (словари читаются и проверяются там же). Каталог api/rules/ SHALL содержать словари ОСВ (osv_headers, osv_groups, periods, data_types) и README.

Scenario: Подмена реализации в тесте

  • WHEN в тесте воркеру передан парсер, возвращающий один osvDocument
  • THEN файл результата с этим документом записан в S3, конверт имеет status = done и result_s3 на этот файл

Scenario: Реальная ОСВ через воркер

  • WHEN в интеграционном тесте в S3 лежит rb/2024-FY/ОСВ_66 за 2024.xls и опубликован запрос file_type = XLS
  • THEN после ack публикуется конверт done с result_schema_version = "1.1", файл результата проходит схему v1.1 и содержит один osvDocument с account_code_raw = "66"

Requirement: Копия контракта идентична источнику

api/contracts/*.schema.json SHALL быть байт в байт равны файлам ../docs/contracts/; pydantic-модели SHALL валидировать все ../docs/contracts/examples/*.json и порождать JSON, проходящий схему. Проверка входит в make check.

Scenario: make check

  • WHEN выполняются тесты контракта при доступном ../docs (или ARM_KA_DOCS)
  • THEN идентичность подтверждена, примеры валидны; при недоступном docs тест идентичности пропущен с предупреждением

Requirement: Соединение с NATS по правилам шаблона, без JetStream

При пустых NATS_SEED/NATS_JWT сервис SHALL подключаться без учётных данных, иначе — с nkeys/JWT. Сервис MUST NOT требовать stream'ов или consumer'ов JetStream; при разрыве соединения SHALL переподключаться автоматически и восстанавливать подписку; /healthcheck SHALL отражать состояние (nats = ok | down; off — NATS выключен флагом NATS_ENABLED=false).

Scenario: NATS перезапущен

  • WHEN контейнер NATS перезапущен во время работы сервиса
  • THEN в течение 20 с после старта NATS подписка восстановлена, nats = ok, новый запрос обрабатывается