Skip to content

perf(http): route Telegram bot service and TTS client through the shared pool (#12979) - #13000

Merged
mrveiss merged 2 commits into
Dev_new_guifrom
issue-12979-batch5-http-pool
Jul 30, 2026
Merged

mrveiss merged 2 commits into
Dev_new_guifrom
issue-12979-batch5-http-pool

Conversation

@mrveiss

@mrveiss mrveiss commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Thinking Path

Batch 5 of #12979. Per the consolidated batch recipe on the issue: classify every site by reading it (do not grep-and-sweep), because the *_json() vs tracked_request() split has swung 82%/0%/72%/0% across the first four batches and is a property of the module's contract, not the codebase.

Scope: the two dense single files named in the task — autobot-backend/services/telegram_bot_service.py (8 sites) and autobot-backend/services/tts_client.py (7 sites) — 15 sites total, at the ~15 target, so no third area was added.

Read every site in both files:

  • Telegram (8/8 tracked_request()): every method reads error_text = await response.text() on the non-2xx branch before raising, specifically to log the response body — get_json()/post_json() would raise ClientResponseError immediately and drop that log detail, a behaviour change in exactly the place batch 3's _trigger_build() finding warned about (raise_for_status-shaped code that isn't a *_json() candidate once you look at what it actually reads). download_file() reads raw bytes(). verify_token() inspects status == 200 and returns a bool sentinel, never raising.
  • TTS client (7/7 tracked_request()): is_available()/list_voices()/delete_voice() swallow failure into a debug/warning log plus a sentinel (bool/[]); synthesize()/clone_voice()/create_voice() read raw bytes() or raise a custom RuntimeError with the response body already read via text(); synthesize_stream() streams length-prefixed chunks from resp.content.readexactly() inside the response block.

No site in either file uses raise_for_status(), a custom connector=, returns a session, or is long-lived — so this batch has zero RAW carve-outs, unlike batch 4's SSRF-pinned connector.

synthesize_stream() is a genuine streaming case: the tracked_request() async with block spans every yield in the generator body. This works because async with remains active across await/yield points in an async generator — the block exits (and the counter decrements) whichever way the generator ends, exhausted normally or closed early by the caller giving up on the stream.

