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 hmac
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
import socket
|
||||||
import threading
|
import threading
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Dict, Any, Optional
|
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.
|
# Нам понадобятся библиотеки vk_api.
|
||||||
try:
|
try:
|
||||||
import vk_api
|
import vk_api
|
||||||
@@ -52,6 +62,10 @@ BAN_SEC = 600
|
|||||||
ADMINS_CACHE_TTL = 30
|
ADMINS_CACHE_TTL = 30
|
||||||
# Как часто фоновый поток проверяет, не пора ли отправить напоминание
|
# Как часто фоновый поток проверяет, не пора ли отправить напоминание
|
||||||
REMINDER_CHECK_INTERVAL_SEC = 60
|
REMINDER_CHECK_INTERVAL_SEC = 60
|
||||||
|
# Сколько секунд основной цикл LongPoll может не подавать признаков жизни,
|
||||||
|
# прежде чем watchdog сочтёт его зависшим и принудительно завершит процесс
|
||||||
|
# (штатный опрос происходит раз в ~25-35 с, так что 5 минут — большой запас)
|
||||||
|
WATCHDOG_TIMEOUT_SEC = 300
|
||||||
|
|
||||||
REMINDER_PERIOD_LABELS = {
|
REMINDER_PERIOD_LABELS = {
|
||||||
"once": "разово",
|
"once": "разово",
|
||||||
@@ -2131,9 +2145,28 @@ class VKBotApplication:
|
|||||||
logger.error(f"Ошибка обработки напоминаний: {e}")
|
logger.error(f"Ошибка обработки напоминаний: {e}")
|
||||||
time.sleep(REMINDER_CHECK_INTERVAL_SEC)
|
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):
|
def run(self):
|
||||||
"""Запуск бесконечного цикла прослушивания событий VK LongPoll."""
|
"""Запуск бесконечного цикла прослушивания событий VK LongPoll."""
|
||||||
|
self._last_alive_ts = time.time()
|
||||||
threading.Thread(target=self._reminders_loop, daemon=True).start()
|
threading.Thread(target=self._reminders_loop, daemon=True).start()
|
||||||
|
threading.Thread(target=self._watchdog_loop, daemon=True).start()
|
||||||
if not self.longpoll:
|
if not self.longpoll:
|
||||||
logger.warning("Бот запущен в режиме ЭМУЛЯЦИИ (без валидных токенов VK API).")
|
logger.warning("Бот запущен в режиме ЭМУЛЯЦИИ (без валидных токенов VK API).")
|
||||||
print("Симуляция работы бота в консоли. Напишите 'выход' для завершения.")
|
print("Симуляция работы бота в консоли. Напишите 'выход' для завершения.")
|
||||||
@@ -2163,9 +2196,16 @@ class VKBotApplication:
|
|||||||
logger.error(f"Не удалось отправить старт-пинг: {e}")
|
logger.error(f"Не удалось отправить старт-пинг: {e}")
|
||||||
|
|
||||||
# Устойчивый цикл: обрыв longpoll-соединения (обычное дело) не должен ронять бота.
|
# Устойчивый цикл: обрыв longpoll-соединения (обычное дело) не должен ронять бота.
|
||||||
|
# Вызываем check() вручную (а не for event in self.longpoll.listen()), чтобы
|
||||||
|
# обновлять "пульс" для watchdog на каждом опросе сервера (раз в ~25-35 с),
|
||||||
|
# а не только когда реально приходит сообщение — иначе в тихие часы без
|
||||||
|
# сообщений watchdog решит, что бот завис, и перезапустит его зря.
|
||||||
while True:
|
while True:
|
||||||
try:
|
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:
|
if event.type == VkBotEventType.MESSAGE_NEW:
|
||||||
message_obj = event.obj.message
|
message_obj = event.obj.message
|
||||||
user_id = message_obj["from_id"]
|
user_id = message_obj["from_id"]
|
||||||
|
|||||||
Reference in New Issue
Block a user