1"""The worker pool every multi-burn Layer X job uses: the optimiser, the trade study and the sweep.
3Each job used to build its own ``ProcessPoolExecutor`` with its own copy of the setup, the card
4seeding and the shutdown, and two of the three waited for a burn to finish (one to two minutes
5flown) before noticing a cancel. One pool here: spawned workers seeded with the parent's engine
6cards, a map that wakes every half second to look for a cancel, and a shutdown that terminates
10from __future__
import annotations
13from typing
import Any, Callable, Dict, List, Optional, Sequence, Tuple
17 """The job was cancelled. Raised, never returned; the router reports it as cancelled and any
18 other exception as a failure."""
20 def __init__(self, message: str =
"cancelled") ->
None:
29 """Seed this process's card cache with the parent's cards, so a worker reuses them."""
32 for key, value
in cards:
33 _card._CACHE[key] = value
37 """One per core less one for the server, never more than there are tasks."""
38 return max(1,
min(cap
or max((os.cpu_count()
or 2) - 1, 1),
max(tasks, 1)))
42 """Spawned workers for whole burns. ``with WorkerPool(n) as pool: pool.map(fn, args, ...)``.
44 With one worker, or where processes cannot be spawned (a sandbox), it runs in this process.
48 from concurrent.futures
import ProcessPoolExecutor
49 from multiprocessing
import get_context
57 self.
_executor = ProcessPoolExecutor(max_workers=self.
workers, mp_context=get_context(
"spawn"),
58 initializer=_init_worker, initargs=(list(_card._CACHE.items()),))
59 except (PermissionError, OSError):
67 def map(self, fn: Callable[[Any], Dict[str, Any]], args: Sequence[Any], *,
68 cancelled: Callable[[], bool] =
lambda:
False,
69 on_result: Optional[Callable[[int, Dict[str, Any]],
None]] =
None) -> List[Dict[str, Any]]:
70 """``fn`` over ``args``, results in order. ``on_result(done_count, result)`` as each lands.
71 Raises :class:`Cancelled` within ``POLL_S`` of ``cancelled()`` turning true."""
72 out: List[Optional[Dict[str, Any]]] = [
None] * len(args)
74 for k, a
in enumerate(args):
80 on_result(k + 1, out[k])
82 from concurrent.futures
import FIRST_COMPLETED, wait
84 futures = {self.
_executor.submit(fn, a): k
for k, a
in enumerate(args)}
85 pending = set(futures)
88 finished, pending = wait(pending, timeout=POLL_S, return_when=FIRST_COMPLETED)
94 on_result(done_count, out[k])
95 if pending
and cancelled():
102 def close(self, cancelled: bool =
False) ->
None:
103 """Shut the workers down. On a cancel they are terminated: a burn in flight is 30-60 s,
104 and leaving it running after the job released its slot stacks the next job on top of it."""
109 for proc
in list((getattr(ex,
"_processes",
None)
or {}).values()):
114 ex.shutdown(wait=
False, cancel_futures=
True)
116 ex.shutdown(wait=
True)
121 def __exit__(self, exc_type: Any, exc: Any, tb: Any) ->
None:
123 self.
close(cancelled=exc_type
is not None)
None __init__(self, str message="cancelled")
None __init__(self, int workers)
"WorkerPool" __enter__(self)
None __exit__(self, Any exc_type, Any exc, Any tb)
List[Dict[str, Any]] map(self, Callable[[Any], Dict[str, Any]] fn, Sequence[Any] args, *, Callable[[], bool] cancelled=lambda:False, Optional[Callable[[int, Dict[str, Any]], None]] on_result=None)
None close(self, bool cancelled=False)
None _init_worker(List[Tuple[Any, Any]] cards)
int default_workers(int tasks, Optional[int] cap=None)