|
1 | 1 | from __future__ import annotations
|
2 | 2 |
|
3 | 3 | import asyncio
|
| 4 | +import concurrent.futures |
4 | 5 | import gc
|
| 6 | +import multiprocessing |
5 | 7 | import os
|
6 | 8 | import signal
|
| 9 | +import socket |
7 | 10 | import sys
|
8 | 11 | import threading
|
9 | 12 | import weakref
|
|
12 | 15 |
|
13 | 16 | import psutil
|
14 | 17 | import pytest
|
15 |
| -from tornado import gen |
16 | 18 | from tornado.ioloop import IOLoop
|
17 |
| -from tornado.locks import Event |
18 | 19 |
|
19 |
| -from distributed.compatibility import LINUX, MACOS, WINDOWS |
| 20 | +from distributed.compatibility import LINUX, MACOS, WINDOWS, to_thread |
20 | 21 | from distributed.metrics import time
|
21 | 22 | from distributed.process import AsyncProcess
|
22 | 23 | from distributed.utils import get_mp_context
|
@@ -238,13 +239,9 @@ async def test_close():
|
238 | 239 | async def test_exit_callback():
|
239 | 240 | to_child = get_mp_context().Queue()
|
240 | 241 | from_child = get_mp_context().Queue()
|
241 |
| - evt = Event() |
| 242 | + evt = asyncio.Event() |
242 | 243 |
|
243 |
| - # FIXME: this breaks if changed to async def... |
244 |
| - @gen.coroutine |
245 | 244 | def on_stop(_proc):
|
246 |
| - assert _proc is proc |
247 |
| - yield gen.moment |
248 | 245 | evt.set()
|
249 | 246 |
|
250 | 247 | # Normal process exit
|
@@ -275,10 +272,69 @@ def on_stop(_proc):
|
275 | 272 | assert not evt.is_set()
|
276 | 273 |
|
277 | 274 | await proc.terminate()
|
278 |
| - await evt.wait(timedelta(seconds=5)) |
| 275 | + await asyncio.wait_for(evt.wait(), 5) |
279 | 276 | assert evt.is_set()
|
280 | 277 |
|
281 | 278 |
|
| 279 | +def _run_and_close_tornado(async_fn, /, *args, **kwargs): |
| 280 | + tornado_loop = None |
| 281 | + |
| 282 | + async def inner_fn(): |
| 283 | + nonlocal tornado_loop |
| 284 | + tornado_loop = IOLoop.current() |
| 285 | + return await async_fn(*args, **kwargs) |
| 286 | + |
| 287 | + try: |
| 288 | + return asyncio.run(inner_fn()) |
| 289 | + finally: |
| 290 | + tornado_loop.close(all_fds=True) |
| 291 | + |
| 292 | + |
| 293 | +def _write_byte_wait_closed(sock): |
| 294 | + with sock: |
| 295 | + sock.send(b"\x00") |
| 296 | + sock.recv(1) |
| 297 | + |
| 298 | + |
| 299 | +async def _check_process_reaped_elsewhere(): |
| 300 | + loop = asyncio.get_running_loop() |
| 301 | + |
| 302 | + def psutil_terminate(pid): |
| 303 | + proc = psutil.Process(pid) |
| 304 | + proc.terminate() |
| 305 | + proc.wait() |
| 306 | + |
| 307 | + a, b = socket.socketpair() |
| 308 | + with a: |
| 309 | + with b: |
| 310 | + proc = AsyncProcess( |
| 311 | + target=_write_byte_wait_closed, args=(b,), loop=IOLoop.current() |
| 312 | + ) |
| 313 | + await proc.start() |
| 314 | + |
| 315 | + a.setblocking(False) |
| 316 | + assert await loop.sock_recv(a, 1) == b"\00" |
| 317 | + await to_thread(psutil_terminate, proc.pid) |
| 318 | + await proc.join() |
| 319 | + return proc.exitcode |
| 320 | + |
| 321 | + |
| 322 | +def test_process_reaped_elsewhere(cleanup): |
| 323 | + with concurrent.futures.ProcessPoolExecutor( |
| 324 | + max_workers=1, mp_context=multiprocessing.get_context("spawn") |
| 325 | + ) as pool: |
| 326 | + # this needs to be run in a process pool because reaping a process |
| 327 | + # outside multiprocessing causes it to remain in |
| 328 | + # multiprocessing.active_children() forever - which blocks cleanup for |
| 329 | + # 40 seconds |
| 330 | + assert ( |
| 331 | + pool.submit( |
| 332 | + _run_and_close_tornado, _check_process_reaped_elsewhere |
| 333 | + ).result() |
| 334 | + == 255 |
| 335 | + ) |
| 336 | + |
| 337 | + |
282 | 338 | @gen_test()
|
283 | 339 | async def test_child_main_thread():
|
284 | 340 | """
|
|
0 commit comments