Skip to content

Commit 5909387

Browse files
gavin-aguiarroot
andauthored
Reload modules when not in placeholder mode (#1340)
* Reload modules when not in placeholder mode * Flake8 validation fixes * Added unit test * Added unit test * Refactored and added tests * Removed unused import * Fixed consumption test * Removed assert in test * Modified tests * Fixed flake8 validation --------- Co-authored-by: root <root@GavinPC>
1 parent 1a21fa5 commit 5909387

File tree

5 files changed

+154
-23
lines changed

5 files changed

+154
-23
lines changed

azure_functions_worker/dispatcher.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -286,10 +286,7 @@ async def _handle__worker_init_request(self, request):
286286
constants.SHARED_MEMORY_DATA_TRANSFER: _TRUE,
287287
}
288288

289-
# Can detech worker packages only when customer's code is present
290-
# This only works in dedicated and premium sku.
291-
# The consumption sku will switch on environment_reload request.
292-
if not DependencyManager.is_in_linux_consumption():
289+
if DependencyManager.should_load_cx_dependencies():
293290
DependencyManager.prioritize_customer_dependencies()
294291

295292
if DependencyManager.is_in_linux_consumption():
@@ -544,6 +541,8 @@ async def _handle__invocation_request(self, request):
544541

545542
async def _handle__function_environment_reload_request(self, request):
546543
"""Only runs on Linux Consumption placeholder specialization.
544+
This is called only when placeholder mode is true. On worker restarts
545+
worker init request will be called directly.
547546
"""
548547
try:
549548
logger.info('Received FunctionEnvironmentReloadRequest, '

azure_functions_worker/logging.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
CONSOLE_LOG_PREFIX = "LanguageWorkerConsoleLog"
1212
SYSTEM_LOG_PREFIX = "azure_functions_worker"
1313
SDK_LOG_PREFIX = "azure.functions"
14+
SYSTEM_ERROR_LOG_PREFIX = "azure_functions_worker_errors"
1415

1516

16-
logger: logging.Logger = logging.getLogger('azure_functions_worker')
17+
logger: logging.Logger = logging.getLogger(SYSTEM_LOG_PREFIX)
1718
error_logger: logging.Logger = (
18-
logging.getLogger('azure_functions_worker_errors'))
19+
logging.getLogger(SYSTEM_ERROR_LOG_PREFIX))
1920

2021
handler: Optional[logging.Handler] = None
2122
error_handler: Optional[logging.Handler] = None

azure_functions_worker/utils/dependency.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from types import ModuleType
99
from typing import List, Optional
1010

11-
from azure_functions_worker.utils.common import is_true_like
11+
from azure_functions_worker.utils.common import is_true_like, is_envvar_true
1212
from ..constants import (
1313
AZURE_WEBJOBS_SCRIPT_ROOT,
1414
CONTAINER_NAME,
@@ -74,14 +74,23 @@ def initialize(cls):
7474
def is_in_linux_consumption(cls):
7575
return CONTAINER_NAME in os.environ
7676

77+
@classmethod
78+
def should_load_cx_dependencies(cls):
79+
"""
80+
Customer dependencies should be loaded when dependency
81+
isolation is enabled and
82+
1) App is a dedicated app
83+
2) App is linux consumption but not in placeholder mode.
84+
This can happen when the worker restarts for any reason
85+
(OOM, timeouts etc) and env reload request is not called.
86+
"""
87+
return not (DependencyManager.is_in_linux_consumption()
88+
and is_envvar_true("WEBSITE_PLACEHOLDER_MODE"))
89+
7790
@classmethod
7891
@enable_feature_by(
7992
flag=PYTHON_ISOLATE_WORKER_DEPENDENCIES,
80-
flag_default=(
81-
PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT_310 if
82-
is_python_version('3.10') else
83-
PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT
84-
)
93+
flag_default=PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT
8594
)
8695
def use_worker_dependencies(cls):
8796
"""Switch the sys.path and ensure the worker imports are loaded from
@@ -109,11 +118,7 @@ def use_worker_dependencies(cls):
109118
@classmethod
110119
@enable_feature_by(
111120
flag=PYTHON_ISOLATE_WORKER_DEPENDENCIES,
112-
flag_default=(
113-
PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT_310 if
114-
is_python_version('3.10') else
115-
PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT
116-
)
121+
flag_default=PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT
117122
)
118123
def prioritize_customer_dependencies(cls, cx_working_dir=None):
119124
"""Switch the sys.path and ensure the customer's code import are loaded
@@ -147,9 +152,12 @@ def prioritize_customer_dependencies(cls, cx_working_dir=None):
147152
cx_deps_path = cls.cx_deps_path
148153

149154
logger.info(
150-
'Applying prioritize_customer_dependencies: worker_dependencies: '
151-
'%s, customer_dependencies: %s, working_directory: %s',
152-
cls.worker_deps_path, cx_deps_path, working_directory)
155+
'Applying prioritize_customer_dependencies: '
156+
'worker_dependencies_path: %s, customer_dependencies_path: %s, '
157+
'working_directory: %s, Linux Consumption: %s, Placeholder: %s',
158+
cls.worker_deps_path, cx_deps_path, working_directory,
159+
DependencyManager.is_in_linux_consumption(),
160+
is_envvar_true("WEBSITE_PLACEHOLDER_MODE"))
153161

