xoscar 0.9.0__cp312-cp312-macosx_10_13_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xoscar/__init__.py +61 -0
- xoscar/_utils.cpython-312-darwin.so +0 -0
- xoscar/_utils.pxd +36 -0
- xoscar/_utils.pyx +246 -0
- xoscar/_version.py +693 -0
- xoscar/aio/__init__.py +16 -0
- xoscar/aio/base.py +86 -0
- xoscar/aio/file.py +59 -0
- xoscar/aio/lru.py +228 -0
- xoscar/aio/parallelism.py +39 -0
- xoscar/api.py +527 -0
- xoscar/backend.py +67 -0
- xoscar/backends/__init__.py +14 -0
- xoscar/backends/allocate_strategy.py +160 -0
- xoscar/backends/communication/__init__.py +30 -0
- xoscar/backends/communication/base.py +315 -0
- xoscar/backends/communication/core.py +69 -0
- xoscar/backends/communication/dummy.py +253 -0
- xoscar/backends/communication/errors.py +20 -0
- xoscar/backends/communication/socket.py +444 -0
- xoscar/backends/communication/ucx.py +538 -0
- xoscar/backends/communication/utils.py +97 -0
- xoscar/backends/config.py +157 -0
- xoscar/backends/context.py +437 -0
- xoscar/backends/core.py +352 -0
- xoscar/backends/indigen/__init__.py +16 -0
- xoscar/backends/indigen/__main__.py +19 -0
- xoscar/backends/indigen/backend.py +51 -0
- xoscar/backends/indigen/driver.py +26 -0
- xoscar/backends/indigen/fate_sharing.py +221 -0
- xoscar/backends/indigen/pool.py +515 -0
- xoscar/backends/indigen/shared_memory.py +548 -0
- xoscar/backends/message.cpython-312-darwin.so +0 -0
- xoscar/backends/message.pyi +255 -0
- xoscar/backends/message.pyx +646 -0
- xoscar/backends/pool.py +1630 -0
- xoscar/backends/router.py +285 -0
- xoscar/backends/test/__init__.py +16 -0
- xoscar/backends/test/backend.py +38 -0
- xoscar/backends/test/pool.py +233 -0
- xoscar/batch.py +256 -0
- xoscar/collective/__init__.py +27 -0
- xoscar/collective/backend/__init__.py +13 -0
- xoscar/collective/backend/nccl_backend.py +160 -0
- xoscar/collective/common.py +102 -0
- xoscar/collective/core.py +737 -0
- xoscar/collective/process_group.py +687 -0
- xoscar/collective/utils.py +41 -0
- xoscar/collective/xoscar_pygloo.cpython-312-darwin.so +0 -0
- xoscar/collective/xoscar_pygloo.pyi +239 -0
- xoscar/constants.py +23 -0
- xoscar/context.cpython-312-darwin.so +0 -0
- xoscar/context.pxd +21 -0
- xoscar/context.pyx +368 -0
- xoscar/core.cpython-312-darwin.so +0 -0
- xoscar/core.pxd +51 -0
- xoscar/core.pyx +664 -0
- xoscar/debug.py +188 -0
- xoscar/driver.py +42 -0
- xoscar/errors.py +63 -0
- xoscar/libcpp.pxd +31 -0
- xoscar/metrics/__init__.py +21 -0
- xoscar/metrics/api.py +288 -0
- xoscar/metrics/backends/__init__.py +13 -0
- xoscar/metrics/backends/console/__init__.py +13 -0
- xoscar/metrics/backends/console/console_metric.py +82 -0
- xoscar/metrics/backends/metric.py +149 -0
- xoscar/metrics/backends/prometheus/__init__.py +13 -0
- xoscar/metrics/backends/prometheus/prometheus_metric.py +70 -0
- xoscar/nvutils.py +717 -0
- xoscar/profiling.py +260 -0
- xoscar/serialization/__init__.py +20 -0
- xoscar/serialization/aio.py +141 -0
- xoscar/serialization/core.cpython-312-darwin.so +0 -0
- xoscar/serialization/core.pxd +28 -0
- xoscar/serialization/core.pyi +57 -0
- xoscar/serialization/core.pyx +944 -0
- xoscar/serialization/cuda.py +111 -0
- xoscar/serialization/exception.py +48 -0
- xoscar/serialization/mlx.py +67 -0
- xoscar/serialization/numpy.py +82 -0
- xoscar/serialization/pyfury.py +37 -0
- xoscar/serialization/scipy.py +72 -0
- xoscar/serialization/torch.py +180 -0
- xoscar/utils.py +522 -0
- xoscar/virtualenv/__init__.py +34 -0
- xoscar/virtualenv/core.py +268 -0
- xoscar/virtualenv/platform.py +56 -0
- xoscar/virtualenv/utils.py +100 -0
- xoscar/virtualenv/uv.py +321 -0
- xoscar-0.9.0.dist-info/METADATA +230 -0
- xoscar-0.9.0.dist-info/RECORD +94 -0
- xoscar-0.9.0.dist-info/WHEEL +6 -0
- xoscar-0.9.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# Copyright 2022-2023 XProbe Inc.
|
|
2
|
+
# derived from copyright 1999-2021 Alibaba Group Holding Ltd.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import threading
|
|
20
|
+
from typing import Any, Optional, Type, Union
|
|
21
|
+
|
|
22
|
+
from .communication import Client, get_client_type
|
|
23
|
+
|
|
24
|
+
_CACHE_KEY_TYPE = Union[
|
|
25
|
+
tuple[str, Any, Optional[Type[Client]]],
|
|
26
|
+
tuple[str, Any, Optional[Type[Client]], Optional[tuple[str, ...]]],
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Router:
|
|
31
|
+
"""
|
|
32
|
+
Router provides mapping from external address to internal address.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
__slots__ = (
|
|
36
|
+
"_curr_external_addresses",
|
|
37
|
+
"_local_mapping",
|
|
38
|
+
"_mapping",
|
|
39
|
+
"_comm_config",
|
|
40
|
+
"_proxy_config",
|
|
41
|
+
"_cache_local",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
_instance: "Router" | None = None
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def set_instance(router: Optional["Router"]):
|
|
48
|
+
# Default router is set when an actor pool started
|
|
49
|
+
Router._instance = router
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def get_instance() -> "Router" | None:
|
|
53
|
+
return Router._instance
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def get_instance_or_empty() -> "Router":
|
|
57
|
+
return Router._instance or Router(list(), None)
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
external_addresses: list[str],
|
|
62
|
+
local_address: str | None,
|
|
63
|
+
mapping: dict[str, str] | None = None,
|
|
64
|
+
comm_config: dict | None = None,
|
|
65
|
+
proxy_config: dict | None = None,
|
|
66
|
+
):
|
|
67
|
+
self._curr_external_addresses = external_addresses
|
|
68
|
+
self._local_mapping = dict()
|
|
69
|
+
for addr in self._curr_external_addresses:
|
|
70
|
+
self._local_mapping[addr] = local_address
|
|
71
|
+
if mapping is None:
|
|
72
|
+
mapping = dict()
|
|
73
|
+
self._mapping = mapping
|
|
74
|
+
self._comm_config = comm_config or dict()
|
|
75
|
+
self._proxy_config = proxy_config or dict()
|
|
76
|
+
self._cache_local = threading.local()
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def _cache(self) -> dict[_CACHE_KEY_TYPE, Client]:
|
|
80
|
+
try:
|
|
81
|
+
return self._cache_local.cache
|
|
82
|
+
except AttributeError:
|
|
83
|
+
cache = self._cache_local.cache = dict()
|
|
84
|
+
return cache
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def _lock(self) -> asyncio.Lock:
|
|
88
|
+
try:
|
|
89
|
+
return self._cache_local.lock
|
|
90
|
+
except AttributeError:
|
|
91
|
+
lock = self._cache_local.lock = asyncio.Lock()
|
|
92
|
+
return lock
|
|
93
|
+
|
|
94
|
+
def set_mapping(self, mapping: dict[str, str]):
|
|
95
|
+
self._mapping = mapping
|
|
96
|
+
self._cache_local = threading.local()
|
|
97
|
+
|
|
98
|
+
def add_router(self, router: "Router"):
|
|
99
|
+
self._curr_external_addresses.extend(router._curr_external_addresses)
|
|
100
|
+
self._local_mapping.update(router._local_mapping)
|
|
101
|
+
self._mapping.update(router._mapping)
|
|
102
|
+
self._comm_config.update(router._comm_config)
|
|
103
|
+
self._proxy_config.update(router._proxy_config)
|
|
104
|
+
self._cache_local = threading.local()
|
|
105
|
+
|
|
106
|
+
def remove_router(self, router: "Router"):
|
|
107
|
+
for external_address in router._curr_external_addresses:
|
|
108
|
+
try:
|
|
109
|
+
self._curr_external_addresses.remove(external_address)
|
|
110
|
+
except ValueError:
|
|
111
|
+
pass
|
|
112
|
+
for addr in router._local_mapping:
|
|
113
|
+
self._local_mapping.pop(addr, None)
|
|
114
|
+
for addr in router._mapping:
|
|
115
|
+
self._mapping.pop(addr, None)
|
|
116
|
+
self._cache_local = threading.local()
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def external_address(self):
|
|
120
|
+
if self._curr_external_addresses:
|
|
121
|
+
return self._curr_external_addresses[0]
|
|
122
|
+
|
|
123
|
+
def get_internal_address(self, external_address: str) -> str | None:
|
|
124
|
+
try:
|
|
125
|
+
# local address, use dummy address
|
|
126
|
+
return self._local_mapping[external_address]
|
|
127
|
+
except KeyError:
|
|
128
|
+
# try to lookup inner address from address mapping
|
|
129
|
+
return self._mapping.get(external_address)
|
|
130
|
+
|
|
131
|
+
async def get_client(
|
|
132
|
+
self,
|
|
133
|
+
external_address: str,
|
|
134
|
+
from_who: Any = None,
|
|
135
|
+
cached: bool = True,
|
|
136
|
+
proxy_addresses: list[str] | None = None,
|
|
137
|
+
**kw,
|
|
138
|
+
) -> Client:
|
|
139
|
+
async with self._lock:
|
|
140
|
+
proxy_addrs: tuple[str, ...] | None = (
|
|
141
|
+
tuple(proxy_addresses) if proxy_addresses else None
|
|
142
|
+
)
|
|
143
|
+
if (
|
|
144
|
+
cached
|
|
145
|
+
and (external_address, from_who, None, proxy_addrs) in self._cache
|
|
146
|
+
):
|
|
147
|
+
cached_client = self._cache[
|
|
148
|
+
external_address, from_who, None, proxy_addrs
|
|
149
|
+
]
|
|
150
|
+
if cached_client.closed:
|
|
151
|
+
# closed before, ignore it
|
|
152
|
+
del self._cache[external_address, from_who, None, proxy_addrs]
|
|
153
|
+
else:
|
|
154
|
+
return cached_client
|
|
155
|
+
|
|
156
|
+
address = self.get_internal_address(external_address)
|
|
157
|
+
if address is None:
|
|
158
|
+
# no inner address, just use external address
|
|
159
|
+
address = external_address
|
|
160
|
+
# check if proxy address exists
|
|
161
|
+
proxy_address = proxy_addresses[-1] if proxy_addresses else None
|
|
162
|
+
if proxy_address is None:
|
|
163
|
+
proxy_address = self.get_proxy(address)
|
|
164
|
+
if proxy_address and proxy_address != self.external_address:
|
|
165
|
+
address = proxy_address
|
|
166
|
+
else:
|
|
167
|
+
if new_proxy_address := self.get_proxy(proxy_address):
|
|
168
|
+
address = new_proxy_address
|
|
169
|
+
else:
|
|
170
|
+
address = proxy_address
|
|
171
|
+
|
|
172
|
+
client_type: Type[Client] = get_client_type(address)
|
|
173
|
+
client = await self._create_client(client_type, address, **kw)
|
|
174
|
+
if cached:
|
|
175
|
+
self._cache[external_address, from_who, None, proxy_addrs] = client
|
|
176
|
+
return client
|
|
177
|
+
|
|
178
|
+
async def _create_client(
|
|
179
|
+
self, client_type: Type[Client], address: str, **kw
|
|
180
|
+
) -> Client:
|
|
181
|
+
config = client_type.parse_config(self._comm_config)
|
|
182
|
+
if config:
|
|
183
|
+
kw["config"] = config
|
|
184
|
+
local_address = (
|
|
185
|
+
self._curr_external_addresses[0] if self._curr_external_addresses else None
|
|
186
|
+
)
|
|
187
|
+
return await client_type.connect(address, local_address=local_address, **kw)
|
|
188
|
+
|
|
189
|
+
def _get_client_type_to_addresses(
|
|
190
|
+
self, external_address: str
|
|
191
|
+
) -> dict[Type[Client], str]:
|
|
192
|
+
client_type_to_addresses = dict()
|
|
193
|
+
client_type_to_addresses[get_client_type(external_address)] = external_address
|
|
194
|
+
if external_address in self._curr_external_addresses: # pragma: no cover
|
|
195
|
+
# local address, use dummy address
|
|
196
|
+
addr = self._local_mapping.get(external_address)
|
|
197
|
+
client_type = get_client_type(addr) # type: ignore
|
|
198
|
+
client_type_to_addresses[client_type] = addr # type: ignore
|
|
199
|
+
if external_address in self._mapping:
|
|
200
|
+
# try to lookup inner address from address mapping
|
|
201
|
+
addr = self._mapping.get(external_address)
|
|
202
|
+
client_type = get_client_type(addr) # type: ignore
|
|
203
|
+
client_type_to_addresses[client_type] = addr # type: ignore
|
|
204
|
+
return client_type_to_addresses
|
|
205
|
+
|
|
206
|
+
def get_all_client_types(self, external_address: str) -> list[Type[Client]]:
|
|
207
|
+
return list(self._get_client_type_to_addresses(external_address))
|
|
208
|
+
|
|
209
|
+
async def get_client_via_type(
|
|
210
|
+
self,
|
|
211
|
+
external_address: str,
|
|
212
|
+
client_type: Type[Client],
|
|
213
|
+
from_who: Any = None,
|
|
214
|
+
cached: bool = True,
|
|
215
|
+
**kw,
|
|
216
|
+
) -> Client:
|
|
217
|
+
async with self._lock:
|
|
218
|
+
if cached and (external_address, from_who, client_type) in self._cache:
|
|
219
|
+
cached_client = self._cache[external_address, from_who, client_type]
|
|
220
|
+
if cached_client.closed: # pragma: no cover
|
|
221
|
+
# closed before, ignore it
|
|
222
|
+
del self._cache[external_address, from_who, client_type]
|
|
223
|
+
else:
|
|
224
|
+
return cached_client
|
|
225
|
+
|
|
226
|
+
client_type_to_addresses = self._get_client_type_to_addresses(
|
|
227
|
+
external_address
|
|
228
|
+
)
|
|
229
|
+
if client_type not in client_type_to_addresses: # pragma: no cover
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"Client type({client_type}) is not supported for {external_address}"
|
|
232
|
+
)
|
|
233
|
+
address = client_type_to_addresses[client_type]
|
|
234
|
+
client = await self._create_client(client_type, address, **kw)
|
|
235
|
+
if cached:
|
|
236
|
+
self._cache[external_address, from_who, client_type] = client
|
|
237
|
+
return client
|
|
238
|
+
|
|
239
|
+
def get_proxy(self, from_addr: str) -> str | None:
|
|
240
|
+
"""
|
|
241
|
+
Get proxy address that sent to.
|
|
242
|
+
|
|
243
|
+
Some patterns can be supported:
|
|
244
|
+
|
|
245
|
+
1. Direct address mapping, e.g. mapping 127.0.0.1:12345 -> 127.0.0.1:12346
|
|
246
|
+
The message will be sent to 127.0.0.1:12346 as forward one.
|
|
247
|
+
2. Host match, e.g. mapping 127.0.0.1 -> 127.0.0.1:12346
|
|
248
|
+
All the messages that match the host, e.g. 127.0.0.1:12345 and 127.0.0.1:12347
|
|
249
|
+
will be sent to 127.0.0.1:12346 as forward one.
|
|
250
|
+
3. Wildcard, e.g. mapping * -> 127.0.0.1:12346
|
|
251
|
+
All the messages will be sent to 127.0.0.1:12346 as forward one.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
host = from_addr.split(":", 1)[0]
|
|
255
|
+
|
|
256
|
+
proxy_map = self._proxy_config
|
|
257
|
+
addr = proxy_map.get(from_addr)
|
|
258
|
+
if addr and addr != from_addr:
|
|
259
|
+
return addr
|
|
260
|
+
addr = proxy_map.get(host)
|
|
261
|
+
if addr and addr != from_addr:
|
|
262
|
+
return addr
|
|
263
|
+
addr = proxy_map.get("*")
|
|
264
|
+
if addr and addr != from_addr:
|
|
265
|
+
return addr
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
def get_proxies(self, from_addr: str) -> list[str] | None:
|
|
269
|
+
"""
|
|
270
|
+
Get all proxies
|
|
271
|
+
|
|
272
|
+
e.g. Proxy mapping {'a': 'b', 'b': 'c'}
|
|
273
|
+
get_proxies('a') will return ['b', 'c']
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
proxies: list[str] = []
|
|
277
|
+
while True:
|
|
278
|
+
proxy = self.get_proxy(from_addr)
|
|
279
|
+
if not proxies and not proxy:
|
|
280
|
+
return None
|
|
281
|
+
elif not proxy:
|
|
282
|
+
return proxies
|
|
283
|
+
else:
|
|
284
|
+
proxies.append(proxy)
|
|
285
|
+
from_addr = proxy
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copyright 2022-2023 XProbe Inc.
|
|
2
|
+
# derived from copyright 1999-2021 Alibaba Group Holding Ltd.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from .backend import TestActorBackend
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Copyright 2022-2023 XProbe Inc.
|
|
2
|
+
# derived from copyright 1999-2021 Alibaba Group Holding Ltd.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from ...backend import register_backend
|
|
19
|
+
from ..indigen.backend import IndigenActorBackend
|
|
20
|
+
from .pool import TestMainActorPool
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@register_backend
|
|
24
|
+
class TestActorBackend(IndigenActorBackend):
|
|
25
|
+
@staticmethod
|
|
26
|
+
def name():
|
|
27
|
+
return "test"
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
async def create_actor_pool(
|
|
31
|
+
cls, address: str, n_process: int | None = None, **kwargs
|
|
32
|
+
):
|
|
33
|
+
from ..pool import create_actor_pool
|
|
34
|
+
|
|
35
|
+
assert n_process is not None
|
|
36
|
+
return await create_actor_pool(
|
|
37
|
+
address, pool_cls=TestMainActorPool, n_process=n_process, **kwargs
|
|
38
|
+
)
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Copyright 2022-2023 XProbe Inc.
|
|
2
|
+
# derived from copyright 1999-2021 Alibaba Group Holding Ltd.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import sys
|
|
20
|
+
from typing import Any, Optional
|
|
21
|
+
|
|
22
|
+
from ..communication import DummyServer, gen_local_address
|
|
23
|
+
from ..config import ActorPoolConfig
|
|
24
|
+
from ..indigen.pool import MainActorPool, SubActorPool
|
|
25
|
+
from ..message import ControlMessage, ControlMessageType, new_message_id
|
|
26
|
+
from ..pool import ActorPoolType
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestMainActorPool(MainActorPool):
|
|
30
|
+
@classmethod
|
|
31
|
+
def get_external_addresses(
|
|
32
|
+
cls,
|
|
33
|
+
address: str,
|
|
34
|
+
n_process: int | None = None,
|
|
35
|
+
ports: list[int] | None = None,
|
|
36
|
+
schemes: list[Optional[str]] | None = None,
|
|
37
|
+
):
|
|
38
|
+
if "://" in address:
|
|
39
|
+
address = address.split("://", 1)[1]
|
|
40
|
+
return super().get_external_addresses(address, n_process=n_process, ports=ports)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def gen_internal_address(
|
|
44
|
+
cls, process_index: int, external_address: str | None = None
|
|
45
|
+
) -> str:
|
|
46
|
+
return f"dummy://{process_index}"
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
async def start_sub_pool(
|
|
50
|
+
cls,
|
|
51
|
+
actor_pool_config: ActorPoolConfig,
|
|
52
|
+
process_index: int,
|
|
53
|
+
start_python: str | None = None,
|
|
54
|
+
):
|
|
55
|
+
return await cls._create_sub_pool_test(actor_pool_config, process_index, 0)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
async def wait_sub_pools_ready(cls, create_pool_tasks: list[asyncio.Task]):
|
|
59
|
+
addresses = []
|
|
60
|
+
tasks = []
|
|
61
|
+
for t in create_pool_tasks:
|
|
62
|
+
pool_task, external_addresses = await t
|
|
63
|
+
tasks.append(pool_task)
|
|
64
|
+
addresses.append(external_addresses)
|
|
65
|
+
return tasks, addresses
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
async def _create_sub_pool_test(
|
|
69
|
+
cls,
|
|
70
|
+
actor_config: ActorPoolConfig,
|
|
71
|
+
process_index: int,
|
|
72
|
+
main_pool_pid: int,
|
|
73
|
+
):
|
|
74
|
+
pool: TestSubActorPool = await TestSubActorPool.create(
|
|
75
|
+
{
|
|
76
|
+
"actor_pool_config": actor_config,
|
|
77
|
+
"process_index": process_index,
|
|
78
|
+
"main_pool_pid": main_pool_pid,
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
await pool.start()
|
|
82
|
+
actor_config.reset_pool_external_address(process_index, [pool.external_address])
|
|
83
|
+
cur_pool_config = actor_config.get_pool_config(process_index)
|
|
84
|
+
return None, cur_pool_config["external_address"]
|
|
85
|
+
|
|
86
|
+
def _sync_pool_config(self, actor_pool_config: ActorPoolConfig):
|
|
87
|
+
# test pool does not create routers, thus can skip this step
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
async def append_sub_pool(
|
|
91
|
+
self,
|
|
92
|
+
label: str | None = None,
|
|
93
|
+
internal_address: str | None = None,
|
|
94
|
+
external_address: str | None = None,
|
|
95
|
+
env: dict | None = None,
|
|
96
|
+
modules: list[str] | None = None,
|
|
97
|
+
suspend_sigint: bool | None = None,
|
|
98
|
+
use_uvloop: bool | None = None,
|
|
99
|
+
logging_conf: dict | None = None,
|
|
100
|
+
start_python: str | None = None,
|
|
101
|
+
kwargs: dict | None = None,
|
|
102
|
+
):
|
|
103
|
+
external_address = (
|
|
104
|
+
external_address
|
|
105
|
+
or TestMainActorPool.get_external_addresses(
|
|
106
|
+
self.external_address, n_process=1
|
|
107
|
+
)[-1]
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# use last process index's logging_conf and use_uv_loop config if not provide
|
|
111
|
+
actor_pool_config = self._config.as_dict()
|
|
112
|
+
last_process_index = self._config.get_process_indexes()[-1]
|
|
113
|
+
last_logging_conf = actor_pool_config["pools"][last_process_index][
|
|
114
|
+
"logging_conf"
|
|
115
|
+
]
|
|
116
|
+
last_use_uv_loop = actor_pool_config["pools"][last_process_index]["use_uvloop"]
|
|
117
|
+
_logging_conf = logging_conf or last_logging_conf
|
|
118
|
+
_use_uv_loop = use_uvloop if use_uvloop is not None else last_use_uv_loop
|
|
119
|
+
|
|
120
|
+
process_index = next(TestMainActorPool.process_index_gen(external_address))
|
|
121
|
+
internal_address = internal_address or TestMainActorPool.gen_internal_address(
|
|
122
|
+
process_index, external_address
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
self._config.add_pool_conf(
|
|
126
|
+
process_index,
|
|
127
|
+
label,
|
|
128
|
+
internal_address,
|
|
129
|
+
external_address,
|
|
130
|
+
env,
|
|
131
|
+
modules,
|
|
132
|
+
suspend_sigint,
|
|
133
|
+
_use_uv_loop,
|
|
134
|
+
_logging_conf,
|
|
135
|
+
kwargs,
|
|
136
|
+
)
|
|
137
|
+
pool_task = asyncio.create_task(
|
|
138
|
+
TestMainActorPool.start_sub_pool(self._config, process_index)
|
|
139
|
+
)
|
|
140
|
+
tasks, addresses = await TestMainActorPool.wait_sub_pools_ready([pool_task])
|
|
141
|
+
|
|
142
|
+
self.attach_sub_process(addresses[0][0], tasks[0])
|
|
143
|
+
|
|
144
|
+
control_message = ControlMessage(
|
|
145
|
+
message_id=new_message_id(),
|
|
146
|
+
address=self.external_address,
|
|
147
|
+
control_message_type=ControlMessageType.sync_config,
|
|
148
|
+
content=self._config,
|
|
149
|
+
)
|
|
150
|
+
await self.handle_control_command(control_message)
|
|
151
|
+
|
|
152
|
+
return addresses[0][0]
|
|
153
|
+
|
|
154
|
+
async def kill_sub_pool(
|
|
155
|
+
self, process: asyncio.subprocess.Process, force: bool = False
|
|
156
|
+
):
|
|
157
|
+
# Test pool uses None for processes, so skip if process is None
|
|
158
|
+
if process is None:
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
if force:
|
|
162
|
+
try:
|
|
163
|
+
process.kill()
|
|
164
|
+
except ProcessLookupError:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
# Ensure process is completely terminated and cleaned up
|
|
168
|
+
try:
|
|
169
|
+
# Wait for process to complete
|
|
170
|
+
if process.returncode is None:
|
|
171
|
+
try:
|
|
172
|
+
await asyncio.wait_for(process.wait(), timeout=5.0)
|
|
173
|
+
except asyncio.TimeoutError:
|
|
174
|
+
pass
|
|
175
|
+
except ProcessLookupError:
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
# Python 3.13 specific cleanup for waitpid threads
|
|
179
|
+
if sys.version_info >= (3, 13):
|
|
180
|
+
try:
|
|
181
|
+
# Close the transport to clean up waitpid thread
|
|
182
|
+
if hasattr(process, "_transport") and process._transport:
|
|
183
|
+
process._transport.close()
|
|
184
|
+
# Also try to close the pipe transport if it exists
|
|
185
|
+
if hasattr(process, "_pipes") and process._pipes:
|
|
186
|
+
for pipe in process._pipes.values():
|
|
187
|
+
if hasattr(pipe, "close"):
|
|
188
|
+
pipe.close()
|
|
189
|
+
except Exception:
|
|
190
|
+
# Ignore errors during cleanup
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
async def is_sub_pool_alive(self, process: asyncio.subprocess.Process):
|
|
194
|
+
# Test pool uses None for processes, so always return True
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class TestSubActorPool(SubActorPool):
|
|
199
|
+
def _sync_pool_config(self, actor_pool_config: ActorPoolConfig):
|
|
200
|
+
# test pool does not create routers, thus can skip this step
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
@classmethod
|
|
204
|
+
async def create(cls, config: dict) -> ActorPoolType:
|
|
205
|
+
kw: dict[str, Any] = dict()
|
|
206
|
+
cls._parse_config(config, kw)
|
|
207
|
+
process_index: int = kw["process_index"]
|
|
208
|
+
actor_pool_config = kw["config"] # type: ActorPoolConfig
|
|
209
|
+
external_addresses = actor_pool_config.get_pool_config(process_index)[
|
|
210
|
+
"external_address"
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
def handle_channel(channel):
|
|
214
|
+
return pool.on_new_channel(channel)
|
|
215
|
+
|
|
216
|
+
# create servers
|
|
217
|
+
server_addresses = external_addresses + [gen_local_address(process_index)]
|
|
218
|
+
server_addresses = sorted(set(server_addresses))
|
|
219
|
+
servers = await cls._create_servers(
|
|
220
|
+
server_addresses, handle_channel, actor_pool_config.get_comm_config()
|
|
221
|
+
)
|
|
222
|
+
cls._update_stored_addresses(servers, server_addresses, actor_pool_config, kw)
|
|
223
|
+
|
|
224
|
+
# create pool
|
|
225
|
+
pool = cls(**kw)
|
|
226
|
+
return pool # type: ignore
|
|
227
|
+
|
|
228
|
+
async def stop(self):
|
|
229
|
+
# do not close dummy server
|
|
230
|
+
self._servers = [
|
|
231
|
+
s for s in self._servers[:-1] if not isinstance(s, DummyServer)
|
|
232
|
+
]
|
|
233
|
+
await super().stop()
|