diff --git a/README.md b/README.md index 6b17dd1..4bdedbb 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,8 @@ py-hamt provides efficient storage and retrieval of large sets of key-value mapp dClimate primarily created this for storing large [zarrs](https://zarr.dev/) on IPFS. To see this in action, see our [data ETLs](https://github.com/dClimate/etl-scripts). # Installation and Usage -To install, since we do not publish this package to PyPI, add this library to your project directly from git. ```sh -pip install 'git+https://github.com/dClimate/py-hamt' +pip install py-hamt ``` For usage information, take a look at our [API documentation](https://dclimate.github.io/py-hamt/py_hamt.html), major items have example code. diff --git a/py_hamt/store.py b/py_hamt/store.py index e0838b9..015a2c6 100644 --- a/py_hamt/store.py +++ b/py_hamt/store.py @@ -217,12 +217,13 @@ def _loop_session(self) -> aiohttp.ClientSession: try: return self._session_per_loop[loop] except KeyError: - # Create a session with a connector that closes more quickly + # Create a connector that keeps connections alive for reuse. + # Cleaning up closed connections ensures resources are eventually + # released even if the user forgets to explicitly close the session. connector = aiohttp.TCPConnector( limit=64, limit_per_host=32, - force_close=True, # Force close connections - enable_cleanup_closed=True, # Enable cleanup of closed connections + enable_cleanup_closed=True, ) sess = aiohttp.ClientSession( @@ -261,6 +262,24 @@ async def __aenter__(self) -> "KuboCAS": async def __aexit__(self, *exc: Any) -> None: await self.aclose() + def __del__(self) -> None: + """Best-effort close for internally-created sessions.""" + if not self._owns_session: + return + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = None + + try: + if loop is None or not loop.is_running(): + asyncio.run(self.aclose()) + else: + loop.create_task(self.aclose()) + except Exception: + # Suppress all errors during interpreter shutdown or loop teardown + pass + # --------------------------------------------------------------------- # # save() – now uses the per-loop session # # --------------------------------------------------------------------- # diff --git a/pyproject.toml b/pyproject.toml index 08ed51a..f8ad8e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "py-hamt" -version = "3.0.0" +version = "3.0.1" description = "HAMT implementation for a content-addressed storage system." readme = "README.md" requires-python = ">=3.12" diff --git a/tests/test_kubocas_session.py b/tests/test_kubocas_session.py index eeb102a..48a1b04 100644 --- a/tests/test_kubocas_session.py +++ b/tests/test_kubocas_session.py @@ -99,3 +99,24 @@ async def test_distinct_loops_get_distinct_sessions(): # Clean‑up await kubo.aclose() assert secondary_session.closed + + +@pytest.mark.asyncio +async def test_del_closes_session(): + """`KuboCAS` should close sessions when the instance is garbage collected.""" + kubo = KuboCAS( + rpc_base_url="http://127.0.0.1:5001", + gateway_base_url="http://127.0.0.1:8080", + ) + + session = await _maybe_await(kubo._loop_session()) + assert not session.closed + + # Drop the reference and force garbage collection + del kubo + import gc + + gc.collect() + await asyncio.sleep(0) + + assert session.closed