From e31476ebab29f88bcb4bb6f98a4f7959115f085a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 24 Aug 2026 19:14:53 +0300 Subject: [PATCH] =?UTF-8?q?v0.0.72:=20=D1=80=D0=B5=D1=82=D1=80=D0=B0=D0=B8?= =?UTF-8?q?=20pull=20(3=20=D0=BF=D0=BE=D0=BF=D1=8B=D1=82=D0=BA=D0=B8),=20?= =?UTF-8?q?=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=BD=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=20=D0=BB=D0=B8=D0=BC=D0=B8?= =?UTF-8?q?=D1=82=D0=B0=20=D1=81=D0=B5=D1=81=D1=81=D0=B8=D0=B8,=20=D1=81?= =?UTF-8?q?=D1=87=D1=91=D1=82=D1=87=D0=B8=D0=BA=20=D0=B4=D0=B5=D0=B4=D1=83?= =?UTF-8?q?=D0=BF=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- History/README.md | 1 + .../2026-08-24-pull-retry-limit-counter.md | 26 ++++++++++++++ site/app.py | 2 +- site/routes/api_bp.py | 36 ++++++++++++++++--- site/templates/index.html | 17 ++++----- 5 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 History/code-fixes/2026-08-24-pull-retry-limit-counter.md diff --git a/History/README.md b/History/README.md index 0a0c138..0feb2df 100644 --- a/History/README.md +++ b/History/README.md @@ -106,6 +106,7 @@ - **2026-08-24-time-tickers-estimates.md** — v0.0.64–0.0.65: тикер текущего файла на всех этапах, мгновенные оценки времени по файлам и суммарно. - **2026-08-24-folder-select-implemented.md** — v0.0.68: кнопка «Выбрать папку» (webkitdirectory, рекурсивно, относительный путь). - **2026-08-24-folder-zip-not-lost.md** — v0.0.69: архивы из папки без документов не теряются (добавляются как есть); раскрытие zip с документами; склонение счётчика. +- **2026-08-24-pull-retry-limit-counter.md** — v0.0.72: ретраи pull из ВМ, корректная ошибка лимита сессии, счётчик дедупа. - **2026-08-24-upload-logic-schema.md** — подробная схема логики загрузки + найденные баги/несоответствия. - **2026-08-24-help-button-limits-text.md** — v0.0.70: заметная кнопка HELP (вместо «?») + пояснение статуса «пропущен» в ограничениях. - **2026-08-24-ui-locks-session-freeze.md** — v0.0.66: блокировки UI (выбор/удаление) на время работы, заморозка сессии + кнопка «Новая сессия». diff --git a/History/code-fixes/2026-08-24-pull-retry-limit-counter.md b/History/code-fixes/2026-08-24-pull-retry-limit-counter.md new file mode 100644 index 0000000..bbf7088 --- /dev/null +++ b/History/code-fixes/2026-08-24-pull-retry-limit-counter.md @@ -0,0 +1,26 @@ +# v0.0.72 — Ретраи pull, корректная ошибка лимита, счётчик дедупа (2026-08-24) + +_code-fixes. По итогам код-ревью (см. `infra/2026-08-24-upload-logic-schema.md`)._ + +## 1. Ретраи pull (site/routes/api_bp.py, `upload_refs`) +- `PULL_RETRIES = 3`, `PULL_RETRY_DELAY = 2` (сек). +- Pull из ВМ-буфера теперь в цикле: при любой ошибке (DNS `gaierror -5`, сеть) — до 3 попыток + с паузой 2с. После исчерпания — проброс исходной ошибки (502 «Pull failed»). +- Закрывает инцидент 18:42 (разовый DNS-сбой ронял всю загрузку). + +## 2. Корректная ошибка лимита сессии (`upload_refs`) +- `add_file` возвращает `False` и для «сессия исчезла», и для «превышен лимит 500МБ». +- Теперь: при `False` — если `get_files(sid) is None` → 404 «Session not found»; + иначе (лимит) → файл пропускается (`delete` с ВМ) и загрузка продолжается. + Раньше оба случая давали ложное «Session not found». + +## 3. Счётчик «Добавлено из папки» (site/templates/index.html) +- `addFileWithDedup` теперь возвращает `true` (файл добавлен) / `false` (дедуп). +- `added++` только при `true` — дубли больше не завышают счётчик. +- Проверено: папка [Договор.txt, Договор.txt (дубль), Акт.txt] → «Добавлено из папки: 2 файла». + +## Проверка +- `node --check` — OK, `py_compile` (app.py, api_bp.py) — OK. +- Локально: счётчик дедупа работает (2 файла при одном дубле). +- Ретраи/лимит — на проде после деплоя. +- Версия 0.0.71 → 0.0.72. diff --git a/site/app.py b/site/app.py index 7458893..8db6a45 100644 --- a/site/app.py +++ b/site/app.py @@ -21,7 +21,7 @@ if _sys_path_root not in sys.path: sys.path.insert(0, _sys_path_root) # Версия приложения (меняется при изменениях) -VERSION = "0.0.71" +VERSION = "0.0.72" def setup_logging(): diff --git a/site/routes/api_bp.py b/site/routes/api_bp.py index 61214b5..be0d935 100644 --- a/site/routes/api_bp.py +++ b/site/routes/api_bp.py @@ -30,6 +30,10 @@ from session import (create_session, add_file, get_files, store_result, api_bp = Blueprint("api", __name__, url_prefix="/api") log = logging.getLogger("routes.api_bp") +# Ретраи pull из ВМ-буфера: защита от разовых DNS/сетевых сбоев (gaierror -5 и т.п.) +PULL_RETRIES = 3 +PULL_RETRY_DELAY = 2 # секунды между попытками + def _disconnect_exceptions(): """Исключения, означающие отключение клиента SSE.""" @@ -105,9 +109,23 @@ def upload_refs(): except Exception: pass continue - with client.stream("GET", url) as resp: - resp.raise_for_status() - content = b"".join(resp.iter_bytes()) + # Pull с ретраями: разовые DNS/сетевые сбои не роняют всю загрузку + content = None + last_err = None + for attempt in range(PULL_RETRIES): + try: + with client.stream("GET", url) as resp: + resp.raise_for_status() + content = b"".join(resp.iter_bytes()) + last_err = None + break + except Exception as e: + last_err = e + log.warning("upload_refs: pull attempt %d/%d failed sid=%s file=%r: %r", + attempt + 1, PULL_RETRIES, sid, name, e) + time.sleep(PULL_RETRY_DELAY) + if content is None: + raise last_err if last_err else RuntimeError("pull failed") log.info("upload_refs: pulled sid=%s file=%r size=%d", sid, name, len(content)) if len(content) > MAX_FILE_BYTES: log.warning("upload_refs: pulled file exceeds %dMB, skip sid=%s file=%r size=%d", @@ -118,8 +136,16 @@ def upload_refs(): pass continue if not add_file(sid, name, content): - log.warning("upload_refs: session not found/limit, sid=%s file=%r", sid, name) - return jsonify({"ok": False, "error": "Session not found"}), 404 + # Различить: сессия исчезла vs превышен суммарный лимит сессии + if get_files(sid) is None: + log.warning("upload_refs: session not found, sid=%s file=%r", sid, name) + return jsonify({"ok": False, "error": "Session not found"}), 404 + log.warning("upload_refs: session limit exceeded, skip sid=%s file=%r", sid, name) + try: + client.delete(url) + except Exception: + pass + continue try: client.delete(url) # убрать файл с ВМ после загрузки except Exception: diff --git a/site/templates/index.html b/site/templates/index.html index e97617d..133bc3e 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -527,7 +527,7 @@ function addFileWithDedup(file) { const idx = sf.findIndex(f => f.name === name); if (idx >= 0) sf[idx] = new File([file], name, { lastModified: mtime }); } - return; + return false; // дедуп: файл не добавлен (обновлена только дата) } // Имя то же, размер другой — добавить с суффиксом _2, _3... const dot = name.lastIndexOf('.'); @@ -548,9 +548,10 @@ function addFileWithDedup(file) { if (over) { overNames.add(name); sf.push(new File([file], name, { lastModified: mtime })); - return; + return true; } sf.push(new File([file], name, { lastModified: mtime })); + return true; } fi.addEventListener('change', async () => { @@ -613,21 +614,17 @@ folderInput.addEventListener('change', async () => { if (nested.length) { for (const nf of nested) { const nm = relDir ? relDir + '/' + nf.name : nf.name; - addFileWithDedup(new File([nf], nm, { lastModified: nf.lastModified })); - added++; + if (addFileWithDedup(new File([nf], nm, { lastModified: nf.lastModified }))) added++; } } else { // в архиве нет документов — добавить архив как есть, чтобы не терялся - addFileWithDedup(new File([f], rel, { lastModified: f.lastModified })); - added++; + if (addFileWithDedup(new File([f], rel, { lastModified: f.lastModified }))) added++; } } catch (err) { - addFileWithDedup(new File([f], rel, { lastModified: f.lastModified })); // zip как есть - added++; + if (addFileWithDedup(new File([f], rel, { lastModified: f.lastModified }))) added++; // zip как есть } } else if (DOC_EXTS.some(e => low.endsWith(e))) { - addFileWithDedup(new File([f], rel, { lastModified: f.lastModified })); - added++; + if (addFileWithDedup(new File([f], rel, { lastModified: f.lastModified }))) added++; } // иначе — не документ, пропускаем }