@@ -456,6 +456,132 @@ async def resolve_func_args_with_di(
456456 return args
457457
458458
459+ def _get_handler_raw_config (
460+ handler : domain .ActionHandlerDeclaration ,
461+ runner_context : context .RunnerContext ,
462+ ) -> dict [str , typing .Any ]:
463+ handler_global_config = runner_context .project .action_handler_configs .get (
464+ handler .source , None
465+ )
466+ handler_raw_config = {}
467+ if handler_global_config is not None :
468+ handler_raw_config = handler_global_config
469+ if handler_raw_config == {}:
470+ # still empty, just assign
471+ handler_raw_config = handler .config
472+ else :
473+ # not empty anymore, deep merge
474+ handler_config_merger .merge (handler_raw_config , handler .config )
475+ return handler_raw_config
476+
477+
478+ async def ensure_handler_instantiated (
479+ handler : domain .ActionHandlerDeclaration ,
480+ handler_cache : domain .ActionHandlerCache ,
481+ action_exec_info : domain .ActionExecInfo ,
482+ runner_context : context .RunnerContext ,
483+ ) -> None :
484+ """Ensure handler is instantiated and initialized, populating handler_cache.
485+
486+ If handler is already instantiated (handler_cache.instance is not None), this is
487+ a no-op. Otherwise, imports the handler class, resolves DI, instantiates it,
488+ calls on_initialize lifecycle hook if present, and caches the result.
489+ """
490+ if handler_cache .instance is not None :
491+ return
492+
493+ handler_raw_config = _get_handler_raw_config (handler , runner_context )
494+
495+ logger .trace (f"Load action handler { handler .name } " )
496+ try :
497+ action_handler = run_utils .import_module_member_by_source_str (
498+ handler .source
499+ )
500+ except ModuleNotFoundError as error :
501+ logger .error (
502+ f"Source of action handler { handler .name } '{ handler .source } '"
503+ " could not be imported"
504+ )
505+ logger .error (error )
506+ raise ActionFailedException (
507+ f"Import of action handler '{ handler .name } ' failed: { handler .source } "
508+ ) from error
509+
510+ def get_handler_config (param_type ):
511+ # validate config using pydantic
512+ try :
513+ config_type = pydantic_dataclass (param_type )
514+ except pydantic .ValidationError as exception :
515+ raise ActionFailedException (exception .errors ()) from exception
516+ return config_type (** handler_raw_config )
517+
518+ def get_process_executor (param_type ):
519+ return action_exec_info .process_executor
520+
521+ exec_info = domain .ActionHandlerExecInfo ()
522+ # save immediately in context to be able to shutdown it if the first execution
523+ # is interrupted by stopping ER
524+ handler_cache .exec_info = exec_info
525+ if inspect .isclass (action_handler ):
526+ args = await resolve_func_args_with_di (
527+ func = action_handler .__init__ ,
528+ known_args = {
529+ "config" : get_handler_config ,
530+ "process_executor" : get_process_executor ,
531+ },
532+ params_to_ignore = ["self" ],
533+ )
534+
535+ if "lifecycle" in args :
536+ exec_info .lifecycle = args ["lifecycle" ]
537+
538+ handler_instance = action_handler (** args )
539+ handler_cache .instance = handler_instance
540+
541+ service_instances = [
542+ instance
543+ for instance in args .values ()
544+ if isinstance (instance , service .Service )
545+ ]
546+ handler_cache .used_services = service_instances
547+ for service_instance in service_instances :
548+ if service_instance not in runner_context .running_services :
549+ runner_context .running_services [service_instance ] = (
550+ domain .RunningServiceInfo (used_by = [])
551+ )
552+
553+ runner_context .running_services [service_instance ].used_by .append (
554+ handler_instance
555+ )
556+
557+ else :
558+ # handler is a plain function, not a class — nothing to instantiate
559+ handler_cache .exec_info = exec_info
560+ exec_info .status = domain .ActionHandlerExecInfoStatus .INITIALIZED
561+ return
562+
563+ if (
564+ exec_info .lifecycle is not None
565+ and exec_info .lifecycle .on_initialize_callable is not None
566+ ):
567+ logger .trace (f"Initialize { handler .name } action handler" )
568+ try :
569+ initialize_callable_result = (
570+ exec_info .lifecycle .on_initialize_callable ()
571+ )
572+ if inspect .isawaitable (initialize_callable_result ):
573+ await initialize_callable_result
574+ except Exception as e :
575+ logger .error (
576+ f"Failed to initialize action handler { handler .name } : { e } "
577+ )
578+ raise ActionFailedException (
579+ f"Initialisation of action handler '{ handler .name } ' failed: { e } "
580+ ) from e
581+
582+ exec_info .status = domain .ActionHandlerExecInfoStatus .INITIALIZED
583+
584+
459585async def execute_action_handler (
460586 handler : domain .ActionHandlerDeclaration ,
461587 payload : code_action .RunActionPayload | None ,
@@ -475,19 +601,6 @@ async def execute_action_handler(
475601 start_time = time .time_ns ()
476602 execution_result : code_action .RunActionResult | None = None
477603
478- handler_global_config = runner_context .project .action_handler_configs .get (
479- handler .source , None
480- )
481- handler_raw_config = {}
482- if handler_global_config is not None :
483- handler_raw_config = handler_global_config
484- if handler_raw_config == {}:
485- # still empty, just assign
486- handler_raw_config = handler .config
487- else :
488- # not empty anymore, deep merge
489- handler_config_merger .merge (handler_raw_config , handler .config )
490-
491604 if handler_cache .instance is not None :
492605 handler_instance = handler_cache .instance
493606 handler_run_func = handler_instance .run
@@ -497,92 +610,21 @@ async def execute_action_handler(
497610 f"R{ run_id } | Instance of action handler { handler .name } found in cache"
498611 )
499612 else :
500- logger .trace (f"R{ run_id } | Load action handler { handler .name } " )
501- try :
613+ await ensure_handler_instantiated (
614+ handler = handler ,
615+ handler_cache = handler_cache ,
616+ action_exec_info = action_exec_info ,
617+ runner_context = runner_context ,
618+ )
619+ if handler_cache .instance is not None :
620+ handler_run_func = handler_cache .instance .run
621+ else :
622+ # handler is a plain function
502623 action_handler = run_utils .import_module_member_by_source_str (
503624 handler .source
504625 )
505- except ModuleNotFoundError as error :
506- logger .error (
507- f"R{ run_id } | Source of action handler { handler .name } '{ handler .source } '"
508- " could not be imported"
509- )
510- logger .error (error )
511- raise ActionFailedException (
512- f"Import of action handler '{ handler .name } ' failed(Run { run_id } ): { handler .source } "
513- ) from error
514-
515- def get_handler_config (param_type ):
516- # validate config using pydantic
517- try :
518- config_type = pydantic_dataclass (param_type )
519- except pydantic .ValidationError as exception :
520- raise ActionFailedException (exception .errors ()) from exception
521- return config_type (** handler_raw_config )
522-
523- def get_process_executor (param_type ):
524- return action_exec_info .process_executor
525-
526- exec_info = domain .ActionHandlerExecInfo ()
527- # save immediately in context to be able to shutdown it if the first execution
528- # is interrupted by stopping ER
529- handler_cache .exec_info = exec_info
530- if inspect .isclass (action_handler ):
531- args = await resolve_func_args_with_di (
532- func = action_handler .__init__ ,
533- known_args = {
534- "config" : get_handler_config ,
535- "process_executor" : get_process_executor ,
536- },
537- params_to_ignore = ["self" ],
538- )
539-
540- if "lifecycle" in args :
541- exec_info .lifecycle = args ["lifecycle" ]
542-
543- handler_instance = action_handler (** args )
544- handler_cache .instance = handler_instance
545- handler_run_func = handler_instance .run
546-
547- service_instances = [
548- instance
549- for instance in args .values ()
550- if isinstance (instance , service .Service )
551- ]
552- handler_cache .used_services = service_instances
553- for service_instance in service_instances :
554- if service_instance not in runner_context .running_services :
555- runner_context .running_services [service_instance ] = (
556- domain .RunningServiceInfo (used_by = [])
557- )
558-
559- runner_context .running_services [service_instance ].used_by .append (
560- handler_instance
561- )
562-
563- else :
564626 handler_run_func = action_handler
565-
566- if (
567- exec_info .lifecycle is not None
568- and exec_info .lifecycle .on_initialize_callable is not None
569- ):
570- logger .trace (f"R{ run_id } | Initialize { handler .name } action handler" )
571- try :
572- initialize_callable_result = (
573- exec_info .lifecycle .on_initialize_callable ()
574- )
575- if inspect .isawaitable (initialize_callable_result ):
576- await initialize_callable_result
577- except Exception as e :
578- logger .error (
579- f"R{ run_id } | Failed to initialize action handler { handler .name } : { e } "
580- )
581- raise ActionFailedException (
582- f"Initialisation of action handler '{ handler .name } ' failed(Run { run_id } ): { e } "
583- ) from e
584-
585- exec_info .status = domain .ActionHandlerExecInfoStatus .INITIALIZED
627+ exec_info = handler_cache .exec_info
586628
587629 def get_run_payload (param_type ):
588630 return payload
0 commit comments