xoscar 0.7.17__tar.gz → 0.8.0rc1__tar.gz
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.
Potentially problematic release.
This version of xoscar might be problematic. Click here for more details.
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/PKG-INFO +1 -1
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/pyproject.toml +2 -2
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/communication/socket.py +4 -4
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/core.py +57 -9
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/indigen/pool.py +59 -15
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/pool.py +19 -14
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/test/pool.py +37 -1
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/utils.py +5 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar.egg-info/PKG-INFO +1 -1
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/MANIFEST.in +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/setup.cfg +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/setup.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/versioneer.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/__init__.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/_utils.pxd +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/_utils.pyx +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/_version.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/aio/__init__.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/aio/base.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/aio/file.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/aio/lru.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/aio/parallelism.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/api.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backend.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/__init__.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/allocate_strategy.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/communication/__init__.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/communication/base.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/communication/core.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/communication/dummy.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/communication/errors.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/communication/ucx.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/communication/utils.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/config.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/context.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/indigen/__init__.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/indigen/__main__.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/indigen/backend.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/indigen/driver.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/indigen/fate_sharing.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/indigen/shared_memory.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/message.pyi +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/message.pyx +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/router.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/test/__init__.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/backends/test/backend.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/batch.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/collective/__init__.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/collective/backend/__init__.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/collective/backend/nccl_backend.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/collective/common.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/collective/core.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/collective/process_group.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/collective/utils.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/collective/xoscar_pygloo.pyi +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/constants.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/context.pxd +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/context.pyx +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/core.pxd +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/core.pyx +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/debug.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/driver.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/errors.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/libcpp.pxd +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/metrics/__init__.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/metrics/api.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/metrics/backends/__init__.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/metrics/backends/console/__init__.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/metrics/backends/console/console_metric.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/metrics/backends/metric.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/metrics/backends/prometheus/__init__.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/metrics/backends/prometheus/prometheus_metric.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/nvutils.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/profiling.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/serialization/__init__.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/serialization/aio.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/serialization/core.pxd +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/serialization/core.pyi +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/serialization/core.pyx +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/serialization/cuda.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/serialization/exception.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/serialization/mlx.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/serialization/numpy.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/serialization/pyfury.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/serialization/scipy.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/virtualenv/__init__.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/virtualenv/core.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/virtualenv/platform.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/virtualenv/utils.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar/virtualenv/uv.py +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar.egg-info/SOURCES.txt +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar.egg-info/dependency_links.txt +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar.egg-info/not-zip-safe +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar.egg-info/requires.txt +0 -0
- {xoscar-0.7.17 → xoscar-0.8.0rc1}/xoscar.egg-info/top_level.txt +0 -0
|
@@ -51,6 +51,6 @@ log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(linen
|
|
|
51
51
|
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
|
|
52
52
|
|
|
53
53
|
[tool.cibuildwheel]
|
|
54
|
-
build = ["cp39-*", "cp310-*", "cp311-*", "cp312-*"]
|
|
55
|
-
skip = "pp* *musllinux* *i686
|
|
54
|
+
build = ["cp39-*", "cp310-*", "cp311-*", "cp312-*", "cp313-*"]
|
|
55
|
+
skip = "pp* *musllinux* *i686 cp38-win32 cp39-win32 cp310-win32 cp311-win32 cp312-win32 cp313-win32"
|
|
56
56
|
manylinux-x86_64-image = "manylinux2014"
|
|
@@ -31,7 +31,7 @@ from urllib.parse import urlparse
|
|
|
31
31
|
from ..._utils import to_binary
|
|
32
32
|
from ...constants import XOSCAR_CONNECT_TIMEOUT, XOSCAR_UNIX_SOCKET_DIR
|
|
33
33
|
from ...serialization import AioDeserializer, AioSerializer, deserialize
|
|
34
|
-
from ...utils import classproperty, implements, is_py_312, is_v6_ip
|
|
34
|
+
from ...utils import classproperty, implements, is_py_312, is_py_312_or_above, is_v6_ip
|
|
35
35
|
from .base import Channel, ChannelType, Client, Server
|
|
36
36
|
from .core import register_client, register_server
|
|
37
37
|
from .errors import ChannelClosed
|
|
@@ -192,9 +192,9 @@ class _BaseSocketServer(Server, metaclass=ABCMeta):
|
|
|
192
192
|
@implements(Server.stop)
|
|
193
193
|
async def stop(self):
|
|
194
194
|
self._aio_server.close()
|
|
195
|
-
# Python 3.12
|
|
196
|
-
# `wait_closed` leads to hang
|
|
197
|
-
if not
|
|
195
|
+
# Python 3.12+: # https://github.com/python/cpython/issues/104344
|
|
196
|
+
# `wait_closed` leads to hang in Python 3.12 and 3.13
|
|
197
|
+
if not is_py_312_or_above():
|
|
198
198
|
await self._aio_server.wait_closed()
|
|
199
199
|
# close all channels
|
|
200
200
|
await asyncio.gather(
|
|
@@ -244,7 +244,27 @@ def _cancel_all_tasks(loop):
|
|
|
244
244
|
for task in to_cancel:
|
|
245
245
|
task.cancel()
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
# In Python 3.13+, we need to use a different approach to avoid deadlocks
|
|
248
|
+
# when shutting down event loops in threads
|
|
249
|
+
if hasattr(asyncio, "run"):
|
|
250
|
+
# For Python 3.13+, use a more robust approach
|
|
251
|
+
async def _gather_cancelled():
|
|
252
|
+
await asyncio.gather(*to_cancel, return_exceptions=True)
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
# Try to run the gather in the current loop context
|
|
256
|
+
if loop.is_running():
|
|
257
|
+
# If loop is running, schedule the gather
|
|
258
|
+
asyncio.run_coroutine_threadsafe(_gather_cancelled(), loop)
|
|
259
|
+
else:
|
|
260
|
+
# If loop is not running, we can run it directly
|
|
261
|
+
loop.run_until_complete(_gather_cancelled())
|
|
262
|
+
except RuntimeError:
|
|
263
|
+
# If we can't run the gather, just log and continue
|
|
264
|
+
logger.debug("Could not gather cancelled tasks during shutdown")
|
|
265
|
+
else:
|
|
266
|
+
# For older Python versions, use the original approach
|
|
267
|
+
loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True))
|
|
248
268
|
|
|
249
269
|
for task in to_cancel:
|
|
250
270
|
if task.cancelled():
|
|
@@ -263,8 +283,15 @@ def _safe_run_forever(loop):
|
|
|
263
283
|
try:
|
|
264
284
|
loop.run_forever()
|
|
265
285
|
finally:
|
|
266
|
-
|
|
267
|
-
|
|
286
|
+
try:
|
|
287
|
+
_cancel_all_tasks(loop)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.debug("Error during task cancellation: %s", e)
|
|
290
|
+
finally:
|
|
291
|
+
try:
|
|
292
|
+
loop.stop()
|
|
293
|
+
except Exception as e:
|
|
294
|
+
logger.debug("Error stopping loop: %s", e)
|
|
268
295
|
|
|
269
296
|
|
|
270
297
|
class ActorCaller:
|
|
@@ -273,12 +300,31 @@ class ActorCaller:
|
|
|
273
300
|
class _RefHolder:
|
|
274
301
|
pass
|
|
275
302
|
|
|
276
|
-
_close_loop =
|
|
277
|
-
_close_thread =
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
303
|
+
_close_loop = None
|
|
304
|
+
_close_thread = None
|
|
305
|
+
_initialized = False
|
|
306
|
+
|
|
307
|
+
@classmethod
|
|
308
|
+
def _ensure_initialized(cls):
|
|
309
|
+
if not cls._initialized:
|
|
310
|
+
cls._close_loop = asyncio.new_event_loop()
|
|
311
|
+
cls._close_thread = threading.Thread(
|
|
312
|
+
target=_safe_run_forever, args=(cls._close_loop,), daemon=True
|
|
313
|
+
)
|
|
314
|
+
cls._close_thread.start()
|
|
315
|
+
atexit.register(cls._cleanup)
|
|
316
|
+
cls._initialized = True
|
|
317
|
+
|
|
318
|
+
@classmethod
|
|
319
|
+
def _cleanup(cls):
|
|
320
|
+
if cls._close_loop and cls._close_loop.is_running():
|
|
321
|
+
try:
|
|
322
|
+
cls._close_loop.call_soon_threadsafe(cls._close_loop.stop)
|
|
323
|
+
# Give the loop a moment to stop
|
|
324
|
+
if cls._close_thread:
|
|
325
|
+
cls._close_thread.join(timeout=0.5) # Shorter timeout for tests
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.debug("Error during cleanup: %s", e)
|
|
282
328
|
|
|
283
329
|
def __init__(self):
|
|
284
330
|
self._thread_local = threading.local()
|
|
@@ -294,6 +340,8 @@ class ActorCaller:
|
|
|
294
340
|
# If the thread exit, we clean the related actor callers and channels.
|
|
295
341
|
|
|
296
342
|
def _cleanup():
|
|
343
|
+
self._ensure_initialized()
|
|
344
|
+
# Use the background thread for cleanup
|
|
297
345
|
asyncio.run_coroutine_threadsafe(actor_caller.stop(), self._close_loop)
|
|
298
346
|
logger.debug(
|
|
299
347
|
"Clean up the actor caller due to thread exit: %s", thread_info
|
|
@@ -418,26 +418,70 @@ class MainActorPool(MainActorPoolBase):
|
|
|
418
418
|
async def kill_sub_pool(
|
|
419
419
|
self, process: asyncio.subprocess.Process, force: bool = False
|
|
420
420
|
):
|
|
421
|
+
# First, try to terminate the process gracefully
|
|
422
|
+
if not force:
|
|
423
|
+
try:
|
|
424
|
+
process.terminate()
|
|
425
|
+
# Wait for graceful termination
|
|
426
|
+
try:
|
|
427
|
+
await asyncio.wait_for(process.wait(), timeout=2.0)
|
|
428
|
+
except asyncio.TimeoutError:
|
|
429
|
+
# Process didn't terminate gracefully, force kill
|
|
430
|
+
force = True
|
|
431
|
+
except ProcessLookupError:
|
|
432
|
+
# Process already terminated
|
|
433
|
+
pass
|
|
434
|
+
|
|
435
|
+
# Force kill if needed or if graceful termination failed
|
|
436
|
+
if force:
|
|
437
|
+
try:
|
|
438
|
+
process.kill()
|
|
439
|
+
except ProcessLookupError:
|
|
440
|
+
# Process already dead
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
# Ensure process is completely terminated and cleaned up
|
|
421
444
|
try:
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
445
|
+
# Wait for process to complete
|
|
446
|
+
if process.returncode is None:
|
|
447
|
+
try:
|
|
448
|
+
await asyncio.wait_for(process.wait(), timeout=5.0)
|
|
449
|
+
except asyncio.TimeoutError:
|
|
450
|
+
pass
|
|
451
|
+
except ProcessLookupError:
|
|
452
|
+
# Process already terminated
|
|
453
|
+
pass
|
|
425
454
|
|
|
426
|
-
|
|
427
|
-
|
|
455
|
+
# Python 3.13 specific cleanup for waitpid threads
|
|
456
|
+
if sys.version_info >= (3, 13):
|
|
428
457
|
try:
|
|
429
|
-
|
|
430
|
-
|
|
458
|
+
# Close the transport to clean up waitpid thread
|
|
459
|
+
if hasattr(process, "_transport") and process._transport:
|
|
460
|
+
process._transport.close()
|
|
461
|
+
# Also try to close the pipe transport if it exists
|
|
462
|
+
if hasattr(process, "_pipes") and process._pipes:
|
|
463
|
+
for pipe in process._pipes.values():
|
|
464
|
+
if hasattr(pipe, "close"):
|
|
465
|
+
pipe.close()
|
|
466
|
+
except Exception:
|
|
467
|
+
# Ignore errors during cleanup
|
|
431
468
|
pass
|
|
432
469
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
p.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
470
|
+
# Additional cleanup using psutil to ensure process tree is terminated
|
|
471
|
+
try:
|
|
472
|
+
p = psutil.Process(process.pid)
|
|
473
|
+
if p.is_running():
|
|
474
|
+
# Kill the entire process tree
|
|
475
|
+
for child in p.children(recursive=True):
|
|
476
|
+
try:
|
|
477
|
+
child.kill()
|
|
478
|
+
except psutil.NoSuchProcess:
|
|
479
|
+
pass
|
|
480
|
+
p.kill()
|
|
481
|
+
p.wait(timeout=2.0)
|
|
482
|
+
except (psutil.NoSuchProcess, psutil.TimeoutExpired):
|
|
483
|
+
# Process already dead or couldn't be killed
|
|
484
|
+
pass
|
|
441
485
|
|
|
442
486
|
async def is_sub_pool_alive(self, process: asyncio.subprocess.Process):
|
|
443
487
|
return process.returncode is None
|
|
@@ -1337,7 +1337,9 @@ class MainActorPoolBase(ActorPoolBase):
|
|
|
1337
1337
|
return pool
|
|
1338
1338
|
|
|
1339
1339
|
async def start_monitor(self):
|
|
1340
|
-
if
|
|
1340
|
+
# Only start monitor if there are sub processes to monitor
|
|
1341
|
+
# This prevents hanging when n_process=0
|
|
1342
|
+
if self._monitor_task is None and self.sub_processes:
|
|
1341
1343
|
self._monitor_task = asyncio.create_task(self.monitor_sub_pools())
|
|
1342
1344
|
return self._monitor_task
|
|
1343
1345
|
|
|
@@ -1351,7 +1353,12 @@ class MainActorPoolBase(ActorPoolBase):
|
|
|
1351
1353
|
self._auto_recover = False
|
|
1352
1354
|
self._stopped.set()
|
|
1353
1355
|
if self._monitor_task and not self._monitor_task.done():
|
|
1354
|
-
|
|
1356
|
+
# Cancel the monitor task to ensure it exits immediately
|
|
1357
|
+
self._monitor_task.cancel()
|
|
1358
|
+
try:
|
|
1359
|
+
await self._monitor_task
|
|
1360
|
+
except asyncio.CancelledError:
|
|
1361
|
+
pass # Expected when cancelling the task
|
|
1355
1362
|
self._monitor_task = None
|
|
1356
1363
|
await self.stop_sub_pools()
|
|
1357
1364
|
await super().stop()
|
|
@@ -1406,19 +1413,17 @@ class MainActorPoolBase(ActorPoolBase):
|
|
|
1406
1413
|
)
|
|
1407
1414
|
try:
|
|
1408
1415
|
if timeout is None:
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
force = True
|
|
1419
|
-
except (ConnectionError, ServerClosed): # pragma: no cover
|
|
1416
|
+
# Use a short timeout for graceful shutdown to avoid hanging
|
|
1417
|
+
timeout = 2.0
|
|
1418
|
+
|
|
1419
|
+
call = asyncio.create_task(self.call(address, stop_message))
|
|
1420
|
+
try:
|
|
1421
|
+
await asyncio.wait_for(call, timeout)
|
|
1422
|
+
except (futures.TimeoutError, asyncio.TimeoutError):
|
|
1423
|
+
force = True
|
|
1424
|
+
except (ConnectionError, ServerClosed):
|
|
1420
1425
|
# process dead maybe, ignore it
|
|
1421
|
-
|
|
1426
|
+
force = True
|
|
1422
1427
|
# kill process
|
|
1423
1428
|
await self.kill_sub_pool(process, force=force)
|
|
1424
1429
|
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
import asyncio
|
|
19
|
+
import sys
|
|
19
20
|
from typing import Any, Optional
|
|
20
21
|
|
|
21
22
|
from ..communication import DummyServer, gen_local_address
|
|
@@ -153,9 +154,44 @@ class TestMainActorPool(MainActorPool):
|
|
|
153
154
|
async def kill_sub_pool(
|
|
154
155
|
self, process: asyncio.subprocess.Process, force: bool = False
|
|
155
156
|
):
|
|
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
|
|
157
192
|
|
|
158
193
|
async def is_sub_pool_alive(self, process: asyncio.subprocess.Process):
|
|
194
|
+
# Test pool uses None for processes, so always return True
|
|
159
195
|
return True
|
|
160
196
|
|
|
161
197
|
|
|
@@ -470,6 +470,11 @@ def is_py_312():
|
|
|
470
470
|
return sys.version_info[:2] == (3, 12)
|
|
471
471
|
|
|
472
472
|
|
|
473
|
+
@lru_cache
|
|
474
|
+
def is_py_312_or_above():
|
|
475
|
+
return sys.version_info[:2] >= (3, 12)
|
|
476
|
+
|
|
477
|
+
|
|
473
478
|
def is_v4_zero_ip(ip_port_addr: str) -> bool:
|
|
474
479
|
return ip_port_addr.split("://")[-1].startswith("0.0.0.0:")
|
|
475
480
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|