Skip to content

SettingsApi ModuleNotFoundError fix when extensions enabled #1305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Aug 25, 2023
10 changes: 8 additions & 2 deletions azure_functions_worker/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import os
import pathlib
import sys

# Capabilities
Expand Down Expand Up @@ -40,14 +41,19 @@
PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT_310 = False
PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT = False
PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT_39 = True
PYTHON_EXTENSIONS_RELOAD_FUNCTIONS = "PYTHON_EXTENSIONS_RELOAD_FUNCTIONS"

# External Site URLs
MODULE_NOT_FOUND_TS_URL = "https://aka.ms/functions-modulenotfound"

# new programming model script file name
SCRIPT_FILE_NAME = "function_app.py"

PYTHON_LANGUAGE_RUNTIME = "python"

# Settings for V2 programming model
RETRY_POLICY = "retry_policy"

# Paths
CUSTOMER_PACKAGES_PATH = os.path.join(pathlib.Path.home(),
pathlib.Path(
"site/wwwroot/.python_packages"))
55 changes: 36 additions & 19 deletions azure_functions_worker/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@
PYTHON_THREADPOOL_THREAD_COUNT_MAX_37,
PYTHON_THREADPOOL_THREAD_COUNT_MIN,
PYTHON_ENABLE_DEBUG_LOGGING, SCRIPT_FILE_NAME,
PYTHON_LANGUAGE_RUNTIME)
PYTHON_LANGUAGE_RUNTIME, CUSTOMER_PACKAGES_PATH)
from .extension import ExtensionManager
from .logging import disable_console_logging, enable_console_logging
from .logging import enable_debug_logging_recommendation
from .logging import (logger, error_logger, is_system_log_category,
CONSOLE_LOG_PREFIX, format_exception)
from .utils.common import get_app_setting, is_envvar_true
Expand Down Expand Up @@ -263,9 +262,14 @@ async def _dispatch_grpc_request(self, request):

async def _handle__worker_init_request(self, request):
logger.info('Received WorkerInitRequest, '
'python version %s, worker version %s, request ID %s',
sys.version, VERSION, self.request_id)
enable_debug_logging_recommendation()
'python version %s, '
'worker version %s, '
'request ID %s.'
' To enable debug level logging, please refer to '
'https://aka.ms/python-enable-debug-logging',
sys.version,
VERSION,
self.request_id)

worker_init_request = request.worker_init_request
host_capabilities = worker_init_request.capabilities
Expand Down Expand Up @@ -293,6 +297,10 @@ async def _handle__worker_init_request(self, request):
"Importing azure functions in WorkerInitRequest")
import azure.functions # NoQA

if CUSTOMER_PACKAGES_PATH not in sys.path:
logger.warning("Customer packages not in sys path. "
"This should never happen! ")

# loading bindings registry and saving results to a static
# dictionary which will be later used in the invocation request
bindings.load_binding_registry()
Expand Down Expand Up @@ -363,6 +371,7 @@ async def _handle__function_load_request(self, request):
'Received WorkerLoadRequest, request ID %s, function_id: %s,'
'function_name: %s,', self.request_id, function_id, function_name)

programming_model = "V1"
try:
if not self._functions.get_function(function_id):
if function_metadata.properties.get("worker_indexed", False) \
Expand All @@ -371,8 +380,7 @@ async def _handle__function_load_request(self, request):
# indexing is enabled and load request is called without
# calling the metadata request. In this case we index the
# function and update the workers registry
logger.info(f"Indexing function {function_name} in the "
f"load request")
programming_model = "V2"
_ = self.index_functions(function_path)
else:
# legacy function
Expand All @@ -385,17 +393,24 @@ async def _handle__function_load_request(self, request):
self._functions.add_function(
function_id, func, func_request.metadata)

ExtensionManager.function_load_extension(
try:
ExtensionManager.function_load_extension(
function_name,
func_request.metadata.directory
)
except Exception as ex:
logging.error("Failed to load extensions: ", ex)
raise

logger.info('Successfully processed FunctionLoadRequest, '
'request ID: %s, '
'function ID: %s,'
'function Name: %s,'
'programming model: %s',
self.request_id,
function_id,
function_name,
func_request.metadata.directory
)

logger.info('Successfully processed FunctionLoadRequest, '
'request ID: %s, '
'function ID: %s,'
'function Name: %s', self.request_id,
function_id,
function_name)
programming_model)

