ResearchIT / tests /test_http_client.py
siddhm11
Claude Opus 5
perf(http): share one pooled client instead of handshaking per call
54b1a49
Raw History Blame Contribute Delete
3.03 kB
"""
The shared connection pool.
Every outbound call used to build its own `httpx.AsyncClient` inside an
`async with`, which closed the pool at the end of the block β€” so each Turso
fetch, arXiv fallback and 60s replication tick paid a fresh DNS+TCP+TLS
handshake. Measured against live Turso: 143ms median per-call vs 31ms pooled,
a 112ms saving on every request after the first.
"""
import asyncio
import httpx
import pytest
from app import http_client
@pytest.fixture(autouse=True)
async def _reset():
await http_client.aclose()
yield
await http_client.aclose()
async def test_returns_the_same_client_within_one_loop():
"""Pooling is the entire point β€” a new client per call would defeat it."""
assert http_client.get_client() is http_client.get_client()
async def test_client_is_configured_for_reuse():
c = http_client.get_client()
assert isinstance(c, httpx.AsyncClient)
assert not c.is_closed
async def test_aclose_is_idempotent():
http_client.get_client()
await http_client.aclose()
await http_client.aclose() # must not raise
assert http_client.stats()["created"] is False
async def test_client_survives_being_closed_and_reused():
"""A closed pool must be rebuilt, not handed back closed."""
first = http_client.get_client()
await http_client.aclose()
second = http_client.get_client()
assert second is not first
assert not second.is_closed
def test_client_is_rebound_when_the_event_loop_changes():
"""The regression this module actually shipped with.
An AsyncClient holds connections bound to the loop that created them. A
pooled connection from a dead loop does not report itself closed β€” it
raises "Event loop is closed" on first use. pytest builds a fresh loop per
test, so caching across loops broke a previously-passing arXiv test.
"""
held = {}
async def grab(key):
# Keep a real reference. Comparing id() would be unsound: the first
# client is freed when the loop ends and CPython readily hands the same
# address to the replacement.
held[key] = http_client.get_client()
asyncio.run(grab("first"))
asyncio.run(grab("second"))
assert held["first"] is not held["second"], (
"the same client was reused across two different event loops"
)
def test_a_live_request_works_after_the_loop_is_replaced():
"""End-to-end version of the above, against a local in-memory transport."""
async def call():
transport = httpx.MockTransport(lambda r: httpx.Response(200, text="ok"))
client = http_client.get_client()
# Swap in a transport so this stays offline but still exercises the
# real client object and its loop binding.
client._transport = transport
resp = await client.get("https://example.invalid/")
await http_client.aclose()
return resp.text
assert asyncio.run(call()) == "ok"
assert asyncio.run(call()) == "ok" # second loop, must not raise