Commit Graph

39 Commits

Author SHA1 Message Date
claude code agent 444bd3e42f fix(stability): stage 2 — atomic media downloads, in-flight dedup, FloodWait->429, bounded semaphore
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>
2026-07-05 07:40:47 +03:00
claude code agent 03d1de2954 fix(stability): declare pytest-asyncio, bound /raw_json RPC, assert FloodWait backoff, dedupe gate+timeout (review round 1)
F1 [WARNING] pytest-asyncio was not declared, so the 6 new async stage-1 tests
ERROR on a clean checkout (async def not natively supported) — zero regression
protection, masked by a globally-installed plugin. Added pytest-asyncio to
requirements.txt + asyncio_mode=auto to tests/pytest.ini. Verified on a fresh venv
from requirements alone: the tests collect and run (180 passed).

F2 [WARNING] The /raw_json endpoint's get_messages was the one remaining live RPC
without a timeout, violating the stage-1 DoD ('every Telegram RPC is bounded').
Wrapped it in asyncio.wait_for(..., 30) mirroring PostParser.get_post. (It is not
under the tg_rpc gate, so its blast radius was one request, not the app.)

F3 [WARNING] The worker test globally no-op'd asyncio.sleep, so the dedicated
except FloodWait branch was indistinguishable from the generic handler — deleting
it kept the test green. The sleep stub now records delays and the test asserts the
FloodWait backoff of 6 (=min(1+5,900)), distinct from the success path's 2.

F5 [low] The tricky 'gate outside, timeout inside' nesting was open-coded at 3
sites (each re-deriving the invariant). Extracted tg_rpc_bounded(timeout) into
tg_throttle (using asyncio.timeout()); the 3 sites now use it, so a future call
site cannot silently wrap the gate entry and reopen the hang-under-backpressure.

F4 [low] Documented TG_RPC_TIMEOUT in the dockercompose.yml environment block
next to the other TG_RPC_* knobs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 07:12:10 +03:00
claude code agent 4daf611f05 fix(stability): stage 1 — timeouts on all TG RPC + resilient download worker + supervision
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>
2026-07-05 06:51:08 +03:00
vvzvlad 021f16cf14 feat(client): add watchdog heartbeat and diagnostics counters
Introduce `tg_watchdog_heartbeat_every` to emit periodic INFO heartbeats.
Add cumulative diagnostics counters and richer logging for watchdog probes,
restart reasons, and disconnect‑flap triggers. Update defaults and tests
accordingly.
2026-06-04 19:19:22 +03:00
vvzvlad 79b127d406 feat(client): add watchdog and disconnect flap handling
Introduce an active watchdog that probes the Telegram client to detect
zombie sessions and restart them in‑process. Add configurable disconnect
flap detection with a sliding window to trigger restarts after repeated
disconnects. New environment variables and config entries are added, and
the Kurigram dependency is now version‑pinned.
2026-06-04 19:02:09 +03:00
vvzvlad 577093b9fa test(postparser): use StrWithHtml for Pyrogram text/caption mocks
Replace MagicMock based text/caption mocks with a lightweight StrWithHtml
subclass that provides the required `.html` property while behaving like a
regular string. This fixes length and strip checks in the parser and
simplifies test setup. Also adds default proxy settings to mock_config for
test completeness.
2026-05-18 00:15:53 +03:00
vvzvlad e9949a7eab update pylint configurations in test 2025-04-26 02:34:08 +04:00
vvzvlad 58253e15e0 add test for get_all_possible_flags 2025-04-26 02:31:16 +04:00
vvzvlad d2870bc0f6 add youtube title 2025-04-18 22:58:25 +03:00
vvzvlad d0584d4525 more logging 2025-04-18 21:52:17 +03:00
vvzvlad bb5931389a add fwd title prefix 2025-04-18 03:52:43 +03:00
vvzvlad 8282f84b4a add fwd test 2025-04-18 03:42:36 +03:00
vvzvlad b10a1f40b5 Ensure forward_origin is always present in message object 2025-04-18 03:40:11 +03:00
vvzvlad 30ab46e21f Refactor tests 2025-04-17 20:05:04 +03:00
vvzvlad ecaba7e8bb new truncate_title logic 2025-04-17 19:43:10 +03:00
vvzvlad 77132eebb9 tests refactoring 2025-04-16 03:05:55 +03:00
vvzvlad b5bca04f38 add author_info tests 2025-04-16 02:57:21 +03:00
vvzvlad 82233377fd refactoring tests names 2025-04-16 02:56:48 +03:00
vvzvlad f9b277424b Add poll flag 2025-04-15 19:14:49 +03:00
vvzvlad 5e7937f57e Refactor string trimming logic 2025-04-15 18:09:49 +03:00
vvzvlad ff9ebeaa70 Refactor PostParser title generation 2025-04-15 18:03:16 +03:00
vvzvlad 1871843b17 small refactoring 2025-04-15 02:43:26 +03:00
vvzvlad defba4cdaf Enhance PostParser to handle service messages 2025-04-13 19:14:44 +03:00
vvzvlad 09d3fcf5a1 add only_link flag 2025-04-13 18:59:57 +03:00
vvzvlad 7199a955cd new forward_origin 2025-04-13 03:47:12 +03:00
vvzvlad 0a4f001acd tests: replace 'clown' and 'poo' flags with 'clownpoo' 2025-04-12 20:11:56 +03:00
vvzvlad 7d9b0bfd65 t.me/boost links as donat flag 2025-04-11 03:04:38 +03:00
vvzvlad 64368a0d83 Refactor test files to disable pylint warnings and ensure proper formatting 2025-04-11 02:54:13 +03:00
vvzvlad 7a95211fe7 Update post parser to exclude t.me links from flag detection 2025-04-11 02:50:53 +03:00
vvzvlad 78255e51a2 Enhance post parser to flag foreign channels for boost links 2025-04-11 02:48:42 +03:00
vvzvlad be0ad5b0d2 remove unused import in test_post_parser_flags.py 2025-04-11 02:23:05 +03:00
vvzvlad 8a3664e245 add flags extraction tests 2025-04-11 02:22:41 +03:00
vvzvlad c730d5bb7f Refactor title generation to remove punctuation 2025-04-09 03:07:12 +03:00
vvzvlad c78d8c93f6 Enhance title generation 2025-04-09 02:48:02 +03:00
vvzvlad 2a8326be06 Implement URL-based title generation 2025-04-08 18:38:13 +03:00
vvzvlad ee56ac41f2 Enhance title generation 2025-04-08 18:25:15 +03:00
vvzvlad d2a857d169 Refactor title generation 2025-04-08 18:16:46 +03:00
vvzvlad 5d1e109858 add tests 2025-04-08 17:34:39 +03:00
vvzvlad 129b07abda add logging 2025-04-04 22:09:52 +03:00