11import importlib .util
22import logging
33import os
4+ import sys
45from pathlib import Path
56from typing import Any , Dict , Optional
67
8+ from constants import MAX_IMPORT_RECOVERY_ATTEMPTS
79from logger import setup_logging
810from unpack_volume import maybe_unpack
911from version import format_version_banner
@@ -26,13 +28,121 @@ def _is_deployed_mode() -> bool:
2628 return bool (os .getenv ("FLASH_RESOURCE_NAME" ))
2729
2830
31+ class _HandlerRecoveryError (RuntimeError ):
32+ """Raised by _exec_handler_module when on-the-fly recovery fails.
33+
34+ Distinguished from generic RuntimeError so _load_generated_handler can
35+ re-raise it without wrapping, while still wrapping user-code RuntimeErrors.
36+ """
37+
38+
39+ def _extract_missing_package (error : ImportError ) -> str | None :
40+ """Extract the top-level package name from an ImportError.
41+
42+ Returns the root package name (e.g. 'numpy' from 'numpy.core') or None
43+ if the module name cannot be determined.
44+ """
45+ module_name : str | None = getattr (error , "name" , None )
46+ if not module_name :
47+ return None
48+ return module_name .split ("." )[0 ]
49+
50+
51+ def _try_install_missing_package (package_name : str ) -> bool :
52+ """Attempt to install a missing package on-the-fly via DependencyInstaller.
53+
54+ Returns True if installation succeeded, False otherwise.
55+ """
56+ from dependency_installer import DependencyInstaller
57+
58+ installer = DependencyInstaller ()
59+ result = installer .install_dependencies ([package_name ])
60+ return bool (result .success )
61+
62+
63+ def _exec_handler_module (
64+ spec : importlib .machinery .ModuleSpec ,
65+ handler_file : Path ,
66+ ) -> Any :
67+ """Execute a handler module spec, installing missing packages on-the-fly.
68+
69+ When a deployed handler fails to import due to a missing package (e.g.
70+ numpy excluded from the build artifact but needed at runtime), this
71+ function installs the package and retries. This adds to cold start time
72+ but prevents a fatal crash.
73+
74+ Returns:
75+ The loaded module object.
76+
77+ Raises:
78+ _HandlerRecoveryError: If the handler cannot be loaded after recovery attempts.
79+ """
80+ installed_packages : list [str ] = []
81+
82+ for _attempt in range (MAX_IMPORT_RECOVERY_ATTEMPTS + 1 ):
83+ mod = importlib .util .module_from_spec (spec )
84+ try :
85+ spec .loader .exec_module (mod ) # type: ignore[union-attr]
86+ return mod
87+ except ModuleNotFoundError as e :
88+ if len (installed_packages ) >= MAX_IMPORT_RECOVERY_ATTEMPTS :
89+ raise _HandlerRecoveryError (
90+ f"Generated handler { handler_file } failed to load after "
91+ f"installing { len (installed_packages )} missing packages: "
92+ f"{ installed_packages } . Too many missing dependencies — "
93+ f"redeploy with 'flash deploy'."
94+ ) from e
95+
96+ package_name = _extract_missing_package (e )
97+ if not package_name or package_name in installed_packages :
98+ raise _HandlerRecoveryError (
99+ "Import is still failing after attempted automatic recovery "
100+ "or the missing dependency could not be determined. "
101+ "Inspect your handler and its dependencies, then redeploy "
102+ "with 'flash deploy'."
103+ ) from e
104+
105+ logger .warning (
106+ "Package '%s' is not in the build artifact. Installing on-the-fly. "
107+ "This adds to cold start time — consider adding it to your "
108+ "dependencies list to include it in the build artifact." ,
109+ package_name ,
110+ )
111+
112+ if not _try_install_missing_package (package_name ):
113+ raise _HandlerRecoveryError (
114+ f"Failed to install missing package '{ package_name } '. "
115+ f"Generated handler { handler_file } cannot be loaded. "
116+ f"Redeploy with 'flash deploy'."
117+ ) from e
118+
119+ installed_packages .append (package_name )
120+ # Clear the failed module from sys.modules so the retry gets a fresh import
121+ for key in list (sys .modules ):
122+ if key == package_name or key .startswith (f"{ package_name } ." ):
123+ del sys .modules [key ]
124+ importlib .invalidate_caches ()
125+ logger .info ("Installed '%s', retrying handler load" , package_name )
126+
127+ raise _HandlerRecoveryError (
128+ f"Generated handler { handler_file } failed to load after installing "
129+ f"{ len (installed_packages )} missing packages: { installed_packages } . "
130+ f"Too many missing dependencies — redeploy with 'flash deploy'."
131+ )
132+
133+
29134def _load_generated_handler () -> Optional [Any ]:
30135 """Load Flash-generated handler for deployed QB mode.
31136
32137 Checks for a handler_<resource_name>.py file generated by the flash
33138 build pipeline. These handlers accept plain JSON input without
34139 FunctionRequest/cloudpickle serialization.
35140
141+ If the handler fails to import due to a missing package, attempts
142+ on-the-fly installation before giving up. This handles cases where
143+ a package was excluded from the build artifact (e.g. size-prohibitive
144+ packages) but is needed at runtime.
145+
36146 In deployed mode (FLASH_RESOURCE_NAME set), failures are fatal.
37147 FunctionRequest fallback is only valid for Live Serverless workers.
38148
@@ -68,20 +178,16 @@ def _load_generated_handler() -> Optional[Any]:
68178 f"The file may be corrupted. Redeploy with 'flash deploy'."
69179 )
70180
71- mod = importlib .util .module_from_spec (spec )
72181 try :
73- spec .loader .exec_module (mod )
74- except ImportError as e :
75- raise RuntimeError (
76- f"Generated handler { handler_file } failed to import: { e } . "
77- f"This usually means a dependency was built for the wrong Python version. "
78- f"Redeploy with 'flash deploy'."
79- ) from e
182+ mod = _exec_handler_module (spec , handler_file )
80183 except SyntaxError as e :
81184 raise RuntimeError (
82185 f"Generated handler { handler_file } has a syntax error: { e } . "
83186 f"This indicates a bug in the flash build pipeline."
84187 ) from e
188+ except _HandlerRecoveryError :
189+ # Recovery-specific RuntimeErrors from _exec_handler_module — already formatted
190+ raise
85191 except Exception as e :
86192 raise RuntimeError (
87193 f"Generated handler { handler_file } failed to load: { e } ({ type (e ).__name__ } ). "
0 commit comments