|
3 | 3 | from time import sleep, time |
4 | 4 | from queue import Queue |
5 | 5 | from threading import Thread |
| 6 | +from unittest.mock import MagicMock |
6 | 7 |
|
7 | 8 | from executorlib import SingleNodeExecutor |
8 | 9 | from executorlib.executor.single import create_single_node_executor |
9 | | -from executorlib.task_scheduler.interactive.dependency import _execute_tasks_with_dependencies |
| 10 | +from executorlib.task_scheduler.interactive.dependency import ( |
| 11 | + _execute_tasks_with_dependencies, |
| 12 | + _update_waiting_task, |
| 13 | +) |
10 | 14 | from executorlib.standalone.serialize import cloudpickle_register |
11 | 15 | from executorlib.standalone.interactive.spawner import MpiExecSpawner |
12 | 16 |
|
@@ -300,6 +304,43 @@ def test_future_input_dict(self): |
300 | 304 | ) |
301 | 305 | self.assertEqual(fs.result()["a"], 4) |
302 | 306 |
|
| 307 | + def test_update_waiting_task_batched_exception(self): |
| 308 | + """_update_waiting_task catches exceptions from batched_futures and sets them on the batch future.""" |
| 309 | + executor_queue = Queue() |
| 310 | + batch_future = Future() |
| 311 | + |
| 312 | + # A mock skip_lst future: done(), exception() returns None (passes get_exception_lst), |
| 313 | + # but result() raises -- triggering the except block in _update_waiting_task. |
| 314 | + mock_skip_future = MagicMock() |
| 315 | + mock_skip_future.done.return_value = True |
| 316 | + mock_skip_future.exception.return_value = None |
| 317 | + mock_skip_future.result.side_effect = RuntimeError("unexpected skip error") |
| 318 | + |
| 319 | + task_dict = { |
| 320 | + "fn": "batched", |
| 321 | + "args": (), |
| 322 | + "kwargs": { |
| 323 | + "lst": [], |
| 324 | + "n": 3, |
| 325 | + "skip_lst": [mock_skip_future], |
| 326 | + }, |
| 327 | + "future": batch_future, |
| 328 | + "future_lst": [mock_skip_future], |
| 329 | + "resource_dict": {}, |
| 330 | + } |
| 331 | + |
| 332 | + result_lst = _update_waiting_task( |
| 333 | + wait_lst=[task_dict], |
| 334 | + executor_queue=executor_queue, |
| 335 | + refresh_rate=0.0, |
| 336 | + ) |
| 337 | + |
| 338 | + # The batch future must have the exception propagated (not crashed the scheduler) |
| 339 | + self.assertTrue(batch_future.done()) |
| 340 | + self.assertIsInstance(batch_future.exception(), RuntimeError) |
| 341 | + # The failed task is consumed (not re-queued in the wait list) |
| 342 | + self.assertEqual(len(result_lst), 0) |
| 343 | + |
303 | 344 |
|
304 | 345 | class TestExecutorErrors(unittest.TestCase): |
305 | 346 | def test_block_allocation_false_one_worker(self): |
|
0 commit comments