docs(scan-gateway): README и спека — запись .part по №32/№35

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-07 04:05:35 +04:00
parent cb8c71fc26
commit 1c39ca1b73
2 changed files with 11 additions and 8 deletions

View File

@ -64,11 +64,14 @@ ack (с `attempt` повторного запроса); файл в ИС вто
`<SCAN_TMP_DIR>` (по умолчанию `Tmp`), затем он публикуется в `Input` под настоящим именем. `Tmp` лежит рядом с `Input`
и `Output`, на той же ФС/шаре: публикация атомарна, а ИС не может забрать недописанный файл (временных файлов в
`Input` нет — решение владельца 06.10.2026, у FileWorker `.tmp` лежит рядом с назначением). В режиме `local` публикация —
жёсткая ссылка `.part` → `Input/<имя>` (`FileExistsError`, если назначение есть) и удаление `.part`; на файловой
системе без жёстких ссылок (`EPERM`/`EXDEV`/`ENOTSUP`) — `os.replace`, если назначения ещё нет (решение №17). В
режиме `smb` — `smbclient.rename` (поднимает `FileExistsError`, если назначение есть). Если `Input/<job_id>.<ext>`
уже существует (повторная публикация задания после потери ack в другом процессе), файл считается доставленным:
назначение не перезаписывается, задание продолжается опросом `Output/<job_id>/`. Каталоги `Input` и `Tmp`
жёсткая ссылка `.part` → `Input/<имя>` и удаление `.part`; на файловой системе без жёстких ссылок
(`EPERM`/`EXDEV`/`ENOTSUP`/`EOPNOTSUPP` от самой ссылки) — `os.replace`, если назначения ещё нет (решение №17).
`.part` создаётся заново: остаток прежней записи удаляется (ошибка удаления подавляется), файл открывается с `O_EXCL`;
поверх оставшегося `.part` пишем только если это не жёсткая ссылка (`st_nlink == 1`) — иначе исключение (решение №32),
чтобы не испортить уже опубликованный файл. В режиме `smb` — `smbclient.rename` (поднимает `FileExistsError`, если
назначение есть). Если `Input/<job_id>.<ext>` уже существует (повторная публикация задания после потери ack в другом
процессе), файл считается доставленным: ничего не пишется, назначение не перезаписывается, `.part` не трогается (при
`SCAN_CLEANUP=true` существующий `.part` снимается), задание продолжается опросом `Output/<job_id>/` (решение №35). Каталоги `Input` и `Tmp`
создаются при необходимости (без родителей), корень горячей папки сам не создаётся: нет корня — `RECOGNITION_FAILED`.
- Ответ ИС — каталог `<SCAN_OUTPUT_DIR>/<job_id>/`: ИС кладёт туда **все** распознанные документы задания одновременно,
по одному `*.json` на файл (`Output/<job_id>/*.json`). Шлюз читает **все** `*.json` каталога и склеивает их в один
@ -105,8 +108,8 @@ ack (с `attempt` повторного запроса); файл в ИС вто
от `SCAN_CLEANUP` (в `local` — удаление после жёсткой ссылки, ошибка удаления подавляется; в `smb` `.part`
переименовывается во входящий и отдельного удаления не требует): иначе при выключенной очистке на каждое задание
оставалась бы лишняя ссылка, а ошибка удаления на шаре с неподтверждёнными правами превращала бы доставленный файл в
`RECOGNITION_FAILED`. **Флаг управляет остатками:** осиротевшие `.part` (оборванная запись), `.part` при уже
существующем назначении, каталог ответа, входящий файл и `.done`. При `SCAN_CLEANUP=false` сервис не удаляет ничего из
`RECOGNITION_FAILED`. **Флаг управляет остатками:** осиротевшие `.part` (оборванная запись, `.part` при уже
существующем назначении), каталог ответа, входящий файл и `.done`. При `SCAN_CLEANUP=false` сервис не удаляет ничего из
этого и не пишет в лог об осиротевших `.part` (решение №27): они накапливаются в `Tmp` и убираются вручную или
включением флага.
- Права на SMB: для очистки учётной записи шары нужны права на удаление в `Input`, `Output` (включая каталог

View File

@ -129,7 +129,7 @@
`<Input>/<job_id>.<ext>`, его вариант `<Input>/<job_id>.<ext>.done` и временный `<Tmp>/.<job_id>.<ext>.part`, если они
существуют. При `SCAN_CLEANUP=false` (по умолчанию) сервис MUST NOT удалять ничего, включая осиротевшие временные файлы (`.part`, ставший
целью успешной публикации, снимается самой записью — это завершение «rename без замены», а не очистка; ошибка его снятия
запись не прерывает, решение №26). При
запись не прерывает, решение №26; запись собственного `.part` заново удаляет прежний остаток под тем же именем, решение №32). При
`TIMEOUT`, `FILE_READ_ERROR`, `RECOGNITION_FAILED` записи очистка MUST NOT выполняться. Ошибка удаления SHALL быть
записана в лог одной строкой на задание и MUST NOT менять конверт, `metrics.warnings` и `/healthcheck`.