|
17 | 17 | from typing import TYPE_CHECKING |
18 | 18 | from typing import Any |
19 | 19 | from typing import Literal |
| 20 | +from typing import TypeAlias |
20 | 21 | from typing import overload |
21 | 22 |
|
22 | 23 | from . import gateway_bootstrap |
@@ -328,25 +329,44 @@ def waitclose(self) -> None: |
328 | 329 | raise first |
329 | 330 |
|
330 | 331 |
|
| 332 | +TermKillFunc: TypeAlias = Callable[[], object] |
| 333 | +TermKillPair: TypeAlias = tuple[TermKillFunc, TermKillFunc] |
| 334 | + |
| 335 | + |
331 | 336 | def safe_terminate( |
332 | | - execmodel: ExecModel, timeout: float | None, list_of_paired_functions |
| 337 | + execmodel: ExecModel, |
| 338 | + timeout: float | None, |
| 339 | + list_of_paired_functions: Sequence[TermKillPair], |
333 | 340 | ) -> None: |
| 341 | + """Run terminate/kill pairs in parallel with a hard wait bound. |
| 342 | +
|
| 343 | + Each termfunc is given ``timeout``. If it does not finish, killfunc runs. |
| 344 | + Waiting for the worker pool is also bounded so a stuck kill cannot hang |
| 345 | + the caller forever (see issues #43 / #221). |
| 346 | + """ |
334 | 347 | workerpool = WorkerPool(execmodel) |
335 | 348 |
|
336 | | - def termkill(termfunc, killfunc) -> None: |
| 349 | + def termkill(termfunc: TermKillFunc, killfunc: TermKillFunc) -> None: |
337 | 350 | termreply = workerpool.spawn(termfunc) |
338 | 351 | try: |
339 | 352 | termreply.get(timeout=timeout) |
340 | 353 | except OSError: |
341 | 354 | killfunc() |
342 | 355 |
|
343 | | - replylist = [] |
344 | | - for termfunc, killfunc in list_of_paired_functions: |
345 | | - reply = workerpool.spawn(termkill, termfunc, killfunc) |
346 | | - replylist.append(reply) |
| 356 | + replylist = [ |
| 357 | + workerpool.spawn(termkill, termfunc, killfunc) |
| 358 | + for termfunc, killfunc in list_of_paired_functions |
| 359 | + ] |
| 360 | + # Allow term timeout plus a kill attempt; never block indefinitely. |
| 361 | + wait_timeout = None if timeout is None else timeout * 2 |
347 | 362 | for reply in replylist: |
348 | | - reply.get() |
349 | | - workerpool.waitall(timeout=timeout) |
| 363 | + try: |
| 364 | + reply.waitfinish(timeout=wait_timeout) |
| 365 | + except OSError: |
| 366 | + # termkill still running (typically stuck in killfunc). |
| 367 | + continue |
| 368 | + reply.get() # propagate worker exceptions, if any |
| 369 | + workerpool.waitall(timeout=wait_timeout) |
350 | 370 |
|
351 | 371 |
|
352 | 372 | default_group = Group() |
|
0 commit comments