4daf611f05
Eliminates the app-wide hangs where one stuck Telegram RPC holds the single
tg_rpc gate permit forever (freezing every RSS/HTML request) and where the
background download worker dies silently.
1.1 Every RPC under the global gate is now bounded by asyncio.wait_for
(config tg_rpc_timeout, env TG_RPC_TIMEOUT, default 60). The wait_for wraps
ONLY the RPC body, never the gate acquire, so queue backpressure stays
legitimate; the paginated get_chat_history is collected in an inner coroutine
so the async-for can be bounded. A timeout propagates out of the gate so its
permit is released (no leak).
1.2 The other live RPC paths get the same treatment: _reply_enrichment's
get_messages now runs under the gate + timeout; PostParser.get_post is bounded
at 30s (and a stray print removed); /health's get_me at 10s.
1.3 background_download_worker: queue.get() moved out of the try so task_done()
in finally balances exactly one get (no ValueError that killed the worker);
FloodWait is caught BEFORE the generic Exception (sleeps, does not flood);
download_new_files uses put_nowait so a full queue no longer blocks the
sweeper forever.
1.4 Both background tasks run under a _supervised() wrapper: a crash or an
unexpected return is logged CRITICAL and restarted (restarts rate-limited to
once per 60s so a hard-failing task can't spin), while CancelledError is
propagated to the child for a clean shutdown.
Tests (tests/test_stage1_hangs.py): gate timeout releases the permit + a second
call succeeds; gate cancel mid-spacing releases the permit; the worker survives
Exception and FloodWait with balanced task_done; and _supervised restarts a
crashing task (rate-limited) and an unexpected return, and propagates cancellation
to its child. 180 passed (174 baseline + 6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
37 lines
1.1 KiB
Python
37 lines
1.1 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
def setup_logging(level_name: str = "INFO") -> None:
|
|
"""No-op logging setup for tests (mirrors config.setup_logging signature)."""
|
|
return None
|
|
|
|
def get_settings():
|
|
"""
|
|
Mock config for testing without requiring TG_API_ID and TG_API_HASH
|
|
"""
|
|
return {
|
|
"tg_api_id": 12345,
|
|
"tg_api_hash": "test_hash",
|
|
"session_path": "tests/test_data",
|
|
"api_host": "127.0.0.1",
|
|
"api_port": 8080,
|
|
"pyrogram_bridge_url": "http://test.example.com",
|
|
"log_level": "DEBUG",
|
|
"debug": False,
|
|
"token": "test_token",
|
|
"time_based_merge": False,
|
|
"show_bridge_link": False,
|
|
"show_post_flags": True,
|
|
"proxy": None,
|
|
"trusted_proxies": [],
|
|
"tg_rpc_timeout": 60,
|
|
"tg_watchdog_enabled": True,
|
|
"tg_watchdog_interval": 60,
|
|
"tg_watchdog_timeout": 10,
|
|
"tg_watchdog_failures": 3,
|
|
"tg_watchdog_restart_timeout": 90,
|
|
"tg_watchdog_heartbeat_every": 30,
|
|
"tg_disconnect_flap_limit": 3,
|
|
"tg_disconnect_flap_window": 120,
|
|
}
|