Ревью PR #3561, две находки.
1. Ручные запуски из админки (avito/cian city sweep, cian_full_load,
yandex_full_load, yandex_city_sweep) идут мимо scheduler._dispatch: в except
был только logger.exception, и отказ «пул пуст» до try-финализатора пайплайна
оставлял строку running до zombie. Логика финализации переехала из
планировщика в runs.mark_crashed; её зовут планировщик и все пять ручек.
2. Обычный путь пайплайна — mark_failed и raise; планировщик звал mark_failed
второй раз. UPDATE — no-op, но _alert_on_run_id срабатывал снова с тем же
стриком, и на вехе лестницы в Sentry уходил дубль (+ WARNING «no-op»).
mark_crashed сначала читает статус и финализирует только running.
Тесты по значению над двойником строки scrape_runs и боевыми mark_*:
статус banned/infra или failed после планировщика и после каждой из пяти ручек,
одна отправка в Sentry на неудаче пайплайна.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>