added bounded close session for curl_cffi/past_prices to pricing model

This commit is contained in:
TPV committed 2026-07-28 09:15:38 -07:00
1 parent c29032a765
commit 8a1c1893b7
4 files changed
+191 -3

No files matched your search

+135
View File
@@ -0,0 +1,135 @@
"""unit test: a session's close cannot park the caller indefinitely
run from the repo root:
uv run pytest tests/test_session_close_budget.py
offline — no network. curl_cffi's close awaits the multi handle with no timeout
of its own, so a wedged transfer would park whatever closes the session,
including an `async with` exit. These build a real session and wedge the
backend's close, asserting the bound lives on the session itself so every call
site gets it without arranging anything.
"""
import asyncio
import logging
from aioproxies import AioProxies
from monitor_utils import sessions
from monitor_utils.sessions import BoundedCloseSession, get_default_session
def _proxies(tmp_path) -> AioProxies:
"""A one-entry pool in the host:port:user:pass format monitors ship."""
proxy_file = tmp_path / "proxies.txt"
proxy_file.write_text("proxy.example.io:8080:user:secret\n")
return AioProxies.from_file(path=str(proxy_file))
async def _wedge_close(session) -> asyncio.Event:
"""Replace the backend's close with one that never returns.
Builds the backend session first so TLSSession.close actually reaches the
backend rather than short-circuiting on an unbuilt session.
"""
session.session # force lazy construction
started = asyncio.Event()
async def wedged(inner_session):
started.set()
await asyncio.sleep(3600)
session.backend.close = wedged
return started
async def _really_close(session, original_close):
"""Close for real, so the test does not leak the curl handle it allocated."""
session.backend.close = original_close
await session.close()
def test_close_is_abandoned_when_it_exceeds_the_budget(monkeypatch, caplog, tmp_path):
"""A close that never returns is abandoned and logged, not awaited forever."""
monkeypatch.setattr(sessions, "SESSION_CLOSE_TIMEOUT_SECONDS", 0.05)
proxies = _proxies(tmp_path)
async def run():
session = get_default_session(proxies, timeout=8)
original_close = session.backend.close
started = await _wedge_close(session)
# wait_for is the assertion: an unbounded close never returns here
await asyncio.wait_for(session.close(), timeout=5)
assert started.is_set()
await _really_close(session, original_close)
with caplog.at_level(logging.ERROR, logger="monitor_utils.sessions"):
asyncio.run(run())
assert any(
record.message == "session close exceeded budget, abandoning session"
for record in caplog.records
)
def test_async_with_exit_is_bounded(monkeypatch, caplog, tmp_path):
"""The bound applies through `async with`, which is how monitors close."""
monkeypatch.setattr(sessions, "SESSION_CLOSE_TIMEOUT_SECONDS", 0.05)
proxies = _proxies(tmp_path)
async def run():
session = get_default_session(proxies, timeout=8)
original_close = session.backend.close
started = await _wedge_close(session)
async with session:
pass
assert started.is_set()
await _really_close(session, original_close)
with caplog.at_level(logging.ERROR, logger="monitor_utils.sessions"):
asyncio.run(asyncio.wait_for(run(), timeout=5))
assert any(
record.message == "session close exceeded budget, abandoning session"
for record in caplog.records
)
def test_healthy_close_completes_and_is_not_logged(caplog, tmp_path):
"""The normal path closes fully and reports nothing."""
proxies = _proxies(tmp_path)
async def run():
session = get_default_session(proxies, timeout=8)
session.session # force lazy construction so close has work to do
await session.close()
return session
with caplog.at_level(logging.ERROR, logger="monitor_utils.sessions"):
session = asyncio.run(run())
assert session._is_closed()
assert not caplog.records
def test_get_default_session_returns_a_bounded_session(tmp_path):
"""The factory hands out the bounded type, not a bare TLSSession."""
proxies = _proxies(tmp_path)
async def run():
session = get_default_session(proxies, timeout=8)
try:
return isinstance(session, BoundedCloseSession)
finally:
await session.close()
assert asyncio.run(run())