|
1 | | -import asyncio |
2 | | -import os |
3 | | -import weakref |
| 1 | +import threading |
4 | 2 | from typing import Dict, Optional |
5 | 3 |
|
6 | 4 | import httpx |
7 | | -from httpx import Limits |
8 | | -from httpx._types import ProxyTypes |
9 | | - |
10 | | -from e2b.api import connection_retries, make_async_logging_event_hooks |
| 5 | +from pyqwest import HTTPTransport |
| 6 | + |
| 7 | +from e2b.api import ( |
| 8 | + connection_retries, |
| 9 | + make_async_logging_event_hooks, |
| 10 | + pool_idle_timeout, |
| 11 | + pool_max_idle_per_host, |
| 12 | + proxy_to_url, |
| 13 | +) |
| 14 | +from e2b.api.client_async import AsyncApiPyqwestTransport, ConnectionRetryTransport |
11 | 15 | from e2b.api.metadata import default_headers |
12 | 16 | from e2b.exceptions import AuthenticationException |
13 | 17 | from e2b.volume.client.client import AuthenticatedClient as AsyncVolumeApiClient |
14 | | -from e2b.volume.connection_config import VolumeConnectionConfig |
15 | | - |
16 | | -limits = Limits( |
17 | | - max_keepalive_connections=int(os.getenv("E2B_MAX_KEEPALIVE_CONNECTIONS", "20")), |
18 | | - max_connections=int(os.getenv("E2B_MAX_CONNECTIONS", "2000")), |
19 | | - keepalive_expiry=int(os.getenv("E2B_KEEPALIVE_EXPIRY", "300")), |
20 | | -) |
21 | | - |
22 | | -TransportKey = Optional[ProxyTypes] |
| 18 | +from e2b.volume.connection_config import FILE_TIMEOUT, VolumeConnectionConfig |
23 | 19 |
|
24 | 20 |
|
25 | 21 | def get_api_client(config: VolumeConnectionConfig, **kwargs) -> AsyncVolumeApiClient: |
@@ -56,35 +52,38 @@ def get_api_client(config: VolumeConnectionConfig, **kwargs) -> AsyncVolumeApiCl |
56 | 52 | ) |
57 | 53 |
|
58 | 54 |
|
59 | | -class AsyncTransportWithLogger(httpx.AsyncHTTPTransport): |
60 | | - # Keyed weakly by the event loop object itself, not id(loop) — CPython |
61 | | - # reuses object ids, so a new loop could otherwise inherit a transport |
62 | | - # bound to a previous, closed loop. |
63 | | - _instances: weakref.WeakKeyDictionary[ |
64 | | - asyncio.AbstractEventLoop, |
65 | | - Dict[TransportKey, "AsyncTransportWithLogger"], |
66 | | - ] = weakref.WeakKeyDictionary() |
67 | | - |
68 | | - @property |
69 | | - def pool(self): |
70 | | - return self._pool |
71 | | - |
72 | | - |
73 | | -def get_transport(config: VolumeConnectionConfig) -> AsyncTransportWithLogger: |
74 | | - loop = asyncio.get_running_loop() |
75 | | - loop_instances = AsyncTransportWithLogger._instances.get(loop) |
76 | | - if loop_instances is None: |
77 | | - loop_instances = {} |
78 | | - AsyncTransportWithLogger._instances[loop] = loop_instances |
79 | | - |
80 | | - key: TransportKey = config.proxy |
81 | | - transport = loop_instances.get(key) |
82 | | - if transport is None: |
83 | | - transport = AsyncTransportWithLogger( |
84 | | - limits=limits, |
85 | | - proxy=config.proxy, |
86 | | - retries=connection_retries, |
87 | | - ) |
88 | | - loop_instances[key] = transport |
89 | | - |
90 | | - return transport |
| 55 | +_transport_lock = threading.Lock() |
| 56 | +# One transport (= one connection pool) per proxy; None is the direct pool. |
| 57 | +# pyqwest's I/O runs on its own Rust runtime, so unlike the httpx transport |
| 58 | +# this replaced, the transport is not bound to an event loop and the cache |
| 59 | +# is process-global rather than per-loop. |
| 60 | +_transports: Dict[Optional[str], AsyncApiPyqwestTransport] = {} |
| 61 | + |
| 62 | + |
| 63 | +def get_transport(config: VolumeConnectionConfig) -> AsyncApiPyqwestTransport: |
| 64 | + """The shared pyqwest-backed httpx transport for volume content API calls. |
| 65 | +
|
| 66 | + ``read_timeout`` is the idle bound on every read: it resets after each |
| 67 | + successful read, so it caps how long a streamed download may stall |
| 68 | + without limiting total transfer time. It is fixed per transport — the |
| 69 | + adapter's per-request timeouts are whole-request deadlines, and the sync |
| 70 | + adapter does not bound body reads at all. |
| 71 | + """ |
| 72 | + proxy_url = proxy_to_url(config.proxy) |
| 73 | + with _transport_lock: |
| 74 | + transport = _transports.get(proxy_url) |
| 75 | + if transport is None: |
| 76 | + transport = AsyncApiPyqwestTransport( |
| 77 | + ConnectionRetryTransport( |
| 78 | + HTTPTransport( |
| 79 | + tls_include_system_certs=True, |
| 80 | + proxy=proxy_url, |
| 81 | + pool_idle_timeout=pool_idle_timeout, |
| 82 | + pool_max_idle_per_host=pool_max_idle_per_host, |
| 83 | + read_timeout=FILE_TIMEOUT, |
| 84 | + ), |
| 85 | + max_retries=connection_retries, |
| 86 | + ) |
| 87 | + ) |
| 88 | + _transports[proxy_url] = transport |
| 89 | + return transport |
0 commit comments