[Fable 5][P2] nspd_geo: WAF-backoff sleep (до 240с) > 2-мин порога cleanup_zombies → дубль-запуск того же job двумя воркерами #1215

Closed
opened 2026-06-13 04:49:44 +00:00 by bot-backend · 0 comments
Collaborator

Из армейского ревью Claude Fable 5 (2026-06-11)

Confirmed by 2/2 адверсариальных верификатора, severity P2 (concurrency), confidence 0.7.

Файл

backend/app/workers/tasks/nspd_geo.py:583

Evidence (цитата кода)

UPDATE nspd_geo_jobs
   SET status = 'queued',
       error = COALESCE(error, 'auto-resume by beat cleanup')
 WHERE status IN ('running', 'paused')
   AND heartbeat_at < NOW() - INTERVAL '2 minutes'
RETURNING job_id

Why bug

В WAF-ветке task спит wait = 30min(consecutive_waf,8) = до 240с (строки 449, 467), heartbeat обновляется ДО сна (строка 458). cleanup_zombies бежит каждую минуту (beat ' * * * *') и при consecutive_waf>=5 (sleep 150-240с > 120с порога) ре-enqueue'ит ЖИВОЙ job. SELECT pending-таргетов без FOR UPDATE SKIP LOCKED → два воркера дублируют NSPD-запросы по уже-WAF-заблокированному сервису (углубляя бан), затирают счётчики done/failed друг друга. Бонус: 'paused' после 8 WAF возобновляется через 2 мин вместо паузы — защита от WAF аннулирована.

Suggested fix

Поднять zombie-порог выше max backoff (например 10 мин) или heartbeat'ить ПОСЛЕ сна/в цикле сна; для 'paused' — отдельный больший cooldown. В идеале — claim таргетов через UPDATE ... WHERE status='pending' RETURNING (FOR UPDATE SKIP LOCKED) и guard от второго инстанса job_id.

Verify notes

  • [real/P2] Heartbeat коммитится ДО time.sleep (458→467); при consecutive_waf>=6 sleep 180–240с > 120с порог + 60с beat-период → cleanup (cron каждую минуту) гарантированно ре-enqueue'ит живой job; worker concurrency=8 стартует дубль параллельно. SELECT pending без SKIP LOCKED → двойные запросы к банящему NSPD, новый инстанс обходит backoff (waf=0), счётчики затираются. При 8 WAF sleep ДО 'paused' → авто-resume за ≤60с. Не P1: UPSERT идемпотентны.
  • [real/P2] Heartbeat коммитится до sleep; wait=150-240с при waf>=5 > порога 120с, при waf>=6 окно >=60с — beat-тик (60с) попадает гарантированно. Защит нет: concurrency=8 на geo, _start_job без CAS, цикл не перечитывает статус, SELECT pending без SKIP LOCKED (оба берут один target), apply_async без task_id. Дубли затирают счётчики абсолютными значениями, 'paused' возобновляется через ~2-3 мин вместо паузы. Доменные UPSERT идемпотентны, job ручной — P2.

Provenance

  • Finders: workers
  • Confidence: 0.7
  • Originally batched at: main@d796027
## Из армейского ревью Claude Fable 5 (2026-06-11) Confirmed by 2/2 адверсариальных верификатора, severity **P2** (concurrency), confidence 0.7. ## Файл [`backend/app/workers/tasks/nspd_geo.py:583`](backend/app/workers/tasks/nspd_geo.py:583) ## Evidence (цитата кода) ``` UPDATE nspd_geo_jobs SET status = 'queued', error = COALESCE(error, 'auto-resume by beat cleanup') WHERE status IN ('running', 'paused') AND heartbeat_at < NOW() - INTERVAL '2 minutes' RETURNING job_id ``` ## Why bug В WAF-ветке task спит wait = 30*min(consecutive_waf,8) = до 240с (строки 449, 467), heartbeat обновляется ДО сна (строка 458). cleanup_zombies бежит каждую минуту (beat '* * * * *') и при consecutive_waf>=5 (sleep 150-240с > 120с порога) ре-enqueue'ит ЖИВОЙ job. SELECT pending-таргетов без FOR UPDATE SKIP LOCKED → два воркера дублируют NSPD-запросы по уже-WAF-заблокированному сервису (углубляя бан), затирают счётчики done/failed друг друга. Бонус: 'paused' после 8 WAF возобновляется через 2 мин вместо паузы — защита от WAF аннулирована. ## Suggested fix Поднять zombie-порог выше max backoff (например 10 мин) или heartbeat'ить ПОСЛЕ сна/в цикле сна; для 'paused' — отдельный больший cooldown. В идеале — claim таргетов через UPDATE ... WHERE status='pending' RETURNING (FOR UPDATE SKIP LOCKED) и guard от второго инстанса job_id. ## Verify notes - [real/P2] Heartbeat коммитится ДО time.sleep (458→467); при consecutive_waf>=6 sleep 180–240с > 120с порог + 60с beat-период → cleanup (cron каждую минуту) гарантированно ре-enqueue'ит живой job; worker concurrency=8 стартует дубль параллельно. SELECT pending без SKIP LOCKED → двойные запросы к банящему NSPD, новый инстанс обходит backoff (waf=0), счётчики затираются. При 8 WAF sleep ДО 'paused' → авто-resume за ≤60с. Не P1: UPSERT идемпотентны. - [real/P2] Heartbeat коммитится до sleep; wait=150-240с при waf>=5 > порога 120с, при waf>=6 окно >=60с — beat-тик (60с) попадает гарантированно. Защит нет: concurrency=8 на geo, _start_job без CAS, цикл не перечитывает статус, SELECT pending без SKIP LOCKED (оба берут один target), apply_async без task_id. Дубли затирают счётчики абсолютными значениями, 'paused' возобновляется через ~2-3 мин вместо паузы. Доменные UPSERT идемпотентны, job ручной — P2. ## Provenance - Finders: `workers` - Confidence: 0.7 - Originally batched at: main@d796027
bot-backend added the
Fable 5 ревью
label 2026-06-13 04:49:44 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1215
No description provided.