Skip to content

Commit c9a63be

Browse files
author
Victoria Hall
committed
merge
2 parents e80f37d + 8ebd94e commit c9a63be

File tree

12 files changed

+442
-68
lines changed

12 files changed

+442
-68
lines changed

azure_functions_worker/constants.py

+5
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,8 @@
5858

5959
# Base extension supported Python minor version
6060
BASE_EXT_SUPPORTED_PY_MINOR_VERSION = 8
61+
62+
# Flag to index functions in handle init request
63+
PYTHON_ENABLE_INIT_INDEXING = "PYTHON_ENABLE_INIT_INDEXING"
64+
65+
METADATA_PROPERTIES_WORKER_INDEXED = "worker_indexed"

azure_functions_worker/dispatcher.py

+92-41
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
PYTHON_ENABLE_DEBUG_LOGGING,
3131
PYTHON_SCRIPT_FILE_NAME,
3232
PYTHON_SCRIPT_FILE_NAME_DEFAULT,
33-
PYTHON_LANGUAGE_RUNTIME)
33+
PYTHON_LANGUAGE_RUNTIME, PYTHON_ENABLE_INIT_INDEXING,
34+
METADATA_PROPERTIES_WORKER_INDEXED)
3435
from .extension import ExtensionManager
3536
from .logging import disable_console_logging, enable_console_logging
3637
from .logging import (logger, error_logger, is_system_log_category,
@@ -72,9 +73,12 @@ def __init__(self, loop: BaseEventLoop, host: str, port: int,
7273
self._function_data_cache_enabled = False
7374
self._functions = functions.Registry()
7475
self._shmem_mgr = SharedMemoryManager()
75-
7676
self._old_task_factory = None
7777

78+
# Used to store metadata returns
79+
self._function_metadata_result = None
80+
self._function_metadata_exception = None
81+
7882
# We allow the customer to change synchronous thread pool max worker
7983
# count by setting the PYTHON_THREADPOOL_THREAD_COUNT app setting.
8084
# For 3.[6|7|8] The default value is 1.
@@ -297,6 +301,14 @@ async def _handle__worker_init_request(self, request):
297301
# dictionary which will be later used in the invocation request
298302
bindings.load_binding_registry()
299303

304+
if is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
305+
try:
306+
self.load_function_metadata(
307+
worker_init_request.function_app_directory,
308+
caller_info="worker_init_request")
309+
except Exception as ex:
310+
self._function_metadata_exception = ex
311+
300312
return protos.StreamingMessage(
301313
request_id=self.request_id,
302314
worker_init_response=protos.WorkerInitResponse(
@@ -313,82 +325,114 @@ async def _handle__worker_status_request(self, request):
313325
request_id=request.request_id,
314326
worker_status_response=protos.WorkerStatusResponse())
315327

328+
def load_function_metadata(self, function_app_directory, caller_info):
329+
"""
330+
This method is called to index the functions in the function app
331+
directory and save the results in function_metadata_result or
332+
function_metadata_exception in case of an exception.
333+
"""
334+
script_file_name = get_app_setting(
335+
setting=PYTHON_SCRIPT_FILE_NAME,
336+
default_value=f'{PYTHON_SCRIPT_FILE_NAME_DEFAULT}')
337+
338+
logger.debug(
339+
'Received load metadata request from %s, request ID %s, '
340+
'script_file_name: %s',
341+
caller_info, self.request_id, script_file_name)
342+
343+
validate_script_file_name(script_file_name)
344+
function_path = os.path.join(function_app_directory,
345+
script_file_name)
346+
347+
self._function_metadata_result = (
348+
self.index_functions(function_path)) \
349+
if os.path.exists(function_path) else None
350+
316351
async def _handle__functions_metadata_request(self, request):
317352
metadata_request = request.functions_metadata_request
318-
directory = metadata_request.function_app_directory
353+
function_app_directory = metadata_request.function_app_directory
354+
319355
script_file_name = get_app_setting(
320356
setting=PYTHON_SCRIPT_FILE_NAME,
321357
default_value=f'{PYTHON_SCRIPT_FILE_NAME_DEFAULT}')
322-
function_path = os.path.join(directory, script_file_name)
358+
function_path = os.path.join(function_app_directory,
359+
script_file_name)
323360

324361
logger.info(
325-
'Received WorkerMetadataRequest, request ID %s, function_path: %s',
362+
'Received WorkerMetadataRequest, request ID %s, '
363+
'function_path: %s',
326364
self.request_id, function_path)
327365

328-
try:
329-
validate_script_file_name(script_file_name)
330-
331-
if not os.path.exists(function_path):
332-
# Fallback to legacy model
333-
return protos.StreamingMessage(
334-
request_id=request.request_id,
335-
function_metadata_response=protos.FunctionMetadataResponse(
336-
use_default_metadata_indexing=True,
337-
result=protos.StatusResult(
338-
status=protos.StatusResult.Success)))
339-
340-
fx_metadata_results = self.index_functions(function_path)
366+
if not is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
367+
try:
368+
self.load_function_metadata(
369+
function_app_directory,
370+
caller_info="functions_metadata_request")
371+
except Exception as ex:
372+
self._function_metadata_exception = ex
341373

374+
if self._function_metadata_exception:
342375
return protos.StreamingMessage(
343376
request_id=request.request_id,
344377
function_metadata_response=protos.FunctionMetadataResponse(
345-
function_metadata_results=fx_metadata_results,
346378
result=protos.StatusResult(
347-
status=protos.StatusResult.Success)))
379+
status=protos.StatusResult.Failure,
380+
exception=self._serialize_exception(
381+
self._function_metadata_exception))))
382+
else:
383+
metadata_result = self._function_metadata_result
348384

