|
34 | 34 | ALWAYS_LOADED_MODULES = [] |
35 | 35 | # Extensions that will always be loaded if installed. They don't expose commands but hook into CLI core. |
36 | 36 | ALWAYS_LOADED_EXTENSIONS = ['azext_ai_examples', 'azext_next'] |
| 37 | +# Timeout (in seconds) for loading a single module. Acts as a safety valve to prevent indefinite hangs |
37 | 38 | MODULE_LOAD_TIMEOUT_SECONDS = 30 |
| 39 | +# Maximum number of worker threads for parallel module loading. |
38 | 40 | MAX_WORKER_THREAD_COUNT = 4 |
39 | 41 |
|
40 | 42 |
|
@@ -200,12 +202,13 @@ def _configure_style(self): |
200 | 202 |
|
201 | 203 |
|
202 | 204 | class ModuleLoadResult: # pylint: disable=too-few-public-methods |
203 | | - def __init__(self, module_name, command_table, group_table, elapsed_time, error=None): |
| 205 | + def __init__(self, module_name, command_table, group_table, elapsed_time, error=None, traceback_str=None): |
204 | 206 | self.module_name = module_name |
205 | 207 | self.command_table = command_table |
206 | 208 | self.group_table = group_table |
207 | 209 | self.elapsed_time = elapsed_time |
208 | 210 | self.error = error |
| 211 | + self.traceback_str = traceback_str |
209 | 212 |
|
210 | 213 |
|
211 | 214 | class MainCommandsLoader(CLICommandsLoader): |
@@ -552,45 +555,50 @@ def _load_modules(self, args, command_modules): |
552 | 555 | future_to_module = {executor.submit(self._load_single_module, mod, args): mod |
553 | 556 | for mod in command_modules if mod not in BLOCKED_MODS} |
554 | 557 |
|
555 | | - for future in future_to_module: |
| 558 | + for future in concurrent.futures.as_completed(future_to_module): |
556 | 559 | try: |
557 | 560 | result = future.result(timeout=MODULE_LOAD_TIMEOUT_SECONDS) |
558 | 561 | results.append(result) |
559 | 562 | except concurrent.futures.TimeoutError: |
560 | 563 | mod = future_to_module[future] |
561 | 564 | logger.warning("Module '%s' load timeout after %s seconds", mod, MODULE_LOAD_TIMEOUT_SECONDS) |
562 | | - future.cancel() |
563 | 565 | results.append(ModuleLoadResult(mod, {}, {}, 0, |
564 | 566 | Exception(f"Module '{mod}' load timeout"))) |
565 | 567 | except (ImportError, AttributeError, TypeError, ValueError) as ex: |
566 | 568 | mod = future_to_module[future] |
567 | 569 | logger.warning("Module '%s' load failed: %s", mod, ex) |
568 | 570 | results.append(ModuleLoadResult(mod, {}, {}, 0, ex)) |
| 571 | + except Exception as ex: |
| 572 | + mod = future_to_module[future] |
| 573 | + logger.warning("Module '%s' load failed with unexpected exception: %s", mod, ex) |
| 574 | + results.append(ModuleLoadResult(mod, {}, {}, 0, ex)) |
569 | 575 |
|
570 | 576 | return results |
571 | 577 |
|
572 | 578 | def _load_single_module(self, mod, args): |
573 | 579 | from azure.cli.core.breaking_change import import_module_breaking_changes |
574 | 580 | from azure.cli.core.commands import _load_module_command_loader |
| 581 | + import traceback |
575 | 582 | try: |
576 | 583 | start_time = timeit.default_timer() |
577 | 584 | module_command_table, module_group_table = _load_module_command_loader(self, args, mod) |
578 | 585 | import_module_breaking_changes(mod) |
579 | 586 | elapsed_time = timeit.default_timer() - start_time |
580 | 587 | return ModuleLoadResult(mod, module_command_table, module_group_table, elapsed_time) |
581 | 588 | except Exception as ex: # pylint: disable=broad-except |
582 | | - return ModuleLoadResult(mod, {}, {}, 0, ex) |
| 589 | + tb_str = traceback.format_exc() |
| 590 | + return ModuleLoadResult(mod, {}, {}, 0, ex, tb_str) |
583 | 591 |
|
584 | 592 | def _handle_module_load_error(self, result): |
585 | 593 | """Handle errors that occurred during module loading.""" |
586 | | - import traceback |
587 | 594 | from azure.cli.core import telemetry |
588 | 595 |
|
589 | 596 | logger.error("Error loading command module '%s': %s", result.module_name, result.error) |
590 | 597 | telemetry.set_exception(exception=result.error, |
591 | 598 | fault_type='module-load-error-' + result.module_name, |
592 | 599 | summary='Error loading module: {}'.format(result.module_name)) |
593 | | - logger.debug(traceback.format_exc()) |
| 600 | + if result.traceback_str: |
| 601 | + logger.debug(result.traceback_str) |
594 | 602 |
|
595 | 603 | def _process_successful_load(self, result): |
596 | 604 | """Process successfully loaded module results.""" |
|
0 commit comments