diff --git a/loadtest/README.md b/loadtest/README.md index cf3e4669d..9e85f5ce2 100644 --- a/loadtest/README.md +++ b/loadtest/README.md @@ -20,11 +20,16 @@ client + server _request handling_ from provisioning. | `h2_single_conn.py` | Raw `httpx` HTTP/2 on a single warmed connection (50-request burst). | | `raw_fetch_test.py` | Raw `httpx` HTTP/1.1 keep-alive baseline. | | `alpn_check.py` | Confirms the origin negotiates `h2` via TLS ALPN. | +| `pool_check.py` | Verifies that multiple sync `Runloop` instances share a single connection pool (transport object identity check, no real requests). | The raw-transport probes compare httpx HTTP/2 multiplexing against HTTP/1.1 directly — the same comparison that motivates the SDK's shared HTTP/2 pool. They're kept so the comparison stays reproducible. +`pool_check.py` is a lightweight sanity check (no API key, no real requests) that +confirms multiple sync `Runloop` instances reuse a single underlying transport +object — preventing file-descriptor exhaustion when many clients are instantiated. + ```sh # Install the SDK from this checkout (editable): cd /path/to/api-client-python && uv sync diff --git a/loadtest/pool_check.py b/loadtest/pool_check.py new file mode 100644 index 000000000..966857c57 --- /dev/null +++ b/loadtest/pool_check.py @@ -0,0 +1,58 @@ +"""Verify that multiple Runloop (sync) instances share a single connection pool. + +Spins up N SDK instances and checks that the number of open connections to +api.runloop.ai does not grow linearly with instance count — it should stay at +one (or a small fixed number) because all instances share the same underlying +httpx transport. + +Usage: + uv run python loadtest/pool_check.py + +No API key is required — we only check the transport object identity and the +OS-level connection count, not make real requests. +""" + +from __future__ import annotations + +import os +import sys +import resource + +from runloop_api_client import Runloop + +HOST = "api.runloop.ai" +N = 20 + + +def main() -> None: + fd_before = resource.getrlimit(resource.RLIMIT_NOFILE)[0] + print(f"FD limit: {fd_before}") + print(f"Creating {N} Runloop instances...\n") + + clients: list[Runloop] = [] + for _ in range(N): + clients.append(Runloop(bearer_token=os.environ.get("RUNLOOP_API_KEY", "dummy"))) + + # All instances should reference the same shared transport object. + transport_ids: set[int] = set() + for c in clients: + t = getattr(c._client, "_transport", None) + if t is not None: + transport_ids.add(id(t)) + + print(f"Distinct transport objects across {N} instances: {len(transport_ids)}") + if len(transport_ids) == 1: + print("PASS — all instances share one transport (connection pool).") + else: + print("FAIL — instances have separate transports; FD exhaustion is possible.") + sys.exit(1) + + # Close all clients. + for c in clients: + c.close() + + print("\nDone.") + + +if __name__ == "__main__": + main()