154162
cls._remove_from_sys_path(cls.worker_deps_path)
155163
cls._add_to_sys_path(cls.cx_deps_path, True)
@@ -176,6 +184,9 @@ def reload_customer_libraries(cls, cx_working_dir: str):
176184
Depends on the PYTHON_ISOLATE_WORKER_DEPENDENCIES, the actual behavior
177185
differs.
178186
187+
This is called only when placeholder mode is true. In the case of a
188+
worker restart, this will not be called.
189+
179190
Parameters
180191
----------
181192
cx_working_dir: str

tests/consumption_tests/test_linux_consumption.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Licensed under the MIT License.
33
import os
44
import sys
5+
from time import sleep
56
from unittest import TestCase, skipIf
67

78
from requests import Request
@@ -248,6 +249,76 @@ def test_opencensus_with_extensions_enabled(self):
248249
resp = ctrl.send_request(req)
249250
self.assertEqual(resp.status_code, 200)
250251

252+
@skipIf(sys.version_info.minor != 9,
253+
"This is testing only for python39 where extensions"
254+
"enabled by default")
255+
def test_reload_variables_after_timeout_error(self):
256+
"""
257+
A function app with HTTPtrigger which has a function timeout of
258+
20s. The app as a sleep of 30s which should trigger a timeout
259+
"""
260+
with LinuxConsumptionWebHostController(_DEFAULT_HOST_VERSION,
261+
self._py_version) as ctrl:
262+
ctrl.assign_container(env={
263+
"AzureWebJobsStorage": self._storage,
264+
"SCM_RUN_FROM_PACKAGE": self._get_blob_url(
265+
"TimeoutError"),
266+
"PYTHON_ISOLATE_WORKER_DEPENDENCIES": "1",
267+
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
268+
})
269+
req = Request('GET', f'{ctrl.url}/api/hello')
270+
resp = ctrl.send_request(req)
271+
self.assertEqual(resp.status_code, 500)
272+
273+
sleep(2)
274+
logs = ctrl.get_container_logs()
275+
self.assertRegex(
276+
logs,
277+
r"Applying prioritize_customer_dependencies: "
278+
r"worker_dependencies_path: \/azure-functions-host\/"
279+
r"workers\/python\/.*?\/LINUX\/X64,"
280+
r" customer_dependencies_path: \/home\/site\/wwwroot\/"
281+
r"\.python_packages\/lib\/site-packages, working_directory:"
282+
r" \/home\/site\/wwwroot, Linux Consumption: True,"
283+
r" Placeholder: False")
284+
self.assertNotIn("Failure Exception: ModuleNotFoundError",
285+
logs)
286+
287+
@skipIf(sys.version_info.minor != 9,
288+
"This is testing only for python39 where extensions"
289+
"enabled by default")
290+
def test_reload_variables_after_oom_error(self):
291+
"""
292+
A function app with HTTPtrigger mocking error code 137
293+
"""
294+
with LinuxConsumptionWebHostController(_DEFAULT_HOST_VERSION,
295+
self._py_version) as ctrl:
296+
ctrl.assign_container(env={
297+
"AzureWebJobsStorage": self._storage,
298+
"SCM_RUN_FROM_PACKAGE": self._get_blob_url(
299+
"OOMError"),
300+
"PYTHON_ISOLATE_WORKER_DEPENDENCIES": "1",
301+
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
302+
})
303+
req = Request('GET', f'{ctrl.url}/api/httptrigger')
304+
resp = ctrl.send_request(req)
305+
self.assertEqual(resp.status_code, 500)
306+
307+
sleep(2)
308+
logs = ctrl.get_container_logs()
309+
self.assertRegex(
310+
logs,
311+
r"Applying prioritize_customer_dependencies: "
312+
r"worker_dependencies_path: \/azure-functions-host\/"
313+
r"workers\/python\/.*?\/LINUX\/X64,"
314+
r" customer_dependencies_path: \/home\/site\/wwwroot\/"
315+
r"\.python_packages\/lib\/site-packages, working_directory:"
316+
r" \/home\/site\/wwwroot, Linux Consumption: True,"
317+
r" Placeholder: False")
318+
319+
self.assertNotIn("Failure Exception: ModuleNotFoundError",
320+
logs)
321+
251322
def _get_blob_url(self, scenario_name: str) -> str:
252323
return (
253324
f'https://pythonworker{self._py_shortform}sa.blob.core.windows.net/'

tests/unittests/test_dispatcher.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,7 @@ async def test_dispatcher_functions_metadata_request_legacy_fallback(self):
589589
protos.StatusResult.Success)
590590

