444bd3e42f
Fixes flaky static media serving: partial files served as 'ready', truncated
stubs living for an hour, and app-wide hangs under a saturated download path.
2.1 _download_atomic(file_id, final_path, timeout): downloads to a unique
{final}.part.{hex}, validates size>0, and publishes solely via os.rename
(atomic on POSIX); a finally ALWAYS removes the partial (timeout/cancel/
zero-size/race-loser). Every downloader call routes through it, so a file at a
FINAL name ({fid} / temp_{fid}) is GUARANTEED complete (grep-proven: the only
safe_download_media caller passes a .part. path). Big-video timeout scales with
size (min/max/min-speed knobs, documented in dockercompose.yml). The sweeper
regex now matches both .part. and legacy .tmp. stubs.
2.2 In-flight dedup registry: the first request for a key runs the download in a
DETACHED task sharing a Future; the task's finally sets the Future AND pops the
key. Waiters await asyncio.shield(fut) so a client disconnect / waiter timeout
cancels only the waiter, never the download — no hung waiters, no stuck key, a
failed download frees the key for retry.
2.3 FloodWait->429: handler before except RPCError (FloodWait subclasses it),
Retry-After = min(value + rand(1,30), 300); propagates from the detached task
through the Future.
2.4 Serving a temp_* file touches its mtime so the 1h sweeper can't delete a
video out from under a viewer.
2.5 The HTTP download semaphore acquire is bounded (wait_for 30 -> 503 +
Retry-After); the permit is released only if the acquire succeeded. The
request-scoped-permit trade-off (a disconnect can transiently exceed the
download count) is documented inline for stage-7 prod observation.
Tests (tests/test_stage2_static.py, 13): atomic publish/clean on every exit incl.
FloodWait-through-the-finally; concurrent big-video serves no partial; dedup runs
one download, a cancelled waiter doesn't hang others, a failed download frees the
key; FloodWait -> 429; mtime touch; sweeper cleans .part./.tmp./stale temp_ but
keeps fresh files. 193 passed (180 baseline + 13).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
40 lines
1.2 KiB
Python
40 lines
1.2 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,
|
|
"media_download_timeout_min": 120,
|
|
"media_download_timeout_max": 1800,
|
|
"media_download_min_speed": 256 * 1024,
|
|
}
|