Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions py_hamt/store_httpx.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,21 @@ def __init__(
# helper: get or create the client bound to the current running loop #
# --------------------------------------------------------------------- #
def _loop_client(self) -> httpx.AsyncClient:
"""Get or create a client for the current event loop."""
"""Get or create a client for the current event loop.

If the instance was previously closed but owns its clients, a fresh
client mapping is lazily created on demand. Users that supplied their
own ``httpx.AsyncClient`` still receive an error when the instance has
been closed, as we cannot safely recreate their client.
"""
if self._closed:
raise RuntimeError("KuboCAS is closed; create a new instance")
if not self._owns_client:
raise RuntimeError("KuboCAS is closed; create a new instance")
# We previously closed all internally-owned clients. Reset the
# state so that new clients can be created lazily.
self._closed = False
self._client_per_loop = {}

loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
try:
return self._client_per_loop[loop]
Expand All @@ -245,7 +257,7 @@ def _loop_client(self) -> httpx.AsyncClient:
headers=self._default_headers,
auth=self._default_auth,
limits=httpx.Limits(max_connections=64, max_keepalive_connections=32),
# Uncomment when they finally support Robost HTTP/2 GOAWAY responses
# Uncomment when they finally support Robust HTTP/2 GOAWAY responses
# http2=True,
)
self._client_per_loop[loop] = client
Expand Down
33 changes: 25 additions & 8 deletions tests/test_kubocas_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import unittest
from threading import Event, Thread

import httpx
import pytest

from py_hamt import KuboCAS
Expand Down Expand Up @@ -189,18 +190,34 @@ def fake_run(coro):


@pytest.mark.asyncio
async def test_loop_client_raises_after_close():
"""
Verify that calling _loop_client() on a closed KuboCAS instance
raises a RuntimeError.
"""
# Arrange: Create a KuboCAS instance
async def test_loop_client_reopens_after_close():
"""Calling _loop_client() after aclose() recreates a fresh client."""
cas = KuboCAS()

# Act: Close the instance. This should set the internal _closed flag.
first = cas._loop_client()
await cas.aclose()

# Assert: Check that calling _loop_client again raises the expected error.
# Should no longer raise; instead a new client is created.
reopened = cas._loop_client()
assert isinstance(reopened, httpx.AsyncClient)
assert reopened is not first
assert cas._closed is False

await cas.aclose()


@pytest.mark.asyncio
async def test_loop_client_rejects_reuse_of_external_client(global_client_session):
"""Calling _loop_client() after aclose() raises when client is user-supplied."""
cas = KuboCAS(
client=global_client_session,
rpc_base_url="http://127.0.0.1:5001",
gateway_base_url="http://127.0.0.1:8080",
)
assert cas._loop_client() is global_client_session

await cas.aclose()
cas._closed = True # simulate closed instance with external client
with pytest.raises(RuntimeError, match="KuboCAS is closed; create a new instance"):
cas._loop_client()

Expand Down