From 6ee4e6cf0f3f04ae1c62906b0f3cba32e72cb1fb Mon Sep 17 00:00:00 2001 From: Kenik Date: Sun, 6 Sep 2026 13:44:45 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D1=89=D0=B8=D1=82=D0=B0=20=D0=BE?= =?UTF-8?q?=D1=82=20=D1=82=D0=B8=D1=85=D0=BE=D0=B3=D0=BE=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=20(=D0=B1=D0=B5=D0=B7=20=D1=82=D0=B0=D0=B9=D0=BC=D0=B0?= =?UTF-8?q?=D1=83=D1=82=D0=B0=20=D1=81=D0=B5=D1=82=D0=B8=20+=20watchdog)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Причина 'бот в моменте перестаёт работать' без ошибок в логах: vk_api ставит таймаут только на сам LongPoll-запрос (wait+10 с), а обычные вызовы API (messages.send и т.п.) идут без таймаута вообще. Если VK/сеть один раз не ответит на такой вызов — соединение виснет навсегда, а так как обработка сообщений однопоточная (внутри цикла LongPoll), весь бот намертво стопорится без единой строки в логе. Docker restart: unless-stopped не спасает — процесс не падает, а просто висит. - socket.setdefaulttimeout(60) — подстраховка для всех сетевых вызовов без явного timeout (vk_api, requests в mailer.py и загрузке фото). - Ручной вызов longpoll.check() вместо for event in listen() — даёт watchdog 'пульс' на каждом опросе сервера (~25-35 с), а не только при реальных сообщениях. - Watchdog-поток: если цикл не подавал признаков жизни дольше 5 минут, принудительно завершает процесс (os._exit), чтобы Docker реально перезапустил контейнер. --- bot.py | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index a3887f5..e51d9a6 100644 --- a/bot.py +++ b/bot.py @@ -10,10 +10,20 @@ import time import hmac import hashlib import logging +import socket import threading from collections import defaultdict from typing import Dict, Any, Optional +# Глобальный таймаут для всех сетевых сокетов без явного timeout. +# Без этого библиотека vk_api ставит таймаут только на сам LongPoll-запрос +# (wait+10 сек), а обычные вызовы API (messages.send и т.п.) идут без +# таймаута вообще — если VK/сеть один раз не ответит, соединение висит +# бесконечно и весь однопоточный цикл обработки сообщений намертво стопорится +# без единой строки в логе. Docker restart:unless-stopped при этом не +# помогает — процесс не падает, а просто зависает. +socket.setdefaulttimeout(60) + # Нам понадобятся библиотеки vk_api. try: import vk_api @@ -52,6 +62,10 @@ BAN_SEC = 600 ADMINS_CACHE_TTL = 30 # Как часто фоновый поток проверяет, не пора ли отправить напоминание REMINDER_CHECK_INTERVAL_SEC = 60 +# Сколько секунд основной цикл LongPoll может не подавать признаков жизни, +# прежде чем watchdog сочтёт его зависшим и принудительно завершит процесс +# (штатный опрос происходит раз в ~25-35 с, так что 5 минут — большой запас) +WATCHDOG_TIMEOUT_SEC = 300 REMINDER_PERIOD_LABELS = { "once": "разово", @@ -2131,9 +2145,28 @@ class VKBotApplication: logger.error(f"Ошибка обработки напоминаний: {e}") time.sleep(REMINDER_CHECK_INTERVAL_SEC) + def _watchdog_loop(self): + """Фоновый страж: если основной цикл LongPoll не подавал признаков + жизни дольше WATCHDOG_TIMEOUT_SEC (завис на каком-то вызове без + таймаута, дедлок и т.п.), принудительно завершает процесс — чтобы + Docker (restart: unless-stopped) реально перезапустил контейнер. + Без этого зависший, но не упавший процесс никто не перезапустит.""" + while True: + time.sleep(30) + stale_for = time.time() - self._last_alive_ts + if stale_for > WATCHDOG_TIMEOUT_SEC: + logger.critical( + f"Watchdog: основной цикл не подавал признаков жизни {stale_for:.0f} с " + f"(порог {WATCHDOG_TIMEOUT_SEC} с). Похоже на зависание — принудительный выход " + f"для перезапуска контейнером." + ) + os._exit(1) + def run(self): """Запуск бесконечного цикла прослушивания событий VK LongPoll.""" + self._last_alive_ts = time.time() threading.Thread(target=self._reminders_loop, daemon=True).start() + threading.Thread(target=self._watchdog_loop, daemon=True).start() if not self.longpoll: logger.warning("Бот запущен в режиме ЭМУЛЯЦИИ (без валидных токенов VK API).") print("Симуляция работы бота в консоли. Напишите 'выход' для завершения.") @@ -2163,9 +2196,16 @@ class VKBotApplication: logger.error(f"Не удалось отправить старт-пинг: {e}") # Устойчивый цикл: обрыв longpoll-соединения (обычное дело) не должен ронять бота. + # Вызываем check() вручную (а не for event in self.longpoll.listen()), чтобы + # обновлять "пульс" для watchdog на каждом опросе сервера (раз в ~25-35 с), + # а не только когда реально приходит сообщение — иначе в тихие часы без + # сообщений watchdog решит, что бот завис, и перезапустит его зря. while True: try: - for event in self.longpoll.listen(): + self._last_alive_ts = time.time() + events = self.longpoll.check() + self._last_alive_ts = time.time() + for event in events: if event.type == VkBotEventType.MESSAGE_NEW: message_obj = event.obj.message user_id = message_obj["from_id"]