improve monitoring resilience

This commit is contained in:
2025-12-14 23:06:22 +02:00
parent 3e787e7286
commit 66d00a3734
5 changed files with 48 additions and 2 deletions

View File

@@ -4,3 +4,5 @@ MINECRAFT_HOST=example.org
MINECRAFT_PORT=25565 MINECRAFT_PORT=25565
POLL_INTERVAL_SECONDS=30 POLL_INTERVAL_SECONDS=30
STATUS_FILE_PATH=data/status.json STATUS_FILE_PATH=data/status.json
REQUEST_TIMEOUT_SECONDS=5
OFFLINE_AFTER_FAILURES=2

View File

@@ -19,6 +19,8 @@ MINECRAFT_HOST=example.org
MINECRAFT_PORT=25565 MINECRAFT_PORT=25565
POLL_INTERVAL_SECONDS=30 POLL_INTERVAL_SECONDS=30
STATUS_FILE_PATH=data/status.json STATUS_FILE_PATH=data/status.json
REQUEST_TIMEOUT_SECONDS=5
OFFLINE_AFTER_FAILURES=2
``` ```
3) Запустіть бота: 3) Запустіть бота:
```bash ```bash
@@ -82,6 +84,8 @@ docker-compose up -d
- Усі параметри задаються через змінні оточення (див. `.env.example`). - Усі параметри задаються через змінні оточення (див. `.env.example`).
- `POLL_INTERVAL_SECONDS` — частота опитування. - `POLL_INTERVAL_SECONDS` — частота опитування.
- `STATUS_FILE_PATH` — куди писати JSON. Скрипт сам створить директорію, якщо її немає. - `STATUS_FILE_PATH` — куди писати JSON. Скрипт сам створить директорію, якщо її немає.
- `REQUEST_TIMEOUT_SECONDS` — таймаут запиту до сервера (щоб не ловити «вічний» конект).
- `OFFLINE_AFTER_FAILURES` — скільки послідовних помилок/таймаутів вважати перед тим, як оголосити офлайн.
## Перевірка ## Перевірка
- Запустіть бота і в Telegram відправте `/start`, потім `/status`, щоб побачити поточні дані. - Запустіть бота і в Telegram відправте `/start`, потім `/status`, щоб побачити поточні дані.

View File

@@ -12,6 +12,8 @@ class Settings:
minecraft_port: int minecraft_port: int
poll_interval_seconds: int poll_interval_seconds: int
status_file_path: str status_file_path: str
request_timeout_seconds: int
offline_after_failures: int
def load_settings() -> Settings: def load_settings() -> Settings:
@@ -23,6 +25,8 @@ def load_settings() -> Settings:
port = os.getenv("MINECRAFT_PORT", "25565") port = os.getenv("MINECRAFT_PORT", "25565")
interval = os.getenv("POLL_INTERVAL_SECONDS", "30") interval = os.getenv("POLL_INTERVAL_SECONDS", "30")
status_file_path = os.getenv("STATUS_FILE_PATH", "data/status.json") 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: if not token:
raise RuntimeError("TELEGRAM_BOT_TOKEN is required") raise RuntimeError("TELEGRAM_BOT_TOKEN is required")
@@ -41,6 +45,16 @@ def load_settings() -> Settings:
except ValueError as exc: except ValueError as exc:
raise RuntimeError("POLL_INTERVAL_SECONDS must be an integer") from 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( return Settings(
telegram_bot_token=token, telegram_bot_token=token,
telegram_chat_id=int(chat_id), telegram_chat_id=int(chat_id),
@@ -48,4 +62,6 @@ def load_settings() -> Settings:
minecraft_port=parsed_port, minecraft_port=parsed_port,
poll_interval_seconds=parsed_interval, poll_interval_seconds=parsed_interval,
status_file_path=status_file_path, status_file_path=status_file_path,
request_timeout_seconds=parsed_timeout,
offline_after_failures=parsed_offline_after,
) )

View File

@@ -29,6 +29,8 @@ async def main() -> None:
status_file=Path(settings.status_file_path), status_file=Path(settings.status_file_path),
poll_interval_seconds=settings.poll_interval_seconds, poll_interval_seconds=settings.poll_interval_seconds,
activity=activity, activity=activity,
request_timeout_seconds=settings.request_timeout_seconds,
offline_after_failures=settings.offline_after_failures,
) )
dp.include_router(setup_handlers(monitor, activity)) dp.include_router(setup_handlers(monitor, activity))

View File

@@ -35,16 +35,25 @@ class MinecraftMonitor:
status_file: Path, status_file: Path,
poll_interval_seconds: int, poll_interval_seconds: int,
activity: ChatActivityTracker, activity: ChatActivityTracker,
request_timeout_seconds: int,
offline_after_failures: int,
) -> None: ) -> None:
self.server = JavaServer(host, port=port) self.server = JavaServer(host, port=port)
self.status_file = status_file self.status_file = status_file
self.poll_interval_seconds = poll_interval_seconds 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._previous_snapshot: Optional[ServerSnapshot] = None
self.activity = activity self.activity = activity
self._consecutive_failures = 0
async def fetch_status(self) -> ServerSnapshot: async def fetch_status(self) -> ServerSnapshot:
try: 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)() motd = getattr(response.description, "to_plain", lambda: None)()
player_names = [player.name for player in response.players.sample or []] player_names = [player.name for player in response.players.sample or []]
return ServerSnapshot( return ServerSnapshot(
@@ -57,7 +66,20 @@ class MinecraftMonitor:
player_names=player_names, player_names=player_names,
) )
except Exception: 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( return ServerSnapshot(
online=False, online=False,
motd=None, motd=None,