Уведомления о падении фоновых задач
Фоновая задача упала — и об этом легко не узнать: пользователь ничего не видел, в логах воркера тихо, а письмо про сбойный экспорт так и не ушло. Хуже всего, когда джоба исчерпала все ретраи и просто «умерла». Ловим именно этот момент — финальный провал — и шлём пуш с именем задачи и ошибкой.
Общие правила
Заголовок раздела «Общие правила»- Только на сервере. Хук вешаем внутри воркера,
notify()вызываем оттуда. - Ловим финальный провал, а не каждый ретрай. Иначе одна задача с 5 ретраями устроит 5 пушей. Нужен момент, когда попытки кончились.
- Дедуп по классу задачи. Если «упало 200 писем», хочется один пуш
«
SendEmail× 200», а не 200 отдельных. Дедуп по имени класса джобы.
Базовая обёртка
Заголовок раздела «Базовая обёртка»Берём notify() из обзорной страницы:
import os, requests
def notify(title, message, priority=5): requests.post( f"{os.environ['NOTIFLY_URL']}/message", params={"token": os.environ["NOTIFLY_TOKEN"]}, json={"title": title, "message": message, "priority": priority}, timeout=5, )Celery (Python)
Заголовок раздела «Celery (Python)»Сигнал task_failure срабатывает, когда задача упала окончательно (ретраи
кидают Retry и сюда не попадают). Дедуп — по имени задачи:
import time, tracebackfrom collections import defaultdictfrom celery.signals import task_failurefrom notifly import notify
_last = defaultdict(float) # имя задачи -> время последнего пуша_COOLDOWN = 300 # 5 минут на один и тот же класс задачи
@task_failure.connectdef on_task_failure(sender=None, task_id=None, exception=None, **kw): name = sender.name if sender else "unknown" if time.time() - _last[name] < _COOLDOWN: return # дедуп по классу задачи _last[name] = time.time()
tb = "".join(traceback.format_exception(exception))[-1200:] notify( f"🔧 Джоба упала: {name}", f"task_id={task_id}\n{type(exception).__name__}: {exception}\n\n{tb}", 8, )BullMQ (Node.js)
Заголовок раздела «BullMQ (Node.js)»Слушаем событие failed, но шлём пуш только когда исчерпаны все попытки
(attemptsMade >= opts.attempts):
const {Worker} = require('bullmq');const {notify} = require('./notifly');
const lastSent = new Map(); // имя джобы -> timestampconst COOLDOWN_MS = 300_000; // 5 минут на класс
const worker = new Worker('emails', async (job) => { await processJob(job);}, {connection: {host: 'localhost', port: 6379}});
worker.on('failed', (job, err) => { if (!job) return; // шлём только на финальный провал, а не на каждый ретрай if (job.attemptsMade < (job.opts.attempts || 1)) return;
const key = job.name; if (Date.now() - (lastSent.get(key) || 0) < COOLDOWN_MS) return; // дедуп по классу lastSent.set(key, Date.now());
notify( `🔧 Джоба упала: ${job.name}`, `id=${job.id}, попыток=${job.attemptsMade}\n${err.message}\n\n${err.stack?.slice(0, 1200)}`, 8 );});Sidekiq (Ruby)
Заголовок раздела «Sidekiq (Ruby)»У Sidekiq есть death_handlers — вызываются, когда задача исчерпала все ретраи
и уезжает в Dead set. Это ровно тот момент, который нам нужен:
require 'net/http'require 'json'
$notifly_last = {} # класс джобы -> время последнего пуша
def notifly(title, message, priority = 5) uri = URI("#{ENV['NOTIFLY_URL']}/message?token=#{ENV['NOTIFLY_TOKEN']}") Net::HTTP.post(uri, {title: title, message: message, priority: priority}.to_json, 'Content-Type' => 'application/json')rescue => e warn "notifly failed: #{e.message}"end
Sidekiq.configure_server do |config| config.death_handlers << ->(job, ex) { klass = job['class'] # дедуп по классу задачи: не чаще раза в 5 минут next if $notifly_last[klass] && Time.now - $notifly_last[klass] < 300 $notifly_last[klass] = Time.now
notifly( "🔧 Джоба умерла: #{klass}", "jid=#{job['jid']}\n#{ex.class}: #{ex.message}", 8 ) }endЧто ещё присылать
Заголовок раздела «Что ещё присылать»- Растёт глубина очереди. Если необработанных задач > N — воркер не справляется; тихий алерт (приоритет 5).
- Долгая задача зависла. Джоба выполняется дольше обычного в разы — стоит посмотреть; сюда же хорошо ложится heartbeat.
- Обработка ретраев. Отдельно можно сообщать «задача ушла на 3-й ретрай» — тихо (приоритет 3), чтобы поймать нестабильность раньше финального провала.
- Тихие падения становятся видимыми. Фоновая задача не кричит пользователю — теперь она кричит вам.
- Контекст в сообщении. Имя задачи, id и ошибка — обычно этого достаточно, чтобы понять, куда смотреть.
- Без шторма. Дедуп по классу: один битый деплой не превращается в сотни пушей.