139 lines
15 KiB
Markdown
139 lines
15 KiB
Markdown
# Design
|
||
|
||
## Context
|
||
|
||
Мотивация и объём — proposal.md. Сейчас upload создаёт recognition_job в queued, а recognition_exchange.py
|
||
публикует outbox, принимает ack/результаты и закрывает просроченные задания. attempt при повторной публикации
|
||
не растёт. Механизма ручной новой попытки нет. file_status.py уже защищает файл от событий нетекущего задания.
|
||
DocumentService умеет confirm; привязки централизованы в consolidation_documents.py. Модель и миграции — v5.19.
|
||
|
||
Основание для ОСВ — DBML v5.20 и §9.5 п.12; для повтора — AURORA-1442, DBML v5.21,
|
||
§9.4 и §9.5 п.13. FILE_REPROCESS уже опубликован в модели v5.21; изменение enum в v5.20 не требуется.
|
||
Пакет по consolidation_id и периметру выбранной версии дополнительно согласован пользователем.
|
||
Этот change содержит только план: соседний docs не изменён.
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:** атомарные правки и повтор, сохранение первичных данных и истории, отсутствие дублей при гонках,
|
||
единая проверка допустимости для одиночного и пакетного повтора, совместимость с действующим outbox.
|
||
|
||
**Non-Goals:** новые фоновые воркеры, доставка через JetStream, вычисляемые ORM-таблицы, изменение сумм и
|
||
субсчетов ОСВ. Денежные значения сохраняют Decimal/numeric(18,2), рубли; правка шапки их не пересчитывает.
|
||
|
||
## Decisions
|
||
|
||
### D1. Сервис ручного повтора и API
|
||
|
||
Новый services/file_retry.py, фабрика DI и тонкие маршруты:
|
||
|
||
- POST /api/v1/files/{file_id}/retry → 202 FileOut.
|
||
- POST /api/v1/consolidations/versions/{version_id}/files/retry → 202 RetryBatchOut.
|
||
- PATCH /api/v1/documents/{id}/osv-header → 200 DocumentOut (endpoints/files/documents.py).
|
||
|
||
Первые два маршрута не требуют чтения/записи S3 и синхронной публикации NATS: достаточно транзакции outbox.
|
||
Для файла повторно используется тот же s3_object; path загрузки содержит id первой попытки, это допустимо:
|
||
ключ исходника берётся из реестра, не конструируется из id новой попытки.
|
||
Ответы и коды отказов определены в дельтах primary-files/documents-api; новые типы DTO — в schemas/files.py
|
||
и schemas/documents.py. Названия retry-маршрутов и 202 — проектное решение, в исходной задаче они не заданы.
|
||
|
||
### D2. Новая попытка вместо обновления прежнего job
|
||
|
||
Создать новый UUID job, queued, attempt + 1; перенести file_id, pipeline, max_attempts.
|
||
schema_version брать из CONTRACT_VERSION, request_subject — из текущих настроек NATS, как при загрузке;
|
||
версию контракта и subject прежнего задания не копировать. Ошибки, метрики, result_s3_object_id, service metadata, message ids
|
||
и времена старой попытки не копируются. requested_at — сейчас, reply_deadline формируется публикатором.
|
||
Переключить current_recognition_job_id и вернуть файл из ERROR в UPLOADED. Прежний job неизменен.
|
||
Это соответствует уточнённой задаче и не требует изменения дедупа парсера по старому job_id (OQ-73).
|
||
Автоматический republish остаётся прежним job/attempt; это другая операция.
|
||
|
||
### D3. Пакет, частичные отказы и версии
|
||
|
||
Отбор repositories/files.py::retry_candidates выбирает только ERROR-файлы выбранной консолидации
|
||
по stored_file.consolidation_id, с ЮЛ из периметра версии и policy ЮЛ, до любых блокировок.
|
||
Остальные статусы не блокируются и не попадают в skipped; после блокировки статус ERROR перепроверяется.
|
||
Файл без документов не отбирается через consolidation_document. Дедуп по file_id.
|
||
Согласованный пользователем общий набор у нескольких версий сохраняется; повтор не создаёт версионной копии файла.
|
||
|
||
Проектное решение для смешанного пакета: общие ошибки прав/видимости/статуса версии отклоняют запрос целиком;
|
||
непригодность конкретного файла возвращается в skipped, остальные принимаются. accepted и skipped стабильны
|
||
по file_id. Пустой пакет — 202. Неожиданная ошибка БД откатывает весь запрос, а не маскируется как skipped.
|
||
Одна транзакция на запрос, сервис не вызывает commit. Новых переходов статуса версии от ручного retry нет.
|
||
|
||
Допустимые версии — IN_WORK_STATUSES; брать пользовательскую consolidation_lock не требуется.
|
||
Для одиночного файла с consolidation_id проверить наличие такой версии с его ЮЛ; файл без контекста — по policy ЮЛ.
|
||
Это консервативное проектное правило для отсутствующего version_id, его нужно явно перенести в §9.4.
|
||
|
||
### D4. Блокировки и гонки
|
||
|
||
Retry следует порядку работающего обмена: текущее задание FOR UPDATE → файл FOR UPDATE → версии по id.
|
||
В одиночном повторе только рабочие версии берутся FOR SHARE: статус и периметр защищены от изменений,
|
||
обычное чтение не блокируется, VERIFIED/CANCELLED вообще не блокируются.
|
||
После блокировки файла перечитать current_recognition_job_id; если изменился, одиночный запрос получает
|
||
FILE_NOT_RETRYABLE, пакет пропускает файл. Не захватывать новое текущее задание, уже удерживая файл:
|
||
это инвертирует порядок обмена. Лимит и отсутствие документов проверять заново под блокировкой.
|
||
Для пакета сначала блокировать все отобранные прежние jobs по id, затем файлы по id, затем версии по id;
|
||
после захвата версий перечитать статус и состав периметра. Не начинать пакет с FOR UPDATE версии.
|
||
OpenAPI одиночного повтора описывает FILE_NOT_RETRYABLE/MAX_ATTEMPTS_REACHED/VERSION_NOT_EDITABLE;
|
||
для пакетного 409 только VERSION_NOT_EDITABLE, остальные причины возвращаются в skipped.
|
||
|
||
Для PATCH — файл → документ FOR NO KEY UPDATE → существующая advisory-блокировка привязок ГК → версии
|
||
по id, как при confirm/delete. Проверить связанные статусы под блокировками и не использовать
|
||
lock_editable_version, требующий пользовательскую consolidation_lock. Сериализация на файле защищает
|
||
счёт от параллельного PATCH; уникальный индекс остаётся последней защитой, IntegrityError переводится
|
||
в DUPLICATE_ACCOUNT через SAVEPOINT с повторным чтением конфликтующего document_id.
|
||
|
||
### D5. Шапка и подтверждение
|
||
|
||
OsvHeaderPatch использует StrictInt для period_year, StrictStr для period_quarter/osv_account_code,
|
||
StrictBool для is_cumulative; каждое поле допускает null, отсутствие определяется exclude_unset.
|
||
Pydantic возвращает стандартный 422 для неверного типа до вызова обработчика. Порядок доменных
|
||
ошибок §9.5 п.12 применяется к телу с корректными типами; границы года и набор кварталов проверяет домен.
|
||
Чистая функция domain/osv_header.py строит итоговую шапку, нормализует счёт существующей
|
||
normalize_account_code, различает отсутствующие и переданные поля, проверяет целостность периода.
|
||
ORM-операции — отдельный services/osv_header.py с доступом из DocumentService/DI.
|
||
Пустое тело или нормализованное равенство текущему — no-op; проверки доступа и конфликтов сохраняются.
|
||
|
||
Перед первой фактической правкой сохранить все четыре исходных поля JSON, используя сериализуемые
|
||
строки enum. На каждой правке сравнивать итог с неизменным original_values. При возврате исходного
|
||
is_modified снимается, original_values и история остаются. Не создавать новый document/file_ordinal.
|
||
У CONFIRMED снять подтверждение и только рабочие привязки; CANCELLED сохранить. Другие статусы не менять.
|
||
period_check/counterparty_check и document_issue не пересчитываются этим PATCH.
|
||
Повторный confirm использует существующие правила привязки по исправленным полям, автозамена файлов не вызывается.
|
||
|
||
### D6. Аудит и согласование модели
|
||
|
||
FILE_REPROCESS — значение audit_action из DBML v5.21; раздел PRIMARY_DOCUMENTS, объект stored_file,
|
||
одно событие на принятый файл. before_value — прежнее задание: job_id, attempt, status задания, error_code;
|
||
after_value — новое задание: job_id, attempt. Для пакета в after_value добавить consolidation_id.
|
||
Сохранить автора, IP и User-Agent. Статус файла в этот payload не подставляется.
|
||
Автоматический обмен по OQ-55 по-прежнему без такого аудита. PATCH — RECOGNIZED_DATA_EDIT, RECOGNITION,
|
||
объект document, изменённые поля и сброшенное подтверждение, unbound_version_ids. Всё в транзакции мутации.
|
||
Переносить аудит повтора на FILE_UPLOAD отвергнуто: физической загрузки нет.
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- Поздний ответ старого job → существующая защита финального статуса и current job, регрессия inbox и маппера.
|
||
- Взаимоблокировка с confirm/delete/status → единый порядок и интеграционные тесты на отдельных соединениях.
|
||
- Пакет охватывает общие файлы консолидации → явно описанный состав и повторная проверка текущего job.
|
||
- Расхождение с источником истины → сверка ORM с опубликованной DBML v5.21; исходная v5.20 остаётся неизменной.
|
||
- Папка ../fixtures сейчас недоступна → до golden/e2e обеспечить доступ к репозиторию реальных ОСВ РБ;
|
||
отсутствие данных не считать успешной проверкой. Гонки и статусы проверяются фабриками БД без поддельных форматов.
|
||
- Временный флаг периода может маскировать исправление периода → тесты с флагом false и true, сам флаг не удалять.
|
||
- Откат после возврата к исходному удаляет original_values при is_modified=false → следовать утверждённому
|
||
условию v5.20; историю правок сохраняет audit_event, исходник/JSON остаются в S3; ограничение описать в README.
|
||
|
||
## Migration Plan
|
||
|
||
1. Использовать опубликованные DBML v5.20/v5.21 и §9.5 п.12–13. В docs уточнить согласованный отбор пакета
|
||
и описание API; повторно вводить FILE_REPROCESS в модель не требуется.
|
||
2. Миграция 0008_dbml_v5_20 после 0007: только четыре колонки document; старые строки получают
|
||
is_modified=false, остальные NULL. Существующие документы и привязки не меняются.
|
||
3. Отдельная миграция 0009_dbml_v5_21 после 0008: только FILE_REPROCESS в audit_action,
|
||
после FILE_REPLACE как в DBML. Итоговые DBML_VERSION, путь Makefile и сверка схемы — v5.21.
|
||
4. Сначала схема, затем приложение; перезапуск фоновых задач обычным порядком. Старые job и файлы не мигрировать.
|
||
5. Downgrade 0009 → 0008 запрещён до DDL при наличии FILE_REPROCESS в аудите, события не переписываются.
|
||
На совместимых данных восстанавливается enum v5.20 без FILE_REPROCESS. Downgrade 0008 → 0007
|
||
отдельно запрещён при document.is_modified=true; иначе удаляются только четыре колонки.
|
||
Проверить оба перехода по отдельности, полный цикл на чистой/непустой БД и нестандартный DB_SCHEMA.
|
||
6. Обновить frontend-api-guide и README; UI не выкладывается этой задачей.
|