591591

592-
class TestDispatcherLoadFunctionInInitRequest(testutils.AsyncTestCase):
592+
class TestDispatcherInitRequest(testutils.AsyncTestCase):
593593

594594
def setUp(self):
595595
self._ctrl = testutils.start_mockhost(
@@ -606,8 +606,7 @@ def tearDown(self):
606606
self.mock_version_info.stop()
607607

608608
async def test_dispatcher_load_azfunc_in_init(self):
609-
"""Test if the dispatcher's log can be flushed out during worker
610-
initialization
609+
"""Test if azure functions is loaded during init
611610
"""
612611
async with self._ctrl as host:
613612
r = await host.init_worker('4.15.1')
@@ -618,3 +617,53 @@ async def test_dispatcher_load_azfunc_in_init(self):
618617
1
619618
)
620619
self.assertIn("azure.functions", sys.modules)
620+
621+
async def test_dispatcher_load_modules_dedicated_app(self):
622+
"""Test modules are loaded in dedicated apps
623+
"""
624+
os.environ["PYTHON_ISOLATE_WORKER_DEPENDENCIES"] = "1"
625+
626+
# Dedicated Apps where placeholder mode is not set
627+
async with self._ctrl as host:
628+
r = await host.init_worker('4.15.1')
629+
logs = [log.message for log in r.logs]
630+
self.assertIn(
631+
"Applying prioritize_customer_dependencies: "
632+
"worker_dependencies_path: , customer_dependencies_path: , "
633+
"working_directory: , Linux Consumption: False,"
634+
" Placeholder: False", logs
635+
)
636+
637+
async def test_dispatcher_load_modules_con_placeholder_enabled(self):
638+
"""Test modules are loaded in consumption apps with placeholder mode
639+
enabled.
640+
"""
641+
# Consumption apps with placeholder mode enabled
642+
os.environ["PYTHON_ISOLATE_WORKER_DEPENDENCIES"] = "1"
643+
os.environ["CONTAINER_NAME"] = "test"
644+
os.environ["WEBSITE_PLACEHOLDER_MODE"] = "1"
645+
async with self._ctrl as host:
646+
r = await host.init_worker('4.15.1')
647+
logs = [log.message for log in r.logs]
648+
self.assertNotIn(
649+
"Applying prioritize_customer_dependencies: "
650+
"worker_dependencies_path: , customer_dependencies_path: , "
651+
"working_directory: , Linux Consumption: True,", logs)
652+
653+
async def test_dispatcher_load_modules_con_app_placeholder_disabled(self):
654+
"""Test modules are loaded in consumption apps with placeholder mode
655+
disabled.
656+
"""
657+
# Consumption apps with placeholder mode disabled i.e. worker
658+
# is specialized
659+
os.environ["PYTHON_ISOLATE_WORKER_DEPENDENCIES"] = "1"
660+
os.environ["WEBSITE_PLACEHOLDER_MODE"] = "0"
661+
os.environ["CONTAINER_NAME"] = "test"
662+
async with self._ctrl as host:
663+
r = await host.init_worker('4.15.1')
664+
logs = [log.message for log in r.logs]
665+
self.assertIn(
666+
"Applying prioritize_customer_dependencies: "
667+
"worker_dependencies_path: , customer_dependencies_path: , "
668+
"working_directory: , Linux Consumption: True,"
669+
" Placeholder: False", logs)

0 commit comments

Comments
 (0)