Summary
Long scans run with a workflow deadline (SKILLSPECTOR_MAX_WORKFLOW_SECONDS) emit repeated unretrieved-task errors and steadily leak HTTP connection pools:
Task exception was never retrieved
future: <Task finished coro=<AsyncClient.aclose() done, defined at ...> exception=RuntimeError('Event loop is closed')>
RuntimeError: Event loop is closed
Steps to reproduce
- Set
SKILLSPECTOR_MAX_WORKFLOW_SECONDS so a dynamic deadline is active.
- Run a scan large enough to issue more than 128 LLM calls.
- Watch stderr:
Event loop is closed appears repeatedly once the scan passes the cache limit.
Root cause
Under a workflow deadline every batch is sent with whatever time is left, so each call carries a slightly different timeout float. _model_for_call() expressed that by building a fresh chat model per call.
LangChain caches the underlying httpx client in an lru_cache keyed on (base_url, timeout, socket_options). A deadline that shrinks by a few milliseconds per call is therefore a new cache key every call — a long scan opens one connection pool per LLM request and starts evicting the oldest once it passes 128.
Eviction is what turns the leak into a crash. Each analyzer node is synchronous and reaches run_async() → asyncio.run(), so it owns its event loop. A pool evicted while a later analyzer is running belongs to an earlier, already-closed loop, and the OpenAI SDK's client finaliser reacts to garbage collection with asyncio.get_running_loop().create_task(self.aclose()) — closing the earlier analyzer's sockets from the current analyzer's loop. Nothing awaits that task, so the RuntimeError surfaces only as "Task exception was never retrieved", once per eviction.
Impact
- Noisy, misleading stderr output on every large scan run under a deadline.
- One
httpx connection pool per LLM request instead of one per analyzer.
A 200-call reproduction against the openai 3.11 / httpx 2 combination that reported the failure produced 75 unretrieved Event loop is closed tasks and 220 httpx clients.
Expected behaviour
A bounded scan should reuse one connection pool per analyzer and complete without unretrieved-task errors, while still honouring the dynamic deadline.
Environment
openai 3.11 with httpx 2.x
- Scan run with
SKILLSPECTOR_MAX_WORKFLOW_SECONDS set
Summary
Long scans run with a workflow deadline (
SKILLSPECTOR_MAX_WORKFLOW_SECONDS) emit repeated unretrieved-task errors and steadily leak HTTP connection pools:Steps to reproduce
SKILLSPECTOR_MAX_WORKFLOW_SECONDSso a dynamic deadline is active.Event loop is closedappears repeatedly once the scan passes the cache limit.Root cause
Under a workflow deadline every batch is sent with whatever time is left, so each call carries a slightly different timeout float.
_model_for_call()expressed that by building a fresh chat model per call.LangChain caches the underlying
httpxclient in anlru_cachekeyed on(base_url, timeout, socket_options). A deadline that shrinks by a few milliseconds per call is therefore a new cache key every call — a long scan opens one connection pool per LLM request and starts evicting the oldest once it passes 128.Eviction is what turns the leak into a crash. Each analyzer node is synchronous and reaches
run_async()→asyncio.run(), so it owns its event loop. A pool evicted while a later analyzer is running belongs to an earlier, already-closed loop, and the OpenAI SDK's client finaliser reacts to garbage collection withasyncio.get_running_loop().create_task(self.aclose())— closing the earlier analyzer's sockets from the current analyzer's loop. Nothing awaits that task, so theRuntimeErrorsurfaces only as "Task exception was never retrieved", once per eviction.Impact
httpxconnection pool per LLM request instead of one per analyzer.A 200-call reproduction against the
openai3.11 /httpx2 combination that reported the failure produced 75 unretrievedEvent loop is closedtasks and 220 httpx clients.Expected behaviour
A bounded scan should reuse one connection pool per analyzer and complete without unretrieved-task errors, while still honouring the dynamic deadline.
Environment
openai3.11 withhttpx2.xSKILLSPECTOR_MAX_WORKFLOW_SECONDSset