|
12 | 12 | import traceback |
13 | 13 | from collections import OrderedDict, deque |
14 | 14 | from contextlib import suppress |
| 15 | +from multiprocessing import TimeoutError |
15 | 16 | from typing import ( |
16 | 17 | Any, |
17 | 18 | Callable, |
|
34 | 35 | from testplan.common.utils.path import default_runpath, makedirs, makeemptydirs |
35 | 36 | from testplan.common.utils.strings import slugify, uuid4 |
36 | 37 | from testplan.common.utils.thread import execute_as_thread, interruptible_join |
37 | | -from testplan.common.utils.timing import wait, Timer |
| 38 | +from testplan.common.utils.timing import Timer, wait |
38 | 39 | from testplan.common.utils.validation import is_subclass |
39 | 40 |
|
40 | 41 |
|
@@ -239,32 +240,32 @@ def start(self): |
239 | 240 | else: |
240 | 241 | resource.logger.info("%s started", resource) |
241 | 242 |
|
242 | | - def start_in_pool(self, pool: mp.ThreadPool): |
| 243 | + def start_in_pool( |
| 244 | + self, pool: mp.ThreadPool, timeout: Optional[float] = None |
| 245 | + ): |
243 | 246 | """ |
244 | 247 | Start all resources concurrently in thread pool and log exceptions. |
245 | 248 |
|
246 | 249 | :param pool: thread pool |
247 | 250 | """ |
248 | | - |
249 | | - for resource in self._resources.values(): |
250 | | - if not resource.async_start: |
251 | | - raise RuntimeError( |
252 | | - f"Cannot start resource {resource} in thread pool," |
253 | | - " its `async_start` attribute is set to False" |
254 | | - ) |
255 | | - |
256 | | - def start_in_thread(resource): |
257 | | - resource.start() |
258 | | - resource.wait(resource.STATUS.STARTED) |
259 | | - resource.logger.info("%s started", resource) |
| 251 | + # async_start is meaningless here... |
| 252 | + for r in self._resources.values(): |
| 253 | + if r.auto_start: |
| 254 | + r.cfg.set_local("async_start", False) |
260 | 255 |
|
261 | 256 | async_r = pool.map_async( |
262 | 257 | self._apply_resource_exception_logged( |
263 | | - self.start_exceptions, start_in_thread |
| 258 | + self.start_exceptions, lambda r: r.start() |
264 | 259 | ), |
265 | 260 | (r for r in self._resources.values() if r.auto_start), |
266 | 261 | ) |
267 | | - async_r.wait() |
| 262 | + try: |
| 263 | + async_r.get(timeout) |
| 264 | + except TimeoutError: |
| 265 | + raise RuntimeError( |
| 266 | + f"timeout after {timeout}s when starting resources " |
| 267 | + f"{self._resources.values()} in internal threading pool." |
| 268 | + ) |
268 | 269 |
|
269 | 270 | def sync_stop_resource(self, resource: "Resource"): |
270 | 271 | """ |
@@ -346,42 +347,33 @@ def stop(self, is_reversed: bool = False): |
346 | 347 | resource.force_stop() |
347 | 348 | resource.logger.info("%s stopped", resource) |
348 | 349 |
|
349 | | - def stop_in_pool(self, pool: mp.ThreadPool): |
| 350 | + def stop_in_pool( |
| 351 | + self, pool: mp.ThreadPool, timeout: Optional[float] = None |
| 352 | + ): |
350 | 353 | """ |
351 | 354 | Stop all resources concurrently in thread pool and log exceptions. |
352 | 355 |
|
353 | 356 | :param pool: thread pool |
354 | 357 | """ |
355 | | - for resource in self._resources.values(): |
356 | | - if not resource.async_start: |
357 | | - raise RuntimeError( |
358 | | - f"Cannot stop resource {resource} in thread pool," |
359 | | - " its `async_start` attribute is set to False" |
360 | | - ) |
361 | | - |
362 | | - def stop_in_thread(resource: "Resource"): |
363 | | - try: |
364 | | - if resource.status not in ( |
365 | | - resource.STATUS.STOPPING, |
366 | | - resource.STATUS.STOPPED, |
367 | | - ): |
368 | | - resource.stop() |
369 | | - if resource.status == resource.STATUS.STOPPING: |
370 | | - resource.wait(resource.STATUS.STOPPED) |
371 | | - except: |
372 | | - # Resource status should be STOPPED even it failed to stop |
373 | | - resource.force_stop() |
374 | | - raise |
375 | | - finally: |
376 | | - resource.logger.info("%s stopped", resource) |
| 358 | + # async_start is meaningless here... |
| 359 | + # in practice, stop_in_pool must be called in pair of start_in_pool |
| 360 | + for r in self._resources.values(): |
| 361 | + if r.auto_start: |
| 362 | + r.cfg.set_local("async_start", False) |
377 | 363 |
|
378 | 364 | async_r = pool.map_async( |
379 | 365 | self._apply_resource_exception_logged( |
380 | | - self.stop_exceptions, stop_in_thread |
| 366 | + self.stop_exceptions, lambda r: r.stop() |
381 | 367 | ), |
382 | 368 | self._resources.values(), |
383 | 369 | ) |
384 | | - async_r.wait() |
| 370 | + try: |
| 371 | + async_r.get(timeout) |
| 372 | + except TimeoutError: |
| 373 | + raise RuntimeError( |
| 374 | + f"timeout after {timeout}s when stopping resources " |
| 375 | + f"{self._resources.values()} in internal threading pool." |
| 376 | + ) |
385 | 377 |
|
386 | 378 | def _apply_resource_exception_logged( |
387 | 379 | self, exception_record, func: Callable[["Resource"], None] |
|
0 commit comments