Andrey Epifancev 7d8a506f4d fix(hotfolder): ошибка открытия входящего файла не удаляет файл прошлой доставки (#26, I-1)
Открытие вынесено из блока снятия недописанного файла: при SCAN_CLEANUP=true
удаляется только файл, сбойнувший после открытия. Решения R-5…R-8 — в design.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:54:54 +04:00

7.7 KiB
Raw Blame History

Context

Запись входящего файла сейчас — двухфазная: Tmp/.<имя>.part, затем rename (SMB) / жёсткая ссылка (local) в Input без замены (решения №17, №26, №32, №35 Среза 3a). Старый АРМ писал файл прямо в Input и в контуре работал. Владелец 09.10.2026 выбрал повторить старый АРМ и перезаписывать существующий файл.

Goals / Non-Goals

Goals: прямая запись в Input в обоих режимах; меньше требований к правам на шаре; срок ожидания ответа 20 мин.

Non-Goals: см. proposal.

Decisions

D-1. Прямая запись с усечением (решение владельца 09.10.2026)

SmbHotFolder.put: smbclient.open_file(<Input>\<имя>, mode="wb", **credentials) и write. LocalHotFolder.put: open(<input>/<имя>, "wb") (Path.write_bytes). Существующий файл перезаписывается. Повторная запись в рамках одного процесса по-прежнему исключена дедупликацией по job_id в воркере (требование «Подтверждение запроса до обработки»); перезапись возможна только при повторе задания ядром в другом процессе.

D-2. Каталог Input создаётся, как раньше

Перед записью Input создаётся без родителей, «уже есть» и отказ в правах при существующем каталоге — не ошибка (_ensure_dir в smb, mkdir(exist_ok=True) в local); корень не создаётся. Старый АРМ каталог не создавал, но терпимое создание поведения в контуре не меняет, а dev-режим и тесты на нём держатся.

D-3. Tmp остаётся только у эмулятора

SCAN_TMP_DIR удаляется из настроек шлюза (Settings, extra="ignore" — старое значение в .env не ломает старт). Эмулятор читает свою переменную SCAN_TMP_DIR (EmulatorSettings) и собирает набор ответа через LocalHotFolder.tmp_dir, поэтому параметр tmp_dir у LocalHotFolder остаётся, но put его не использует.

D-4. Срок ожидания 20 мин

SCAN_TIMEOUT_SECONDS по умолчанию 1200. Эффективный дедлайн — min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS), поэтому arm-core поднимает срок ответа OCR до 20 мин отдельным PR (RSHB/arm-core#87).

D-5. Ошибка записи

При исключении во время открытия/записи put снимает недописанный <Input>/<имя> только при включённой очистке (ошибка снятия подавляется, исходное исключение поднимается) — R-3.

Risks / Trade-offs

  • ИС может увидеть недописанный файл (OQ-79) → смягчение: монопольное открытие smbclient на время записи; так же работал старый АРМ. Откат — вернуть двухфазную запись из истории ветки.
  • Ошибка посреди записи оставляет обрезанный файл в Input → удаляется только при SCAN_CLEANUP=true (как .part раньше, D-5); иначе остаётся, конверт RECOGNITION_FAILED.

Decisions made without discussion

# Дата Решение Обоснование Цена ошибки
R-1 2026-10-09 Режим local тоже пишет прямо в Input (не только smb) Одинаковая семантика режимов, один набор тестов; эмулятор пишет/читает локально мгновенно Эмулятор может прочитать недописанный файл — на локальной ФС практически невозможно; откат — вернуть .part в local
R-2 2026-10-09 Input по-прежнему создаётся перед записью (D-2), хотя старый АРМ этого не делал Отказ в правах на mkdir при существующем каталоге уже не ошибка; dev и тесты на этом держатся Лишний вызов mkdir на шаре на каждое задание
R-3 2026-10-09 Ошибка записи: недописанный файл в Input удаляется только при SCAN_CLEANUP=true (D-5) Требование «при SCAN_CLEANUP=false ничего не удалять» (OQ-144) сохраняется Обрезанный файл остаётся в Input, ИС может его распознать с ошибкой; ядро получит RECOGNITION_FAILED от шлюза раньше
R-4 2026-10-09 SCAN_TMP_DIR удаляется из шлюза, у эмулятора остаётся (D-3) Шлюзу каталог больше не нужен; эмулятору нужен для атомарной сборки ответа Нет: extra="ignore" — старые .env стартуют
R-5 2026-10-09 Правка вызова SmbHotFolder в фабрике (gateway/__init__.py) перенесена из задачи 2 плана в задачу 1 Без неё mypy красный между задачами (лишний позиционный аргумент) Нет
R-6 2026-10-09 Сверх плана: tests/unit/conftest.py не передаёт SCAN_TMP_DIR; test_worker.py::test_existing_input_waits_for_answer ждёт перезаписи объектом S3; в test_roundtrip.py эмулятору передаётся литерал "Tmp" Прямые следствия D-1 и D-3 Нет
R-7 2026-10-09 Финальное ревью I-1: ошибка открытия входящего файла ничего не удаляет (открытие вынесено из блока снятия недописанного файла); снимается только файл, сбойнувший после открытия Иначе при SCAN_CLEANUP=true удалялся бы целый файл прошлой доставки, который, возможно, читает ИС Нет
R-8 2026-10-09 Финальное ревью I-1(б): если при повторе задания в другом процессе файл занят ИС (STATUS_SHARING_VIOLATION на открытии), шлюз публикует RECOGNITION_FAILED, а не считает файл доставленным — как старый АРМ (wb без особых случаев) Решение владельца «как в старом АРМ»; повтор в другом процессе — только после потери ack (редко) Ядро может получить RECOGNITION_FAILED раньше успешного ответа первого экземпляра (OCR max_attempts = 1) — аналитику придётся перезагрузить файл; вынесено вопросом владельцу