|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | 5 | import asyncio |
6 | | -import concurrent.futures |
| 6 | +import queue |
| 7 | +import threading |
7 | 8 | from dataclasses import dataclass |
8 | 9 | from typing import Any, Callable, TypeVar |
9 | 10 |
|
@@ -101,20 +102,31 @@ def run(self, initial_input: Any = None) -> Any: |
101 | 102 | def _run_step_with_timeout(self, s: Step, input_val: Any) -> Any: |
102 | 103 | """Run a step in a worker thread with a timeout. |
103 | 104 |
|
104 | | - Uses a ThreadPoolExecutor so the timeout works on any thread |
| 105 | + Uses a daemon worker thread so the timeout works on any thread |
105 | 106 | (not just the main thread) and on all platforms (no SIGALRM). |
106 | 107 |
|
107 | 108 | Note: the worker thread keeps running after a timeout; only the |
108 | | - caller is unblocked. This is inherent to cooperative cancellation |
109 | | - in Python and matches asyncio.wait_for semantics. |
| 109 | + caller is unblocked. The thread is marked daemon so a timed-out step |
| 110 | + cannot keep process shutdown waiting after the caller has moved on. |
110 | 111 | """ |
111 | 112 | assert s.timeout is not None |
112 | | - with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: |
113 | | - future = pool.submit(self._run_step_sync, s, input_val) |
| 113 | + result_queue: queue.Queue[tuple[bool, Any]] = queue.Queue(maxsize=1) |
| 114 | + |
| 115 | + def runner() -> None: |
114 | 116 | try: |
115 | | - return future.result(timeout=s.timeout) |
116 | | - except concurrent.futures.TimeoutError: |
117 | | - raise TimeoutError(f"Step '{s.name}' timed out after {s.timeout}s") from None |
| 117 | + result_queue.put((True, self._run_step_sync(s, input_val))) |
| 118 | + except Exception as exc: |
| 119 | + result_queue.put((False, exc)) |
| 120 | + |
| 121 | + worker = threading.Thread(target=runner, name=f"hawk-workflow-{s.name}", daemon=True) |
| 122 | + worker.start() |
| 123 | + try: |
| 124 | + ok, payload = result_queue.get(timeout=s.timeout) |
| 125 | + except queue.Empty: |
| 126 | + raise TimeoutError(f"Step '{s.name}' timed out after {s.timeout}s") from None |
| 127 | + if ok: |
| 128 | + return payload |
| 129 | + raise payload |
118 | 130 |
|
119 | 131 | def _run_step_sync(self, s: Step, input_val: Any) -> Any: |
120 | 132 | """Run a single step with optional retry.""" |
|
0 commit comments