InquirerPalawan coast guard extends suspension of sea travel for small vesselsPunchSpeed Darlington begs Burna Boy to drop chargesThe Jerusalem PostKatz: Caretaker gov't fully responsible for Israel's security, taking actions against terror groupsBollywood HungamaImtiaz Ali's O Saathi Re starring Aditi Rao Hydari, Avinash Tiwary and Arjun Rampal, to Premiere on Netflix on December 4CNN TürkYurt dışına kaçmaya hazırlanan 4 FETÖ şüphelisi yakalandıZDF heuteAktuelle Pressemitteilungen des ZDFAntara NewsHealth ministry renovates 10,000 homes to curb tuberculosis spreadWirtualna PolskaZłota figurka niedźwiedzia odkryta w Kazachstanie. Ma ponad 2 tys. latRai NewsNations League, alle 20.45 Italia-Francia. Mpabbé out. Diretta su RaiNews.itStraits Times SportATP's 2026 central partnership revenue rose by 49%NumeramaOn a beau chercher, impossible de trouver la Switch 2 moins chère qu’avec ce code promo AliExpressCNN بالعربيةقوات سلفادورية في هايتي تشن هجوماً على عصابة "400 ماوزو".. ما القصة؟
The Daily Newsstand · Free, Always
Thursday, November 12, 2026

Я написал мониторинг, который никогда не падает. Полгода он не падал и ни разу не сработал

Translate

14 октября на dev-стенде в логах API нашлась строка, которая повторялась примерно раз в сорок секунд:

Watchdog ack failed: relation "message_watchdog" does not exist

Таблицы message_watchdog не было ни на dev, ни на проде. Код, который в неё пишет, лежал в репозитории с 15 апреля. Писал его я.

Зачем он был нужен

Я делаю Octery, бота, который отвечает лидам в чатах amoCRM и передаёт сделку менеджеру. Сообщение клиента приходит вебхуком в пайплайн обработки, тот сходит в наш API за настройками и базой знаний, позовёт модель и отправит ответ обратно в чат. Если по дороге что-то упадёт молча, клиент останется без ответа, и никто об этом не узнает.

В апреле я сделал против этого схему ping/ack. В начале обработки пайплайн зовёт POST /watchdog/ping, API записывает строку «сообщение от такого-то контакта принято». После отправки ответа он зовёт /watchdog/ack, и строка помечается отвеченной. Раз в несколько минут отдельная задача по расписанию спрашивает /watchdog/stale: есть ли сообщения старше пяти минут без ack? Если есть, в Telegram уходит алерт.

Всё это держалось на одной таблице, которую создавала миграция V10__message_watchdog.sql.

Как я потерял таблицу

Номер 10 был занят с марта. В папке уже лежал V10__update_bot_config_trigger_with_company_id.sql, и я его не заметил. На двух одинаковых версиях Flyway падает сразу, так что деплой встал. Через шесть дней я переименовал свой файл в V10b__message_watchdog.sql. Деплой позеленел. Коммит назывался «fix: rename duplicate V10 migration to V10b to resolve Flyway version conflict», и тогда мне казалось, что это и правда fix.

Версия миграции во Flyway состоит из чисел через точку или подчёркивание. 10b не разбирается. За такие имена отвечает параметр validateMigrationNaming, и по умолчанию он выключен. В этом режиме Flyway файл с кривым именем просто не считает миграцией и молча идёт дальше. Я поменял ошибку, которая останавливала деплой, на ошибку, которую не видно вообще.

Проверка занимает два запроса:

SELECT version, description
FROM flyway_schema_history
WHERE version LIKE '10%';
-- 10 | update bot config trigger with company id

SELECT to_regclass('message_watchdog');
-- NULL

Десятая версия в истории есть, от соседней миграции. Никакой 10b там нет, таблицы тоже.

Код, который не умел жаловаться

Без таблицы каждый вызов падал на SQL. Падал внутри try, который я написал так:

internal.post('/watchdog/ping', async (c) => {
  try {
    // INSERT INTO message_watchdog ... ON CONFLICT DO UPDATE
    return c.json({ success: true });
  } catch (error) {
    logger.error('Watchdog ping failed', { error });
    // Never crash - this is monitoring, not business logic
    return c.json({ success: true, warning: 'ping registration failed' });
  }
});

Мысль за комментарием верная: сломался мониторинг, а клиент всё равно должен получить ответ. Только применил я её не там. Бота от падения мониторинга уже защищал сам пайплайн: шаг с вызовом ping был настроен продолжать работу при ошибке, и честный 500 максимум оставил бы красную отметку в журнале выполнений. API подстраховал то же место второй раз и заодно убрал эту отметку.

Решал, слать ли алерт, эндпоинт stale. Его catch:

} catch (error) {
  logger.error('Watchdog stale check failed', { error });
  return c.json({
    success: true,
    data: { staleCount: 0, messages: [], error: 'query failed' },
  });
}

staleCount: 0 значит «потерянных сообщений нет». Cron смотрел на счётчик, поле error он не читал. Полгода проверка, которая не могла выполнить ни одного запроса, отвечала тем же, чем ответила бы в хороший день.

Три состояния вместо двух

Пока я это разбирал, стало понятно, что дело шире одного catch. Я думал о проверке как о вопросе с ответом «да или нет»: есть потерянные сообщения или нет. У проверки есть третий ответ, «не смог посмотреть», и он должен звучать громче, чем «всё хорошо». В моём коде третьего ответа не существовало, и он сливался с хорошим.

На практике это выглядит просто. stale при ошибке отдаёт 500 или хотя бы staleCount: null, а cron на null шлёт отдельный алерт «watchdog не работает». Ping и ack тоже отдают ошибку, а тихо продолжать работу остаётся заботой вызывающей стороны, у нас это уже было настроено.

Чем закончилось

Переписывать watchdog мы не стали, а 14 октября удалили. Падения пайплайна к тому времени ловил отдельный обработчик ошибок с алертами, а неотвеченные сообщения ловит тестер бота, который пишет в Telegram как живой клиент и ждёт ответ. Таблицу удалили миграцией V26__drop_message_watchdog.sql с DROP TABLE IF EXISTS. На обоих стендах она ничего не делает, но теперь схема в git совпадает с тем, что лежит в базе. V10b остался в папке, историю миграций мы не переписываем.

Если у вас миграции когда-нибудь переименовывали руками, сравните список файлов в папке с flyway_schema_history. Разница там означает файлы, которые Flyway не видит. И включите validateMigrationNaming=true: с ним такой файл уронит деплой в тот же день, а у меня на это ушло полгода.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.