Skip to content
Closed
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
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 22 additions & 3 deletions py_hamt/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 #
# --------------------------------------------------------------------- #
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
21 changes: 21 additions & 0 deletions tests/test_kubocas_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading