1
1
from azure_functions_worker .utils .common import is_true_like
2
2
from typing import List , Optional
3
+ from types import ModuleType
3
4
import importlib
4
5
import inspect
5
6
import os
9
10
from ..logging import logger
10
11
from ..constants import (
11
12
AZURE_WEBJOBS_SCRIPT_ROOT ,
13
+ CONTAINER_NAME ,
12
14
PYTHON_ISOLATE_WORKER_DEPENDENCIES ,
13
15
PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT ,
14
16
PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT_39
@@ -66,6 +68,10 @@ def initialize(cls):
66
68
cls .cx_working_dir = cls ._get_cx_working_dir ()
67
69
cls .worker_deps_path = cls ._get_worker_deps_path ()
68
70
71
+ @classmethod
72
+ def is_in_linux_consumption (cls ):
73
+ return CONTAINER_NAME in os .environ
74
+
69
75
@classmethod
70
76
@enable_feature_by (
71
77
flag = PYTHON_ISOLATE_WORKER_DEPENDENCIES ,
@@ -87,6 +93,11 @@ def use_worker_dependencies(cls):
87
93
# The following log line will not show up in core tools but should
88
94
# work in kusto since core tools only collects gRPC logs. This function
89
95
# is executed even before the gRPC logging channel is ready.
96
+ logger .info (f'Applying use_worker_dependencies:'
97
+ f' worker_dependencies: { cls .worker_deps_path } ,'
98
+ f' customer_dependencies: { cls .cx_deps_path } ,'
99
+ f' working_directory: { cls .cx_working_dir } ' )
100
+
90
101
cls ._remove_from_sys_path (cls .cx_deps_path )
91
102
cls ._remove_from_sys_path (cls .cx_working_dir )
92
103
cls ._add_to_sys_path (cls .worker_deps_path , True )
@@ -101,7 +112,7 @@ def use_worker_dependencies(cls):
101
112
PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT
102
113
)
103
114
)
104
- def prioritize_customer_dependencies (cls ):
115
+ def prioritize_customer_dependencies (cls , cx_working_dir = None ):
105
116
"""Switch the sys.path and ensure the customer's code import are loaded
106
117
from CX's deppendencies.
107
118
@@ -112,24 +123,50 @@ def prioritize_customer_dependencies(cls):
112
123
As for Linux Consumption, this will only remove worker_deps_path,
113
124
but the customer's path will be loaded in function_environment_reload.
114
125
115
- The search order of a module name in customer frame is:
126
+ The search order of a module name in customer's paths is:
116
127
1. cx_deps_path
117
- 2. cx_working_dir
128
+ 2. worker_deps_path
129
+ 3. cx_working_dir
118
130
"""
131
+ # Try to get the latest customer's working directory
132
+ # cx_working_dir => cls.cx_working_dir => AzureWebJobsScriptRoot
133
+ working_directory : str = ''
134
+ if cx_working_dir :
135
+ working_directory : str = os .path .abspath (cx_working_dir )
136
+ if not working_directory :
137
+ working_directory = cls .cx_working_dir
138
+ if not working_directory :
139
+ working_directory = os .getenv (AZURE_WEBJOBS_SCRIPT_ROOT , '' )
140
+
141
+ # Try to get the latest customer's dependency path
142
+ cx_deps_path : str = cls ._get_cx_deps_path ()
143
+ if not cx_deps_path :
144
+ cx_deps_path = cls .cx_deps_path
145
+
146
+ logger .info ('Applying prioritize_customer_dependencies:'
147
+ f' worker_dependencies: { cls .worker_deps_path } ,'
148
+ f' customer_dependencies: { cx_deps_path } ,'
149
+ f' working_directory: { working_directory } ' )
150
+
119
151
cls ._remove_from_sys_path (cls .worker_deps_path )
120
152
cls ._add_to_sys_path (cls .cx_deps_path , True )
121
- cls ._add_to_sys_path (cls .cx_working_dir , False )
122
153
123
154
# Deprioritize worker dependencies but don't completely remove it
124
155
# Otherwise, it will break some really old function apps, those
125
156
# don't have azure-functions module in .python_packages
126
157
# https://github.com/Azure/azure-functions-core-tools/pull/1498
127
158
cls ._add_to_sys_path (cls .worker_deps_path , False )
128
159
129
- logger .info (f'Start using customer dependencies { cls .cx_deps_path } ' )
160
+ # The modules defined in customer's working directory should have the
161
+ # least priority since we uses the new folder structure.
162
+ # Please check the "Message to customer" section in the following PR:
163
+ # https://github.com/Azure/azure-functions-python-worker/pull/726
164
+ cls ._add_to_sys_path (working_directory , False )
165
+
166
+ logger .info ('Finished prioritize_customer_dependencies' )
130
167
131
168
@classmethod
132
- def reload_azure_google_namespace (cls , cx_working_dir : str ):
169
+ def reload_customer_libraries (cls , cx_working_dir : str ):
133
170
"""Reload azure and google namespace, this including any modules in
134
171
this namespace, such as azure-functions, grpcio, grpcio-tools etc.
135
172
@@ -152,7 +189,7 @@ def reload_azure_google_namespace(cls, cx_working_dir: str):
152
189
use_new = is_true_like (use_new_env )
153
190
154
191
if use_new :
155
- cls .reload_all_namespaces_from_customer_deps (cx_working_dir )
192
+ cls .prioritize_customer_dependencies (cx_working_dir )
156
193
else :
157
194
cls .reload_azure_google_namespace_from_worker_deps ()
158
195
@@ -189,33 +226,6 @@ def reload_azure_google_namespace_from_worker_deps(cls):
189
226
logger .info ('Unable to reload azure.functions. '
190
227
'Using default. Exception:\n {}' .format (ex ))
191
228
192
- @classmethod
193
- def reload_all_namespaces_from_customer_deps (cls , cx_working_dir : str ):
194
- """This is a new implementation of reloading azure and google
195
- namespace from customer's .python_packages folder. Only intended to be
196
- used in Linux Consumption scenario.
197
-
198
- Parameters
199
- ----------
200
- cx_working_dir: str
201
- The path which contains customer's project file (e.g. wwwroot).
202
- """
203
- # Specialized working directory needs to be added
204
- working_directory : str = os .path .abspath (cx_working_dir )
205
-
206
- # Switch to customer deps and clear out all module cache in worker deps
207
- cls ._remove_from_sys_path (cls .worker_deps_path )
208
- cls ._add_to_sys_path (cls .cx_deps_path , True )
209
- cls ._add_to_sys_path (working_directory , False )
210
-
211
- # Deprioritize worker dependencies but don't completely remove it
212
- # Otherwise, it will break some really old function apps, those
213
- # don't have azure-functions module in .python_packages
214
- # https://github.com/Azure/azure-functions-core-tools/pull/1498
215
- cls ._add_to_sys_path (cls .worker_deps_path , False )
216
-
217
- logger .info ('Reloaded all namespaces from customer dependencies' )
218
-
219
229
@classmethod
220
230
def _add_to_sys_path (cls , path : str , add_to_first : bool ):
221
231
"""This will ensure no duplicated path are added into sys.path and
@@ -325,7 +335,18 @@ def _get_worker_deps_path() -> str:
325
335
if worker_deps_paths :
326
336
return worker_deps_paths [0 ]
327
337
328
- # 2. If it fails to find one, try to find one from the parent path
338
+ # 2. Try to find module spec of azure.functions without actually
339
+ # importing it (e.g. lib/site-packages/azure/functions/__init__.py)
340
+ try :
341
+ azf_spec = importlib .util .find_spec ('azure.functions' )
342
+ if azf_spec and azf_spec .origin :
343
+ return os .path .abspath (
344
+ os .path .join (os .path .dirname (azf_spec .origin ), '..' , '..' )
345
+ )
346
+ except ModuleNotFoundError :
347
+ logger .warning ('Cannot locate built-in azure.functions module' )
348
+
349
+ # 3. If it fails to find one, try to find one from the parent path
329
350
# This is used for handling the CI/localdev environment
330
351
return os .path .abspath (
331
352
os .path .join (os .path .dirname (__file__ ), '..' , '..' )
@@ -341,18 +362,34 @@ def _remove_module_cache(path: str):
341
362
path: str
342
363
The module cache to be removed if it is imported from this path.
343
364
"""
344
- all_modules = set (sys .modules .keys ()) - set (sys .builtin_module_names )
345
- for module_name in all_modules :
346
- module = sys .modules [module_name ]
347
- # Module path can be actual file path or a pure namespace path
348
- # For actual files: use __file__ attribute to retrieve module path
349
- # For namespace: use __path__[0] to retrieve module path
350
- module_path = ''
351
- if getattr (module , '__file__' , None ):
352
- module_path = os .path .dirname (module .__file__ )
353
- elif getattr (module , '__path__' , None ) and getattr (
354
- module .__path__ , '_path' , None ):
355
- module_path = module .__path__ ._path [0 ]
356
-
357
- if module_path .startswith (path ):
358
- sys .modules .pop (module_name )
365
+ if not path :
366
+ return
367
+
368
+ not_builtin = set (sys .modules .keys ()) - set (sys .builtin_module_names )
369
+
370
+ # Don't reload azure_functions_worker
371
+ to_be_cleared_from_cache = set ([
372
+ module_name for module_name in not_builtin
373
+ if not module_name .startswith ('azure_functions_worker' )
374
+ ])
375
+
376
+ for module_name in to_be_cleared_from_cache :
377
+ module = sys .modules .get (module_name )
378
+ if not isinstance (module , ModuleType ):
379
+ continue
380
+
381
+ # Module path can be actual file path or a pure namespace path.
382
+ # Both of these has the module path placed in __path__ property
383
+ # The property .__path__ can be None or does not exist in module
384
+ try :
385
+ module_paths = set (getattr (module , '__path__' , None ) or [])
386
+ if hasattr (module , '__file__' ) and module .__file__ :
387
+ module_paths .add (module .__file__ )
388
+
389
+ if any ([p for p in module_paths if p .startswith (path )]):
390
+ sys .modules .pop (module_name )
391
+ except Exception as e :
392
+ logger .warning (
393
+ f'Attempt to remove module cache for { module_name } but'
394
+ f' failed with { e } . Using the original module cache.'
395
+ )
0 commit comments