11 тасок объявляли `max_retries=2`, но ретраи не реализовывали: ни
`autoretry_for` в декораторе, ни вызова `self.retry()` в теле. Celery в
таком виде параметр не применяет — при исключении таска падает с первой
попытки. Читающий код видит «до 3 попыток», а их одна.
Убран `max_retries` у: cbr_macro_sync, rosstat_macro_sync,
developer_registry_refresh, location_refresh, mv_sales_tracker_refresh,
refresh_analytics, refresh_layout_velocity, refresh_quarter_price_index,
scrape_objective.sync_objective_group, supply_layers_refresh,
scrape_kn.scrape_kn_region. Заодно убран `bind=True` там, где `self` не
использовался вовсе; в `scrape_kn_region` он оставлен — `self.request.id`
пишется в kn_scrape_log.
Не тронуты и не должны быть: `resume_kn_run` (max_retries=12 +
настоящий self.retry()), `nspd_sync`/`scrape_cadastre` (autoretry_for),
`nspd_geo`/`objective_etl` (max_retries=0 — честное «ретраев нет»).
Гейт `test_2464_retry_config_is_real.py` разбирает AST всех модулей
`app/workers/tasks/` и требует: если декоратор объявляет ненулевой
max_retries, в нём есть autoretry_for либо в теле функции есть
self.retry(). Три таски из одиннадцати гейт нашёл сверх списка эпика.
Проверка гейта: с фиксом зелено, при возврате `max_retries=2` в
supply_layers_refresh — красно с указанием на эту таску. Плюс два
контроля: гейт видит ≥20 тасок (не молчит из-за пустой выборки) и
признаёт обе законные формы ретраев.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Два concurrency-бага в scrape_kn:
1. Lock value="1" без owner-токена + TTL=30мин = заявленной длительности
sweep'а (нулевой запас). Sweep > TTL → lock истекает, второй sweep
стартует, первый при завершении безусловным r.delete сносит ЧУЖОЙ
lock → возможен третий параллельный.
2. SIGKILL/redeploy mid-sweep: finally не выполняется, lock живёт до
30 мин. worker_ready метит run 'zombie' и enqueue'ит resume_kn_run
БЕЗ countdown, который ловит lock_held и возвращает skipped без
retry → run теряется до недельного beat.
Patch:
- _region_lock: value = uuid4().hex; release через Lua check-and-delete
(`if GET == ARGV[1] then DEL else 0 end`) — не сносим чужой lock.
- _LOCK_TTL_SECONDS 30 → 45 мин (полтора max sweep duration, запас).
- resume_kn_run: при lock_held → raise self.retry(countdown=300,
max_retries=12) → ~час окно для подхвата вместо silent skip.
- scrape_kn_region (scheduled) намеренно остаётся skipped — beat
поднимет в следующий weekly tick (другая семантика).
11 новых юнит-тестов (token uniqueness, Lua guard, retry-not-skip,
existing behaviors). 16/16 scrape_kn тестов зелёные. ruff clean.
Closes#1216
Симптом: POST /admin/scrape/kn возвращает task_id, но run_id не появляется
в kn_scrape_runs. Причина: зомби-worker оставил Redis-lock с TTL=2h, новые
задачи скипаются с reason='lock_held' молча.
Backend:
- _LOCK_TTL_SECONDS 7200 → 1800 (full sweep ~25-30 мин — потолок).
- force_release_lock() helper + новый POST /api/v1/admin/scrape/release-lock.
- TriggerKnRequest +force: bool — при true перед apply_async удалить lock.
- _log_task_received() / _log_task_skipped() — kn_scrape_log пишет «task
подхвачен/skip» с run_id=NULL до основного flow. Теперь UI показывает
whether worker реально получил задачу.
Frontend:
- Чекбокс ⚡ Force в форме запуска.
- Кнопка 🔓 Снять Redis-lock (отдельный endpoint).
- Уведомление о результате release.