fix(consolidation): ревью Task 7 — тест надмножества блокировок хука маппинга, устойчивость загрузки к ошибке перехода
T7.0: двухсессионный тест маппинг ∥ перевод версии в DOCUMENTS_PROCESSING (ловит мутант
statuses={DOCUMENTS_PROCESSING}). T7.1 (R-10, design №43): ошибка перехода после загрузки логируется,
принятые файлы и 201 сохраняются. T7.3: лог пропуска без uploaded_by только при наличии NEEDS_CHECK.
T7.4: TransactionFactory импортируется из session_audit. T7.2/T7.5: дополнена строка design №39.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
1908288613
commit
4cc08cc3de
@ -279,15 +279,15 @@ async def on_documents_mapped(
|
|||||||
заблокированы, автозамена прежних версий уже взяла advisory ЮЛ → старые файлы → advisory ГК → версии): порядок
|
заблокированы, автозамена прежних версий уже взяла advisory ЮЛ → старые файлы → advisory ГК → версии): порядок
|
||||||
«задание → файл → документы → версии» не нарушается, версии блокируются последними.
|
«задание → файл → документы → версии» не нарушается, версии блокируются последними.
|
||||||
"""
|
"""
|
||||||
if file.uploaded_by is None:
|
needs_check = [document for document in documents if document.status is DocumentStatus.NEEDS_CHECK]
|
||||||
|
if not needs_check:
|
||||||
|
return []
|
||||||
|
if file.uploaded_by is None: # лог — только когда переход мог состояться (есть NEEDS_CHECK)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Переход версий по маппингу пропущен: у файла нет uploaded_by (changed_by обязателен). file_id={file_id}",
|
"Переход версий по маппингу пропущен: у файла нет uploaded_by (changed_by обязателен). file_id={file_id}",
|
||||||
file_id=file.id,
|
file_id=file.id,
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
needs_check = [document for document in documents if document.status is DocumentStatus.NEEDS_CHECK]
|
|
||||||
if not needs_check:
|
|
||||||
return []
|
|
||||||
# цикл импортов: consolidation_status → consolidations → consolidation_documents (design №36)
|
# цикл импортов: consolidation_status → consolidations → consolidation_documents (design №36)
|
||||||
from src.services.consolidation_status import transition # noqa: PLC0415
|
from src.services.consolidation_status import transition # noqa: PLC0415
|
||||||
|
|
||||||
|
|||||||
@ -18,8 +18,6 @@ audit-trail «События файлов и документов»; design D-4)
|
|||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import Callable
|
|
||||||
from contextlib import AbstractAsyncContextManager
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@ -58,6 +56,7 @@ from src.services.consolidation_guard import VERSION_NOT_FOUND
|
|||||||
from src.services.documents import file_out
|
from src.services.documents import file_out
|
||||||
from src.services.legal_entities import LEGAL_ENTITY_NOT_FOUND
|
from src.services.legal_entities import LEGAL_ENTITY_NOT_FOUND
|
||||||
from src.services.result_mapping import RESULTS_PREFIX
|
from src.services.result_mapping import RESULTS_PREFIX
|
||||||
|
from src.services.session_audit import TransactionFactory
|
||||||
|
|
||||||
OBJECT_TYPE = "stored_file"
|
OBJECT_TYPE = "stored_file"
|
||||||
UPLOADS_PREFIX = "uploads"
|
UPLOADS_PREFIX = "uploads"
|
||||||
@ -69,9 +68,6 @@ FILE_NOT_FOUND = "файл не найден" # одинаково для чу
|
|||||||
FILE_HAS_CONFIRMED_DOCUMENTS = "FILE_HAS_CONFIRMED_DOCUMENTS"
|
FILE_HAS_CONFIRMED_DOCUMENTS = "FILE_HAS_CONFIRMED_DOCUMENTS"
|
||||||
FILE_SUPERSEDES_OTHERS = "FILE_SUPERSEDES_OTHERS"
|
FILE_SUPERSEDES_OTHERS = "FILE_SUPERSEDES_OTHERS"
|
||||||
|
|
||||||
#: фабрика отдельной короткой транзакции (в проде — `src.db.db.session_transaction`): commit на выходе
|
|
||||||
TransactionFactory = Callable[[], AbstractAsyncContextManager[AsyncSession]]
|
|
||||||
|
|
||||||
|
|
||||||
class StorageUnavailableError(DomainError):
|
class StorageUnavailableError(DomainError):
|
||||||
"""503 `STORAGE_UNAVAILABLE`: S3 не подключено (dev без S3) или не приняло объект."""
|
"""503 `STORAGE_UNAVAILABLE`: S3 не подключено (dev без S3) или не приняло объект."""
|
||||||
@ -168,7 +164,11 @@ class PrimaryFilesService:
|
|||||||
В ОТДЕЛЬНОЙ транзакции (`session_transaction`, как аудит входа): `lock_version` (`FOR UPDATE`) → статус
|
В ОТДЕЛЬНОЙ транзакции (`session_transaction`, как аудит входа): `lock_version` (`FOR UPDATE`) → статус
|
||||||
перечитывается — если уже не `INITIATED` (параллельный переход, аннулирование), ничего не меняется. Версия
|
перечитывается — если уже не `INITIATED` (параллельный переход, аннулирование), ничего не меняется. Версия
|
||||||
блокируется на короткое время и вне транзакции запроса, чтобы загрузка не держала её на время приёма файлов
|
блокируется на короткое время и вне транзакции запроса, чтобы загрузка не держала её на время приёма файлов
|
||||||
(D-9). Транзакция запроса к этому моменту версию не трогала, взаимных ожиданий нет."""
|
(D-9). Транзакция запроса к этому моменту версию не трогала, взаимных ожиданий нет.
|
||||||
|
|
||||||
|
Приём файлов важнее побочного статуса (R-10, design №43): любая ошибка перехода (`Exception`) логируется и
|
||||||
|
НЕ меняет ответ — принятые файлы остаются, версия остаётся `INITIATED` до следующей загрузки или
|
||||||
|
подтверждения. `BaseException` (`asyncio.CancelledError`) не перехватывается."""
|
||||||
# импорт внутри функции — как в consolidation_documents (design №36): модули консолидации импортируют друг друга
|
# импорт внутри функции — как в consolidation_documents (design №36): модули консолидации импортируют друг друга
|
||||||
from src.services.consolidation_status import transition # noqa: PLC0415
|
from src.services.consolidation_status import transition # noqa: PLC0415
|
||||||
|
|
||||||
@ -177,20 +177,27 @@ class PrimaryFilesService:
|
|||||||
from src.db.db import session_transaction # noqa: PLC0415
|
from src.db.db import session_transaction # noqa: PLC0415
|
||||||
|
|
||||||
factory = session_transaction
|
factory = session_transaction
|
||||||
async with factory() as session:
|
try:
|
||||||
version = await consolidations_repo.lock_version(session, version_id)
|
async with factory() as session:
|
||||||
if version is None or version.status is not ConsolidationStatus.INITIATED:
|
version = await consolidations_repo.lock_version(session, version_id)
|
||||||
return
|
if version is None or version.status is not ConsolidationStatus.INITIATED:
|
||||||
await transition(
|
return
|
||||||
session,
|
await transition(
|
||||||
AuditService(session),
|
session,
|
||||||
version=version,
|
AuditService(session),
|
||||||
to=ConsolidationStatus.DOCUMENTS_PROCESSING,
|
version=version,
|
||||||
changed_by=self._audit.user_id,
|
to=ConsolidationStatus.DOCUMENTS_PROCESSING,
|
||||||
comment=None,
|
changed_by=self._audit.user_id,
|
||||||
ip_address=self._audit.ip_address,
|
comment=None,
|
||||||
user_agent=self._audit.user_agent,
|
ip_address=self._audit.ip_address,
|
||||||
now=utc_now(),
|
user_agent=self._audit.user_agent,
|
||||||
|
now=utc_now(),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
"Не удалось перевести версию в DOCUMENTS_PROCESSING после загрузки, файлы приняты. "
|
||||||
|
"error_category=version_transition_error, version_id={version_id}",
|
||||||
|
version_id=version_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
def require_storage(self) -> ObjectStorage:
|
def require_storage(self) -> ObjectStorage:
|
||||||
|
|||||||
@ -17,6 +17,7 @@ from typing import Any
|
|||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from loguru import logger
|
||||||
from sqlalchemy import func, select, update
|
from sqlalchemy import func, select, update
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||||
|
|
||||||
@ -736,3 +737,31 @@ async def test_upload_without_version_does_not_touch_versions(
|
|||||||
assert response.status_code == HTTP_CREATED, response.text
|
assert response.status_code == HTTP_CREATED, response.text
|
||||||
assert await _version_state(seeded, version.id) is ConsolidationStatus.INITIATED
|
assert await _version_state(seeded, version.id) is ConsolidationStatus.INITIATED
|
||||||
assert len(storage.objects) == 1
|
assert len(storage.objects) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_upload_keeps_accepted_files_when_version_transition_fails(
|
||||||
|
auth_client: AuthClientFactory, seeded: AsyncSession, storage: FakeStorage, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
"""R-10 (design №43): приём файлов важнее побочного статуса. Переход версии падает исключением — ответ 201, принятый
|
||||||
|
файл сохранён, версия остаётся `INITIATED`, ошибка — в логе."""
|
||||||
|
|
||||||
|
async def broken_transition(*args: Any, **kwargs: Any) -> None:
|
||||||
|
raise RuntimeError("переход недоступен")
|
||||||
|
|
||||||
|
monkeypatch.setattr("src.services.consolidation_status.transition", broken_transition)
|
||||||
|
version, (member, *_), _ = await _version(seeded)
|
||||||
|
client = await auth_client(KA_RF)
|
||||||
|
messages: list[str] = []
|
||||||
|
sink = logger.add(lambda message: messages.append(str(message)), level="ERROR")
|
||||||
|
try:
|
||||||
|
response = await _post_in_version(client, member.id, version.id, ("osv.xls", _xls("transition-fails")))
|
||||||
|
finally:
|
||||||
|
logger.remove(sink)
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_CREATED, response.text
|
||||||
|
[accepted] = response.json()["accepted"]
|
||||||
|
assert (await seeded.get(StoredFile, uuid.UUID(accepted["file_id"]))) is not None
|
||||||
|
assert len(storage.objects) == 1
|
||||||
|
assert await _version_state(seeded, version.id) is ConsolidationStatus.INITIATED
|
||||||
|
assert any(str(version.id) in message and "version_transition_error" in message for message in messages)
|
||||||
|
|||||||
@ -936,6 +936,18 @@ async def test_on_documents_mapped_without_uploader_logs_and_skips(seeded: Async
|
|||||||
assert any(str(file.id) in message and "uploaded_by" in message for message in messages)
|
assert any(str(file.id) in message and "uploaded_by" in message for message in messages)
|
||||||
|
|
||||||
|
|
||||||
|
async def _mapped_in_own_transaction(maker: async_sessionmaker[AsyncSession], data: dict[str, Any]) -> list[uuid.UUID]:
|
||||||
|
"""Хук маппинга файла `data["file_id"]` в своей транзакции (как цикл маппинга: файл и документы уже есть)."""
|
||||||
|
async with maker() as session, session.begin():
|
||||||
|
file = await session.get(StoredFile, data["file_id"])
|
||||||
|
document = await session.get(Document, data["document_id"])
|
||||||
|
assert file is not None
|
||||||
|
assert document is not None
|
||||||
|
return await consolidation_documents.on_documents_mapped(
|
||||||
|
session, file=file, documents=[document], now=utc_now()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_on_documents_mapped_waits_for_member_edit_and_sees_new_member(
|
async def test_on_documents_mapped_waits_for_member_edit_and_sees_new_member(
|
||||||
committed_sessionmaker: async_sessionmaker[AsyncSession],
|
committed_sessionmaker: async_sessionmaker[AsyncSession],
|
||||||
@ -952,21 +964,43 @@ async def test_on_documents_mapped_waits_for_member_edit_and_sees_new_member(
|
|||||||
assert file is not None
|
assert file is not None
|
||||||
file.uploaded_by = data["user_id"]
|
file.uploaded_by = data["user_id"]
|
||||||
|
|
||||||
async def mapped() -> list[uuid.UUID]:
|
|
||||||
async with maker() as session, session.begin():
|
|
||||||
file = await session.get(StoredFile, data["file_id"])
|
|
||||||
document = await session.get(Document, data["document_id"])
|
|
||||||
assert file is not None
|
|
||||||
assert document is not None
|
|
||||||
return await consolidation_documents.on_documents_mapped(
|
|
||||||
session, file=file, documents=[document], now=utc_now()
|
|
||||||
)
|
|
||||||
|
|
||||||
async with maker() as editor, editor.begin():
|
async with maker() as editor, editor.begin():
|
||||||
assert await repo.lock_version(editor, data["version_id"]) is not None
|
assert await repo.lock_version(editor, data["version_id"]) is not None
|
||||||
editor.add(ConsolidationMember(consolidation_version_id=data["version_id"], legal_entity_id=data["owner_id"]))
|
editor.add(ConsolidationMember(consolidation_version_id=data["version_id"], legal_entity_id=data["owner_id"]))
|
||||||
await editor.flush()
|
await editor.flush()
|
||||||
hook = asyncio.create_task(mapped())
|
hook = asyncio.create_task(_mapped_in_own_transaction(maker, data))
|
||||||
|
await _wait_for_lock_waiter(maker)
|
||||||
|
assert not hook.done()
|
||||||
|
moved = await asyncio.wait_for(hook, LOCK_WAIT_SECONDS)
|
||||||
|
|
||||||
|
assert moved == [data["version_id"]]
|
||||||
|
async with maker() as session:
|
||||||
|
assert await _status(session, data["version_id"]) is ConsolidationStatus.DATA_CORRECTION
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_on_documents_mapped_waits_for_transition_to_documents_processing(
|
||||||
|
committed_sessionmaker: async_sessionmaker[AsyncSession],
|
||||||
|
) -> None:
|
||||||
|
"""R-2/№40, маппинг ∥ загрузка (`after_upload_to_version`): версия `INITIATED` держится транзакцией, которая
|
||||||
|
переводит её в `DOCUMENTS_PROCESSING` (не зафиксировано). Хук блокирует надмножество версий «в работе» (а не только
|
||||||
|
`DOCUMENTS_PROCESSING`), поэтому встаёт в ожидание строки версии и после commit видит `DOCUMENTS_PROCESSING` —
|
||||||
|
переход в `DATA_CORRECTION` не теряется. Хук, ищущий версии только в `DOCUMENTS_PROCESSING`, снимок `INITIATED`
|
||||||
|
пропустил бы без ожидания и вернул бы `[]`."""
|
||||||
|
maker = committed_sessionmaker
|
||||||
|
data = await _committed_setup(maker, members=2, spare=0) # владелец документа — член периметра, версия INITIATED
|
||||||
|
async with maker() as session, session.begin():
|
||||||
|
file = await session.get(StoredFile, data["file_id"])
|
||||||
|
assert file is not None
|
||||||
|
file.uploaded_by = data["user_id"]
|
||||||
|
|
||||||
|
async with maker() as uploader, uploader.begin():
|
||||||
|
version = await repo.lock_version(uploader, data["version_id"])
|
||||||
|
assert version is not None
|
||||||
|
assert version.status is ConsolidationStatus.INITIATED
|
||||||
|
version.status = ConsolidationStatus.DOCUMENTS_PROCESSING
|
||||||
|
await uploader.flush()
|
||||||
|
hook = asyncio.create_task(_mapped_in_own_transaction(maker, data))
|
||||||
await _wait_for_lock_waiter(maker)
|
await _wait_for_lock_waiter(maker)
|
||||||
assert not hook.done()
|
assert not hook.done()
|
||||||
moved = await asyncio.wait_for(hook, LOCK_WAIT_SECONDS)
|
moved = await asyncio.wait_for(hook, LOCK_WAIT_SECONDS)
|
||||||
|
|||||||
@ -223,7 +223,8 @@ legal_entity_name, account_code, missing_quarters[]}}`. Фильтры `GET …/
|
|||||||
| 36 | 2026-10-06 | Переход версии при подтверждении пишется `transition(...)` с `user_id` подтвердившего, но без IP/user-agent (сигнатура хука `on_document_confirmed(session, audit: AuditService, *, document, user_id, now)` фиксирована планом); `transition` импортируется внутри хука (цикл `consolidation_status → consolidations → consolidation_documents`) | audit-trail требует для автоматического перехода только `user_id` инициатора | в `CONSOLIDATION_STATUS_CHANGE` автоматического перехода нет IP/UA — они есть в соседнем `DOCUMENT_CONFIRM` той же транзакции |
|
| 36 | 2026-10-06 | Переход версии при подтверждении пишется `transition(...)` с `user_id` подтвердившего, но без IP/user-agent (сигнатура хука `on_document_confirmed(session, audit: AuditService, *, document, user_id, now)` фиксирована планом); `transition` импортируется внутри хука (цикл `consolidation_status → consolidations → consolidation_documents`) | audit-trail требует для автоматического перехода только `user_id` инициатора | в `CONSOLIDATION_STATUS_CHANGE` автоматического перехода нет IP/UA — они есть в соседнем `DOCUMENT_CONFIRM` той же транзакции |
|
||||||
| 37 | 2026-10-06 | Отдельная транзакция перехода после загрузки (`after_upload_to_version`) берётся из `app.state.session_audit.transaction` (та же фабрика `session_transaction`, что у аудита входа; без неё — `src.db.db.session_transaction`); `get_primary_files_service` передаёт её в `PrimaryFilesService(…, transaction)` | отдельной зависимости «фабрика транзакций» в приложении нет, а фикстуры тестов уже подставляют фабрику в `SessionAudit`; не меняем `app.py` и `conftest` | если `SessionAudit` когда-либо уйдёт из `app.state`, сервис молча возьмёт боевой `session_transaction` (в тестах это была бы чужая БД) |
|
| 37 | 2026-10-06 | Отдельная транзакция перехода после загрузки (`after_upload_to_version`) берётся из `app.state.session_audit.transaction` (та же фабрика `session_transaction`, что у аудита входа; без неё — `src.db.db.session_transaction`); `get_primary_files_service` передаёт её в `PrimaryFilesService(…, transaction)` | отдельной зависимости «фабрика транзакций» в приложении нет, а фикстуры тестов уже подставляют фабрику в `SessionAudit`; не меняем `app.py` и `conftest` | если `SessionAudit` когда-либо уйдёт из `app.state`, сервис молча возьмёт боевой `session_transaction` (в тестах это была бы чужая БД) |
|
||||||
| 38 | 2026-10-06 | Переход после загрузки пишет `CONSOLIDATION_STATUS_CHANGE` с IP и user-agent запроса (в отличие от хука подтверждения, №36); `transition` вызывается из сервиса загрузки с `RequestAudit`-контекстом, но в своей транзакции и своим `AuditService(session)` | сервис знает IP/UA запроса, а `transition` их принимает; потери информации нет | нет |
|
| 38 | 2026-10-06 | Переход после загрузки пишет `CONSOLIDATION_STATUS_CHANGE` с IP и user-agent запроса (в отличие от хука подтверждения, №36); `transition` вызывается из сервиса загрузки с `RequestAudit`-контекстом, но в своей транзакции и своим `AuditService(session)` | сервис знает IP/UA запроса, а `transition` их принимает; потери информации нет | нет |
|
||||||
| 39 | 2026-10-06 | Версия в статусе `DOCUMENTS_PROCESSING`/`DATA_CORRECTION` при загрузке не меняется, `after_upload_to_version` — no-op; если между проверкой и переходом версию аннулировали/перевели, переход не выполняется (статус перечитывается под `FOR UPDATE`), загрузка при этом остаётся принятой (201) | проверка до приёма и переход после — две разные транзакции, окно между ними неустранимо без блокировки версии, которую спека запрещает | версия, аннулированная в окне, остаётся аннулированной; файл принят и виден в ЮЛ, но к версии не привязан (привязка — только подтверждением) |
|
| 39 | 2026-10-06 | Версия в статусе `DOCUMENTS_PROCESSING`/`DATA_CORRECTION` при загрузке не меняется, `after_upload_to_version` — no-op; если между проверкой и переходом версию аннулировали/перевели, переход не выполняется (статус перечитывается под `FOR UPDATE`), загрузка при этом остаётся принятой (201) | проверка до приёма и переход после — две разные транзакции, окно между ними неустранимо без блокировки версии, которую спека запрещает | версия, аннулированная в окне, остаётся аннулированной; файл принят и виден в ЮЛ, но к версии не привязан (привязка — только подтверждением). Окно покрывает и удаление ЮЛ из периметра между проверкой и переходом: версия всё равно перейдёт в `DOCUMENTS_PROCESSING` — допустимо. Отдельная транзакция перехода открывает второе подключение к БД, пока запрос держит своё: при множестве параллельных загрузок в контексте версии это давит на пул (по умолчанию 5+10) — пока теоретически |
|
||||||
| 40 | 2026-10-06 | `on_documents_mapped` блокирует через `lock_versions_for_entity` надмножество по `IN_WORK_STATUSES` и фильтрует `DOCUMENTS_PROCESSING` ПОСЛЕ блокировки, а не передаёт `statuses={DOCUMENTS_PROCESSING}`; вызывается в `_map_candidate` ПОСЛЕ `map_file`, то есть после автозамены | при фильтре статуса в блокирующем запросе версия, которую параллельно переводят `INITIATED → DOCUMENTS_PROCESSING`, не ждалась бы; порядок «файл → документы → advisory ГК → версии» сохраняется, автозамена внутри `map_file` берёт advisory ЮЛ → старые файлы → advisory ГК → версии раньше хука; вызов до автозамены держал бы версии и затем ждал старые файлы (встречная блокировка с подтверждением) | блокируется чуть больше версий ГК (все «в работе»), на время короткой транзакции маппинга |
|
| 40 | 2026-10-06 | `on_documents_mapped` блокирует через `lock_versions_for_entity` надмножество по `IN_WORK_STATUSES` и фильтрует `DOCUMENTS_PROCESSING` ПОСЛЕ блокировки, а не передаёт `statuses={DOCUMENTS_PROCESSING}`; вызывается в `_map_candidate` ПОСЛЕ `map_file`, то есть после автозамены | при фильтре статуса в блокирующем запросе версия, которую параллельно переводят `INITIATED → DOCUMENTS_PROCESSING`, не ждалась бы; порядок «файл → документы → advisory ГК → версии» сохраняется, автозамена внутри `map_file` берёт advisory ЮЛ → старые файлы → advisory ГК → версии раньше хука; вызов до автозамены держал бы версии и затем ждал старые файлы (встречная блокировка с подтверждением) | блокируется чуть больше версий ГК (все «в работе»), на время короткой транзакции маппинга |
|
||||||
| 41 | 2026-10-06 | Хук получает документы файла отдельным `SELECT` по `stored_file_id` после `map_file` (возвращаемое значение `map_file` — предупреждения — не менялось) | сигнатура `map_file` и её тесты не трогаются; документы только что записаны в этой же транзакции | один лишний запрос на файл |
|
| 41 | 2026-10-06 | Хук получает документы файла отдельным `SELECT` по `stored_file_id` после `map_file` (возвращаемое значение `map_file` — предупреждения — не менялось) | сигнатура `map_file` и её тесты не трогаются; документы только что записаны в этой же транзакции | один лишний запрос на файл |
|
||||||
| 42 | 2026-10-06 | Переход по маппингу — только если среди документов файла есть `NEEDS_CHECK`, подходящий версии по `fits` (`fits` — D-5: период, год, член); статус файла/документа в других статусах (`RECOGNIZED`) переход не вызывает, привязки не создаются | спека osv-result-mapping | версия с неподходящим по периоду `NEEDS_CHECK` документом остаётся в `DOCUMENTS_PROCESSING` до подтверждения подходящего |
|
| 42 | 2026-10-06 | Переход по маппингу — только если среди документов файла есть `NEEDS_CHECK`, подходящий версии по `fits` (`fits` — D-5: период, год, член); статус файла/документа в других статусах (`RECOGNIZED`) переход не вызывает, привязки не создаются | спека osv-result-mapping | версия с неподходящим по периоду `NEEDS_CHECK` документом остаётся в `DOCUMENTS_PROCESSING` до подтверждения подходящего |
|
||||||
|
| 43 | 2026-10-06 | R-10: ошибка (`Exception`) отдельной транзакции перехода после загрузки (`after_upload_to_version`) логируется (`logger.exception`, `error_category=version_transition_error`) и не меняет ответ: 201 с принятыми файлами; `BaseException` (`asyncio.CancelledError`) не перехватывается | приём файлов важнее побочного статуса; иначе 500 откатил бы транзакцию запроса, а принятые объекты S3 остались бы сиротами (как в `SessionAudit`, ошибка записи побочного события ответ не меняет) | версия может остаться `INITIATED` после загрузки до следующей загрузки или подтверждения документа; сбой виден только в логе |
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user