Skip to content

Commit b39729a

Browse files
committed
Eager initialization of action handlers: if IDE is active, all handlers, if not only handlers of actions which will be run
1 parent cc3288a commit b39729a

13 files changed

Lines changed: 360 additions & 113 deletions

File tree

finecode_extension_runner/src/finecode_extension_runner/_services/run_action.py

Lines changed: 137 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
459585
async 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

finecode_extension_runner/src/finecode_extension_runner/lsp_server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ async def update_config(
404404
)
405405
for svc in config.get("services", [])
406406
],
407+
handlers_to_initialize=config.get("handlers_to_initialize"),
407408
)
408409
response = await services.update_config(
409410
request=request,

finecode_extension_runner/src/finecode_extension_runner/schemas.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ class UpdateConfigRequest(BaseSchema):
4040
actions: dict[str, Action]
4141
action_handler_configs: dict[str, dict[str, Any]]
4242
services: list[ServiceDeclaration] = field(default_factory=list)
43+
# If provided, eagerly instantiate these handlers after config update.
44+
# Keys are action names, values are lists of handler names within that action.
45+
# None means no eager initialization (lazy, on first use).
46+
handlers_to_initialize: dict[str, list[str]] | None = None
4347

4448

4549
@dataclass

finecode_extension_runner/src/finecode_extension_runner/services.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
ActionFailedException,
1616
StopWithResponse,
1717
run_action_raw,
18+
create_action_exec_info,
19+
ensure_handler_instantiated,
1820
)
1921
from finecode_extension_runner.di import bootstrap as di_bootstrap
2022

@@ -122,9 +124,78 @@ def current_env_name_getter() -> str:
122124
service_declarations=request.services,
123125
)
124126

127+
if request.handlers_to_initialize is not None:
128+
await initialize_handlers(request.handlers_to_initialize)
129+
125130
return schemas.UpdateConfigResponse()
126131

127132

133+
async def initialize_handlers(
134+
handlers_by_action: dict[str, list[str]],
135+
) -> None:
136+
"""Eagerly instantiate and initialize handlers.
137+
138+
This is called after update_config to pre-instantiate handlers so that
139+
services (like LSP services) are started early rather than on first use.
140+
141+
Args:
142+
handlers_by_action: mapping of action name → list of handler names
143+
to eagerly initialize.
144+
"""
145+
if global_state.runner_context is None:
146+
logger.warning("Cannot initialize handlers: runner context is not set")
147+
return
148+
149+
runner_context = global_state.runner_context
150+
project = runner_context.project
151+
152+
for action_name, handler_names in handlers_by_action.items():
153+
action_def = project.actions.get(action_name)
154+
if action_def is None:
155+
logger.warning(
156+
f"Action '{action_name}' not found, skipping handler initialization"
157+
)
158+
continue
159+
160+
if action_name in runner_context.action_cache_by_name:
161+
action_cache = runner_context.action_cache_by_name[action_name]
162+
else:
163+
action_cache = domain.ActionCache()
164+
runner_context.action_cache_by_name[action_name] = action_cache
165+
166+
if action_cache.exec_info is None:
167+
action_cache.exec_info = create_action_exec_info(action_def)
168+
169+
handlers_to_init = [
170+
h for h in action_def.handlers if h.name in handler_names
171+
]
172+
for handler in handlers_to_init:
173+
if handler.name in action_cache.handler_cache_by_name:
174+
handler_cache = action_cache.handler_cache_by_name[handler.name]
175+
if handler_cache.instance is not None:
176+
continue
177+
else:
178+
handler_cache = domain.ActionHandlerCache()
179+
action_cache.handler_cache_by_name[handler.name] = handler_cache
180+
181+
try:
182+
await ensure_handler_instantiated(
183+
handler=handler,
184+
handler_cache=handler_cache,
185+
action_exec_info=action_cache.exec_info,
186+
runner_context=runner_context,
187+
)
188+
logger.trace(
189+
f"Eagerly initialized handler '{handler.name}' "
190+
f"for action '{action_name}'"
191+
)
192+
except Exception as e:
193+
logger.error(
194+
f"Failed to eagerly initialize handler '{handler.name}' "
195+
f"for action '{action_name}': {e}"
196+
)
197+
198+
128199
def reload_action(action_name: str) -> None:
129200
if global_state.runner_context is None:
130201
# TODO: raise error
@@ -231,6 +302,7 @@ def shutdown_action_handler(
231302
if isinstance(used_service, service.DisposableService):
232303
try:
233304
used_service.dispose()
305+
logger.trace(f"Disposed service: {used_service}")
234306
except Exception as exception:
235307
logger.error(f"Failed to dispose service: {used_service}")
236308
logger.exception(exception)

src/finecode/cli_app/commands/run_cmd.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,9 @@ async def run_actions(
156156

157157
try:
158158
await run_service.start_required_environments(
159-
actions_by_projects, ws_context, update_config_in_running_runners=True
159+
actions_by_projects,
160+
ws_context,
161+
update_config_in_running_runners=True,
160162
)
161163
except run_service.StartingEnvironmentsFailed as exception:
162164
raise RunFailed(

0 commit comments

Comments
 (0)