STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
pool.py
Go to the documentation of this file.
1"""The worker pool every multi-burn Layer X job uses: the optimiser, the trade study and the sweep.
2
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
7the workers on one.
8"""
9
10from __future__ import annotations
11
12import os
13from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
14
15
16class Cancelled(RuntimeError):
17 """The job was cancelled. Raised, never returned; the router reports it as cancelled and any
18 other exception as a failure."""
19
20 def __init__(self, message: str = "cancelled") -> None:
21 super().__init__(message)
22
23
24#: How often a wait looks for a cancel [s].
25POLL_S = 0.5
26
27
28def _init_worker(cards: List[Tuple[Any, Any]]) -> None:
29 """Seed this process's card cache with the parent's cards, so a worker reuses them."""
30 from engine.layerx import card as _card
31
32 for key, value in cards:
33 _card._CACHE[key] = value
34
35
36def default_workers(tasks: int, cap: Optional[int] = None) -> int:
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)))
39
40
42 """Spawned workers for whole burns. ``with WorkerPool(n) as pool: pool.map(fn, args, ...)``.
43
44 With one worker, or where processes cannot be spawned (a sandbox), it runs in this process.
45 """
46
47 def __init__(self, workers: int) -> None:
48 from concurrent.futures import ProcessPoolExecutor
49 from multiprocessing import get_context
50
51 from engine.layerx import card as _card
52
53 self.workers = max(1, int(workers))
54 self._executor: Any = None
55 if self.workers > 1:
56 try:
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):
60 self._executor = None
61 self._cancelled = False
62
63 @property
64 def parallel(self) -> bool:
65 return self._executor is not None
66
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)
73 if self._executor is None:
74 for k, a in enumerate(args):
75 if cancelled():
76 self._cancelled = True
77 raise Cancelled()
78 out[k] = fn(a)
79 if on_result:
80 on_result(k + 1, out[k]) # type: ignore[arg-type]
81 return out # type: ignore[return-value]
82 from concurrent.futures import FIRST_COMPLETED, wait
83
84 futures = {self._executor.submit(fn, a): k for k, a in enumerate(args)}
85 pending = set(futures)
86 done_count = 0
87 while pending:
88 finished, pending = wait(pending, timeout=POLL_S, return_when=FIRST_COMPLETED)
89 for fut in finished:
90 k = futures[fut]
91 out[k] = fut.result()
92 done_count += 1
93 if on_result:
94 on_result(done_count, out[k]) # type: ignore[arg-type]
95 if pending and cancelled():
96 for fut in pending:
97 fut.cancel()
98 self._cancelled = True
99 raise Cancelled()
100 return out # type: ignore[return-value]
101
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."""
105 ex, self._executor = self._executor, None
106 if ex is None:
107 return
108 if cancelled or self._cancelled:
109 for proc in list((getattr(ex, "_processes", None) or {}).values()):
110 try:
111 proc.terminate()
112 except Exception: # noqa: BLE001 - already gone
113 pass
114 ex.shutdown(wait=False, cancel_futures=True)
115 else:
116 ex.shutdown(wait=True)
117
118 def __enter__(self) -> "WorkerPool":
119 return self
120
121 def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
122 # Any exception ends the job: terminate rather than wait out burns nobody will read.
123 self.close(cancelled=exc_type is not None)
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
None __init__(self, str message="cancelled")
Definition pool.py:20
None __init__(self, int workers)
Definition pool.py:47
"WorkerPool" __enter__(self)
Definition pool.py:118
None __exit__(self, Any exc_type, Any exc, Any tb)
Definition pool.py:121
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)
Definition pool.py:69
None close(self, bool cancelled=False)
Definition pool.py:102
None _init_worker(List[Tuple[Any, Any]] cards)
Definition pool.py:28
int default_workers(int tasks, Optional[int] cap=None)
Definition pool.py:36