Merge: защита от тихого зависания бота (watchdog + сетевой таймаут)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user