From 7b4c5599c815cd6381f8433f11f7653e2d9c42f1 Mon Sep 17 00:00:00 2001 From: Francesc Alted Date: Sat, 8 Nov 2025 13:55:36 +0100 Subject: [PATCH] Preliminary http2 optimization for clients. Check that this not interfere with pyodide. --- caterva2/api_utils.py | 38 +++++++++++++++++------ caterva2/client.py | 11 +++++++ examples/large-dataset-indexing.py | 48 ++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 4 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 examples/large-dataset-indexing.py diff --git a/caterva2/api_utils.py b/caterva2/api_utils.py index d25a13b7..7f6c005c 100644 --- a/caterva2/api_utils.py +++ b/caterva2/api_utils.py @@ -14,6 +14,35 @@ import blosc2 import httpx +# Shared HTTP client instances for connection pooling +_http_client = None +_async_http_client = None + + +def get_client(return_async_client=False): + """Get a shared HTTP client instance with HTTP/2 support and connection pooling.""" + global _http_client, _async_http_client + + if return_async_client: + if _async_http_client is None: + _async_http_client = httpx.AsyncClient(http2=True) + return _async_http_client + else: + if _http_client is None: + _http_client = httpx.Client(http2=True) + return _http_client + + +def close_clients(): + """Close shared HTTP clients. Call this when done with the API.""" + global _http_client, _async_http_client + if _http_client is not None: + _http_client.close() + _http_client = None + if _async_http_client is not None: + _async_http_client.close() + _async_http_client = None + def slice_to_string(slice_): if slice_ is None or slice_ == () or slice_ == slice(None): @@ -178,15 +207,6 @@ def unfold_file(remotepath, urlbase, auth_cookie=None): # -def get_client(return_async_client=False): - if return_async_client: - client_class = httpx.AsyncClient - else: - client_class = httpx.Client - - return client_class() - - def _xget(url, params=None, headers=None, timeout=5, auth_cookie=None): client = get_client() if auth_cookie: diff --git a/caterva2/client.py b/caterva2/client.py index a923caeb..99ea16b8 100644 --- a/caterva2/client.py +++ b/caterva2/client.py @@ -719,6 +719,17 @@ def __init__(self, urlbase, auth=None, timeout=5): self.urlbase, {"username": username, "password": password}, timeout=self.timeout ) + def __enter__(self): + """Enter context manager - HTTP clients created lazily on first use.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Exit context manager - close HTTP clients.""" + from .api_utils import close_clients + + close_clients() + return False + def get_roots(self): """ Retrieves the list of available roots. diff --git a/examples/large-dataset-indexing.py b/examples/large-dataset-indexing.py new file mode 100644 index 00000000..20aa20fd --- /dev/null +++ b/examples/large-dataset-indexing.py @@ -0,0 +1,48 @@ +############################################################################### +# Caterva2 - On demand access to remote Blosc2 data repositories +# +# Copyright (c) 2025 ironArray SLU +# https://www.blosc.org +# License: GNU Affero General Public License v3.0 +# See LICENSE.txt for details about copyright and rights to use. +############################################################################### + +import math +import time + +import blosc2 + +import caterva2 as cat2 + +urlbase = "https://cat2.cloud/demo" +root = "@public" +dataset = "examples/cube-1k-1k-1k.b2nd" + +# Blosc2 approach: simple, but allows slicing of remote datasets +start = time.time() +urlpath = blosc2.URLPath(f"{root}/{dataset}", urlbase) +blosc2_ds = blosc2.open(urlpath, mode="r") +print(f"Dataset size: {math.prod(blosc2_ds.shape) * blosc2_ds.dtype.itemsize / 2**20:.3f} MB") +print(f"Blosc2 - Open dataset: {(time.time() - start) * 1000:.0f} ms") + +print(type(blosc2_ds), blosc2_ds.shape, blosc2_ds.dtype) +start = time.time() +print(blosc2_ds[500:502, 302, 900:905]) +print(f"Blosc2 - Slice dataset: {(time.time() - start) * 1000:.0f} ms") + +# Caterva2 approach: more flexible, allowing remote management in the server +# Use Client as context manager for proper HTTP/2 connection cleanup +with cat2.Client(urlbase) as client: + # Warmup: establish HTTP/2 connection + _ = client.get(root) + + # Now measure with warm connection + start = time.time() + myroot = client.get(root) + cat2_ds = myroot[dataset] + print(f"Caterva2 - Get dataset handle: {(time.time() - start) * 1000:.0f} ms") + + print(type(cat2_ds), cat2_ds.shape, cat2_ds.dtype) + start = time.time() + print(cat2_ds[500:502, 302, 900:905]) + print(f"Caterva2 - Slice dataset: {(time.time() - start) * 1000:.0f} ms") diff --git a/pyproject.toml b/pyproject.toml index b2207c02..00db5be3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ classifiers = [ ] dependencies = [ "blosc2>=3.2.0", - "httpx", + "httpx[http2]", ] [tool.hatch.version]