44from asyncio import Lock
55from functools import cached_property
66from logging import getLogger
7- from typing import TYPE_CHECKING , ClassVar , Literal , NamedTuple , overload
8-
9- from apify_client import ApifyClientAsync
7+ from typing import TYPE_CHECKING , ClassVar , Literal , overload
108
119from ._utils import hash_api_public_base_url_and_token
1210
1311if TYPE_CHECKING :
1412 from collections .abc import Callable
1513 from types import TracebackType
1614
15+ from apify_client import ApifyClientAsync
1716 from apify_client ._resource_clients import (
1817 DatasetClientAsync ,
1918 DatasetCollectionClientAsync ,
@@ -35,6 +34,7 @@ async def open_by_alias(
3534 storage_type : Literal ['Dataset' ],
3635 collection_client : DatasetCollectionClientAsync ,
3736 get_resource_client_by_id : Callable [[str ], DatasetClientAsync ],
37+ api_client : ApifyClientAsync ,
3838 configuration : Configuration ,
3939) -> DatasetClientAsync : ...
4040
@@ -46,6 +46,7 @@ async def open_by_alias(
4646 storage_type : Literal ['KeyValueStore' ],
4747 collection_client : KeyValueStoreCollectionClientAsync ,
4848 get_resource_client_by_id : Callable [[str ], KeyValueStoreClientAsync ],
49+ api_client : ApifyClientAsync ,
4950 configuration : Configuration ,
5051) -> KeyValueStoreClientAsync : ...
5152
@@ -57,6 +58,7 @@ async def open_by_alias(
5758 storage_type : Literal ['RequestQueue' ],
5859 collection_client : RequestQueueCollectionClientAsync ,
5960 get_resource_client_by_id : Callable [[str ], RequestQueueClientAsync ],
61+ api_client : ApifyClientAsync ,
6062 configuration : Configuration ,
6163) -> RequestQueueClientAsync : ...
6264
@@ -69,6 +71,7 @@ async def open_by_alias(
6971 KeyValueStoreCollectionClientAsync | RequestQueueCollectionClientAsync | DatasetCollectionClientAsync
7072 ),
7173 get_resource_client_by_id : Callable [[str ], KeyValueStoreClientAsync | RequestQueueClientAsync | DatasetClientAsync ],
74+ api_client : ApifyClientAsync ,
7275 configuration : Configuration ,
7376) -> KeyValueStoreClientAsync | RequestQueueClientAsync | DatasetClientAsync :
7477 """Open storage by alias, creating it if necessary.
@@ -81,6 +84,8 @@ async def open_by_alias(
8184 storage_type: The type of storage to open.
8285 collection_client: The Apify API collection client for the storage type.
8386 get_resource_client_by_id: A callable that takes a storage ID and returns the resource client.
87+ api_client: The Apify API client used for the storage operation. Reused to access the default KVS that
88+ holds the alias mapping, so alias resolution does not spin up its own client.
8489 configuration: Configuration object containing API credentials and settings.
8590
8691 Returns:
@@ -94,6 +99,7 @@ async def open_by_alias(
9499 storage_type = storage_type ,
95100 alias = alias ,
96101 configuration = configuration ,
102+ api_client = api_client ,
97103 ) as alias_resolver :
98104 storage_id = await alias_resolver .resolve_id ()
99105
@@ -111,13 +117,6 @@ async def open_by_alias(
111117 return get_resource_client_by_id (raw_metadata .id )
112118
113119
114- class _ApiClientCacheKey (NamedTuple ):
115- """Cache key for `AliasResolver._api_clients` — identifies an `ApifyClientAsync` by its credentials."""
116-
117- token : str | None
118- api_url : str | None
119-
120-
121120class AliasResolver :
122121 """Class for handling aliases.
123122
@@ -142,21 +141,19 @@ class AliasResolver:
142141 _alias_init_lock : Lock | None = None
143142 """Lock for creating alias storages. Only one alias storage can be created at the time. Global for all instances."""
144143
145- _api_clients : ClassVar [dict [_ApiClientCacheKey , ApifyClientAsync ]] = {}
146- """Cache of Apify API clients keyed by `(token, api_url)`. Reused across instances so that repeated alias
147- resolution does not create (and leak) a fresh unclosed `ApifyClientAsync` on every call."""
148-
149144 default_storage_key : ClassVar [str ] = '__default__'
150145
151146 def __init__ (
152147 self ,
153148 storage_type : Literal ['Dataset' , 'KeyValueStore' , 'RequestQueue' ],
154149 alias : str ,
155150 configuration : Configuration ,
151+ api_client : ApifyClientAsync ,
156152 ) -> None :
157153 self ._storage_type = storage_type
158154 self ._alias = alias
159155 self ._configuration = configuration
156+ self ._api_client = api_client
160157
161158 async def __aenter__ (self ) -> AliasResolver :
162159 """Context manager to prevent race condition in alias creation."""
@@ -184,26 +181,22 @@ async def _get_alias_init_lock(cls) -> Lock:
184181 cls ._alias_init_lock = Lock ()
185182 return cls ._alias_init_lock
186183
187- @classmethod
188- async def _get_alias_map (cls , configuration : Configuration ) -> dict [str , str ]:
184+ async def _get_alias_map (self ) -> dict [str , str ]:
189185 """Get the aliases and storage ids mapping from the default kvs.
190186
191- Mapping is loaded from kvs only once and is shared for all instances of the AliasResolver class.
192-
193- Args:
194- configuration: Configuration object to use for accessing the default KVS.
187+ Mapping is loaded from kvs only once and is shared for all instances of the `AliasResolver` class.
195188
196189 Returns:
197190 Map of aliases and storage ids.
198191 """
199- if not cls ._alias_map_loaded and configuration .is_at_home :
200- default_kvs_client = await cls ._get_default_kvs_client (configuration )
192+ if not AliasResolver ._alias_map_loaded and self . _configuration .is_at_home :
193+ default_kvs_client = self ._get_default_kvs_client ()
201194
202- record = await default_kvs_client .get_record (cls ._ALIAS_MAPPING_KEY )
203- cls ._alias_map = record .get ('value' , {}) if record else {}
204- cls ._alias_map_loaded = True
195+ record = await default_kvs_client .get_record (self ._ALIAS_MAPPING_KEY )
196+ AliasResolver ._alias_map = record .get ('value' , {}) if record else {}
197+ AliasResolver ._alias_map_loaded = True
205198
206- return cls ._alias_map
199+ return AliasResolver ._alias_map
207200
208201 async def resolve_id (self ) -> str | None :
209202 """Get id of the aliased storage.
@@ -223,12 +216,12 @@ async def resolve_id(self) -> str | None:
223216 return storage_id
224217
225218 # Fallback to the mapping saved in the default KVS
226- return (await self ._get_alias_map (self . _configuration )).get (self ._storage_key , None )
219+ return (await self ._get_alias_map ()).get (self ._storage_key , None )
227220
228221 async def store_mapping (self , storage_id : str ) -> None :
229222 """Add alias and related storage id to the mapping in default kvs and local in-memory mapping."""
230223 # Update in-memory mapping
231- alias_map = await self ._get_alias_map (self . _configuration )
224+ alias_map = await self ._get_alias_map ()
232225 alias_map [self ._storage_key ] = storage_id
233226
234227 if not self ._configuration .is_at_home :
@@ -237,7 +230,7 @@ async def store_mapping(self, storage_id: str) -> None:
237230 )
238231 return
239232
240- default_kvs_client = await self ._get_default_kvs_client (self . _configuration )
233+ default_kvs_client = self ._get_default_kvs_client ()
241234
242235 try :
243236 record = await default_kvs_client .get_record (self ._ALIAS_MAPPING_KEY )
@@ -260,24 +253,14 @@ def _storage_key(self) -> str:
260253 ]
261254 )
262255
263- @classmethod
264- async def _get_default_kvs_client (cls , configuration : Configuration ) -> KeyValueStoreClientAsync :
256+ def _get_default_kvs_client (self ) -> KeyValueStoreClientAsync :
265257 """Get a client for the default key-value store.
266258
267- The underlying `ApifyClientAsync` is cached per `(token, api_url)` and reused across calls, so repeated
268- alias resolution does not create (and leak) a fresh unclosed client every time.
259+ Derived from the injected `ApifyClientAsync`, so alias resolution shares the same HTTP client (and its
260+ connection pool and event loop affinity) as the storage operation that triggered it, instead of creating
261+ and leaking its own.
269262 """
270- if not configuration .default_key_value_store_id :
263+ if not self . _configuration .default_key_value_store_id :
271264 raise ValueError ("'Configuration.default_key_value_store_id' must be set." )
272265
273- cache_key = _ApiClientCacheKey (configuration .token , configuration .api_base_url )
274- apify_client_async = cls ._api_clients .get (cache_key )
275- if apify_client_async is None :
276- apify_client_async = ApifyClientAsync (
277- token = configuration .token ,
278- api_url = configuration .api_base_url ,
279- max_retries = 8 ,
280- )
281- cls ._api_clients [cache_key ] = apify_client_async
282-
283- return apify_client_async .key_value_store (key_value_store_id = configuration .default_key_value_store_id )
266+ return self ._api_client .key_value_store (key_value_store_id = self ._configuration .default_key_value_store_id )
0 commit comments