349-
except Exception as ex:
350385
return protos.StreamingMessage(
351-
request_id=self.request_id,
386+
request_id=request.request_id,
352387
function_metadata_response=protos.FunctionMetadataResponse(
388+
use_default_metadata_indexing=False if metadata_result else
389+
True,
390+
function_metadata_results=metadata_result,
353391
result=protos.StatusResult(
354-
status=protos.StatusResult.Failure,
355-
exception=self._serialize_exception(ex))))
392+
status=protos.StatusResult.Success)))
356393

357394
async def _handle__function_load_request(self, request):
358395
func_request = request.function_load_request
359396
function_id = func_request.function_id
360397
function_metadata = func_request.metadata
361398
function_name = function_metadata.name
399+
function_app_directory = function_metadata.directory
362400

363401
logger.info(
364402
'Received WorkerLoadRequest, request ID %s, function_id: %s,'
365-
'function_name: %s,', self.request_id, function_id, function_name)
403+
'function_name: %s',
404+
self.request_id, function_id, function_name)
366405

367406
programming_model = "V2"
368407
try:
369408
if not self._functions.get_function(function_id):
370-
script_file_name = get_app_setting(
371-
setting=PYTHON_SCRIPT_FILE_NAME,
372-
default_value=f'{PYTHON_SCRIPT_FILE_NAME_DEFAULT}')
373-
validate_script_file_name(script_file_name)
374-
function_path = os.path.join(
375-
function_metadata.directory,
376-
script_file_name)
377-
378-
if function_metadata.properties.get("worker_indexed", False) \
379-
or os.path.exists(function_path):
409+
410+
if function_metadata.properties.get(
411+
METADATA_PROPERTIES_WORKER_INDEXED, False):
380412
# This is for the second worker and above where the worker
381413
# indexing is enabled and load request is called without
382414
# calling the metadata request. In this case we index the
383415
# function and update the workers registry
384-
_ = self.index_functions(function_path)
416+
417+
try:
418+
self.load_function_metadata(
419+
function_app_directory,
420+
caller_info="functions_load_request")
421+
except Exception as ex:
422+
self._function_metadata_exception = ex
423+
424+
# For the second worker, if there was an exception in
425+
# indexing, we raise it here
426+
if self._function_metadata_exception:
427+
raise Exception(self._function_metadata_exception)
428+
385429
else:
386430
# legacy function
387431
programming_model = "V1"
388432

389433
func = loader.load_function(
390-
func_request.metadata.name,
391-
func_request.metadata.directory,
434+
function_name,
435+
function_app_directory,
392436
func_request.metadata.script_file,
393437
func_request.metadata.entry_point)
394438

@@ -562,6 +606,7 @@ async def _handle__function_environment_reload_request(self, request):
562606

563607
func_env_reload_request = \
564608
request.function_environment_reload_request
609+
directory = func_env_reload_request.function_app_directory
565610

566611
# Append function project root to module finding sys.path
567612
if func_env_reload_request.function_app_directory:
@@ -587,14 +632,20 @@ async def _handle__function_environment_reload_request(self, request):
587632
root_logger.setLevel(logging.DEBUG)
588633

