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.
Files changed (94) hide show
  1. xoscar/__init__.py +61 -0
  2. xoscar/_utils.cpython-312-darwin.so +0 -0
  3. xoscar/_utils.pxd +36 -0
  4. xoscar/_utils.pyx +246 -0
  5. xoscar/_version.py +693 -0
  6. xoscar/aio/__init__.py +16 -0
  7. xoscar/aio/base.py +86 -0
  8. xoscar/aio/file.py +59 -0
  9. xoscar/aio/lru.py +228 -0
  10. xoscar/aio/parallelism.py +39 -0
  11. xoscar/api.py +527 -0
  12. xoscar/backend.py +67 -0
  13. xoscar/backends/__init__.py +14 -0
  14. xoscar/backends/allocate_strategy.py +160 -0
  15. xoscar/backends/communication/__init__.py +30 -0
  16. xoscar/backends/communication/base.py +315 -0
  17. xoscar/backends/communication/core.py +69 -0
  18. xoscar/backends/communication/dummy.py +253 -0
  19. xoscar/backends/communication/errors.py +20 -0
  20. xoscar/backends/communication/socket.py +444 -0
  21. xoscar/backends/communication/ucx.py +538 -0
  22. xoscar/backends/communication/utils.py +97 -0
  23. xoscar/backends/config.py +157 -0
  24. xoscar/backends/context.py +437 -0
  25. xoscar/backends/core.py +352 -0
  26. xoscar/backends/indigen/__init__.py +16 -0
  27. xoscar/backends/indigen/__main__.py +19 -0
  28. xoscar/backends/indigen/backend.py +51 -0
  29. xoscar/backends/indigen/driver.py +26 -0
  30. xoscar/backends/indigen/fate_sharing.py +221 -0
  31. xoscar/backends/indigen/pool.py +515 -0
  32. xoscar/backends/indigen/shared_memory.py +548 -0
  33. xoscar/backends/message.cpython-312-darwin.so +0 -0
  34. xoscar/backends/message.pyi +255 -0
  35. xoscar/backends/message.pyx +646 -0
  36. xoscar/backends/pool.py +1630 -0
  37. xoscar/backends/router.py +285 -0
  38. xoscar/backends/test/__init__.py +16 -0
  39. xoscar/backends/test/backend.py +38 -0
  40. xoscar/backends/test/pool.py +233 -0
  41. xoscar/batch.py +256 -0
  42. xoscar/collective/__init__.py +27 -0
  43. xoscar/collective/backend/__init__.py +13 -0
  44. xoscar/collective/backend/nccl_backend.py +160 -0
  45. xoscar/collective/common.py +102 -0
  46. xoscar/collective/core.py +737 -0
  47. xoscar/collective/process_group.py +687 -0
  48. xoscar/collective/utils.py +41 -0
  49. xoscar/collective/xoscar_pygloo.cpython-312-darwin.so +0 -0
  50. xoscar/collective/xoscar_pygloo.pyi +239 -0
  51. xoscar/constants.py +23 -0
  52. xoscar/context.cpython-312-darwin.so +0 -0
  53. xoscar/context.pxd +21 -0
  54. xoscar/context.pyx +368 -0
  55. xoscar/core.cpython-312-darwin.so +0 -0
  56. xoscar/core.pxd +51 -0
  57. xoscar/core.pyx +664 -0
  58. xoscar/debug.py +188 -0
  59. xoscar/driver.py +42 -0
  60. xoscar/errors.py +63 -0
  61. xoscar/libcpp.pxd +31 -0
  62. xoscar/metrics/__init__.py +21 -0
  63. xoscar/metrics/api.py +288 -0
  64. xoscar/metrics/backends/__init__.py +13 -0
  65. xoscar/metrics/backends/console/__init__.py +13 -0
  66. xoscar/metrics/backends/console/console_metric.py +82 -0
  67. xoscar/metrics/backends/metric.py +149 -0
  68. xoscar/metrics/backends/prometheus/__init__.py +13 -0
  69. xoscar/metrics/backends/prometheus/prometheus_metric.py +70 -0
  70. xoscar/nvutils.py +717 -0
  71. xoscar/profiling.py +260 -0
  72. xoscar/serialization/__init__.py +20 -0
  73. xoscar/serialization/aio.py +141 -0
  74. xoscar/serialization/core.cpython-312-darwin.so +0 -0
  75. xoscar/serialization/core.pxd +28 -0
  76. xoscar/serialization/core.pyi +57 -0
  77. xoscar/serialization/core.pyx +944 -0
  78. xoscar/serialization/cuda.py +111 -0
  79. xoscar/serialization/exception.py +48 -0
  80. xoscar/serialization/mlx.py +67 -0
  81. xoscar/serialization/numpy.py +82 -0
  82. xoscar/serialization/pyfury.py +37 -0
  83. xoscar/serialization/scipy.py +72 -0
  84. xoscar/serialization/torch.py +180 -0
  85. xoscar/utils.py +522 -0
  86. xoscar/virtualenv/__init__.py +34 -0
  87. xoscar/virtualenv/core.py +268 -0
  88. xoscar/virtualenv/platform.py +56 -0
  89. xoscar/virtualenv/utils.py +100 -0
  90. xoscar/virtualenv/uv.py +321 -0
  91. xoscar-0.9.0.dist-info/METADATA +230 -0
  92. xoscar-0.9.0.dist-info/RECORD +94 -0
  93. xoscar-0.9.0.dist-info/WHEEL +6 -0
  94. 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()