| 221 | |
| 222 | |
| 223 | class PolicyController: |
| 224 | def __init__(self, player_name: str, path: str, *, timeout: float, max_errors: int): |
| 225 | self.player_name = player_name |
| 226 | self.path = path |
| 227 | self.timeout = max(float(timeout), 0.01) |
| 228 | self.max_errors = max(int(max_errors), 1) |
| 229 | self.disabled = False |
| 230 | self.decisions = 0 |
| 231 | self.policy_errors = 0 |
| 232 | self.invalid_decisions = 0 |
| 233 | self.error_samples: list[dict[str, str]] = [] |
| 234 | self.startup_error: str | None = None |
| 235 | self._next_request_id = 0 |
| 236 | self._start_worker() |
| 237 | |
| 238 | def _start_worker(self) -> None: |
| 239 | self.startup_error = None |
| 240 | ctx = mp.get_context("spawn") |
| 241 | self.command_queue = ctx.Queue() |
| 242 | self.result_queue = ctx.Queue() |
| 243 | self.startup_queue = ctx.Queue() |
| 244 | self.process = ctx.Process( |
| 245 | target=policy_worker, |
| 246 | args=(self.command_queue, self.result_queue, self.startup_queue, self.player_name, self.path), |
| 247 | ) |
| 248 | self.process.start() |
| 249 | startup_timeout = max(self.timeout, 10.0) |
| 250 | try: |
| 251 | startup_message = self.startup_queue.get(timeout=startup_timeout) |
| 252 | except queue.Empty: |
| 253 | self.startup_error = f"policy import exceeded {startup_timeout}s timeout" |
| 254 | self.close() |
| 255 | return |
| 256 | if "error" in startup_message: |
| 257 | self.startup_error = startup_message["error"] |
| 258 | self.close() |
| 259 | |
| 260 | def _record_error(self, event: str, error: str, *, invalid: bool = False) -> None: |
| 261 | self.policy_errors += 1 |
| 262 | if invalid: |
| 263 | self.invalid_decisions += 1 |
| 264 | if len(self.error_samples) < 5: |
| 265 | self.error_samples.append({"event": event, "error": error}) |
| 266 | if self.policy_errors >= self.max_errors: |
| 267 | self.disabled = True |
| 268 | self.close() |
| 269 | |
| 270 | def close(self) -> None: |
| 271 | with contextlib.suppress(Exception): |
| 272 | self.command_queue.put_nowait(None) |
| 273 | self.process.join(0.1) |
| 274 | if self.process.is_alive(): |
| 275 | self.process.terminate() |
| 276 | self.process.join(0.1) |
| 277 | if self.process.is_alive(): |
| 278 | self.process.kill() |
| 279 | self.process.join() |
| 280 | |