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"]