Архив рубрики ~Лента новостей~

Как научить Telegram-бота на Python не терять задачи пользователей при перезапуске. Кейс ассистента для hh.ru

Как научить Telegram-бота на Python не терять задачи пользователей при перезапуске. Кейс ассистента для hh.ru

Привет, Хабр!

Мы тут в свободное время пилим проект, который должен решить боль многих айтишников, — автоматизировать рутинный поиск работы. Идея выросла в Telegram-бота «Аврора» , который на «автопилоте» ищет вакансии на hh.ru и откликается на них.

Но чтобы «автопилот» был полезным, он должен быть надежным. Никому не нужен ассистент, который при первом же деплое новой версии или падении сервера забывает, что он делал, и какие вакансии уже отправил.

Сегодня я хочу рассказать не столько о самом боте, сколько о конкретной инженерной задаче, с которой мы столкнулись: как обеспечить персистентность и «бесшовное» возобновление длительных пользовательских задач при перезапуске сервиса.

Под катом — наш подход к Graceful Shutdown, восстановлению сессий и немного про то, как LLM (в нашем случае Gemini) генерирует поисковые запросы.

Проблема: «Автопилот» — это stateful-процесс

Сначала кратко о том, как работает основная логика, чтобы был понятен масштаб проблемы.

  1. Пользователь логинится через OAuth hh.ru и выбирает свое резюме.

  2. Дальше начинается магия. Мы не просим пользователя вводить python AND (django OR flask) NOT (bitrix). Вместо этого мы берем текст его резюме, скармливаем его LLM (Gemini) и просим: «На основе этого резюме, сгенерируй оптимальный поисковый custom_query для API hh.ru».

  3. Этот запрос (например, (Python OR Go) AND (backend OR developer) AND (PostgreSQL OR ClickHouse)) сохраняется в нашей PostgreSQL.

  4. Пользователь нажимает «Запустить автопилот».

И вот тут начинается самое интересное. «Автопилот» — это не разовая функция. Это длительный асинхронный процесс, который должен:

  • Периодически (раз в N минут) ходить в API hh.ru с тем самым custom_query.

  • Для каждой найденной вакансии снова обращаться к LLM, передавая ей резюме пользователя и текст вакансии, с задачей «Напиши персонализированное сопроводительное письмо».

  • Автоматически откликаться на вакансию с этим письмом.

  • Вести учет откликов, чтобы не спамить.

Этот процесс может работать часами и днями. А теперь представим, что нам нужно выкатить новую версию бота. Мы гасим systemd юнит, Docker-контейнер останавливается… и все активные поиски пользователей прерываются. При следующем запуске бот не помнит, кто что искал и на чем остановился. Это фиаско.

Решение: State-машина в PostgreSQL и Graceful Shutdown

Проблема очевидна: состояние «автопилота» должно жить не в оперативной памяти бота, а во внешней базе данных. Мы используем PostgreSQL.

1. Модель данных (упрощенно)

Нам потребовалось несколько таблиц, но ключевая — это user_sessions (или autopilot_state).

SQL

— Упрощенная схема для понимания CREATE TABLE user_sessions ( user_id BIGINT PRIMARY KEY, hh_resume_id VARCHAR(255), custom_query TEXT, — Тот самый, что сгенерил LLM session_status VARCHAR(50) DEFAULT ‘stopped’, — ‘running’, ‘paused’, ‘stopped’ last_run_time TIMESTAMP, last_processed_vacancy_id VARCHAR(100), — Для пагинации и исключения дублей — … другие поля, токены и т.д. );

Когда пользователь запускает «автопилот», мы не просто запускаем asyncio таску. Мы меняем session_status в БД на ‘running’.

2. Механизм «Graceful Shutdown» (Изящной остановки)

Мы используем aiogram 3.x. Нам нужно отловить сигнал об остановке процесса (например, SIGTERM, который посылает docker stop или systemd).

В главном файле запуска бота (__main__.py или app.py) мы вешаем обработчики на эти сигналы.

Python