What Changed

  • autobot-backend/services/telegram_bot_service.py: 8 raw aiohttp.ClientSession() → 0, all via get_http_client().tracked_request(method, url, ...). Removed the now-unused import aiohttp.
  • autobot-backend/services/tts_client.py: 7 raw aiohttp.ClientSession(timeout=...) → 0, all via tracked_request(). is_available(), list_voices(), and delete_voice() pass suppress_error_log=True since each already downgrades the failure to a debug/warning log plus a sentinel — the pooled client's default ERROR log would otherwise turn an expected condition (TTS worker not running) into noise on a poll loop.
  • autobot-backend/services/tts_client_test.py: retargeted from patch("aiohttp.ClientSession", ...) to patch("services.tts_client.get_http_client", ...), since the code no longer touches aiohttp.ClientSession at the call site. The fake client's tracked_request() returns the same async-context-manager mock the old fake session's .post() returned.
  • autobot-slm-backend/tests/services/tts_contract_test.py: a second, non-mock test seam, found by exercising rather than by the repo-wide patch(.*ClientSession grep. This is a static regex contract test (bug(tts): synthesis 404s — deployed worker lacks /tts/synthesize/stream because autobot-tts-worker has NO resolve path AND NO drift visibility (silent skew on every backend update); /tts/clone-voice is a dead call that exists nowhere #12886) that parses tts_client.py's source for the session.<verb>(f"{self.base_url}/path") shape to confirm every backend call is served by the worker template. The regex didn't match the new tracked_request("VERB", f"{self.base_url}/path", ...) shape and went 8 failed / 1 passed. Added a second regex for the pooled shape alongside the original (not a replacement), so a future raw-session call site is still caught by the same test.
  • telegram_bot_service.py has no direct unit test exercising its HTTP methods (api/telegram_bot_test.py covers webhook wiring and command handling only), so there was no seam to retarget there.

Verification

(a) py_compile, flake8, black --check --line-length=120 — all clean on the 4 changed files.

(b) Local aiohttp server exercise — all 15 converted sites driven on success and failure branches against a real local server (29 exercised paths, 0 unexpected):

exercised paths       : 29
unexpected            : 0  []
server-side requests  : 29
still-acquired        : 0
active_requests       : 0
ALL ASSERTIONS PASSED

(c) Baseline vs after (cp-swapped origin/Dev_new_gui, never git stash) — repo-wide grep -rn 'patch(.*ClientSession' confirmed the only mock-based seam for these two modules is tts_client_test.py (fixed above); the regex-based seam in tts_contract_test.py was caught by running it, not by that grep, which is why "exercise every path" and "diff against baseline" both matter independently. Curated regression scope (both files' direct tests plus every caller's test file: api/telegram_bot_test.py, api/voice_stream_test.py, tests/test_voice_503_guard.py, integrations/protocols_conformance_test.py, tests/services/notification_service_test.py, autobot-slm-backend/tests/services/tts_contract_test.py):

baseline (origin/Dev_new_gui): 119 passed, 0 failed
after this branch:             119 passed, 0 failed

A full autobot-backend/ suite run was attempted for both states but hung indefinitely on a pre-existing, unrelated security/security_api_e2e_test.py before reaching services/ alphabetically — confirmed pre-existing since it hung identically with the baseline (origin) files in place. The curated scope above stands in for it; it covers every known caller of the two converted modules.

(d) Ceiling guard — autobot-backend/tests/test_raw_client_session_ceiling_12992.py does not exist on this branch's base (origin/Dev_new_gui @ 352e07cc7); it ships in still-open PR #12996, which currently has 10 red required checks (SAST, api-wiring, code-quality, smoke-test, etc.) and is not close to merging. There is therefore no ceiling constant in this PR to lower. Using that test's own AST-walker logic (copied, not modified) as a standalone script for measurement only: 87 on origin/Dev_new_gui @ 352e07cc7 → 72 on this branch, exactly −15 (the 15 sites converted here). Whoever finishes #12996 will re-measure against whatever tip it rebases onto, per that file's own "re-measure immediately before push" rule — this PR does not need to (and cannot, since the file isn't part of its diff) touch it.

Model Used

Claude Sonnet 5

…red pool (#12979)

Converts the 15 remaining per-request aiohttp.ClientSession constructions in
services/telegram_bot_service.py (8) and services/tts_client.py (7) onto the
shared pooled client, so Telegram Bot API and TTS-worker calls reuse
connections instead of re-resolving DNS and re-establishing TLS per request.

All 15 sites use tracked_request() (0% get_json()/post_json()) -- every site
either reads text() on the error branch before raising (to log the response
body, which post_json()/get_json() would drop), reads raw bytes via read(),
or returns a sentinel (bool/dict/list) rather than raising on non-2xx. This
matches the utils/ and connectors/ batches: modules whose job is to turn a
remote failure into a value, not an integration client written to raise.

synthesize_stream() streams chunks via a length-prefixed wire format; the
tracked_request() async-with block spans every yield in the generator body,
so the connection stays open for the whole stream and releases (and
decrements the active-request counter) whichever way the generator ends --
exhausted normally or closed early by the caller.

Health-probe-style calls that already swallow failure into a debug/warning
log plus a sentinel return (is_available, list_voices, delete_voice) pass
suppress_error_log=True so the shared client's default ERROR log does not
turn an expected condition into noise on a poll loop.

Test-seam retargets (conversion moves the seam, a passing suite is not
evidence on its own):
- services/tts_client_test.py stubbed aiohttp.ClientSession directly, which
  stopped intercepting once the code routed through the pool. Retargeted to
  patch services.tts_client.get_http_client with a fake client whose
  tracked_request() returns the same async-context-manager mock.
- autobot-slm-backend/tests/services/tts_contract_test.py is a second,
  non-mock seam: a static regex parses tts_client.py's source for the
  session.<verb>(f"{self.base_url}/path") call shape to check every backend
  call is served by the worker template. The regex did not match the new
  tracked_request("VERB", f"{self.base_url}/path", ...) shape and went
  8 failed / 1 passed. Added a second regex for the pooled shape rather than
  replacing the first, so a future raw-session call site is still caught.

telegram_bot_service.py has no direct unit test exercising its HTTP methods;
api/telegram_bot_test.py covers webhook wiring and command handling only, so
no seam existed there.

Verified: py_compile, flake8, black --check --line-length=120 all clean on
the 4 changed files. Local aiohttp server exercise covering all 15 sites,
success and failure branches (29 paths, 0 unexpected): pooled connector
still-acquired=0, active_requests counter back to 0. Baseline (cp-swapped
origin/Dev_new_gui) vs after: 119 passed / 0 failed both times across the
curated regression scope (services/api/tests callers of these two modules
plus the slm-backend contract test) -- a full autobot-backend suite run was
attempted but hung on a pre-existing, unrelated security/security_api_e2e_test.py
before reaching services/, so the curated scope stands in for it.

AST count (autobot-backend/tests/test_raw_client_session_ceiling_12992.py's
own walker logic): 87 on origin/Dev_new_gui at 352e07c -> 72 on this
branch, exactly -15. That ceiling test file does not exist on this branch's
base yet -- it ships in still-open, red PR #12996 -- so there is no ceiling
constant to lower in this PR; whoever finishes #12996 re-measures against
whatever tip it rebases onto per that file's own re-measure-before-push rule.

Refs #12979.
@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No hardcoded values detected that have SSOT config equivalents!

@mrveiss
mrveiss merged commit f713c25 into Dev_new_gui Jul 30, 2026
40 of 41 checks passed
@mrveiss
mrveiss deleted the issue-12979-batch5-http-pool branch July 30, 2026 00:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant