From 66d00a373409d7317353c34a669e04bfd6871e80 Mon Sep 17 00:00:00 2001 From: MrAkells Date: Sun, 14 Dec 2025 23:06:22 +0200 Subject: [PATCH] improve monitoring resilience --- .env.example | 2 ++ README.md | 4 ++++ config.py | 16 ++++++++++++++++ main.py | 2 ++ status_service.py | 26 ++++++++++++++++++++++++-- 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 0476801..0db29e5 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,5 @@ MINECRAFT_HOST=example.org MINECRAFT_PORT=25565 POLL_INTERVAL_SECONDS=30 STATUS_FILE_PATH=data/status.json +REQUEST_TIMEOUT_SECONDS=5 +OFFLINE_AFTER_FAILURES=2 diff --git a/README.md b/README.md index ae6c2b6..5c15b17 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ MINECRAFT_HOST=example.org MINECRAFT_PORT=25565 POLL_INTERVAL_SECONDS=30 STATUS_FILE_PATH=data/status.json +REQUEST_TIMEOUT_SECONDS=5 +OFFLINE_AFTER_FAILURES=2 ``` 3) Запустіть бота: ```bash @@ -82,6 +84,8 @@ docker-compose up -d - Усі параметри задаються через змінні оточення (див. `.env.example`). - `POLL_INTERVAL_SECONDS` — частота опитування. - `STATUS_FILE_PATH` — куди писати JSON. Скрипт сам створить директорію, якщо її немає. +- `REQUEST_TIMEOUT_SECONDS` — таймаут запиту до сервера (щоб не ловити «вічний» конект). +- `OFFLINE_AFTER_FAILURES` — скільки послідовних помилок/таймаутів вважати перед тим, як оголосити офлайн. ## Перевірка - Запустіть бота і в Telegram відправте `/start`, потім `/status`, щоб побачити поточні дані. diff --git a/config.py b/config.py index ad121fe..74e1603 100644 --- a/config.py +++ b/config.py @@ -12,6 +12,8 @@ class Settings: minecraft_port: int poll_interval_seconds: int status_file_path: str + request_timeout_seconds: int + offline_after_failures: int def load_settings() -> Settings: @@ -23,6 +25,8 @@ def load_settings() -> Settings: port = os.getenv("MINECRAFT_PORT", "25565") interval = os.getenv("POLL_INTERVAL_SECONDS", "30") status_file_path = os.getenv("STATUS_FILE_PATH", "data/status.json") + request_timeout = os.getenv("REQUEST_TIMEOUT_SECONDS", "5") + offline_after_failures = os.getenv("OFFLINE_AFTER_FAILURES", "2") if not token: raise RuntimeError("TELEGRAM_BOT_TOKEN is required") @@ -41,6 +45,16 @@ def load_settings() -> Settings: except ValueError as exc: raise RuntimeError("POLL_INTERVAL_SECONDS must be an integer") from exc + try: + parsed_timeout = int(request_timeout) + except ValueError as exc: + raise RuntimeError("REQUEST_TIMEOUT_SECONDS must be an integer") from exc + + try: + parsed_offline_after = int(offline_after_failures) + except ValueError as exc: + raise RuntimeError("OFFLINE_AFTER_FAILURES must be an integer") from exc + return Settings( telegram_bot_token=token, telegram_chat_id=int(chat_id), @@ -48,4 +62,6 @@ def load_settings() -> Settings: minecraft_port=parsed_port, poll_interval_seconds=parsed_interval, status_file_path=status_file_path, + request_timeout_seconds=parsed_timeout, + offline_after_failures=parsed_offline_after, ) diff --git a/main.py b/main.py index 2f42fea..791d10d 100644 --- a/main.py +++ b/main.py @@ -29,6 +29,8 @@ async def main() -> None: status_file=Path(settings.status_file_path), poll_interval_seconds=settings.poll_interval_seconds, activity=activity, + request_timeout_seconds=settings.request_timeout_seconds, + offline_after_failures=settings.offline_after_failures, ) dp.include_router(setup_handlers(monitor, activity)) diff --git a/status_service.py b/status_service.py index 6aded34..40bd2b1 100644 --- a/status_service.py +++ b/status_service.py @@ -35,16 +35,25 @@ class MinecraftMonitor: status_file: Path, poll_interval_seconds: int, activity: ChatActivityTracker, + request_timeout_seconds: int, + offline_after_failures: int, ) -> None: self.server = JavaServer(host, port=port) self.status_file = status_file self.poll_interval_seconds = poll_interval_seconds + self.request_timeout_seconds = request_timeout_seconds + self.offline_after_failures = max(1, offline_after_failures) self._previous_snapshot: Optional[ServerSnapshot] = None self.activity = activity + self._consecutive_failures = 0 async def fetch_status(self) -> ServerSnapshot: try: - response = await self.server.async_status() + response = await asyncio.wait_for( + self.server.async_status(), + timeout=self.request_timeout_seconds, + ) + self._consecutive_failures = 0 motd = getattr(response.description, "to_plain", lambda: None)() player_names = [player.name for player in response.players.sample or []] return ServerSnapshot( @@ -57,7 +66,20 @@ class MinecraftMonitor: player_names=player_names, ) except Exception: - # Treat any exception as offline state but keep last known data safe. + self._consecutive_failures += 1 + if self._consecutive_failures < self.offline_after_failures: + # Возвращаем последнее известное состояние, помечая время обновления. + if self._previous_snapshot: + return ServerSnapshot( + online=self._previous_snapshot.online, + motd=self._previous_snapshot.motd, + version=self._previous_snapshot.version, + latency_ms=self._previous_snapshot.latency_ms, + players_online=self._previous_snapshot.players_online, + players_max=self._previous_snapshot.players_max, + player_names=self._previous_snapshot.player_names, + ) + # После порога считаем сервер офлайн. return ServerSnapshot( online=False, motd=None,