import asyncio import signal from aiogram import Bot, Dispatcher # … импорты наших сервисов (db_service, autopilot_service) async def on_shutdown(dp: Dispatcher, bot: Bot): «»» Вызывается при получении сигнала SIGTERM или SIGINT. «»» logging.warning(«Получен сигнал остановки. Переводим активные сессии в ‘paused’…») # Здесь мы идем в нашу БД и все сессии со статусом ‘running’ # атомарно переводим в статус ‘paused’. await db_service.pause_all_active_autopilots() logging.warning(«Все активные задачи приостановлены. Завершение работы…») # Даем aiogram штатно завершить обработку текущих апдейтов await dp.storage.close() await bot.session.close() async def main(): # … инициализация бота, диспатчера, роутеров … # Добавляем обработчики сигналов loop = asyncio.get_running_loop() for sig in (signal.SIGINT, signal.SIGTERM): loop.add_signal_handler(sig, lambda: asyncio.create_task(on_shutdown(dp, bot))) # Запускаем автопилот для тех, кто был ‘paused’ await autopilot_service.resume_paused_sessions() # Запускаем поллинг await dp.start_polling(bot) if __name__ == «__main__»: asyncio.run(main())

Что здесь происходит:

  1. При получении SIGTERM, asyncio создает задачу on_shutdown.

  2. on_shutdown не останавливает поиск, а просто идет в PostgreSQL и всем, у кого session_status = ‘running’, ставит session_status = ‘paused’.

  3. После этого бот штатно завершает работу.

Сам воркер «автопилота» в своем цикле while True должен после каждой итерации (например, после обработки одной вакансии) проверять свой статус в БД. Если он стал ‘paused’ или ‘stopped’, воркер должен корректно завершить свою работу (return или break).

3. Механизм «Auto-Resume» (Авто-возобновления)

Теперь самое главное. Когда бот стартует, нам нужно поднять все «уснувшие» задачи. Для этого в main() перед запуском поллинга мы вызываем специальную функцию resume_paused_sessions().

Python

# Внутри нашего autopilot_service async def resume_paused_sessions(self): «»» Вызывается один раз при старте бота. Ищет всех пользователей со статусом ‘paused’ и запускает для них воркеры. «»» paused_users = await self.db.get_users_by_status(‘paused’) if not paused_users: logging.info(«Приостановленных сессий не найдено.») return logging.info(f»Найдено {len(paused_users)} приостановленных сессий. Возобновление…») tasks = [] for user_session in paused_users: # Важно: Сначала меняем статус в БД на ‘running’ await self.db.update_session_status(user_session.user_id, ‘running’) # И только потом создаем асинхронную задачу на запуск tasks.append( asyncio.create_task( self.run_autopilot_for_user(user_session.user_id) ) ) await asyncio.gather(*tasks) logging.info(«Все приостановленные сессии успешно возобновлены.»)

Итог этого блока

Мы получили «пуленепробиваемую» систему:

  • Деплой: Мы спокойно перезапускаем бота. SIGTERM -> задачи в БД переводятся в ‘paused’ -> бот гаснет.

  • Старт: Бот запускается -> читает БД -> видит все ‘paused’ задачи -> меняет их на ‘running’ и запускает воркеры.

  • Крэш: Если бот упал без Graceful Shutdown (OOM Killer, kill -9), задачи остаются в БД со статусом ‘running’. Нам нужен отдельный watchdog-механизм, который при старте проверяет задачи ‘running’ и, если они «подвисли», тоже их перезапускает (например, по last_run_time).

Что еще под капотом (и над чем работаем)

Описанный механизм — это лишь один из кубиков. Вокруг него построено много всего:

  • Keep-Alive: Отдельный механизм, который восстанавливает сессию пользователя (токены hh.ru), если его почему-то нет в нашей БД, но он пишет боту (например, если мы чистили кэши).

  • Динамические ReplyKeyboardMarkup: Чтобы не просить пользователей писать /start после каждого обновления, мы сделали механизм, который при изменении «версии» кнопок в коде автоматически обновляет клавиатуру всем пользователям при их следующем сообщении.

  • Роли Admin/User: Админы видят расширенный интерфейс прямо в Telegram, могут смотреть статистику, статусы сессий и т.д.

Текущая задача: Мы продолжаем отладку механизма Graceful Shutdown и авто-возобновления, так как при больших нагрузках всплывают нюансы с гонкой состояний (race conditions) в asyncio. Параллельно добавляем новые админские функции для управления пользователями.

Заключение