589634
# Reload azure google namespaces
590-
DependencyManager.reload_customer_libraries(
591-
func_env_reload_request.function_app_directory
592-
)
635+
DependencyManager.reload_customer_libraries(directory)
593636

594637
# calling load_binding_registry again since the
595638
# reload_customer_libraries call clears the registry
596639
bindings.load_binding_registry()
597640

641+
if is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
642+
try:
643+
self.load_function_metadata(
644+
directory,
645+
caller_info="environment_reload_request")
646+
except Exception as ex:
647+
self._function_metadata_exception = ex
648+
598649
# Change function app directory
599650
if getattr(func_env_reload_request,
600651
'function_app_directory', None):

azure_functions_worker/loader.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from .utils.common import get_app_setting
2020
from .constants import MODULE_NOT_FOUND_TS_URL, PYTHON_SCRIPT_FILE_NAME, \
2121
PYTHON_SCRIPT_FILE_NAME_DEFAULT, PYTHON_LANGUAGE_RUNTIME, \
22-
CUSTOMER_PACKAGES_PATH, RETRY_POLICY
22+
CUSTOMER_PACKAGES_PATH, RETRY_POLICY, METADATA_PROPERTIES_WORKER_INDEXED
2323
from .logging import logger
2424
from .utils.wrappers import attach_message_to_exception
2525

@@ -145,7 +145,7 @@ def process_indexed_function(functions_registry: functions.Registry,
145145
bindings=binding_protos,
146146
raw_bindings=raw_bindings,
147147
retry_options=retry_protos,
148-
properties={"worker_indexed": "True"})
148+
properties={METADATA_PROPERTIES_WORKER_INDEXED: "True"})
149149

150150
fx_metadata_results.append(function_metadata)
151151

azure_functions_worker/utils/app_setting_manager.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT_39,
1212
PYTHON_ENABLE_DEBUG_LOGGING,
1313
FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED,
14-
PYTHON_SCRIPT_FILE_NAME)
14+
PYTHON_SCRIPT_FILE_NAME, PYTHON_ENABLE_INIT_INDEXING)
1515

1616

1717
def get_python_appsetting_state():
@@ -23,7 +23,8 @@ def get_python_appsetting_state():
2323
PYTHON_ENABLE_DEBUG_LOGGING,
2424
PYTHON_ENABLE_WORKER_EXTENSIONS,
2525
FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED,
26-
PYTHON_SCRIPT_FILE_NAME]
26+
PYTHON_SCRIPT_FILE_NAME,
27+
PYTHON_ENABLE_INIT_INDEXING]
2728

