Repository navigation
perf(http): route Telegram bot service and TTS client through the shared pool (#12979) - #13000
Merged
Merged
Conversation
…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.
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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()vstracked_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) andautobot-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:
tracked_request()): every method readserror_text = await response.text()on the non-2xx branch before raising, specifically to log the response body —get_json()/post_json()would raiseClientResponseErrorimmediately 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 rawbytes().verify_token()inspectsstatus == 200and returns a bool sentinel, never raising.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 rawbytes()or raise a customRuntimeErrorwith the response body already read viatext();synthesize_stream()streams length-prefixed chunks fromresp.content.readexactly()inside the response block.No site in either file uses
raise_for_status(), a customconnector=, 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: thetracked_request()async withblock spans everyyieldin the generator body. This works becauseasync withremains active acrossawait/yieldpoints 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 rawaiohttp.ClientSession()→ 0, all viaget_http_client().tracked_request(method, url, ...). Removed the now-unusedimport aiohttp.autobot-backend/services/tts_client.py: 7 rawaiohttp.ClientSession(timeout=...)→ 0, all viatracked_request().is_available(),list_voices(), anddelete_voice()passsuppress_error_log=Truesince 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 frompatch("aiohttp.ClientSession", ...)topatch("services.tts_client.get_http_client", ...), since the code no longer touchesaiohttp.ClientSessionat the call site. The fake client'stracked_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-widepatch(.*ClientSessiongrep. 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 parsestts_client.py's source for thesession.<verb>(f"{self.base_url}/path")shape to confirm every backend call is served by the worker template. The regex didn't match the newtracked_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.pyhas no direct unit test exercising its HTTP methods (api/telegram_bot_test.pycovers 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):
(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 istts_client_test.py(fixed above); the regex-based seam intts_contract_test.pywas 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):A full
autobot-backend/suite run was attempted for both states but hung indefinitely on a pre-existing, unrelatedsecurity/security_api_e2e_test.pybefore reachingservices/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.pydoes 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 onorigin/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