return protos.StreamingMessage(
request_id=self.request_id,
Expand Down Expand Up @@ -532,8 +547,10 @@ async def _handle__function_environment_reload_request(self, request):
"""
try:
logger.info('Received FunctionEnvironmentReloadRequest, '
'request ID: %s', self.request_id)
enable_debug_logging_recommendation()
'request ID: %s,'
' To enable debug level logging, please refer to '
'https://aka.ms/python-enable-debug-logging',
self.request_id)

func_env_reload_request = \
request.function_environment_reload_request
Expand Down
5 changes: 2 additions & 3 deletions azure_functions_worker/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,8 @@ def _try_get_sdk_with_extension_enabled(cls) -> Optional[ModuleType]:
@classmethod
def _info_extension_is_enabled(cls, sdk):
logger.info(
'Python Worker Extension is enabled in azure.functions (%s).',
get_sdk_version(sdk)
)
'Python Worker Extension is enabled in azure.functions (%s). '
'Sdk path: %s', get_sdk_version(sdk), sdk.__file__)

@classmethod
def _info_discover_extension_list(cls, function_name, sdk):
Expand Down
14 changes: 5 additions & 9 deletions azure_functions_worker/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,12 @@
from . import protos, functions
from .bindings.retrycontext import RetryPolicy
from .constants import MODULE_NOT_FOUND_TS_URL, SCRIPT_FILE_NAME, \
PYTHON_LANGUAGE_RUNTIME, RETRY_POLICY
PYTHON_LANGUAGE_RUNTIME, RETRY_POLICY, CUSTOMER_PACKAGES_PATH
from .utils.wrappers import attach_message_to_exception

_AZURE_NAMESPACE = '__app__'
_DEFAULT_SCRIPT_FILENAME = '__init__.py'
_DEFAULT_ENTRY_POINT = 'main'

PKGS_PATH = pathlib.Path("site/wwwroot/.python_packages")
home = pathlib.Path.home()
pkgs_path = os.path.join(home, PKGS_PATH)


_submodule_dirs = []


Expand Down Expand Up @@ -140,7 +134,8 @@ def process_indexed_function(functions_registry: functions.Registry,
f' guide: {MODULE_NOT_FOUND_TS_URL} ',
debug_logs='Error in load_function. '
f'Sys Path: {sys.path}, Sys Module: {sys.modules},'
f'python-packages Path exists: {os.path.exists(pkgs_path)}')
'python-packages Path exists: '
f'{os.path.exists(CUSTOMER_PACKAGES_PATH)}')
def load_function(name: str, directory: str, script_file: str,
entry_point: Optional[str]):
dir_path = pathlib.Path(directory)
Expand Down Expand Up @@ -191,7 +186,8 @@ def load_function(name: str, directory: str, script_file: str,
message=f'Troubleshooting Guide: {MODULE_NOT_FOUND_TS_URL}',
debug_logs='Error in index_function_app. '
f'Sys Path: {sys.path}, Sys Module: {sys.modules},'
f'python-packages Path exists: {os.path.exists(pkgs_path)}')
'python-packages Path exists: '
f'{os.path.exists(CUSTOMER_PACKAGES_PATH)}')
def index_function_app(function_path: str):
module_name = pathlib.Path(function_path).stem
imported_module = importlib.import_module(module_name)
Expand Down
5 changes: 0 additions & 5 deletions azure_functions_worker/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,6 @@ def enable_console_logging() -> None:
logger.addHandler(handler)


def enable_debug_logging_recommendation():
logging.info("To enable debug level logging, please refer to "
"https://aka.ms/python-enable-debug-logging")


def is_system_log_category(ctg: str) -> bool:
"""Check if the logging namespace belongs to system logs. Category starts
with the following name will be treated as system logs.
Expand Down
40 changes: 25 additions & 15 deletions azure_functions_worker/utils/common.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
from typing import Optional, Callable
from types import ModuleType
import importlib
import os
import sys
import importlib
from types import ModuleType
from typing import Optional, Callable

from azure_functions_worker.constants import CUSTOMER_PACKAGES_PATH, \
PYTHON_EXTENSIONS_RELOAD_FUNCTIONS


def is_true_like(setting: str) -> bool:
Expand Down Expand Up @@ -110,19 +113,26 @@ def get_sdk_from_sys_path() -> ModuleType:
ModuleType
The azure.functions that is loaded from the first sys.path entry
"""
backup_azure_functions = None
backup_azure = None

if 'azure.functions' in sys.modules:
backup_azure_functions = sys.modules.pop('azure.functions')
if 'azure' in sys.modules:
backup_azure = sys.modules.pop('azure')
if is_envvar_true(PYTHON_EXTENSIONS_RELOAD_FUNCTIONS):
backup_azure_functions = None
backup_azure = None

if 'azure.functions' in sys.modules:
backup_azure_functions = sys.modules.pop('azure.functions')
if 'azure' in sys.modules:
backup_azure = sys.modules.pop('azure')

module = importlib.import_module('azure.functions')

if backup_azure:
sys.modules['azure'] = backup_azure
if backup_azure_functions:
sys.modules['azure.functions'] = backup_azure_functions

module = importlib.import_module('azure.functions')
return module

if backup_azure:
sys.modules['azure'] = backup_azure
if backup_azure_functions:
sys.modules['azure.functions'] = backup_azure_functions
if CUSTOMER_PACKAGES_PATH not in sys.path:
sys.path.insert(0, CUSTOMER_PACKAGES_PATH)

return module
return importlib.import_module('azure.functions')
10 changes: 5 additions & 5 deletions azure_functions_worker/utils/dependency.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
from azure_functions_worker.utils.common import is_true_like
from typing import List, Optional
from types import ModuleType
import importlib
import inspect
import os
import re
import sys
from types import ModuleType
from typing import List, Optional

from ..logging import logger
from azure_functions_worker.utils.common import is_true_like
from ..constants import (
AZURE_WEBJOBS_SCRIPT_ROOT,
CONTAINER_NAME,
PYTHON_ISOLATE_WORKER_DEPENDENCIES,
PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT,
PYTHON_ISOLATE_WORKER_DEPENDENCIES_DEFAULT_310
)
from ..logging import logger
from ..utils.common import is_python_version
from ..utils.wrappers import enable_feature_by

Expand Down Expand Up @@ -226,7 +226,7 @@ def reload_azure_google_namespace_from_worker_deps(cls):
logger.info('Reloaded azure.functions module now at %s',
inspect.getfile(sys.modules['azure.functions']))
except Exception as ex:
logger.info(
logger.warning(
'Unable to reload azure.functions. Using default. '
'Exception:\n%s', ex)

Expand Down
25 changes: 24 additions & 1 deletion tests/consumption_tests/test_linux_consumption.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,14 +217,37 @@ def test_pinning_functions_to_older_version(self):
"AzureWebJobsStorage": self._storage,
"SCM_RUN_FROM_PACKAGE": self._get_blob_url(
"PinningFunctions"),
"PYTHON_ISOLATE_WORKER_DEPENDENCIES": "1"
"PYTHON_ISOLATE_WORKER_DEPENDENCIES": "1",
})
req = Request('GET', f'{ctrl.url}/api/HttpTrigger1')
resp = ctrl.send_request(req)

self.assertEqual(resp.status_code, 200)
self.assertIn("Func Version: 1.11.1", resp.text)

@skipIf(sys.version_info.minor != 10,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want the same for 3.9? and see what the experience is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed. It should have the same behavior since extensions is enabled in this test.

"This is testing only for python310")
def test_opencensus_with_extensions_enabled(self):
"""A function app with extensions enabled containing the
following libraries:

azure-functions, azure-eventhub, azure-storage-blob, numpy,
cryptography, pyodbc, requests

should return 200 after importing all libraries.
"""
with LinuxConsumptionWebHostController(_DEFAULT_HOST_VERSION,
self._py_version) as ctrl:
ctrl.assign_container(env={
"AzureWebJobsStorage": self._storage,
"SCM_RUN_FROM_PACKAGE": self._get_blob_url("Opencensus"),
"PYTHON_ENABLE_WORKER_EXTENSIONS": "1",
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
})
req = Request('GET', f'{ctrl.url}/api/opencensus')
resp = ctrl.send_request(req)
self.assertEqual(resp.status_code, 200)

def _get_blob_url(self, scenario_name: str) -> str:
return (
f'https://pythonworker{self._py_shortform}sa.blob.core.windows.net/'
Expand Down
Loading