2829
app_setting_states = "".join(
2930
f"{app_setting}: {current_vars[app_setting]} | "

azure_functions_worker/utils/common.py

-1
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,3 @@ def validate_script_file_name(file_name: str):
153153
pattern = re.compile(r'^[a-zA-Z0-9_][a-zA-Z0-9_\-]*\.py$')
154154
if not pattern.match(file_name):
155155
raise InvalidFileNameError(file_name)
156-
return True

tests/consumption_tests/test_linux_consumption.py

+29-12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
from requests import Request
99

10+
from azure_functions_worker.constants import PYTHON_ENABLE_INIT_INDEXING, \
11+
PYTHON_ENABLE_WORKER_EXTENSIONS, PYTHON_ISOLATE_WORKER_DEPENDENCIES, \
12+
PYTHON_ENABLE_DEBUG_LOGGING
1013
from tests.utils.testutils_lc import (
1114
LinuxConsumptionWebHostController
1215
)
@@ -107,7 +110,7 @@ def test_new_protobuf(self):
107110
ctrl.assign_container(env={
108111
"AzureWebJobsStorage": self._storage,
109112
"SCM_RUN_FROM_PACKAGE": self._get_blob_url("NewProtobuf"),
110-
"PYTHON_ISOLATE_WORKER_DEPENDENCIES": "1"
113+
PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1"
111114
})
112115
req = Request('GET', f'{ctrl.url}/api/HttpTrigger')
113116
resp = ctrl.send_request(req)
@@ -137,7 +140,7 @@ def test_old_protobuf(self):
137140
ctrl.assign_container(env={
138141
"AzureWebJobsStorage": self._storage,
139142
"SCM_RUN_FROM_PACKAGE": self._get_blob_url("OldProtobuf"),
140-
"PYTHON_ISOLATE_WORKER_DEPENDENCIES": "1"
143+
PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1"
141144
})
142145
req = Request('GET', f'{ctrl.url}/api/HttpTrigger')
143146
resp = ctrl.send_request(req)
@@ -189,7 +192,7 @@ def test_debug_logging_enabled(self):
189192
"AzureWebJobsStorage": self._storage,
190193
"SCM_RUN_FROM_PACKAGE": self._get_blob_url(
191194
"EnableDebugLogging"),
192-
"PYTHON_ENABLE_DEBUG_LOGGING": "1"
195+
PYTHON_ENABLE_DEBUG_LOGGING: "1"
193196
})
194197
req = Request('GET', f'{ctrl.url}/api/HttpTrigger1')
195198
resp = ctrl.send_request(req)
@@ -218,7 +221,7 @@ def test_pinning_functions_to_older_version(self):
218221
"AzureWebJobsStorage": self._storage,
219222
"SCM_RUN_FROM_PACKAGE": self._get_blob_url(
220223
"PinningFunctions"),
221-
"PYTHON_ISOLATE_WORKER_DEPENDENCIES": "1",
224+
PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1",
222225
})
223226
req = Request('GET', f'{ctrl.url}/api/HttpTrigger1')
224227
resp = ctrl.send_request(req)
@@ -232,8 +235,7 @@ def test_opencensus_with_extensions_enabled(self):
232235
"""A function app with extensions enabled containing the
233236
following libraries:
234237
235-
azure-functions, azure-eventhub, azure-storage-blob, numpy,
236-
cryptography, pyodbc, requests
238+
azure-functions, opencensus
237239
238240
should return 200 after importing all libraries.
239241
"""
@@ -242,8 +244,25 @@ def test_opencensus_with_extensions_enabled(self):
242244
ctrl.assign_container(env={
243245
"AzureWebJobsStorage": self._storage,
244246
"SCM_RUN_FROM_PACKAGE": self._get_blob_url("Opencensus"),
245-
"PYTHON_ENABLE_WORKER_EXTENSIONS": "1",
246-
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
247+
PYTHON_ENABLE_WORKER_EXTENSIONS: "1"
248+
})
249+
req = Request('GET', f'{ctrl.url}/api/opencensus')
250+
resp = ctrl.send_request(req)
251+
self.assertEqual(resp.status_code, 200)
252+
253+
@skipIf(sys.version_info.minor != 10,
254+
"This is testing only for python310")
255+
def test_opencensus_with_extensions_enabled_init_indexing(self):
256+
"""
257+
A function app with init indexing enabled
258+
"""
259+
with LinuxConsumptionWebHostController(_DEFAULT_HOST_VERSION,
260+
self._py_version) as ctrl:
261+
ctrl.assign_container(env={
262+
"AzureWebJobsStorage": self._storage,
263+
"SCM_RUN_FROM_PACKAGE": self._get_blob_url("Opencensus"),
264+
PYTHON_ENABLE_WORKER_EXTENSIONS: "1",
265+
PYTHON_ENABLE_INIT_INDEXING: "true"
247266
})
248267
req = Request('GET', f'{ctrl.url}/api/opencensus')
249268
resp = ctrl.send_request(req)
@@ -263,8 +282,7 @@ def test_reload_variables_after_timeout_error(self):
263282
"AzureWebJobsStorage": self._storage,
264283
"SCM_RUN_FROM_PACKAGE": self._get_blob_url(
265284
"TimeoutError"),
266-
"PYTHON_ISOLATE_WORKER_DEPENDENCIES": "1",
267-
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
285+
PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1"
268286
})
269287
req = Request('GET', f'{ctrl.url}/api/hello')
270288
resp = ctrl.send_request(req)
@@ -297,8 +315,7 @@ def test_reload_variables_after_oom_error(self):
297315
"AzureWebJobsStorage": self._storage,
298316
"SCM_RUN_FROM_PACKAGE": self._get_blob_url(
299317
"OOMError"),
300-
"PYTHON_ISOLATE_WORKER_DEPENDENCIES": "1",
301-
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
318+
PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1"
302319
})
303320
req = Request('GET', f'{ctrl.url}/api/httptrigger')
304321
resp = ctrl.send_request(req)

0 commit comments

Comments
 (0)