Создание stateful-бота, который выполняет длительные фоновые задачи, — это всегда вызов. Нельзя просто положить все в RAM и надеяться на лучшее. Использование PostgreSQL как «единого источника правды» о состоянии сессий и реализация механизмов GraceEOF

  • Название: «Аврора»

  • Логин в Telegram: @AuroraCareer

  • Стек: Python, aiogram, PostgreSQL, Gemini API, hh.ru API.

Буду рад услышать в комментариях, с какими проблемами вы сталкивались при создании подобных «долгоживущих» ботов. Как вы решаете задачи персистентности? Может, мы где-то изобрели велосипед?

Спасибо за внимание!

Источник: habr.com

❌ Нет тегов для этой статьи

Оцените материал:

Поделиться
Понравилась статья? Расскажите другим
ВКонтакте
Читайте также
Новости робототехники Датчики давления могут повысить точность захвата роботов. Новости робототехники Компания Poseidon Aerospace привлекла 60 миллионов долларов перед первым испытательным полетом без пилота. Архив рубрики ~Коротко из Telegram~ Телевизоры LG могут следить за тем, что происходит у вас… Архив рубрики ~Коротко из Telegram~ Крошечная open‑source‑модель MiniCPM5-2B обошла конкурентов до 4B параметров ⚡ Канал… Архив рубрики ~Коротко из Telegram~ Бесплатная обработка голоса в аудио Если записываешь голос на встроенный… Архив рубрики ~Обо всем~ Конец универсального диагноза болезни Паркинсона: почему 2026 год станет годом, когда неврология перейдет на высокоточное исследование. Архив рубрики ~Коротко из Telegram~ Дата-центр Stargate в Абилине назвали «местом рождения AGI» Глава Crusoe… Архив рубрики ~Коротко из Telegram~ Небольшой дайджест — коротко о найденных новостях 1️⃣ Российский облачный… Архив рубрики ~Коротко из Telegram~ Офлайн-ИИ на вашем телефоне В новых айфонах и топовых Android-флагманах… Архив рубрики ~Коротко из Telegram~ Почему видео от нейросети выглядит «мёртво» — и как это… Архив рубрики ~Коротко из Telegram~ #слухи Apple корректирует планы по выпуску новых моделей MacBook с… Архив рубрики ~Обо всем~ Как вырастить один кристалл размером с ладонь и повесить на него десять тонн Архив рубрики ~Коротко из Telegram~ Инструмент полезен настолько, насколько вы знаете что он умеет. Разбираем… Новости робототехники MassRobotics рассматривает опрос участников по ограничениям FCC Новости робототехники Датчики давления могут повысить точность захвата роботов. Новости робототехники Компания Poseidon Aerospace привлекла 60 миллионов долларов перед первым испытательным полетом без пилота. Архив рубрики ~Коротко из Telegram~ Телевизоры LG могут следить за тем, что происходит у вас… Архив рубрики ~Коротко из Telegram~ Крошечная open‑source‑модель MiniCPM5-2B обошла конкурентов до 4B параметров ⚡ Канал… Архив рубрики ~Коротко из Telegram~ Бесплатная обработка голоса в аудио Если записываешь голос на встроенный… Архив рубрики ~Обо всем~ Конец универсального диагноза болезни Паркинсона: почему 2026 год станет годом, когда неврология перейдет на высокоточное исследование. Архив рубрики ~Коротко из Telegram~ Дата-центр Stargate в Абилине назвали «местом рождения AGI» Глава Crusoe… Архив рубрики ~Коротко из Telegram~ Небольшой дайджест — коротко о найденных новостях 1️⃣ Российский облачный… Архив рубрики ~Коротко из Telegram~ Офлайн-ИИ на вашем телефоне В новых айфонах и топовых Android-флагманах… Архив рубрики ~Коротко из Telegram~ Почему видео от нейросети выглядит «мёртво» — и как это… Архив рубрики ~Коротко из Telegram~ #слухи Apple корректирует планы по выпуску новых моделей MacBook с… Архив рубрики ~Обо всем~ Как вырастить один кристалл размером с ладонь и повесить на него десять тонн Архив рубрики ~Коротко из Telegram~ Инструмент полезен настолько, насколько вы знаете что он умеет. Разбираем… Новости робототехники MassRobotics рассматривает опрос участников по ограничениям FCC