Repository navigation
fix(http): balance _active_requests in response-consuming helpers (#12981) - #12989
Merged
Merged
Conversation
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
This was referenced Jul 29, 2026
mrveiss
added a commit
that referenced
this pull request
Jul 29, 2026
…12979) (#12994) Converts the 25 remaining per-request aiohttp.ClientSession constructions in integrations/cicd_integration.py (14) and integrations/version_control_integration.py (11) onto the shared pooled client, so Jenkins/GitLab CI/CircleCI/GitLab/Bitbucket calls reuse connections instead of re-resolving DNS and re-establishing TLS per request. Per-shape split (18 safe / 7 careful): - 16 get_json() + 2 post_json() where the site already did raise_for_status() then read json(). - 7 tracked_request(): 5 test_connection() sites that inspect response.status and return a structured IntegrationHealth, plus Jenkins _get_build_log() (reads text()) and Jenkins _trigger_build() (POST that never reads a body -- Jenkins answers buildWithParameters with an empty 201, so post_json() would raise ContentTypeError on the success path). tracked_request() is used rather than 'async with await client.get(...)' because the latter releases the connection but leaves _active_requests permanently incremented (#12981/#12989). The five test_connection() sites pass suppress_error_log=True: each already logs the failure itself and encodes it as IntegrationStatus.UNHEALTHY, so the manager's default ERROR log would only duplicate it. Also extracts BitbucketIntegration._request_kwargs(), replacing the same headers/timeout/optional-auth block repeated at five call sites. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
mrveiss
added a commit
that referenced
this pull request
Jul 29, 2026
…12992) Both monitoring_integration.py test_connection sites used the pre-#12989 recipe (async with await client.get(...)). That releases the connection but increments _active_requests without ever decrementing it, so the counter grew 1:1 with requests, skewing _adjust_pool_size() utilisation and making the _active_requests == 0 deferred-recreation gate unreachable. Converted both to tracked_request("GET", ...), which owns the counter as well as the response block. The status-inspection logic is unchanged: these two sites must return a structured IntegrationHealth rather than raise, so they remain deliberate carve-outs from the *_json() helpers. Also adds a pytest ratchet guarding the #12979 sweep: raw aiohttp.ClientSession(...) constructions under autobot-backend/ are counted via AST and asserted not to exceed a recorded ceiling of 112, so the existing backlog stays green while newly introduced raw sessions fail the suite.
mrveiss
added a commit
that referenced
this pull request
Jul 29, 2026
…12992) Both monitoring_integration.py test_connection sites used the pre-#12989 recipe (async with await client.get(...)). That releases the connection but increments _active_requests without ever decrementing it, so the counter grew 1:1 with requests, skewing _adjust_pool_size() utilisation and making the _active_requests == 0 deferred-recreation gate unreachable. Converted both to tracked_request("GET", ...), which owns the counter as well as the response block. The status-inspection logic is unchanged: these two sites must return a structured IntegrationHealth rather than raise, so they remain deliberate carve-outs from the *_json() helpers. Also adds a pytest ratchet guarding the #12979 sweep: raw aiohttp.ClientSession(...) constructions under autobot-backend/ are counted via AST and asserted not to exceed a recorded ceiling of 112, so the existing backlog stays green while newly introduced raw sessions fail the suite.
mrveiss
added a commit
that referenced
this pull request
Jul 30, 2026
…12992) (#12996) Both monitoring_integration.py test_connection sites used the pre-#12989 recipe (async with await client.get(...)). That releases the connection but increments _active_requests without ever decrementing it, so the counter grew 1:1 with requests, skewing _adjust_pool_size() utilisation and making the _active_requests == 0 deferred-recreation gate unreachable. Converted both to tracked_request("GET", ...), which owns the counter as well as the response block. The status-inspection logic is unchanged: these two sites must return a structured IntegrationHealth rather than raise, so they remain deliberate carve-outs from the *_json() helpers. Also adds a pytest ratchet guarding the #12979 sweep: raw aiohttp.ClientSession(...) constructions under autobot-backend/ are counted via AST and asserted not to exceed a recorded ceiling of 112, so the existing backlog stays green while newly introduced raw sessions fail the suite.
mrveiss
added a commit
that referenced
this pull request
Jul 30, 2026
…ring+service_monitor+env_analyzer through the shared pool (#12979) Batch 8 of the raw-aiohttp.ClientSession sweep: integrations/base.py, github_integration.py, microsoft365_integration.py, notion_integration.py, api/monitoring.py, api/service_monitor.py, code_analysis/src/env_analyzer.py now route through autobot_shared.http_client.get_http_client()'s tracked_request() instead of constructing per-request sessions. All 9 sites use tracked_request() (none raise on HTTP status, so get_json()/post_json() would change error-path contracts). api/marketplace_sources.py's _fetch_catalog_document stays RAW: it pins an SSRF-guarded TCPConnector that the pooled client cannot express per-request, the same carve-out category as knowledge/connectors/oauth_flow.py. Also fixed a pre-existing #12981-class counter leak found while touching api/monitoring.py: _query_prometheus_instant/_query_prometheus_range already used the shared client but via the pre-#12989 `async with await client.get()` form, which releases the connection but never balances _active_requests. Dropped env_analyzer.py's EnvironmentAnalyzer._call_ollama_filter session parameter: its sole caller always passed session=None, so the reused-session branch was dead code the pooled client now makes unnecessary. Ceiling lowered 53 -> 30 in tests/test_raw_client_session_ceiling_12992.py, re-measured by AST walker against a tree freshly synced to origin/Dev_new_gui immediately before push.
mrveiss
added a commit
that referenced
this pull request
Jul 30, 2026
…ring+service_monitor+env_analyzer through the shared pool (#12979) (#13003) Batch 8 of the raw-aiohttp.ClientSession sweep: integrations/base.py, github_integration.py, microsoft365_integration.py, notion_integration.py, api/monitoring.py, api/service_monitor.py, code_analysis/src/env_analyzer.py now route through autobot_shared.http_client.get_http_client()'s tracked_request() instead of constructing per-request sessions. All 9 sites use tracked_request() (none raise on HTTP status, so get_json()/post_json() would change error-path contracts). api/marketplace_sources.py's _fetch_catalog_document stays RAW: it pins an SSRF-guarded TCPConnector that the pooled client cannot express per-request, the same carve-out category as knowledge/connectors/oauth_flow.py. Also fixed a pre-existing #12981-class counter leak found while touching api/monitoring.py: _query_prometheus_instant/_query_prometheus_range already used the shared client but via the pre-#12989 `async with await client.get()` form, which releases the connection but never balances _active_requests. Dropped env_analyzer.py's EnvironmentAnalyzer._call_ollama_filter session parameter: its sole caller always passed session=None, so the reused-session branch was dead code the pooled client now makes unnecessary. Ceiling lowered 53 -> 30 in tests/test_raw_client_session_ceiling_12992.py, re-measured by AST walker against a tree freshly synced to origin/Dev_new_gui immediately before push.
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
HTTPClientManager.request()increments_active_requestson every call but only decrements it on the failure path, delegating the success-path decrement to the caller. Repo-wide, exactly one production caller honoured that contract (autobot-backend/chat_workflow/manager.py:2147, a streaming path) against ~151 shared-client call sites.get_json()/post_json()consume the response internally and never decremented at all, so the counter only ever grew.The counter is not cosmetic — it drives two live mechanisms:
_adjust_pool_size()computesutilization = _active_requests / _current_pool_size. A monotonically rising numerator pins utilisation above the 0.7 grow threshold permanently, so the pool ratchets toward_pool_max(200) regardless of real concurrency, and the< 0.2shrink branch becomes unreachable._handle_pool_recreation()defers recreation while_active_requests > 0, and onlydecrement_active()reaching0applies it. With a counter that never returns to zero, a pending pool resize is never applied.It also makes
get_stats()["active_requests"]andutilizationwrong for anything reading them.The fix had to avoid adding more caller obligations — a contract that ~151 call sites already ignore will keep being ignored. So the decrement moved into the helper that owns the full lifecycle, where it cannot be forgotten, rather than being spread across call sites. The raw-response path (
request()/get()/post()) stays caller-owned exactly as documented, because streaming callers legitimately need the slot held past the initial request (#680).What Changed
Scope is
autobot_shared/http_client.py+ its test file. No call sites were converted.tracked_request()async context manager — issues the request, yields the response insideasync with response, and decrements in afinally. It owns request + response body + counter as one unit. Named for symmetry with the existingtracked_session().get_json()/post_json()now use it, so both balance the counter automatically. These were the two internal response-consuming helpers;get()/post()return raw responses and remain caller-owned.request(),get(), andpost(): the returned response is caller-owned on the success path and requiresdecrement_active(), withtracked_request()called out as the forget-proof alternative.example_usage()streaming sample, which demonstrated the unbalanced pattern (async with await http_client.get(...)with no decrement) and was effectively teaching the bug.No double-decrement is possible: if
request()itself raises, it decrements on its own error path and thetryintracked_request()is never entered.Verification
New tests fail on the old code and pass on the new — confirmed by temporarily restoring the old
get_json()body:assert 7 == 2is the reported regression exactly: 5 sequentialget_json()calls left the counter 5 higher than baseline.After the fix:
Seven tests added, covering every case: baseline restored after a successful
get_json(), after a successfulpost_json(), after a transport failure, after araise_for_status()failure mid-block, and after 10 sequential requests (baseline, not baseline+10). Plustracked_request()exception-safety.Streaming contract asserted unchanged —
test_streaming_contract_unchanged_single_decrement()verifiesrequest()still hands back an un-decremented slot (baseline + 1while held, so mid-stream pool recreation is still correctly deferred) and that one callerdecrement_active()returns it to baseline.Full suite, no regressions:
1137 baseline + 7 new = 1144. No pre-existing test changed state.
The sole production
decrement_active()caller (chat_workflow/manager.py:2147) is a streaming Ollama path built on rawpost()— untouched by this change, and not at risk of double-decrementing.Model Used
claude-opus-5
Closes #12981