from __future__ import annotations import asyncio from concurrent.futures import ThreadPoolExecutor from time import sleep from typing import Any import pytest from cmvr_edge_ai.workers import ( PersistentProcessWorker, ProcessWorkerError, RemoteWorkerError, ) def _worker_handler(payload: Any) -> Any: if payload == "explode": raise RuntimeError("intentional remote failure") if payload == "slow": sleep(0.3) return "late-result" return payload * 2 def _worker_factory(): # type: ignore[no-untyped-def] """Top-level factory so the spawn start method can import and pickle it.""" return _worker_handler async def _keep_restricted_event_loop_responsive() -> None: """Provide wakeups where sandboxed self-pipe notifications are unavailable.""" while True: await asyncio.sleep(0.01) def test_spawn_worker_returns_results_and_surfaces_remote_errors() -> None: async def exercise(): # type: ignore[no-untyped-def] loop = asyncio.get_running_loop() host_executor = ThreadPoolExecutor(max_workers=2) loop.set_default_executor(host_executor) ticker = asyncio.create_task(_keep_restricted_event_loop_responsive()) worker = PersistentProcessWorker( _worker_factory, start_method="spawn", queue_capacity=1, ) try: await worker.start() assert await worker.submit(21, timeout_s=10) == 42 with pytest.raises(RemoteWorkerError) as captured: await worker.submit("explode", timeout_s=10) assert captured.value.remote_type == "RuntimeError" assert "intentional remote failure" in str(captured.value) finally: await worker.stop(timeout_s=5) ticker.cancel() await asyncio.gather(ticker, return_exceptions=True) host_executor.shutdown(wait=True, cancel_futures=True) loop._default_executor = None # type: ignore[attr-defined] return worker worker = asyncio.run(exercise()) assert worker.is_alive is False def test_timed_out_request_poisons_worker_and_rejects_later_submits() -> None: async def exercise(): # type: ignore[no-untyped-def] loop = asyncio.get_running_loop() host_executor = ThreadPoolExecutor(max_workers=2) loop.set_default_executor(host_executor) ticker = asyncio.create_task(_keep_restricted_event_loop_responsive()) worker = PersistentProcessWorker( _worker_factory, start_method="spawn", queue_capacity=1, ) try: await worker.start() with pytest.raises(TimeoutError, match="timed out"): await worker.submit("slow", timeout_s=0.05) assert worker.is_alive is True with pytest.raises(ProcessWorkerError, match="worker is poisoned"): await worker.submit(22, timeout_s=1) finally: await worker.stop(timeout_s=2) ticker.cancel() await asyncio.gather(ticker, return_exceptions=True) host_executor.shutdown(wait=True, cancel_futures=True) loop._default_executor = None # type: ignore[attr-defined] return worker worker = asyncio.run(exercise()) assert worker.is_alive is False