-
Notifications
You must be signed in to change notification settings - Fork 2
fix: stop PulseAgent blocking the hypercode-core event loop #311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,18 @@ | ||
| import logging | ||
| import requests | ||
| import httpx | ||
| from app.agents.brain import brain | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| # Built once, at import. Constructing an AsyncClient builds an SSL context and | ||
| # loads the CA bundle — hundreds of ms of *synchronous* work. Doing that inside | ||
| # process() stalls the loop harder than the sync requests.get it replaced. | ||
| # Measured in-container, worst loop stall per call: | ||
| # requests.get (sync) 53 ms | ||
| # AsyncClient built per call 284 ms <- worse than the bug | ||
| # shared AsyncClient (this) 8 ms | ||
| _http = httpx.AsyncClient(timeout=5.0) | ||
|
|
||
|
Comment on lines
+2
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win Unit tests will break: they still mock The removal of The tests must be migrated to mock 🔧 Example test migration pattern # In test_agent_pulse.py
- def fake_get(url: str, params: dict, timeout: int):
- return _Response(200, {...})
-
- monkeypatch.setattr(pulse_mod.requests, "get", fake_get)
+ async def fake_get(url, params=None):
+ return _Response(200, {...})
+
+ monkeypatch.setattr(pulse_mod._http, "get", fake_get)🤖 Prompt for AI Agents |
||
| class PulseAgent: | ||
| def __init__(self): | ||
| self.brain = brain | ||
|
|
@@ -16,8 +25,10 @@ async def process(self, payload=None, conversation_id: str | None = None): | |
| # 1. Grab raw vitals from Prometheus (Checking what services are 'up') | ||
| try: | ||
| # Using 'up' query to see which targets are up | ||
| # This query returns status of all scraped targets | ||
| res = requests.get(self.prometheus_url, params={'query': 'up'}, timeout=5) | ||
| # This query returns status of all scraped targets. | ||
| # Awaited, not sync: this runs on hypercode-core's event loop, and a | ||
| # blocking call here delays every request the API is serving. | ||
| res = await _http.get(self.prometheus_url, params={'query': 'up'}) | ||
| if res.status_code == 200: | ||
| raw_data = res.json() | ||
| # Extract relevant info to keep prompt size down | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Module-level
AsyncClientis never closed — connection pool leaks on shutdown.The shared
_httpclient is intentionally long-lived, but there is noaclose()call anywhere. On process restart or graceful shutdown, the underlying connection pool and SSL context are not disposed, which can leak sockets and file descriptors.Consider wiring an
aclose()into the application's lifespan/shutdown handler, or use a pattern that ties the client lifecycle to the app:♻️ Suggested lifecycle management
Then invoke
close_http_client()from the app's shutdown signal handler (e.g., FastAPIlifespanor equivalent).📝 Committable suggestion
🤖 Prompt for AI Agents
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Is it safe to create httpx.AsyncClient at module import time (outside an event loop) and use it later inside an async context in httpx 0.28.1?💡 Result:
It is generally unsafe and not recommended to create an
httpx.AsyncClientat module import time. While initializing theAsyncClientobject itself does not immediately require an active event loop, the client's internal mechanisms (such as connection pooling and transport initialization) are designed to operate within an asynchronous context. Creating it at the module level introduces several risks: 1. Lifecycle Management: TheAsyncClientis intended to be used as a context manager (usingasync with) or explicitly closed usingawait client.aclose()to ensure resources like connection pools are properly released [1][2]. Module-level objects persist for the lifetime of the application, making it difficult to ensure they are shut down gracefully, which can lead to resource leaks and warnings about unclosed clients [3]. 2. Event Loop Binding: While newer versions ofhttpxhave improved compatibility, many asynchronous libraries in Python bind resources to the event loop that is active when the resource is first used or initialized. Creating the client outside of an async context may lead to issues if the client inadvertently attempts to bind to an event loop that is not yet started or is not the intended loop for its operations. 3. Testing and Best Practices: Best practice dictates that theAsyncClientshould be instantiated within the lifecycle of your application's event loop—typically within anasyncfunction, a startup/shutdown handler (if using a framework like FastAPI), or a fixture (if usingpytest-asyncio). This ensures the client is tied to a valid event loop and is correctly closed after use [3][2]. In summary, to avoid unexpected runtime errors, resource leakage, and event loop incompatibility, you should initialize yourhttpx.AsyncClientinside anasyncfunction or within an appropriate lifecycle hook, rather than at the module level [3][2].Citations:
🏁 Script executed:
Repository: welshDog/HyperCode-V2.4
Length of output: 10815
🏁 Script executed:
Repository: welshDog/HyperCode-V2.4
Length of output: 8713
Close the shared
AsyncClienton shutdown. The module-level client is fine to construct, but it is neveraclose()d, so the connection pool stays open for the process lifetime. Wire it into app lifespan teardown or close it explicitly.🤖 Prompt for AI Agents