From efc02f3a4643c6313b6e59d8abf27fd10133b6fe Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Nov 2023 14:41:23 -0500 Subject: [PATCH 001/101] Http Proxy Support Http Proxy Support integration refactor update ext name add to env reload add dep revert --- azure_functions_worker/bindings/meta.py | 36 +- azure_functions_worker/constants.py | 13 + azure_functions_worker/dispatcher.py | 227 +++++++--- azure_functions_worker/functions.py | 18 +- azure_functions_worker/http_proxy.py | 148 +++++++ azure_functions_worker/loader.py | 53 +-- azure_functions_worker/logging.py | 3 +- python/test/worker.config.json | 2 +- setup.py | 5 +- .../fastapi_data_class/function_app.py | 108 +++++ .../function_app.py | 108 +++++ .../function_app.py | 108 +++++ .../function_app.py | 108 +++++ .../http_v2_functions/function_app.py | 419 ++++++++++++++++++ tests/unittests/test_http_functions.py | 7 +- 15 files changed, 1265 insertions(+), 98 deletions(-) create mode 100644 azure_functions_worker/http_proxy.py create mode 100644 tests/endtoend/http_v2_functions/fastapi_data_class/function_app.py create mode 100644 tests/endtoend/http_v2_functions/fastapi_non_streaming_custom_resps/function_app.py create mode 100644 tests/endtoend/http_v2_functions/fastapi_streaming_download_func/function_app.py create mode 100644 tests/endtoend/http_v2_functions/fastapi_streaming_upload_func/function_app.py create mode 100644 tests/unittests/http_functions/http_v2_functions/function_app.py diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index f7a810145..ccfaa8200 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -3,8 +3,9 @@ import sys import typing -from .. import protos +from azure_functions_worker.constants import HTTP, HTTP_TRIGGER +from .. import protos from . import datumdef from . import generic from .shared_memory_data_transfer import SharedMemoryManager @@ -14,6 +15,30 @@ PB_TYPE_RPC_SHARED_MEMORY = 'rpc_shared_memory' BINDING_REGISTRY = None +def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: + ext_base = sys.modules.get('azure.functions.extension.base') + if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled(): + return ext_base.RequestTrackerMeta.check_type(pytype) + + binding = get_binding(bind_name) + return binding.check_input_type_annotation(pytype) + +def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool: + ext_base = sys.modules.get('azure.functions.extension.base') + if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled(): + return ext_base.ResponseTrackerMeta.check_type(pytype) + + binding = get_binding(bind_name) + return binding.check_output_type_annotation(pytype) + + +INPUT_TYPE_CHECK_OVERRIDE_MAP = { + HTTP_TRIGGER: _check_http_input_type_annotation +} + +OUTPUT_TYPE_CHECK_OVERRIDE_MAP = { + HTTP: _check_http_output_type_annotation +} def load_binding_registry() -> None: func = sys.modules.get('azure.functions') @@ -43,11 +68,18 @@ def is_trigger_binding(bind_name: str) -> bool: def check_input_type_annotation(bind_name: str, pytype: type) -> bool: + global INPUT_TYPE_CHECK_OVERRIDE_MAP + if bind_name in INPUT_TYPE_CHECK_OVERRIDE_MAP: + return INPUT_TYPE_CHECK_OVERRIDE_MAP[bind_name](bind_name, pytype) + binding = get_binding(bind_name) return binding.check_input_type_annotation(pytype) - def check_output_type_annotation(bind_name: str, pytype: type) -> bool: + global OUTPUT_TYPE_CHECK_OVERRIDE_MAP + if bind_name in OUTPUT_TYPE_CHECK_OVERRIDE_MAP: + return OUTPUT_TYPE_CHECK_OVERRIDE_MAP[bind_name](bind_name, pytype) + binding = get_binding(bind_name) return binding.check_output_type_annotation(pytype) diff --git a/azure_functions_worker/constants.py b/azure_functions_worker/constants.py index b6cc668b6..7f09d586c 100644 --- a/azure_functions_worker/constants.py +++ b/azure_functions_worker/constants.py @@ -10,6 +10,7 @@ WORKER_STATUS = "WorkerStatus" SHARED_MEMORY_DATA_TRANSFER = "SharedMemoryDataTransfer" FUNCTION_DATA_CACHE = "FunctionDataCache" +HTTP_URI = "HttpUri" # Platform Environment Variables AZURE_WEBJOBS_SCRIPT_ROOT = "AzureWebJobsScriptRoot" @@ -60,3 +61,15 @@ PYTHON_ENABLE_INIT_INDEXING = "PYTHON_ENABLE_INIT_INDEXING" METADATA_PROPERTIES_WORKER_INDEXED = "worker_indexed" + +# HostNames +LOCAL_HOST = "localhost" + +# Header names +X_MS_INVOCATION_ID = "x-ms-invocation-id" + +# Trigger Names +HTTP_TRIGGER = "httpTrigger" + +# Output Names +HTTP = "http" diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 2f94e21ba..35143fb68 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -7,6 +7,7 @@ import asyncio import concurrent.futures +import importlib import logging import os import platform @@ -19,10 +20,10 @@ from datetime import datetime import grpc - +import socket from . import bindings, constants, functions, loader, protos from .bindings.shared_memory_data_transfer import SharedMemoryManager -from .constants import (PYTHON_ROLLBACK_CWD_PATH, +from .constants import (HTTP_TRIGGER, PYTHON_ROLLBACK_CWD_PATH, PYTHON_THREADPOOL_THREAD_COUNT, PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT, PYTHON_THREADPOOL_THREAD_COUNT_MAX_37, @@ -31,8 +32,10 @@ PYTHON_SCRIPT_FILE_NAME, PYTHON_SCRIPT_FILE_NAME_DEFAULT, PYTHON_LANGUAGE_RUNTIME, PYTHON_ENABLE_INIT_INDEXING, + X_MS_INVOCATION_ID, LOCAL_HOST, METADATA_PROPERTIES_WORKER_INDEXED) from .extension import ExtensionManager +from .http_proxy import http_coordinator from .logging import disable_console_logging, enable_console_logging from .logging import (logger, error_logger, is_system_log_category, CONSOLE_LOG_PREFIX, format_exception) @@ -58,6 +61,19 @@ def current(mcls): return disp +def get_unused_tcp_port(): + # Create a TCP socket + tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # Bind it to a free port provided by the OS + tcp_socket.bind(("", 0)) + # Get the port number + port = tcp_socket.getsockname()[1] + # Close the socket + tcp_socket.close() + # Return the port number + return port + + class Dispatcher(metaclass=DispatcherMeta): _GRPC_STOP_RESPONSE = object() @@ -74,6 +90,7 @@ def __init__(self, loop: BaseEventLoop, host: str, port: int, self._functions = functions.Registry() self._shmem_mgr = SharedMemoryManager() self._old_task_factory = None + self.function_metadata_result = None # Used to store metadata returns self._function_metadata_result = None @@ -128,7 +145,7 @@ async def connect(cls, host: str, port: int, worker_id: str, async def dispatch_forever(self): # sourcery skip: swap-if-expression if DispatcherMeta.__current_dispatcher__ is not None: raise RuntimeError('there can be only one running dispatcher per ' - 'process') + 'process') self._old_task_factory = self._loop.get_task_factory() @@ -156,8 +173,10 @@ async def dispatch_forever(self): # sourcery skip: swap-if-expression logging_handler = AsyncLoggingHandler() root_logger = logging.getLogger() + log_level = logging.INFO if not is_envvar_true( PYTHON_ENABLE_DEBUG_LOGGING) else logging.DEBUG + root_logger.setLevel(log_level) root_logger.addHandler(logging_handler) logger.info('Switched to gRPC logging.') @@ -263,59 +282,77 @@ async def _dispatch_grpc_request(self, request): self._grpc_resp_queue.put_nowait(resp) async def _handle__worker_init_request(self, request): - logger.info('Received WorkerInitRequest, ' - 'python version %s, ' - 'worker version %s, ' - 'request ID %s. ' - 'App Settings state: %s. ' - 'To enable debug level logging, please refer to ' - 'https://aka.ms/python-enable-debug-logging', - sys.version, - VERSION, - self.request_id, - get_python_appsetting_state() - ) - - worker_init_request = request.worker_init_request - host_capabilities = worker_init_request.capabilities - if constants.FUNCTION_DATA_CACHE in host_capabilities: - val = host_capabilities[constants.FUNCTION_DATA_CACHE] - self._function_data_cache_enabled = val == _TRUE - - capabilities = { - constants.RAW_HTTP_BODY_BYTES: _TRUE, - constants.TYPED_DATA_COLLECTION: _TRUE, - constants.RPC_HTTP_BODY_ONLY: _TRUE, - constants.WORKER_STATUS: _TRUE, - constants.RPC_HTTP_TRIGGER_METADATA_REMOVED: _TRUE, - constants.SHARED_MEMORY_DATA_TRANSFER: _TRUE, - } - - if DependencyManager.should_load_cx_dependencies(): - DependencyManager.prioritize_customer_dependencies() - - if DependencyManager.is_in_linux_consumption(): - import azure.functions # NoQA - - # loading bindings registry and saving results to a static - # dictionary which will be later used in the invocation request - bindings.load_binding_registry() - - if is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): - try: - self.load_function_metadata( - worker_init_request.function_app_directory, - caller_info="worker_init_request") - except Exception as ex: - self._function_metadata_exception = ex + try: + logger.info('Received WorkerInitRequest, ' + 'python version %s, ' + 'worker version %s, ' + 'request ID %s. ' + 'App Settings state: %s. ' + 'To enable debug level logging, please refer to ' + 'https://aka.ms/python-enable-debug-logging', + sys.version, + VERSION, + self.request_id, + get_python_appsetting_state() + ) + + worker_init_request = request.worker_init_request + directory = worker_init_request.function_app_directory + host_capabilities = worker_init_request.capabilities + if constants.FUNCTION_DATA_CACHE in host_capabilities: + val = host_capabilities[constants.FUNCTION_DATA_CACHE] + self._function_data_cache_enabled = val == _TRUE + + capabilities = { + constants.RAW_HTTP_BODY_BYTES: _TRUE, + constants.TYPED_DATA_COLLECTION: _TRUE, + constants.RPC_HTTP_BODY_ONLY: _TRUE, + constants.WORKER_STATUS: _TRUE, + constants.RPC_HTTP_TRIGGER_METADATA_REMOVED: _TRUE, + constants.SHARED_MEMORY_DATA_TRANSFER: _TRUE, + } + + if DependencyManager.should_load_cx_dependencies(): + DependencyManager.prioritize_customer_dependencies() + + if DependencyManager.is_in_linux_consumption(): + import azure.functions # NoQA + + # loading bindings registry and saving results to a static + # dictionary which will be later used in the invocation request + bindings.load_binding_registry() - return protos.StreamingMessage( - request_id=self.request_id, - worker_init_response=protos.WorkerInitResponse( - capabilities=capabilities, - worker_metadata=self.get_worker_metadata(), - result=protos.StatusResult( - status=protos.StatusResult.Success))) + if is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): + try: + self.load_function_metadata( + worker_init_request.function_app_directory, + caller_info="worker_init_request") + except Exception as ex: + self._function_metadata_exception = ex + + if self._has_http_func: + from azure.functions.extension.base import HttpV2FeatureChecker + + if HttpV2FeatureChecker.http_v2_enabled(): + capabilities[constants.HTTP_URI] = await self._initialize_http_server() + + return protos.StreamingMessage( + request_id=self.request_id, + worker_init_response=protos.WorkerInitResponse( + capabilities=capabilities, + worker_metadata=self.get_worker_metadata(), + result=protos.StatusResult(status=protos.StatusResult.Success), + ), + ) + except Exception as e: + logger.error("Error handling WorkerInitRequest: %s", str(e)) + return protos.StreamingMessage( + request_id=self.request_id, + worker_init_response=protos.WorkerInitResponse( + result=protos.StatusResult(status=protos.StatusResult.Failure, + exception=self._serialize_exception(e)) + ), + ) async def _handle__worker_status_request(self, request): # Logging is not necessary in this request since the response is used @@ -348,6 +385,7 @@ def load_function_metadata(self, function_app_directory, caller_info): self.index_functions(function_path)) \ if os.path.exists(function_path) else None + async def _handle__functions_metadata_request(self, request): metadata_request = request.functions_metadata_request function_app_directory = metadata_request.function_app_directory @@ -466,6 +504,7 @@ async def _handle__function_load_request(self, request): status=protos.StatusResult.Success))) except Exception as ex: + logging.error(ex) return protos.StreamingMessage( request_id=self.request_id, function_load_response=protos.FunctionLoadResponse( @@ -508,19 +547,28 @@ async def _handle__invocation_request(self, request): logger.info(', '.join(function_invocation_logs)) args = {} + for pb in invoc_request.input_data: pb_type_info = fi.input_types[pb.name] + trigger_metadata = None if bindings.is_trigger_binding(pb_type_info.binding_name): trigger_metadata = invoc_request.trigger_metadata - else: - trigger_metadata = None - + args[pb.name] = bindings.from_incoming_proto( pb_type_info.binding_name, pb, trigger_metadata=trigger_metadata, pytype=pb_type_info.pytype, shmem_mgr=self._shmem_mgr) + if fi.trigger_metadata.get('type') == HTTP_TRIGGER: + from azure.functions.extension.base import HttpV2FeatureChecker + http_v2_enabled = HttpV2FeatureChecker.http_v2_enabled() + + if http_v2_enabled: + http_request = await http_coordinator.get_http_request_async( + invocation_id) + args[fi.trigger_metadata.get('param_name')] = http_request + fi_context = self._get_context(invoc_request, fi.name, fi.directory) # Use local thread storage to store the invocation ID @@ -537,14 +585,19 @@ async def _handle__invocation_request(self, request): call_result = await self._run_async_func( fi_context, fi.func, args ) + else: call_result = await self._loop.run_in_executor( self._sync_call_tp, self._run_sync_func, invocation_id, fi_context, fi.func, args) + if call_result is not None and not fi.has_return: raise RuntimeError(f'function {fi.name!r} without a $return ' 'binding returned a non-None value') + + if http_v2_enabled: + http_coordinator.set_http_response(invocation_id, call_result) output_data = [] cache_enabled = self._function_data_cache_enabled @@ -564,10 +617,12 @@ async def _handle__invocation_request(self, request): output_data.append(param_binding) return_value = None - if fi.return_type is not None: + if fi.return_type is not None and not http_v2_enabled: return_value = bindings.to_outgoing_proto( - fi.return_type.binding_name, call_result, - pytype=fi.return_type.pytype) + fi.return_type.binding_name, + call_result, + pytype=fi.return_type.pytype, + ) # Actively flush customer print() function to console sys.stdout.flush() @@ -590,6 +645,7 @@ async def _handle__invocation_request(self, request): status=protos.StatusResult.Failure, exception=self._serialize_exception(ex)))) + async def _handle__function_environment_reload_request(self, request): """Only runs on Linux Consumption placeholder specialization. This is called only when placeholder mode is true. On worker restarts @@ -638,6 +694,7 @@ async def _handle__function_environment_reload_request(self, request): # reload_customer_libraries call clears the registry bindings.load_binding_registry() + capabilities = {} if is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): try: self.load_function_metadata( @@ -645,6 +702,12 @@ async def _handle__function_environment_reload_request(self, request): caller_info="environment_reload_request") except Exception as ex: self._function_metadata_exception = ex + + if self._has_http_func: + from azure.functions.extension.base import HttpV2FeatureChecker + + if HttpV2FeatureChecker.http_v2_enabled(): + capabilities[constants.HTTP_URI] = await self._initialize_http_server() # Change function app directory if getattr(func_env_reload_request, @@ -653,7 +716,7 @@ async def _handle__function_environment_reload_request(self, request): func_env_reload_request.function_app_directory) success_response = protos.FunctionEnvironmentReloadResponse( - capabilities={}, + capabilities=capabilities, worker_metadata=self.get_worker_metadata(), result=protos.StatusResult( status=protos.StatusResult.Success)) @@ -672,10 +735,43 @@ async def _handle__function_environment_reload_request(self, request): request_id=self.request_id, function_environment_reload_response=failure_response) + async def _initialize_http_server(self): + from azure.functions.extension.base import ModuleTrackerMeta, RequestTrackerMeta + + web_extension_mod_name = ModuleTrackerMeta.get_module() + extension_module = importlib.import_module(web_extension_mod_name) + web_app_class = extension_module.WebApp + web_server_class = extension_module.WebServer + + unused_port = get_unused_tcp_port() + + app = web_app_class() + request_type = RequestTrackerMeta.get_request_type() + + @app.route + async def catch_all(request: request_type): # type: ignore + invoc_id = request.headers.get(X_MS_INVOCATION_ID) + if invoc_id is None: + raise ValueError(f"Header {X_MS_INVOCATION_ID} not found") + + http_coordinator.set_http_request(invoc_id, request) + http_resp = await http_coordinator.await_http_response_async(invoc_id) + return http_resp + + web_server = web_server_class(LOCAL_HOST, unused_port, app) + web_server_run_task = web_server.serve() + + loop = asyncio.get_event_loop() + loop.create_task(web_server_run_task) + + return f"http://{LOCAL_HOST}:{unused_port}" + def index_functions(self, function_path: str): indexed_functions = loader.index_function_app(function_path) - logger.info('Indexed function app and found %s functions', - len(indexed_functions)) + logger.info( + "Indexed function app and found %s functions", + len(indexed_functions) + ) if indexed_functions: fx_metadata_results = loader.process_indexed_function( @@ -683,7 +779,9 @@ def index_functions(self, function_path: str): indexed_functions) indexed_function_logs: List[str] = [] + self._has_http_func = False for func in indexed_functions: + self._has_http_func = self._has_http_func or func.is_http_function() function_log = "Function Name: {}, Function Binding: {}" \ .format(func.get_function_name(), [(binding.type, binding.name) for binding in @@ -876,7 +974,6 @@ def gen(resp_queue): class AsyncLoggingHandler(logging.Handler): - def emit(self, record: LogRecord) -> None: # Since we disable console log after gRPC channel is initiated, # we should redirect all the messages into dispatcher. diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index f0926230c..0200c01ae 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -6,6 +6,8 @@ import typing import uuid +from azure_functions_worker.constants import HTTP_TRIGGER + from . import bindings as bindings_utils from . import protos from ._thirdparty import typing_inspect @@ -31,6 +33,7 @@ class FunctionInfo(typing.NamedTuple): output_types: typing.Mapping[str, ParamTypeInfo] return_type: typing.Optional[ParamTypeInfo] + trigger_metadata: typing.Dict[str, typing.Any] class FunctionLoadError(RuntimeError): @@ -297,6 +300,17 @@ def add_func_to_registry_and_return_funcinfo(self, function, str, ParamTypeInfo], return_type: str): + http_trigger_param_name = next( + (input_type for input_type, type_info in input_types.items() if type_info.binding_name == HTTP_TRIGGER), + None + ) + + if http_trigger_param_name is not None: + trigger_metadata = { + "type": HTTP_TRIGGER, + "param_name": http_trigger_param_name + } + function_info = FunctionInfo( func=function, name=function_name, @@ -307,7 +321,9 @@ def add_func_to_registry_and_return_funcinfo(self, function, has_return=has_explicit_return or has_implicit_return, input_types=input_types, output_types=output_types, - return_type=return_type) + return_type=return_type, + trigger_metadata=trigger_metadata) + self._functions[function_id] = function_info return function_info diff --git a/azure_functions_worker/http_proxy.py b/azure_functions_worker/http_proxy.py new file mode 100644 index 000000000..eff55067e --- /dev/null +++ b/azure_functions_worker/http_proxy.py @@ -0,0 +1,148 @@ +import abc +import asyncio +from typing import Dict + +class BaseContextReference(abc.ABC): + def __init__(self, event_class, http_request=None, http_response=None, function=None, fi_context=None, args=None, http_trigger_param_name=None): + self._http_request = http_request + self._http_response = http_response + self._function = function + self._fi_context = fi_context + self._args = args + self._http_trigger_param_name = http_trigger_param_name + self._http_request_available_event = event_class() + self._http_response_available_event = event_class() + self._rpc_invocation_ready_event = event_class() + + @property + def http_request(self): + return self._http_request + + @http_request.setter + def http_request(self, value): + self._http_request = value + self._http_request_available_event.set() + + @property + def http_response(self): + return self._http_response + + @http_response.setter + def http_response(self, value): + self._http_response = value + self._http_response_available_event.set() + + @property + def function(self): + return self._function + + @function.setter + def function(self, value): + self._function = value + + @property + def fi_context(self): + return self._fi_context + + @fi_context.setter + def fi_context(self, value): + self._fi_context = value + + @property + def http_trigger_param_name(self): + return self._http_trigger_param_name + + @http_trigger_param_name.setter + def http_trigger_param_name(self, value): + self._http_trigger_param_name = value + + @property + def args(self): + return self._args + + @args.setter + def args(self, value): + self._args = value + + @property + def http_request_available_event(self): + return self._http_request_available_event + + @property + def http_response_available_event(self): + return self._http_response_available_event + + @property + def rpc_invocation_ready_event(self): + return self._rpc_invocation_ready_event + + +class AsyncContextReference(BaseContextReference): + def __init__(self, http_request=None, http_response=None, function=None, fi_context=None, args=None): + super().__init__(event_class=asyncio.Event, http_request=http_request, http_response=http_response, + function=function, fi_context=fi_context, args=args) + self.is_async = True + + +class SingletonMeta(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] + + +class HttpCoordinator(metaclass=SingletonMeta): + def __init__(self): + self._context_references: Dict[str, BaseContextReference] = {} + + def set_http_request(self, invoc_id, http_request): + if invoc_id not in self._context_references: + self._context_references[invoc_id] = AsyncContextReference() + context_ref = self._context_references.get(invoc_id) + context_ref.http_request = http_request + context_ref.http_request_available_event.set() + + def set_http_response(self, invoc_id, http_response): + if invoc_id not in self._context_references: + raise Exception("No context reference found for invocation %s", invoc_id) + context_ref = self._context_references.get(invoc_id) + context_ref.http_response = http_response + context_ref.http_response_available_event.set() + + async def get_http_request_async(self, invoc_id): + if invoc_id not in self._context_references: + self._context_references[invoc_id] = AsyncContextReference() + + await asyncio.sleep(0) + await self._context_references.get(invoc_id).http_request_available_event.wait() + return self._pop_http_request(invoc_id) + + async def await_http_response_async(self, invoc_id): + if invoc_id not in self._context_references: + raise Exception("No context reference found for invocation %s", invoc_id) + await asyncio.sleep(0) + await self._context_references.get(invoc_id).http_response_available_event.wait() + return self._pop_http_response(invoc_id) + + def _pop_http_request(self, invoc_id): + context_ref = self._context_references.get(invoc_id) + request = context_ref.http_request + if request is not None: + context_ref.http_request = None + return request + + raise Exception("No http request found for invocation %s", invoc_id) + + def _pop_http_response(self, invoc_id): + context_ref = self._context_references.get(invoc_id) + response = context_ref.http_response + if response is not None: + context_ref.http_response = None + return response + + raise Exception("No http response found for invocation %s", invoc_id) + + +http_coordinator = HttpCoordinator() diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index 938fde64c..da706fffd 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -122,31 +122,34 @@ def build_variable_interval_retry(retry, max_retry_count, retry_strategy): def process_indexed_function(functions_registry: functions.Registry, indexed_functions): - fx_metadata_results = [] - for indexed_function in indexed_functions: - function_info = functions_registry.add_indexed_function( - function=indexed_function) - - binding_protos = build_binding_protos(indexed_function) - retry_protos = build_retry_protos(indexed_function) - - function_metadata = protos.RpcFunctionMetadata( - name=function_info.name, - function_id=function_info.function_id, - managed_dependency_enabled=False, # only enabled for PowerShell - directory=function_info.directory, - script_file=indexed_function.function_script_file, - entry_point=function_info.name, - is_proxy=False, # not supported in V4 - language=PYTHON_LANGUAGE_RUNTIME, - bindings=binding_protos, - raw_bindings=indexed_function.get_raw_bindings(), - retry_options=retry_protos, - properties={METADATA_PROPERTIES_WORKER_INDEXED: "True"}) - - fx_metadata_results.append(function_metadata) - - return fx_metadata_results + try: + fx_metadata_results = [] + for indexed_function in indexed_functions: + function_info = functions_registry.add_indexed_function( + function=indexed_function) + + binding_protos = build_binding_protos(indexed_function) + retry_protos = build_retry_protos(indexed_function) + + function_metadata = protos.RpcFunctionMetadata( + name=function_info.name, + function_id=function_info.function_id, + managed_dependency_enabled=False, # only enabled for PowerShell + directory=function_info.directory, + script_file=indexed_function.function_script_file, + entry_point=function_info.name, + is_proxy=False, # not supported in V4 + language=PYTHON_LANGUAGE_RUNTIME, + bindings=binding_protos, + raw_bindings=indexed_function.get_raw_bindings(), + retry_options=retry_protos, + properties={METADATA_PROPERTIES_WORKER_INDEXED: "True"}) + + fx_metadata_results.append(function_metadata) + return fx_metadata_results + except Exception as e: + logger.error(f'Error in process_indexed_function. {e}', exc_info=True) + raise e @attach_message_to_exception( diff --git a/azure_functions_worker/logging.py b/azure_functions_worker/logging.py index adb5ff294..7a5340919 100644 --- a/azure_functions_worker/logging.py +++ b/azure_functions_worker/logging.py @@ -20,7 +20,8 @@ handler: Optional[logging.Handler] = None error_handler: Optional[logging.Handler] = None - +local_handler = logging.FileHandler("E:/projects/AzureFunctionsPythonWorker/log.txt") +logger.addHandler(local_handler) def format_exception(exception: Exception) -> str: msg = str(exception) + "\n" diff --git a/python/test/worker.config.json b/python/test/worker.config.json index 3fc2a9236..05f6d26ed 100644 --- a/python/test/worker.config.json +++ b/python/test/worker.config.json @@ -2,7 +2,7 @@ "description":{ "language":"python", "extensions":[".py"], - "defaultExecutablePath":"python", + "defaultExecutablePath":"E:\\projects\\AzureFunctionsPythonWorker\\.venv_3.8\\Scripts\\python.exe", "defaultWorkerPath":"worker.py", "workerIndexing": "true" } diff --git a/setup.py b/setup.py index a3970fe19..f85f1c332 100644 --- a/setup.py +++ b/setup.py @@ -79,7 +79,7 @@ ) else: INSTALL_REQUIRES.extend( - ("protobuf~=4.22.0", "grpcio-tools~=1.54.2", "grpcio~=1.54.2") + ("protobuf~=4.22.0", "grpcio-tools~=1.54.2", "grpcio~=1.54.2", "azure-functions-extension-base") ) EXTRA_REQUIRES = { @@ -109,7 +109,8 @@ "pandas", "numpy", "pre-commit" - ] + ], + "fastapi": ["azure-functions-extension-fastapi"] } diff --git a/tests/endtoend/http_v2_functions/fastapi_data_class/function_app.py b/tests/endtoend/http_v2_functions/fastapi_data_class/function_app.py new file mode 100644 index 000000000..25900aafb --- /dev/null +++ b/tests/endtoend/http_v2_functions/fastapi_data_class/function_app.py @@ -0,0 +1,108 @@ +import json +import os +import typing + +from azure.eventhub import EventData +from azure.eventhub.aio import EventHubProducerClient + +import azure.functions as func + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +# An HttpTrigger to generating EventHub event from EventHub Output Binding +@app.function_name(name="eventhub_output") +@app.route(route="eventhub_output") +@app.event_hub_output(arg_name="event", + event_hub_name="python-worker-ci-eventhub-one", + connection="AzureWebJobsEventHubConnectionString") +def eventhub_output(req: func.HttpRequest, event: func.Out[str]): + event.set(req.get_body().decode('utf-8')) + return 'OK' + + +# This is an actual EventHub trigger which will convert the event data +# into a storage blob. +@app.function_name(name="eventhub_trigger") +@app.event_hub_message_trigger(arg_name="event", + event_hub_name="python-worker-ci-eventhub-one", + connection="AzureWebJobsEventHubConnectionString" + ) +@app.blob_output(arg_name="$return", + path="python-worker-tests/test-eventhub-triggered.txt", + connection="AzureWebJobsStorage") +def eventhub_trigger(event: func.EventHubEvent) -> bytes: + return event.get_body() + + +# Retrieve the event data from storage blob and return it as Http response +@app.function_name(name="get_eventhub_triggered") +@app.route(route="get_eventhub_triggered") +@app.blob_input(arg_name="file", + path="python-worker-tests/test-eventhub-triggered.txt", + connection="AzureWebJobsStorage") +def get_eventhub_triggered(req: func.HttpRequest, + file: func.InputStream) -> str: + return file.read().decode('utf-8') + + +# Retrieve the event data from storage blob and return it as Http response +@app.function_name(name="get_metadata_triggered") +@app.route(route="get_metadata_triggered") +@app.blob_input(arg_name="file", + path="python-worker-tests/test-metadata-triggered.txt", + connection="AzureWebJobsStorage") +async def get_metadata_triggered(req: func.HttpRequest, + file: func.InputStream) -> str: + return func.HttpResponse(body=file.read().decode('utf-8'), + status_code=200, + mimetype='application/json') + + +# An HttpTrigger to generating EventHub event from azure-eventhub SDK. +# Events generated from azure-eventhub contain the full metadata. +@app.function_name(name="metadata_output") +@app.route(route="metadata_output") +async def metadata_output(req: func.HttpRequest): + # Parse event metadata from http request + json_string = req.get_body().decode('utf-8') + event_dict = json.loads(json_string) + + # Create an EventHub Client and event batch + client = EventHubProducerClient.from_connection_string( + os.getenv('AzureWebJobsEventHubConnectionString'), + eventhub_name='python-worker-ci-eventhub-one-metadata') + + # Generate new event based on http request with full metadata + event_data_batch = await client.create_batch() + event_data_batch.add(EventData(event_dict.get('body'))) + + # Send out event into event hub + try: + await client.send_batch(event_data_batch) + finally: + await client.close() + + return 'OK' + + +@app.function_name(name="metadata_trigger") +@app.event_hub_message_trigger( + arg_name="event", + event_hub_name="python-worker-ci-eventhub-one-metadata", + connection="AzureWebJobsEventHubConnectionString") +@app.blob_output(arg_name="$return", + path="python-worker-tests/test-metadata-triggered.txt", + connection="AzureWebJobsStorage") +async def metadata_trigger(event: func.EventHubEvent) -> bytes: + event_dict: typing.Mapping[str, typing.Any] = { + 'body': event.get_body().decode('utf-8'), + # Uncomment this when the EnqueuedTimeUtc is fixed in azure-functions + # 'enqueued_time': event.enqueued_time.isoformat(), + 'partition_key': event.partition_key, + 'sequence_number': event.sequence_number, + 'offset': event.offset, + 'metadata': event.metadata + } + + return json.dumps(event_dict) diff --git a/tests/endtoend/http_v2_functions/fastapi_non_streaming_custom_resps/function_app.py b/tests/endtoend/http_v2_functions/fastapi_non_streaming_custom_resps/function_app.py new file mode 100644 index 000000000..25900aafb --- /dev/null +++ b/tests/endtoend/http_v2_functions/fastapi_non_streaming_custom_resps/function_app.py @@ -0,0 +1,108 @@ +import json +import os +import typing + +from azure.eventhub import EventData +from azure.eventhub.aio import EventHubProducerClient + +import azure.functions as func + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +# An HttpTrigger to generating EventHub event from EventHub Output Binding +@app.function_name(name="eventhub_output") +@app.route(route="eventhub_output") +@app.event_hub_output(arg_name="event", + event_hub_name="python-worker-ci-eventhub-one", + connection="AzureWebJobsEventHubConnectionString") +def eventhub_output(req: func.HttpRequest, event: func.Out[str]): + event.set(req.get_body().decode('utf-8')) + return 'OK' + + +# This is an actual EventHub trigger which will convert the event data +# into a storage blob. +@app.function_name(name="eventhub_trigger") +@app.event_hub_message_trigger(arg_name="event", + event_hub_name="python-worker-ci-eventhub-one", + connection="AzureWebJobsEventHubConnectionString" + ) +@app.blob_output(arg_name="$return", + path="python-worker-tests/test-eventhub-triggered.txt", + connection="AzureWebJobsStorage") +def eventhub_trigger(event: func.EventHubEvent) -> bytes: + return event.get_body() + + +# Retrieve the event data from storage blob and return it as Http response +@app.function_name(name="get_eventhub_triggered") +@app.route(route="get_eventhub_triggered") +@app.blob_input(arg_name="file", + path="python-worker-tests/test-eventhub-triggered.txt", + connection="AzureWebJobsStorage") +def get_eventhub_triggered(req: func.HttpRequest, + file: func.InputStream) -> str: + return file.read().decode('utf-8') + + +# Retrieve the event data from storage blob and return it as Http response +@app.function_name(name="get_metadata_triggered") +@app.route(route="get_metadata_triggered") +@app.blob_input(arg_name="file", + path="python-worker-tests/test-metadata-triggered.txt", + connection="AzureWebJobsStorage") +async def get_metadata_triggered(req: func.HttpRequest, + file: func.InputStream) -> str: + return func.HttpResponse(body=file.read().decode('utf-8'), + status_code=200, + mimetype='application/json') + + +# An HttpTrigger to generating EventHub event from azure-eventhub SDK. +# Events generated from azure-eventhub contain the full metadata. +@app.function_name(name="metadata_output") +@app.route(route="metadata_output") +async def metadata_output(req: func.HttpRequest): + # Parse event metadata from http request + json_string = req.get_body().decode('utf-8') + event_dict = json.loads(json_string) + + # Create an EventHub Client and event batch + client = EventHubProducerClient.from_connection_string( + os.getenv('AzureWebJobsEventHubConnectionString'), + eventhub_name='python-worker-ci-eventhub-one-metadata') + + # Generate new event based on http request with full metadata + event_data_batch = await client.create_batch() + event_data_batch.add(EventData(event_dict.get('body'))) + + # Send out event into event hub + try: + await client.send_batch(event_data_batch) + finally: + await client.close() + + return 'OK' + + +@app.function_name(name="metadata_trigger") +@app.event_hub_message_trigger( + arg_name="event", + event_hub_name="python-worker-ci-eventhub-one-metadata", + connection="AzureWebJobsEventHubConnectionString") +@app.blob_output(arg_name="$return", + path="python-worker-tests/test-metadata-triggered.txt", + connection="AzureWebJobsStorage") +async def metadata_trigger(event: func.EventHubEvent) -> bytes: + event_dict: typing.Mapping[str, typing.Any] = { + 'body': event.get_body().decode('utf-8'), + # Uncomment this when the EnqueuedTimeUtc is fixed in azure-functions + # 'enqueued_time': event.enqueued_time.isoformat(), + 'partition_key': event.partition_key, + 'sequence_number': event.sequence_number, + 'offset': event.offset, + 'metadata': event.metadata + } + + return json.dumps(event_dict) diff --git a/tests/endtoend/http_v2_functions/fastapi_streaming_download_func/function_app.py b/tests/endtoend/http_v2_functions/fastapi_streaming_download_func/function_app.py new file mode 100644 index 000000000..25900aafb --- /dev/null +++ b/tests/endtoend/http_v2_functions/fastapi_streaming_download_func/function_app.py @@ -0,0 +1,108 @@ +import json +import os +import typing + +from azure.eventhub import EventData +from azure.eventhub.aio import EventHubProducerClient + +import azure.functions as func + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +# An HttpTrigger to generating EventHub event from EventHub Output Binding +@app.function_name(name="eventhub_output") +@app.route(route="eventhub_output") +@app.event_hub_output(arg_name="event", + event_hub_name="python-worker-ci-eventhub-one", + connection="AzureWebJobsEventHubConnectionString") +def eventhub_output(req: func.HttpRequest, event: func.Out[str]): + event.set(req.get_body().decode('utf-8')) + return 'OK' + + +# This is an actual EventHub trigger which will convert the event data +# into a storage blob. +@app.function_name(name="eventhub_trigger") +@app.event_hub_message_trigger(arg_name="event", + event_hub_name="python-worker-ci-eventhub-one", + connection="AzureWebJobsEventHubConnectionString" + ) +@app.blob_output(arg_name="$return", + path="python-worker-tests/test-eventhub-triggered.txt", + connection="AzureWebJobsStorage") +def eventhub_trigger(event: func.EventHubEvent) -> bytes: + return event.get_body() + + +# Retrieve the event data from storage blob and return it as Http response +@app.function_name(name="get_eventhub_triggered") +@app.route(route="get_eventhub_triggered") +@app.blob_input(arg_name="file", + path="python-worker-tests/test-eventhub-triggered.txt", + connection="AzureWebJobsStorage") +def get_eventhub_triggered(req: func.HttpRequest, + file: func.InputStream) -> str: + return file.read().decode('utf-8') + + +# Retrieve the event data from storage blob and return it as Http response +@app.function_name(name="get_metadata_triggered") +@app.route(route="get_metadata_triggered") +@app.blob_input(arg_name="file", + path="python-worker-tests/test-metadata-triggered.txt", + connection="AzureWebJobsStorage") +async def get_metadata_triggered(req: func.HttpRequest, + file: func.InputStream) -> str: + return func.HttpResponse(body=file.read().decode('utf-8'), + status_code=200, + mimetype='application/json') + + +# An HttpTrigger to generating EventHub event from azure-eventhub SDK. +# Events generated from azure-eventhub contain the full metadata. +@app.function_name(name="metadata_output") +@app.route(route="metadata_output") +async def metadata_output(req: func.HttpRequest): + # Parse event metadata from http request + json_string = req.get_body().decode('utf-8') + event_dict = json.loads(json_string) + + # Create an EventHub Client and event batch + client = EventHubProducerClient.from_connection_string( + os.getenv('AzureWebJobsEventHubConnectionString'), + eventhub_name='python-worker-ci-eventhub-one-metadata') + + # Generate new event based on http request with full metadata + event_data_batch = await client.create_batch() + event_data_batch.add(EventData(event_dict.get('body'))) + + # Send out event into event hub + try: + await client.send_batch(event_data_batch) + finally: + await client.close() + + return 'OK' + + +@app.function_name(name="metadata_trigger") +@app.event_hub_message_trigger( + arg_name="event", + event_hub_name="python-worker-ci-eventhub-one-metadata", + connection="AzureWebJobsEventHubConnectionString") +@app.blob_output(arg_name="$return", + path="python-worker-tests/test-metadata-triggered.txt", + connection="AzureWebJobsStorage") +async def metadata_trigger(event: func.EventHubEvent) -> bytes: + event_dict: typing.Mapping[str, typing.Any] = { + 'body': event.get_body().decode('utf-8'), + # Uncomment this when the EnqueuedTimeUtc is fixed in azure-functions + # 'enqueued_time': event.enqueued_time.isoformat(), + 'partition_key': event.partition_key, + 'sequence_number': event.sequence_number, + 'offset': event.offset, + 'metadata': event.metadata + } + + return json.dumps(event_dict) diff --git a/tests/endtoend/http_v2_functions/fastapi_streaming_upload_func/function_app.py b/tests/endtoend/http_v2_functions/fastapi_streaming_upload_func/function_app.py new file mode 100644 index 000000000..25900aafb --- /dev/null +++ b/tests/endtoend/http_v2_functions/fastapi_streaming_upload_func/function_app.py @@ -0,0 +1,108 @@ +import json +import os +import typing + +from azure.eventhub import EventData +from azure.eventhub.aio import EventHubProducerClient + +import azure.functions as func + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +# An HttpTrigger to generating EventHub event from EventHub Output Binding +@app.function_name(name="eventhub_output") +@app.route(route="eventhub_output") +@app.event_hub_output(arg_name="event", + event_hub_name="python-worker-ci-eventhub-one", + connection="AzureWebJobsEventHubConnectionString") +def eventhub_output(req: func.HttpRequest, event: func.Out[str]): + event.set(req.get_body().decode('utf-8')) + return 'OK' + + +# This is an actual EventHub trigger which will convert the event data +# into a storage blob. +@app.function_name(name="eventhub_trigger") +@app.event_hub_message_trigger(arg_name="event", + event_hub_name="python-worker-ci-eventhub-one", + connection="AzureWebJobsEventHubConnectionString" + ) +@app.blob_output(arg_name="$return", + path="python-worker-tests/test-eventhub-triggered.txt", + connection="AzureWebJobsStorage") +def eventhub_trigger(event: func.EventHubEvent) -> bytes: + return event.get_body() + + +# Retrieve the event data from storage blob and return it as Http response +@app.function_name(name="get_eventhub_triggered") +@app.route(route="get_eventhub_triggered") +@app.blob_input(arg_name="file", + path="python-worker-tests/test-eventhub-triggered.txt", + connection="AzureWebJobsStorage") +def get_eventhub_triggered(req: func.HttpRequest, + file: func.InputStream) -> str: + return file.read().decode('utf-8') + + +# Retrieve the event data from storage blob and return it as Http response +@app.function_name(name="get_metadata_triggered") +@app.route(route="get_metadata_triggered") +@app.blob_input(arg_name="file", + path="python-worker-tests/test-metadata-triggered.txt", + connection="AzureWebJobsStorage") +async def get_metadata_triggered(req: func.HttpRequest, + file: func.InputStream) -> str: + return func.HttpResponse(body=file.read().decode('utf-8'), + status_code=200, + mimetype='application/json') + + +# An HttpTrigger to generating EventHub event from azure-eventhub SDK. +# Events generated from azure-eventhub contain the full metadata. +@app.function_name(name="metadata_output") +@app.route(route="metadata_output") +async def metadata_output(req: func.HttpRequest): + # Parse event metadata from http request + json_string = req.get_body().decode('utf-8') + event_dict = json.loads(json_string) + + # Create an EventHub Client and event batch + client = EventHubProducerClient.from_connection_string( + os.getenv('AzureWebJobsEventHubConnectionString'), + eventhub_name='python-worker-ci-eventhub-one-metadata') + + # Generate new event based on http request with full metadata + event_data_batch = await client.create_batch() + event_data_batch.add(EventData(event_dict.get('body'))) + + # Send out event into event hub + try: + await client.send_batch(event_data_batch) + finally: + await client.close() + + return 'OK' + + +@app.function_name(name="metadata_trigger") +@app.event_hub_message_trigger( + arg_name="event", + event_hub_name="python-worker-ci-eventhub-one-metadata", + connection="AzureWebJobsEventHubConnectionString") +@app.blob_output(arg_name="$return", + path="python-worker-tests/test-metadata-triggered.txt", + connection="AzureWebJobsStorage") +async def metadata_trigger(event: func.EventHubEvent) -> bytes: + event_dict: typing.Mapping[str, typing.Any] = { + 'body': event.get_body().decode('utf-8'), + # Uncomment this when the EnqueuedTimeUtc is fixed in azure-functions + # 'enqueued_time': event.enqueued_time.isoformat(), + 'partition_key': event.partition_key, + 'sequence_number': event.sequence_number, + 'offset': event.offset, + 'metadata': event.metadata + } + + return json.dumps(event_dict) diff --git a/tests/unittests/http_functions/http_v2_functions/function_app.py b/tests/unittests/http_functions/http_v2_functions/function_app.py new file mode 100644 index 000000000..f4bcfb36e --- /dev/null +++ b/tests/unittests/http_functions/http_v2_functions/function_app.py @@ -0,0 +1,419 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import asyncio +import hashlib +import json +import logging +import sys +import time +from urllib.request import urlopen +from azure.functions.extension.fastapi import Request, Response +import azure.functions as func + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + +logger = logging.getLogger("my-function") + +# request handling +# request body, query, headers, and route params +# request validation errors +# diff http verbs + +# response handling +# response body, status code, headers +# error responses + +# edge cases +# invalid requests sent behavior with missing body, query, headers, and route params +# request payload exceeds max size +# request payload contains special characters + +@app.route(route="return_str") +def return_str(req: Request) -> str: + return 'Hello World!' + +# +# @app.route(route="accept_json") +# def accept_json(req: Request): +# return json.dumps({ +# 'method': req.method, +# 'url': req.url, +# 'headers': dict(req.headers), +# 'params': dict(req.params), +# 'get_body': req.get_body().decode(), +# 'get_json': req.get_json() +# }) +# +# +# async def nested(): +# try: +# 1 / 0 +# except ZeroDivisionError: +# logger.error('and another error', exc_info=True) +# +# +# @app.route(route="async_logging") +# async def async_logging(req: Request): +# logger.info('hello %s', 'info') +# +# await asyncio.sleep(0.1) +# +# # Create a nested task to check if invocation_id is still +# # logged correctly. +# await asyncio.ensure_future(nested()) +# +# await asyncio.sleep(0.1) +# +# return 'OK-async' +# +# +# @app.route(route="async_return_str") +# async def async_return_str(req: Request): +# await asyncio.sleep(0.1) +# return 'Hello Async World!' +# +# +# @app.route(route="debug_logging") +# def debug_logging(req: Request): +# logging.critical('logging critical', exc_info=True) +# logging.info('logging info', exc_info=True) +# logging.warning('logging warning', exc_info=True) +# logging.debug('logging debug', exc_info=True) +# logging.error('logging error', exc_info=True) +# return 'OK-debug' +# +# +# @app.route(route="debug_user_logging") +# def debug_user_logging(req: Request): +# logger.setLevel(logging.DEBUG) +# +# logging.critical('logging critical', exc_info=True) +# logger.info('logging info', exc_info=True) +# logger.warning('logging warning', exc_info=True) +# logger.debug('logging debug', exc_info=True) +# logger.error('logging error', exc_info=True) +# return 'OK-user-debug' +# +# +# # Attempt to log info into system log from customer code +# disguised_logger = logging.getLogger('azure_functions_worker') +# +# +# async def parallelly_print(): +# await asyncio.sleep(0.1) +# print('parallelly_print') +# +# +# async def parallelly_log_info(): +# await asyncio.sleep(0.2) +# logging.info('parallelly_log_info at root logger') +# +# +# async def parallelly_log_warning(): +# await asyncio.sleep(0.3) +# logging.warning('parallelly_log_warning at root logger') +# +# +# async def parallelly_log_error(): +# await asyncio.sleep(0.4) +# logging.error('parallelly_log_error at root logger') +# +# +# async def parallelly_log_exception(): +# await asyncio.sleep(0.5) +# try: +# raise Exception('custom exception') +# except Exception: +# logging.exception('parallelly_log_exception at root logger', +# exc_info=sys.exc_info()) +# +# +# async def parallelly_log_custom(): +# await asyncio.sleep(0.6) +# logger.info('parallelly_log_custom at custom_logger') +# +# +# async def parallelly_log_system(): +# await asyncio.sleep(0.7) +# disguised_logger.info('parallelly_log_system at disguised_logger') +# +# +# @app.route(route="hijack_current_event_loop") +# async def hijack_current_event_loop(req: Request) -> Response: +# loop = asyncio.get_event_loop() +# +# # Create multiple tasks and schedule it into one asyncio.wait blocker +# task_print: asyncio.Task = loop.create_task(parallelly_print()) +# task_info: asyncio.Task = loop.create_task(parallelly_log_info()) +# task_warning: asyncio.Task = loop.create_task(parallelly_log_warning()) +# task_error: asyncio.Task = loop.create_task(parallelly_log_error()) +# task_exception: asyncio.Task = loop.create_task(parallelly_log_exception()) +# task_custom: asyncio.Task = loop.create_task(parallelly_log_custom()) +# task_disguise: asyncio.Task = loop.create_task(parallelly_log_system()) +# +# # Create an awaitable future and occupy the current event loop resource +# future = loop.create_future() +# loop.call_soon_threadsafe(future.set_result, 'callsoon_log') +# +# # WaitAll +# await asyncio.wait([task_print, task_info, task_warning, task_error, +# task_exception, task_custom, task_disguise, future]) +# +# # Log asyncio low-level future result +# logging.info(future.result()) +# +# return 'OK-hijack-current-event-loop' +# +# +# @app.route(route="no_return") +# def no_return(req: Request): +# logger.info('hi') +# +# +# @app.route(route="no_return_returns") +# def no_return_returns(req): +# return 'ABC' +# +# +# @app.route(route="print_logging") +# def print_logging(req: Request): +# flush_required = False +# is_console_log = False +# is_stderr = False +# message = req.params.get('message', '') +# +# if req.params.get('flush') == 'true': +# flush_required = True +# if req.params.get('console') == 'true': +# is_console_log = True +# if req.params.get('is_stderr') == 'true': +# is_stderr = True +# +# # Adding LanguageWorkerConsoleLog will make function host to treat +# # this as system log and will be propagated to kusto +# prefix = 'LanguageWorkerConsoleLog' if is_console_log else '' +# print(f'{prefix} {message}'.strip(), +# file=sys.stderr if is_stderr else sys.stdout, +# flush=flush_required) +# +# return 'OK-print-logging' +# +# +# @app.route(route="raw_body_bytes") +# def raw_body_bytes(req: Request) -> Response: +# body = req.get_body() +# body_len = str(len(body)) +# +# headers = {'body-len': body_len} +# return Response(body=body, status_code=200, headers=headers) +# +# +# @app.route(route="remapped_context") +# def remapped_context(req: Request): +# return req.method +# +# +# @app.route(route="return_bytes") +# def return_bytes(req: Request): +# # This function will fail, as we don't auto-convert "bytes" to "http". +# return b'Hello World!' +# +# +# @app.route(route="return_context") +# def return_context(req: Request, context: func.Context): +# return json.dumps({ +# 'method': req.method, +# 'ctx_func_name': context.function_name, +# 'ctx_func_dir': context.function_directory, +# 'ctx_invocation_id': context.invocation_id, +# 'ctx_trace_context_Traceparent': context.trace_context.Traceparent, +# 'ctx_trace_context_Tracestate': context.trace_context.Tracestate, +# }) +# +# +# @app.route(route="return_http") +# def return_http(req: Request): +# return Response('

Hello World™

', +# mimetype='text/html') +# +# +# @app.route(route="return_http_404") +# def return_http_404(req: Request): +# return Response('bye', status_code=404) +# +# +# @app.route(route="return_http_auth_admin", auth_level=func.AuthLevel.ADMIN) +# def return_http_auth_admin(req: Request): +# return Response('

Hello World™

', +# mimetype='text/html') +# +# +# @app.route(route="return_http_no_body") +# def return_http_no_body(req: Request): +# return Response() +# +# +# @app.route(route="return_http_redirect") +# def return_http_redirect(req: Request): +# location = 'return_http?code={}'.format(req.params['code']) +# return Response( +# status_code=302, +# headers={'location': location}) +# +# +# @app.route(route="return_out", binding_arg_name="foo") +# def return_out(req: Request, foo: func.Out[Response]): +# foo.set(Response(body='hello', status_code=201)) +# +# +# @app.route(route="return_request") +# def return_request(req: Request): +# params = dict(req.params) +# params.pop('code', None) +# body = req.get_body() +# return json.dumps({ +# 'method': req.method, +# 'url': req.url, +# 'headers': dict(req.headers), +# 'params': params, +# 'get_body': body.decode(), +# 'body_hash': hashlib.sha256(body).hexdigest(), +# }) +# +# +# @app.route(route="return_route_params/{param1}/{param2}") +# def return_route_params(req: Request) -> str: +# return json.dumps(dict(req.route_params)) +# +# +# @app.route(route="sync_logging") +# def main(req: Request): +# try: +# 1 / 0 +# except ZeroDivisionError: +# logger.error('a gracefully handled error', exc_info=True) +# logger.error('a gracefully handled critical error', exc_info=True) +# time.sleep(0.05) +# return 'OK-sync' +# +# +# @app.route(route="unhandled_error") +# def unhandled_error(req: Request): +# 1 / 0 +# +# +# @app.route(route="unhandled_urllib_error") +# def unhandled_urllib_error(req: Request) -> str: +# image_url = req.params.get('img') +# urlopen(image_url).read() +# +# +# class UnserializableException(Exception): +# def __str__(self): +# raise RuntimeError('cannot serialize me') +# +# +# @app.route(route="unhandled_unserializable_error") +# def unhandled_unserializable_error(req: Request) -> str: +# raise UnserializableException('foo') +# +# +# async def try_log(): +# logger.info("try_log") +# +# +# @app.route(route="user_event_loop") +# def user_event_loop(req: Request) -> Response: +# loop = asyncio.SelectorEventLoop() +# asyncio.set_event_loop(loop) +# +# # This line should throws an asyncio RuntimeError exception +# loop.run_until_complete(try_log()) +# loop.close() +# return 'OK-user-event-loop' +# +# +# @app.route(route="multiple_set_cookie_resp_headers") +# def multiple_set_cookie_resp_headers( +# req: Request) -> Response: +# logging.info('Python HTTP trigger function processed a request.') +# resp = Response( +# "This HTTP triggered function executed successfully.") +# +# resp.headers.add("Set-Cookie", +# 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 ' +# '13:55:08 GMT; Path=/; Max-Age=10000000; Secure; ' +# 'HttpOnly') +# resp.headers.add("Set-Cookie", +# 'foo3=43; Domain=example.com; Expires=Thu, 12-Jan-2018 ' +# '13:55:08 GMT; Path=/; Max-Age=10000000; Secure; ' +# 'HttpOnly') +# resp.headers.add("HELLO", 'world') +# +# return resp +# +# +# @app.route(route="response_cookie_header_nullable_bool_err") +# def response_cookie_header_nullable_bool_err( +# req: Request) -> Response: +# logging.info('Python HTTP trigger function processed a request.') +# resp = Response( +# "This HTTP triggered function executed successfully.") +# +# resp.headers.add("Set-Cookie", +# 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 ' +# '13:55:08 GMT; Path=/; Max-Age=10000000; SecureFalse; ' +# 'HttpOnly') +# +# return resp +# +# +# @app.route(route="response_cookie_header_nullable_double_err") +# def response_cookie_header_nullable_double_err( +# req: Request) -> Response: +# logging.info('Python HTTP trigger function processed a request.') +# resp = Response( +# "This HTTP triggered function executed successfully.") +# +# resp.headers.add("Set-Cookie", +# 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 ' +# '13:55:08 GMT; Path=/; Max-Age=Dummy; SecureFalse; ' +# 'HttpOnly') +# +# return resp +# +# +# @app.route(route="response_cookie_header_nullable_timestamp_err") +# def response_cookie_header_nullable_timestamp_err( +# req: Request) -> Response: +# logging.info('Python HTTP trigger function processed a request.') +# resp = Response( +# "This HTTP triggered function executed successfully.") +# +# resp.headers.add("Set-Cookie", 'foo=bar; Domain=123; Expires=Dummy') +# +# return resp +# +# +# @app.route(route="set_cookie_resp_header_default_values") +# def set_cookie_resp_header_default_values( +# req: Request) -> Response: +# logging.info('Python HTTP trigger function processed a request.') +# resp = Response( +# "This HTTP triggered function executed successfully.") +# +# resp.headers.add("Set-Cookie", 'foo=bar') +# +# return resp +# +# +# @app.route(route="set_cookie_resp_header_empty") +# def set_cookie_resp_header_empty( +# req: Request) -> Response: +# logging.info('Python HTTP trigger function processed a request.') +# resp = Response( +# "This HTTP triggered function executed successfully.") +# +# resp.headers.add("Set-Cookie", '') +# +# return resp diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py index 6d4ecbe1d..344ffdce8 100644 --- a/tests/unittests/test_http_functions.py +++ b/tests/unittests/test_http_functions.py @@ -10,7 +10,6 @@ from tests.utils import testutils - class TestHttpFunctions(testutils.WebHostTestCase): @classmethod @@ -462,3 +461,9 @@ def test_no_return(self): def test_no_return_returns(self): r = self.webhost.request('GET', 'no_return_returns') self.assertEqual(r.status_code, 200) + +class TestHttpFunctionsV2(TestHttpFunctions): + @classmethod + def get_script_dir(cls): + return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \ + 'http_v2_functions' From 9922b2ce607ade3149f95fe21eb7765dcab061ed Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 4 Apr 2024 22:41:59 -0700 Subject: [PATCH 002/101] final changes and tests --- azure_functions_worker/constants.py | 2 +- azure_functions_worker/dispatcher.py | 55 ++- azure_functions_worker/http_proxy.py | 5 +- azure_functions_worker/loader.py | 2 +- azure_functions_worker/logging.py | 4 +- python/test/worker.config.json | 2 +- setup.py | 2 +- .../test_linux_consumption.py | 35 ++ .../fastapi/file_name/main.py | 48 ++ .../http_functions_v2/fastapi/function_app.py | 91 ++++ .../fastapi_data_class/function_app.py | 108 ----- .../function_app.py | 108 ----- .../function_app.py | 108 ----- .../function_app.py | 108 ----- tests/endtoend/test_http_functions.py | 166 ++++++- .../http_v2_functions/fastapi/function_app.py | 434 +++++++++++++++++ .../http_v2_functions/function_app.py | 419 ---------------- tests/unittests/test_http_functions.py | 5 - tests/unittests/test_http_functions_v2.py | 457 ++++++++++++++++++ 19 files changed, 1261 insertions(+), 898 deletions(-) create mode 100644 tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py create mode 100644 tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py delete mode 100644 tests/endtoend/http_v2_functions/fastapi_data_class/function_app.py delete mode 100644 tests/endtoend/http_v2_functions/fastapi_non_streaming_custom_resps/function_app.py delete mode 100644 tests/endtoend/http_v2_functions/fastapi_streaming_download_func/function_app.py delete mode 100644 tests/endtoend/http_v2_functions/fastapi_streaming_upload_func/function_app.py create mode 100644 tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py delete mode 100644 tests/unittests/http_functions/http_v2_functions/function_app.py create mode 100644 tests/unittests/test_http_functions_v2.py diff --git a/azure_functions_worker/constants.py b/azure_functions_worker/constants.py index 7f09d586c..b38794017 100644 --- a/azure_functions_worker/constants.py +++ b/azure_functions_worker/constants.py @@ -63,7 +63,7 @@ METADATA_PROPERTIES_WORKER_INDEXED = "worker_indexed" # HostNames -LOCAL_HOST = "localhost" +LOCAL_HOST = "127.0.0.1" # Header names X_MS_INVOCATION_ID = "x-ms-invocation-id" diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 35143fb68..641b2a25e 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -91,6 +91,7 @@ def __init__(self, loop: BaseEventLoop, host: str, port: int, self._shmem_mgr = SharedMemoryManager() self._old_task_factory = None self.function_metadata_result = None + self._has_http_func = False # Used to store metadata returns self._function_metadata_result = None @@ -516,6 +517,7 @@ async def _handle__function_load_request(self, request): async def _handle__invocation_request(self, request): invocation_time = datetime.utcnow() invoc_request = request.invocation_request + trigger_metadata = invoc_request.trigger_metadata invocation_id = invoc_request.invocation_id function_id = invoc_request.function_id @@ -560,6 +562,7 @@ async def _handle__invocation_request(self, request): pytype=pb_type_info.pytype, shmem_mgr=self._shmem_mgr) + http_v2_enabled = False if fi.trigger_metadata.get('type') == HTTP_TRIGGER: from azure.functions.extension.base import HttpV2FeatureChecker http_v2_enabled = HttpV2FeatureChecker.http_v2_enabled() @@ -567,6 +570,11 @@ async def _handle__invocation_request(self, request): if http_v2_enabled: http_request = await http_coordinator.get_http_request_async( invocation_id) + + from azure.functions.extension.base import RequestTrackerMeta + route_params = {key: item.string for key, item in trigger_metadata.items() if key not in ['Headers', 'Query']} + + RequestTrackerMeta.get_synchronizer().sync_route_params(http_request, route_params) args[fi.trigger_metadata.get('param_name')] = http_request fi_context = self._get_context(invoc_request, fi.name, fi.directory) @@ -581,23 +589,26 @@ async def _handle__invocation_request(self, request): for name in fi.output_types: args[name] = bindings.Out() - if fi.is_async: - call_result = await self._run_async_func( - fi_context, fi.func, args - ) - - else: - call_result = await self._loop.run_in_executor( - self._sync_call_tp, - self._run_sync_func, - invocation_id, fi_context, fi.func, args) - - if call_result is not None and not fi.has_return: - raise RuntimeError(f'function {fi.name!r} without a $return ' - 'binding returned a non-None value') - - if http_v2_enabled: - http_coordinator.set_http_response(invocation_id, call_result) + call_result = None + call_error = None + try: + if fi.is_async: + call_result = await self._run_async_func(fi_context, fi.func, args) + else: + call_result = await self._loop.run_in_executor( + self._sync_call_tp, + self._run_sync_func, + invocation_id, fi_context, fi.func, args) + + if call_result is not None and not fi.has_return: + raise RuntimeError(f'function {fi.name!r} without a $return ' + 'binding returned a non-None value') + except Exception as e: + call_error = e + raise + finally: + if http_v2_enabled: + http_coordinator.set_http_response(invocation_id, call_result if call_result is not None else call_error) output_data = [] cache_enabled = self._function_data_cache_enabled @@ -753,9 +764,15 @@ async def catch_all(request: request_type): # type: ignore invoc_id = request.headers.get(X_MS_INVOCATION_ID) if invoc_id is None: raise ValueError(f"Header {X_MS_INVOCATION_ID} not found") - + logger.info('Received HTTP request for invocation %s', invoc_id) http_coordinator.set_http_request(invoc_id, request) http_resp = await http_coordinator.await_http_response_async(invoc_id) + + logger.info('Sending HTTP response for invocation %s', invoc_id) + # if http_resp is an python exception, raise it + if isinstance(http_resp, Exception): + raise http_resp + return http_resp web_server = web_server_class(LOCAL_HOST, unused_port, app) @@ -763,6 +780,7 @@ async def catch_all(request: request_type): # type: ignore loop = asyncio.get_event_loop() loop.create_task(web_server_run_task) + logger.info('HTTP server starting on %s:%s', LOCAL_HOST, unused_port) return f"http://{LOCAL_HOST}:{unused_port}" @@ -779,7 +797,6 @@ def index_functions(self, function_path: str): indexed_functions) indexed_function_logs: List[str] = [] - self._has_http_func = False for func in indexed_functions: self._has_http_func = self._has_http_func or func.is_http_function() function_log = "Function Name: {}, Function Binding: {}" \ diff --git a/azure_functions_worker/http_proxy.py b/azure_functions_worker/http_proxy.py index eff55067e..b56213485 100644 --- a/azure_functions_worker/http_proxy.py +++ b/azure_functions_worker/http_proxy.py @@ -102,14 +102,12 @@ def set_http_request(self, invoc_id, http_request): self._context_references[invoc_id] = AsyncContextReference() context_ref = self._context_references.get(invoc_id) context_ref.http_request = http_request - context_ref.http_request_available_event.set() def set_http_response(self, invoc_id, http_response): if invoc_id not in self._context_references: raise Exception("No context reference found for invocation %s", invoc_id) context_ref = self._context_references.get(invoc_id) context_ref.http_response = http_response - context_ref.http_response_available_event.set() async def get_http_request_async(self, invoc_id): if invoc_id not in self._context_references: @@ -141,8 +139,7 @@ def _pop_http_response(self, invoc_id): if response is not None: context_ref.http_response = None return response - - raise Exception("No http response found for invocation %s", invoc_id) + # If user does not set the response, return nothing and web server will return 200 empty response http_coordinator = HttpCoordinator() diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index da706fffd..e90888e3c 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -146,7 +146,7 @@ def process_indexed_function(functions_registry: functions.Registry, properties={METADATA_PROPERTIES_WORKER_INDEXED: "True"}) fx_metadata_results.append(function_metadata) - return fx_metadata_results + return fx_metadata_results except Exception as e: logger.error(f'Error in process_indexed_function. {e}', exc_info=True) raise e diff --git a/azure_functions_worker/logging.py b/azure_functions_worker/logging.py index 7a5340919..ddc5a7faf 100644 --- a/azure_functions_worker/logging.py +++ b/azure_functions_worker/logging.py @@ -20,8 +20,8 @@ handler: Optional[logging.Handler] = None error_handler: Optional[logging.Handler] = None -local_handler = logging.FileHandler("E:/projects/AzureFunctionsPythonWorker/log.txt") -logger.addHandler(local_handler) +# local_handler = logging.FileHandler("E:/projects/AzureFunctionsPythonWorker/log.txt") +# logger.addHandler(local_handler) def format_exception(exception: Exception) -> str: msg = str(exception) + "\n" diff --git a/python/test/worker.config.json b/python/test/worker.config.json index 05f6d26ed..d530908fc 100644 --- a/python/test/worker.config.json +++ b/python/test/worker.config.json @@ -2,7 +2,7 @@ "description":{ "language":"python", "extensions":[".py"], - "defaultExecutablePath":"E:\\projects\\AzureFunctionsPythonWorker\\.venv_3.8\\Scripts\\python.exe", + "defaultExecutablePath":"E:\\projects\\AzureFunctionsPythonWorker\\.venv_3.9_debug\\Scripts\\python.exe", "defaultWorkerPath":"worker.py", "workerIndexing": "true" } diff --git a/setup.py b/setup.py index f85f1c332..a4f3095a0 100644 --- a/setup.py +++ b/setup.py @@ -110,7 +110,7 @@ "numpy", "pre-commit" ], - "fastapi": ["azure-functions-extension-fastapi"] + "http-v2": ["azure-functions-extension-fastapi", "ujson", "orjson"] } diff --git a/tests/consumption_tests/test_linux_consumption.py b/tests/consumption_tests/test_linux_consumption.py index 195c1591c..3c7232366 100644 --- a/tests/consumption_tests/test_linux_consumption.py +++ b/tests/consumption_tests/test_linux_consumption.py @@ -336,6 +336,41 @@ def test_reload_variables_after_oom_error(self): self.assertNotIn("Failure Exception: ModuleNotFoundError", logs) + @skipIf(sys.version_info.minor != 10, + "This is testing only for python310") + def test_http_v2_fastapi_streaming_upload_download(self): + """ + A function app with init indexing enabled + """ + import random as rand + 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("HttpV2FastApiStreaming"), + PYTHON_ENABLE_INIT_INDEXING: "true", + PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1" + }) + + def generate_random_bytes_stream(): + """Generate a stream of random bytes.""" + yield b'streaming' + yield b'testing' + yield b'response' + yield b'is' + yield b'returned' + + req = Request('POST', f'{ctrl.url}/api/http_v2_fastapi_streaming', data=generate_random_bytes_stream()) + resp = ctrl.send_request(req) + self.assertEqual(resp.status_code, 200) + + streamed_data = b'' + for chunk in resp.iter_content(chunk_size=1024): + if chunk: + streamed_data+= chunk + + self.assertEqual(streamed_data, b'streamingtestingresponseisreturned') + def _get_blob_url(self, scenario_name: str) -> str: return ( f'https://pythonworker{self._py_shortform}sa.blob.core.windows.net/' diff --git a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py new file mode 100644 index 000000000..ad2831f0a --- /dev/null +++ b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py @@ -0,0 +1,48 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from datetime import datetime +import logging +import time + +import azure.functions as func + +from azure.functions.extension.fastapi import Request, Response, StreamingResponse, \ + HTMLResponse, PlainTextResponse, HTMLResponse, JSONResponse, \ + UJSONResponse, ORJSONResponse, RedirectResponse, FileResponse + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +@app.route(route="default_template") +async def default_template(req: Request) -> Response: + logging.info('Python HTTP trigger function processed a request.') + + name = req.query_params.get('name') + if not name: + try: + req_body = await req.json() + except ValueError: + pass + else: + name = req_body.get('name') + + if name: + return Response( + f"Hello, {name}. This HTTP triggered function " + f"executed successfully.") + else: + return Response( + "This HTTP triggered function executed successfully. " + "Pass a name in the query string or in the request body for a" + " personalized response.", + status_code=200 + ) + + +@app.route(route="http_func") +def http_func(req: Request) -> Response: + time.sleep(1) + + current_time = datetime.now().strftime("%H:%M:%S") + return Response(f"{current_time}") diff --git a/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py b/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py new file mode 100644 index 000000000..1870767da --- /dev/null +++ b/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py @@ -0,0 +1,91 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import asyncio +from datetime import datetime +import logging +import time + +import azure.functions as func +from azure.functions.extension.fastapi import Request, Response, StreamingResponse, \ + HTMLResponse, PlainTextResponse, HTMLResponse, JSONResponse, \ + UJSONResponse, ORJSONResponse, RedirectResponse, FileResponse + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +@app.route(route="default_template") +async def default_template(req: Request) -> Response: + logging.info('Python HTTP trigger function processed a request.') + + name = req.query_params.get('name') + if not name: + try: + req_body = await req.json() + except ValueError: + pass + else: + name = req_body.get('name') + + if name: + return Response( + f"Hello, {name}. This HTTP triggered function " + f"executed successfully.") + else: + return Response( + "This HTTP triggered function executed successfully. " + "Pass a name in the query string or in the request body for a" + " personalized response.", + status_code=200 + ) + + +@app.route(route="http_func") +def http_func(req: Request) -> Response: + time.sleep(1) + + current_time = datetime.now().strftime("%H:%M:%S") + return Response(f"{current_time}") + + +@app.route(route="upload_data_stream") +async def upload_data_stream(req: Request) -> Response: + # Define a list to accumulate the streaming data + data_chunks = [] + + async def process_stream(): + async for chunk in req.stream(): + # Append each chunk of streaming data to the list + data_chunks.append(chunk) + + await process_stream() + + # Concatenate the data chunks to form the complete data + complete_data = b"".join(data_chunks) + + # Return the complete data as the response + return Response(content=complete_data, status_code=200) + + +@app.route(route="return_streaming") +async def return_streaming(req: Request) -> StreamingResponse: + async def content(): + yield b"First chunk\n" + yield b"Second chunk\n" + return StreamingResponse(content()) + +@app.route(route="return_html") +def return_html(req: Request) -> HTMLResponse: + html_content = "

Hello, World!

" + return HTMLResponse(content=html_content, status_code=200) + +@app.route(route="return_ujson") +def return_ujson(req: Request) -> UJSONResponse: + return UJSONResponse(content={"message": "Hello, World!"}, status_code=200) + +@app.route(route="return_orjson") +def return_orjson(req: Request) -> ORJSONResponse: + return ORJSONResponse(content={"message": "Hello, World!"}, status_code=200) + +@app.route(route="return_file") +def return_file(req: Request) -> FileResponse: + return FileResponse("function_app.py") \ No newline at end of file diff --git a/tests/endtoend/http_v2_functions/fastapi_data_class/function_app.py b/tests/endtoend/http_v2_functions/fastapi_data_class/function_app.py deleted file mode 100644 index 25900aafb..000000000 --- a/tests/endtoend/http_v2_functions/fastapi_data_class/function_app.py +++ /dev/null @@ -1,108 +0,0 @@ -import json -import os -import typing - -from azure.eventhub import EventData -from azure.eventhub.aio import EventHubProducerClient - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -# An HttpTrigger to generating EventHub event from EventHub Output Binding -@app.function_name(name="eventhub_output") -@app.route(route="eventhub_output") -@app.event_hub_output(arg_name="event", - event_hub_name="python-worker-ci-eventhub-one", - connection="AzureWebJobsEventHubConnectionString") -def eventhub_output(req: func.HttpRequest, event: func.Out[str]): - event.set(req.get_body().decode('utf-8')) - return 'OK' - - -# This is an actual EventHub trigger which will convert the event data -# into a storage blob. -@app.function_name(name="eventhub_trigger") -@app.event_hub_message_trigger(arg_name="event", - event_hub_name="python-worker-ci-eventhub-one", - connection="AzureWebJobsEventHubConnectionString" - ) -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-eventhub-triggered.txt", - connection="AzureWebJobsStorage") -def eventhub_trigger(event: func.EventHubEvent) -> bytes: - return event.get_body() - - -# Retrieve the event data from storage blob and return it as Http response -@app.function_name(name="get_eventhub_triggered") -@app.route(route="get_eventhub_triggered") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-eventhub-triggered.txt", - connection="AzureWebJobsStorage") -def get_eventhub_triggered(req: func.HttpRequest, - file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -# Retrieve the event data from storage blob and return it as Http response -@app.function_name(name="get_metadata_triggered") -@app.route(route="get_metadata_triggered") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-metadata-triggered.txt", - connection="AzureWebJobsStorage") -async def get_metadata_triggered(req: func.HttpRequest, - file: func.InputStream) -> str: - return func.HttpResponse(body=file.read().decode('utf-8'), - status_code=200, - mimetype='application/json') - - -# An HttpTrigger to generating EventHub event from azure-eventhub SDK. -# Events generated from azure-eventhub contain the full metadata. -@app.function_name(name="metadata_output") -@app.route(route="metadata_output") -async def metadata_output(req: func.HttpRequest): - # Parse event metadata from http request - json_string = req.get_body().decode('utf-8') - event_dict = json.loads(json_string) - - # Create an EventHub Client and event batch - client = EventHubProducerClient.from_connection_string( - os.getenv('AzureWebJobsEventHubConnectionString'), - eventhub_name='python-worker-ci-eventhub-one-metadata') - - # Generate new event based on http request with full metadata - event_data_batch = await client.create_batch() - event_data_batch.add(EventData(event_dict.get('body'))) - - # Send out event into event hub - try: - await client.send_batch(event_data_batch) - finally: - await client.close() - - return 'OK' - - -@app.function_name(name="metadata_trigger") -@app.event_hub_message_trigger( - arg_name="event", - event_hub_name="python-worker-ci-eventhub-one-metadata", - connection="AzureWebJobsEventHubConnectionString") -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-metadata-triggered.txt", - connection="AzureWebJobsStorage") -async def metadata_trigger(event: func.EventHubEvent) -> bytes: - event_dict: typing.Mapping[str, typing.Any] = { - 'body': event.get_body().decode('utf-8'), - # Uncomment this when the EnqueuedTimeUtc is fixed in azure-functions - # 'enqueued_time': event.enqueued_time.isoformat(), - 'partition_key': event.partition_key, - 'sequence_number': event.sequence_number, - 'offset': event.offset, - 'metadata': event.metadata - } - - return json.dumps(event_dict) diff --git a/tests/endtoend/http_v2_functions/fastapi_non_streaming_custom_resps/function_app.py b/tests/endtoend/http_v2_functions/fastapi_non_streaming_custom_resps/function_app.py deleted file mode 100644 index 25900aafb..000000000 --- a/tests/endtoend/http_v2_functions/fastapi_non_streaming_custom_resps/function_app.py +++ /dev/null @@ -1,108 +0,0 @@ -import json -import os -import typing - -from azure.eventhub import EventData -from azure.eventhub.aio import EventHubProducerClient - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -# An HttpTrigger to generating EventHub event from EventHub Output Binding -@app.function_name(name="eventhub_output") -@app.route(route="eventhub_output") -@app.event_hub_output(arg_name="event", - event_hub_name="python-worker-ci-eventhub-one", - connection="AzureWebJobsEventHubConnectionString") -def eventhub_output(req: func.HttpRequest, event: func.Out[str]): - event.set(req.get_body().decode('utf-8')) - return 'OK' - - -# This is an actual EventHub trigger which will convert the event data -# into a storage blob. -@app.function_name(name="eventhub_trigger") -@app.event_hub_message_trigger(arg_name="event", - event_hub_name="python-worker-ci-eventhub-one", - connection="AzureWebJobsEventHubConnectionString" - ) -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-eventhub-triggered.txt", - connection="AzureWebJobsStorage") -def eventhub_trigger(event: func.EventHubEvent) -> bytes: - return event.get_body() - - -# Retrieve the event data from storage blob and return it as Http response -@app.function_name(name="get_eventhub_triggered") -@app.route(route="get_eventhub_triggered") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-eventhub-triggered.txt", - connection="AzureWebJobsStorage") -def get_eventhub_triggered(req: func.HttpRequest, - file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -# Retrieve the event data from storage blob and return it as Http response -@app.function_name(name="get_metadata_triggered") -@app.route(route="get_metadata_triggered") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-metadata-triggered.txt", - connection="AzureWebJobsStorage") -async def get_metadata_triggered(req: func.HttpRequest, - file: func.InputStream) -> str: - return func.HttpResponse(body=file.read().decode('utf-8'), - status_code=200, - mimetype='application/json') - - -# An HttpTrigger to generating EventHub event from azure-eventhub SDK. -# Events generated from azure-eventhub contain the full metadata. -@app.function_name(name="metadata_output") -@app.route(route="metadata_output") -async def metadata_output(req: func.HttpRequest): - # Parse event metadata from http request - json_string = req.get_body().decode('utf-8') - event_dict = json.loads(json_string) - - # Create an EventHub Client and event batch - client = EventHubProducerClient.from_connection_string( - os.getenv('AzureWebJobsEventHubConnectionString'), - eventhub_name='python-worker-ci-eventhub-one-metadata') - - # Generate new event based on http request with full metadata - event_data_batch = await client.create_batch() - event_data_batch.add(EventData(event_dict.get('body'))) - - # Send out event into event hub - try: - await client.send_batch(event_data_batch) - finally: - await client.close() - - return 'OK' - - -@app.function_name(name="metadata_trigger") -@app.event_hub_message_trigger( - arg_name="event", - event_hub_name="python-worker-ci-eventhub-one-metadata", - connection="AzureWebJobsEventHubConnectionString") -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-metadata-triggered.txt", - connection="AzureWebJobsStorage") -async def metadata_trigger(event: func.EventHubEvent) -> bytes: - event_dict: typing.Mapping[str, typing.Any] = { - 'body': event.get_body().decode('utf-8'), - # Uncomment this when the EnqueuedTimeUtc is fixed in azure-functions - # 'enqueued_time': event.enqueued_time.isoformat(), - 'partition_key': event.partition_key, - 'sequence_number': event.sequence_number, - 'offset': event.offset, - 'metadata': event.metadata - } - - return json.dumps(event_dict) diff --git a/tests/endtoend/http_v2_functions/fastapi_streaming_download_func/function_app.py b/tests/endtoend/http_v2_functions/fastapi_streaming_download_func/function_app.py deleted file mode 100644 index 25900aafb..000000000 --- a/tests/endtoend/http_v2_functions/fastapi_streaming_download_func/function_app.py +++ /dev/null @@ -1,108 +0,0 @@ -import json -import os -import typing - -from azure.eventhub import EventData -from azure.eventhub.aio import EventHubProducerClient - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -# An HttpTrigger to generating EventHub event from EventHub Output Binding -@app.function_name(name="eventhub_output") -@app.route(route="eventhub_output") -@app.event_hub_output(arg_name="event", - event_hub_name="python-worker-ci-eventhub-one", - connection="AzureWebJobsEventHubConnectionString") -def eventhub_output(req: func.HttpRequest, event: func.Out[str]): - event.set(req.get_body().decode('utf-8')) - return 'OK' - - -# This is an actual EventHub trigger which will convert the event data -# into a storage blob. -@app.function_name(name="eventhub_trigger") -@app.event_hub_message_trigger(arg_name="event", - event_hub_name="python-worker-ci-eventhub-one", - connection="AzureWebJobsEventHubConnectionString" - ) -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-eventhub-triggered.txt", - connection="AzureWebJobsStorage") -def eventhub_trigger(event: func.EventHubEvent) -> bytes: - return event.get_body() - - -# Retrieve the event data from storage blob and return it as Http response -@app.function_name(name="get_eventhub_triggered") -@app.route(route="get_eventhub_triggered") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-eventhub-triggered.txt", - connection="AzureWebJobsStorage") -def get_eventhub_triggered(req: func.HttpRequest, - file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -# Retrieve the event data from storage blob and return it as Http response -@app.function_name(name="get_metadata_triggered") -@app.route(route="get_metadata_triggered") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-metadata-triggered.txt", - connection="AzureWebJobsStorage") -async def get_metadata_triggered(req: func.HttpRequest, - file: func.InputStream) -> str: - return func.HttpResponse(body=file.read().decode('utf-8'), - status_code=200, - mimetype='application/json') - - -# An HttpTrigger to generating EventHub event from azure-eventhub SDK. -# Events generated from azure-eventhub contain the full metadata. -@app.function_name(name="metadata_output") -@app.route(route="metadata_output") -async def metadata_output(req: func.HttpRequest): - # Parse event metadata from http request - json_string = req.get_body().decode('utf-8') - event_dict = json.loads(json_string) - - # Create an EventHub Client and event batch - client = EventHubProducerClient.from_connection_string( - os.getenv('AzureWebJobsEventHubConnectionString'), - eventhub_name='python-worker-ci-eventhub-one-metadata') - - # Generate new event based on http request with full metadata - event_data_batch = await client.create_batch() - event_data_batch.add(EventData(event_dict.get('body'))) - - # Send out event into event hub - try: - await client.send_batch(event_data_batch) - finally: - await client.close() - - return 'OK' - - -@app.function_name(name="metadata_trigger") -@app.event_hub_message_trigger( - arg_name="event", - event_hub_name="python-worker-ci-eventhub-one-metadata", - connection="AzureWebJobsEventHubConnectionString") -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-metadata-triggered.txt", - connection="AzureWebJobsStorage") -async def metadata_trigger(event: func.EventHubEvent) -> bytes: - event_dict: typing.Mapping[str, typing.Any] = { - 'body': event.get_body().decode('utf-8'), - # Uncomment this when the EnqueuedTimeUtc is fixed in azure-functions - # 'enqueued_time': event.enqueued_time.isoformat(), - 'partition_key': event.partition_key, - 'sequence_number': event.sequence_number, - 'offset': event.offset, - 'metadata': event.metadata - } - - return json.dumps(event_dict) diff --git a/tests/endtoend/http_v2_functions/fastapi_streaming_upload_func/function_app.py b/tests/endtoend/http_v2_functions/fastapi_streaming_upload_func/function_app.py deleted file mode 100644 index 25900aafb..000000000 --- a/tests/endtoend/http_v2_functions/fastapi_streaming_upload_func/function_app.py +++ /dev/null @@ -1,108 +0,0 @@ -import json -import os -import typing - -from azure.eventhub import EventData -from azure.eventhub.aio import EventHubProducerClient - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -# An HttpTrigger to generating EventHub event from EventHub Output Binding -@app.function_name(name="eventhub_output") -@app.route(route="eventhub_output") -@app.event_hub_output(arg_name="event", - event_hub_name="python-worker-ci-eventhub-one", - connection="AzureWebJobsEventHubConnectionString") -def eventhub_output(req: func.HttpRequest, event: func.Out[str]): - event.set(req.get_body().decode('utf-8')) - return 'OK' - - -# This is an actual EventHub trigger which will convert the event data -# into a storage blob. -@app.function_name(name="eventhub_trigger") -@app.event_hub_message_trigger(arg_name="event", - event_hub_name="python-worker-ci-eventhub-one", - connection="AzureWebJobsEventHubConnectionString" - ) -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-eventhub-triggered.txt", - connection="AzureWebJobsStorage") -def eventhub_trigger(event: func.EventHubEvent) -> bytes: - return event.get_body() - - -# Retrieve the event data from storage blob and return it as Http response -@app.function_name(name="get_eventhub_triggered") -@app.route(route="get_eventhub_triggered") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-eventhub-triggered.txt", - connection="AzureWebJobsStorage") -def get_eventhub_triggered(req: func.HttpRequest, - file: func.InputStream) -> str: - return file.read().decode('utf-8') - - -# Retrieve the event data from storage blob and return it as Http response -@app.function_name(name="get_metadata_triggered") -@app.route(route="get_metadata_triggered") -@app.blob_input(arg_name="file", - path="python-worker-tests/test-metadata-triggered.txt", - connection="AzureWebJobsStorage") -async def get_metadata_triggered(req: func.HttpRequest, - file: func.InputStream) -> str: - return func.HttpResponse(body=file.read().decode('utf-8'), - status_code=200, - mimetype='application/json') - - -# An HttpTrigger to generating EventHub event from azure-eventhub SDK. -# Events generated from azure-eventhub contain the full metadata. -@app.function_name(name="metadata_output") -@app.route(route="metadata_output") -async def metadata_output(req: func.HttpRequest): - # Parse event metadata from http request - json_string = req.get_body().decode('utf-8') - event_dict = json.loads(json_string) - - # Create an EventHub Client and event batch - client = EventHubProducerClient.from_connection_string( - os.getenv('AzureWebJobsEventHubConnectionString'), - eventhub_name='python-worker-ci-eventhub-one-metadata') - - # Generate new event based on http request with full metadata - event_data_batch = await client.create_batch() - event_data_batch.add(EventData(event_dict.get('body'))) - - # Send out event into event hub - try: - await client.send_batch(event_data_batch) - finally: - await client.close() - - return 'OK' - - -@app.function_name(name="metadata_trigger") -@app.event_hub_message_trigger( - arg_name="event", - event_hub_name="python-worker-ci-eventhub-one-metadata", - connection="AzureWebJobsEventHubConnectionString") -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-metadata-triggered.txt", - connection="AzureWebJobsStorage") -async def metadata_trigger(event: func.EventHubEvent) -> bytes: - event_dict: typing.Mapping[str, typing.Any] = { - 'body': event.get_body().decode('utf-8'), - # Uncomment this when the EnqueuedTimeUtc is fixed in azure-functions - # 'enqueued_time': event.enqueued_time.isoformat(), - 'partition_key': event.partition_key, - 'sequence_number': event.sequence_number, - 'offset': event.offset, - 'metadata': event.metadata - } - - return json.dumps(event_dict) diff --git a/tests/endtoend/test_http_functions.py b/tests/endtoend/test_http_functions.py index 9d6f9fdf6..8d7061878 100644 --- a/tests/endtoend/test_http_functions.py +++ b/tests/endtoend/test_http_functions.py @@ -1,7 +1,9 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import concurrent import os import typing +from concurrent.futures import ThreadPoolExecutor from unittest.mock import patch import requests @@ -220,19 +222,157 @@ def tearDownClass(cls): super().tearDownClass() -class TestHttpFunctionsV2WithInitIndexing(TestHttpFunctionsStein): - - @classmethod - def setUpClass(cls): - os.environ[PYTHON_ENABLE_INIT_INDEXING] = "1" - super().setUpClass() - - @classmethod - def tearDownClass(cls): - # Remove the PYTHON_SCRIPT_FILE_NAME environment variable - os.environ.pop(PYTHON_ENABLE_INIT_INDEXING) - super().tearDownClass() - +class TestHttpFunctionsV2FastApiWithInitIndexing(TestHttpFunctionsWithInitIndexing): + @classmethod + def get_script_dir(cls): + return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ + 'http_functions_v2' / \ + 'fastapi' + + @testutils.retryable_test(3, 5) + def test_return_streaming(self): + """Test if the return_streaming function returns a streaming + response""" + root_url = self.webhost._addr + streaming_url = f'{root_url}/api/return_streaming' + r = requests.get(streaming_url, timeout=REQUEST_TIMEOUT_SEC, stream=True) + self.assertTrue(r.ok) + # Validate streaming content + expected_content = [b"First chunk\n", b"Second chunk\n"] + received_content = [] + for chunk in r.iter_content(chunk_size=1024): + if chunk: + received_content.append(chunk) + self.assertEqual(received_content, expected_content) + + @testutils.retryable_test(3, 5) + def test_return_streaming_concurrently(self): + """Test if the return_streaming function returns a streaming + response concurrently""" + root_url = self.webhost._addr + streaming_url = f'{root_url}/return_streaming' + + # Function to make a streaming request and validate content + def make_request(): + r = requests.get(streaming_url, timeout=REQUEST_TIMEOUT_SEC, + stream=True) + self.assertTrue(r.ok) + expected_content = [b"First chunk\n", b"Second chunk\n"] + received_content = [] + for chunk in r.iter_content(chunk_size=1024): + if chunk: + received_content.append(chunk) + self.assertEqual(received_content, expected_content) + + # Make concurrent requests + with ThreadPoolExecutor(max_workers=2) as executor: + executor.map(make_request, range(2)) + + @testutils.retryable_test(3, 5) + def test_return_html(self): + """Test if the return_html function returns an HTML response""" + root_url = self.webhost._addr + html_url = f'{root_url}/api/return_html' + r = requests.get(html_url, timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual(r.headers['content-type'], + 'text/html; charset=utf-8') + # Validate HTML content + expected_html = "

Hello, World!

" + self.assertEqual(r.text, expected_html) + + @testutils.retryable_test(3, 5) + def test_return_ujson(self): + """Test if the return_ujson function returns a UJSON response""" + root_url = self.webhost._addr + ujson_url = f'{root_url}/api/return_ujson' + r = requests.get(ujson_url, timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual(r.headers['content-type'],'application/json') + self.assertEqual(r.text, '{"message":"Hello, World!"}') + + @testutils.retryable_test(3, 5) + def test_return_orjson(self): + """Test if the return_orjson function returns an ORJSON response""" + root_url = self.webhost._addr + orjson_url = f'{root_url}/api/return_orjson' + r = requests.get(orjson_url, timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual(r.headers['content-type'], 'application/json') + self.assertEqual(r.text, '{"message":"Hello, World!"}') + + @testutils.retryable_test(3, 5) + def test_return_file(self): + """Test if the return_file function returns a file response""" + root_url = self.webhost._addr + file_url = f'{root_url}/api/return_file' + r = requests.get(file_url, timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertIn('@app.route(route="default_template")', r.text) + + @testutils.retryable_test(3, 5) + def test_upload_data_stream(self): + """Test if the upload_data_stream function receives streaming data + and returns the complete data""" + root_url = self.webhost._addr + upload_url = f'{root_url}/api/upload_data_stream' + + # Define the streaming data + data_chunks = [b"First chunk\n", b"Second chunk\n"] + + # Define a function to simulate streaming by reading from an + # iterator + def stream_data(data_chunks): + for chunk in data_chunks: + yield chunk + + # Send a POST request with streaming data + r = requests.post(upload_url, data=stream_data(data_chunks)) + + # Assert that the request was successful + self.assertTrue(r.ok) + + # Assert that the response content matches the concatenation of + # all data chunks + complete_data = b"".join(data_chunks) + self.assertEqual(r.content, complete_data) + + @testutils.retryable_test(3, 5) + def test_upload_data_stream_concurrently(self): + """Test if the upload_data_stream function receives streaming data + and returns the complete data""" + root_url = self.webhost._addr + upload_url = f'{root_url}/api/upload_data_stream' + + # Define the streaming data + data_chunks = [b"First chunk\n", b"Second chunk\n"] + + # Define a function to simulate streaming by reading from an + # iterator + def stream_data(data_chunks): + for chunk in data_chunks: + yield chunk + + # Define the number of concurrent requests + num_requests = 5 + + # Define a function to send a single request + def send_request(): + r = requests.post(upload_url, data=stream_data(data_chunks)) + return r.ok, r.content + + # Send multiple requests concurrently + with concurrent.futures.ThreadPoolExecutor() as executor: + futures = [executor.submit(send_request) for _ in + range(num_requests)] + + # Assert that all requests were successful and the response + # contents are correct + for future in concurrent.futures.as_completed(futures): + ok, content = future.result() + self.assertTrue(ok) + complete_data = b"".join(data_chunks) + self.assertEqual(content, complete_data) class TestUserThreadLoggingHttpFunctions(testutils.WebHostTestCase): """Test the Http trigger that contains logging with user threads. diff --git a/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py b/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py new file mode 100644 index 000000000..25accf853 --- /dev/null +++ b/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py @@ -0,0 +1,434 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import asyncio +import hashlib +import json +import logging +import sys +import time +from urllib.request import urlopen +from azure.functions.extension.fastapi import Request, Response, \ + PlainTextResponse, HTMLResponse, RedirectResponse +import azure.functions as func +from pydantic import BaseModel + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + +logger = logging.getLogger("my-function") + + +class Item(BaseModel): + name: str + description: str + + +@app.route(route="no_type_hint") +def no_type_hint(req): + return 'no_type_hint' + + +@app.route(route="return_int") +def return_int(req) -> int: + return 1000 + + +@app.route(route="return_float") +def return_float(req) -> float: + return 1000.0 + + +@app.route(route="return_bool") +def return_bool(req) -> bool: + return True + + +@app.route(route="return_dict") +def return_dict(req) -> dict: + return {"key": "value"} + + +@app.route(route="return_list") +def return_list(req): + return ["value1", "value2"] + + +@app.route(route="return_pydantic_model") +def return_pydantic_model(req) -> Item: + item = Item(name="item1", description="description1") + return item + + +@app.route(route="return_pydantic_model_with_missing_fields") +def return_pydantic_model_with_missing_fields(req) -> Item: + item = Item(name="item1") + return item + + +@app.route(route="accept_json") +async def accept_json(req: Request): + return await req.json() + + +async def nested(): + try: + 1 / 0 + except ZeroDivisionError: + logger.error('and another error', exc_info=True) + + +@app.route(route="async_logging") +async def async_logging(req: Request): + logger.info('hello %s', 'info') + + await asyncio.sleep(0.1) + + # Create a nested task to check if invocation_id is still + # logged correctly. + await asyncio.ensure_future(nested()) + + await asyncio.sleep(0.1) + + return 'OK-async' + + +@app.route(route="async_return_str") +async def async_return_str(req: Request): + await asyncio.sleep(0.1) + return 'Hello Async World!' + + +@app.route(route="debug_logging") +def debug_logging(req: Request): + logging.critical('logging critical', exc_info=True) + logging.info('logging info', exc_info=True) + logging.warning('logging warning', exc_info=True) + logging.debug('logging debug', exc_info=True) + logging.error('logging error', exc_info=True) + return 'OK-debug' + + +@app.route(route="debug_user_logging") +def debug_user_logging(req: Request): + logger.setLevel(logging.DEBUG) + + logging.critical('logging critical', exc_info=True) + logger.info('logging info', exc_info=True) + logger.warning('logging warning', exc_info=True) + logger.debug('logging debug', exc_info=True) + logger.error('logging error', exc_info=True) + return 'OK-user-debug' + + +# Attempt to log info into system log from customer code +disguised_logger = logging.getLogger('azure_functions_worker') + + +async def parallelly_print(): + await asyncio.sleep(0.1) + print('parallelly_print') + + +async def parallelly_log_info(): + await asyncio.sleep(0.2) + logging.info('parallelly_log_info at root logger') + + +async def parallelly_log_warning(): + await asyncio.sleep(0.3) + logging.warning('parallelly_log_warning at root logger') + + +async def parallelly_log_error(): + await asyncio.sleep(0.4) + logging.error('parallelly_log_error at root logger') + + +async def parallelly_log_exception(): + await asyncio.sleep(0.5) + try: + raise Exception('custom exception') + except Exception: + logging.exception('parallelly_log_exception at root logger', + exc_info=sys.exc_info()) + + +async def parallelly_log_custom(): + await asyncio.sleep(0.6) + logger.info('parallelly_log_custom at custom_logger') + + +async def parallelly_log_system(): + await asyncio.sleep(0.7) + disguised_logger.info('parallelly_log_system at disguised_logger') + + +@app.route(route="hijack_current_event_loop") +async def hijack_current_event_loop(req: Request) -> Response: + loop = asyncio.get_event_loop() + + # Create multiple tasks and schedule it into one asyncio.wait blocker + task_print: asyncio.Task = loop.create_task(parallelly_print()) + task_info: asyncio.Task = loop.create_task(parallelly_log_info()) + task_warning: asyncio.Task = loop.create_task(parallelly_log_warning()) + task_error: asyncio.Task = loop.create_task(parallelly_log_error()) + task_exception: asyncio.Task = loop.create_task(parallelly_log_exception()) + task_custom: asyncio.Task = loop.create_task(parallelly_log_custom()) + task_disguise: asyncio.Task = loop.create_task(parallelly_log_system()) + + # Create an awaitable future and occupy the current event loop resource + future = loop.create_future() + loop.call_soon_threadsafe(future.set_result, 'callsoon_log') + + # WaitAll + await asyncio.wait([task_print, task_info, task_warning, task_error, + task_exception, task_custom, task_disguise, future]) + + # Log asyncio low-level future result + logging.info(future.result()) + + return 'OK-hijack-current-event-loop' + + +@app.route(route="print_logging") +def print_logging(req: Request): + flush_required = False + is_console_log = False + is_stderr = False + message = req.query_params.get('message', '') + + if req.query_params.get('flush') == 'true': + flush_required = True + if req.query_params.get('console') == 'true': + is_console_log = True + if req.query_params.get('is_stderr') == 'true': + is_stderr = True + + # Adding LanguageWorkerConsoleLog will make function host to treat + # this as system log and will be propagated to kusto + prefix = 'LanguageWorkerConsoleLog' if is_console_log else '' + print(f'{prefix} {message}'.strip(), + file=sys.stderr if is_stderr else sys.stdout, + flush=flush_required) + + return 'OK-print-logging' + + +@app.route(route="raw_body_bytes") +async def raw_body_bytes(req: Request) -> Response: + body = await req.body() + body_len = str(len(body)) + + headers = {'body-len': body_len} + return Response(content=body, status_code=200, headers=headers) + + +@app.route(route="remapped_context") +def remapped_context(req: Request): + return req.method + + +@app.route(route="return_bytes") +def return_bytes(req: Request): + return b"Hello World" + + +@app.route(route="return_context") +def return_context(req: Request, context: func.Context): + return { + 'method': req.method, + 'ctx_func_name': context.function_name, + 'ctx_func_dir': context.function_directory, + 'ctx_invocation_id': context.invocation_id, + 'ctx_trace_context_Traceparent': context.trace_context.Traceparent, + 'ctx_trace_context_Tracestate': context.trace_context.Tracestate, + } + + +@app.route(route="return_http") +def return_http(req: Request) -> HTMLResponse: + return HTMLResponse('

Hello World™

') + + +@app.route(route="return_http_404") +def return_http_404(req: Request): + return Response('bye', status_code=404) + + +@app.route(route="return_http_auth_admin", auth_level=func.AuthLevel.ADMIN) +def return_http_auth_admin(req: Request) -> HTMLResponse: + return HTMLResponse('

Hello World™

') + + +@app.route(route="return_http_no_body") +def return_http_no_body(req: Request): + return Response() + + +@app.route(route="return_http_redirect") +def return_http_redirect(req: Request): + return RedirectResponse(url='/api/return_http', status_code=302) + + +@app.route(route="return_request") +async def return_request(req: Request): + params = dict(req.query_params) + params.pop('code', None) # Remove 'code' parameter if present + + # Get the body content and calculate its hash + body = await req.body() + body_hash = hashlib.sha256(body).hexdigest() if body else None + + # Return a dictionary containing request information + return { + 'method': req.method, + 'url': str(req.url), + 'headers': dict(req.headers), + 'params': params, + 'body': body.decode() if body else None, + 'body_hash': body_hash, + } + + +@app.route(route="return_route_params/{param1}/{param2}") +def return_route_params(req: Request) -> str: + # log type of req + logger.info(f"req type: {type(req)}") + # log req path params + logger.info(f"req path params: {req.path_params}") + return req.path_params + + +@app.route(route="sync_logging") +def main(req: Request): + try: + 1 / 0 + except ZeroDivisionError: + logger.error('a gracefully handled error', exc_info=True) + logger.error('a gracefully handled critical error', exc_info=True) + time.sleep(0.05) + return 'OK-sync' + + +@app.route(route="unhandled_error") +def unhandled_error(req: Request): + 1 / 0 + + +@app.route(route="unhandled_urllib_error") +def unhandled_urllib_error(req: Request) -> str: + image_url = req.params.get('img') + urlopen(image_url).read() + + +class UnserializableException(Exception): + def __str__(self): + raise RuntimeError('cannot serialize me') + + +@app.route(route="unhandled_unserializable_error") +def unhandled_unserializable_error(req: Request) -> str: + raise UnserializableException('foo') + + +async def try_log(): + logger.info("try_log") + + +@app.route(route="user_event_loop") +def user_event_loop(req: Request) -> Response: + loop = asyncio.SelectorEventLoop() + asyncio.set_event_loop(loop) + + # This line should throws an asyncio RuntimeError exception + loop.run_until_complete(try_log()) + loop.close() + return 'OK-user-event-loop' + + +@app.route(route="multiple_set_cookie_resp_headers") +async def multiple_set_cookie_resp_headers(req: Request): + logging.info('Python HTTP trigger function processed a request.') + resp = Response( + "This HTTP triggered function executed successfully.") + + expires_1 = "Thu, 12 Jan 2017 13:55:08 GMT" + expires_2 = "Fri, 12 Jan 2018 13:55:08 GMT" + + resp.set_cookie( + key='foo3', + value='42', + domain='example.com', + expires=expires_1, + path='/', + max_age=10000000, + secure=True, + httponly=True, + samesite='Lax' + ) + + resp.set_cookie( + key='foo3', + value='43', + domain='example.com', + expires=expires_2, + path='/', + max_age=10000000, + secure=True, + httponly=True, + samesite='Lax' + ) + + return resp + + +@app.route(route="response_cookie_header_nullable_bool_err") +def response_cookie_header_nullable_bool_err( + req: Request) -> Response: + logging.info('Python HTTP trigger function processed a request.') + resp = Response( + "This HTTP triggered function executed successfully.") + + # Set the cookie with Secure attribute set to False + resp.set_cookie( + key='foo3', + value='42', + domain='example.com', + expires='Thu, 12-Jan-2017 13:55:08 GMT', + path='/', + max_age=10000000, + secure=False, + httponly=True + ) + + return resp + + +@app.route(route="response_cookie_header_nullable_timestamp_err") +def response_cookie_header_nullable_timestamp_err( + req: Request) -> Response: + logging.info('Python HTTP trigger function processed a request.') + resp = Response( + "This HTTP triggered function executed successfully.") + + resp.set_cookie( + key='foo3', + value='42', + domain='example.com' + ) + + return resp + + +@app.route(route="set_cookie_resp_header_default_values") +def set_cookie_resp_header_default_values( + req: Request) -> Response: + logging.info('Python HTTP trigger function processed a request.') + resp = Response( + "This HTTP triggered function executed successfully.") + + resp.set_cookie( + key='foo3', + value='42' + ) + + return resp \ No newline at end of file diff --git a/tests/unittests/http_functions/http_v2_functions/function_app.py b/tests/unittests/http_functions/http_v2_functions/function_app.py deleted file mode 100644 index f4bcfb36e..000000000 --- a/tests/unittests/http_functions/http_v2_functions/function_app.py +++ /dev/null @@ -1,419 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -import asyncio -import hashlib -import json -import logging -import sys -import time -from urllib.request import urlopen -from azure.functions.extension.fastapi import Request, Response -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - -logger = logging.getLogger("my-function") - -# request handling -# request body, query, headers, and route params -# request validation errors -# diff http verbs - -# response handling -# response body, status code, headers -# error responses - -# edge cases -# invalid requests sent behavior with missing body, query, headers, and route params -# request payload exceeds max size -# request payload contains special characters - -@app.route(route="return_str") -def return_str(req: Request) -> str: - return 'Hello World!' - -# -# @app.route(route="accept_json") -# def accept_json(req: Request): -# return json.dumps({ -# 'method': req.method, -# 'url': req.url, -# 'headers': dict(req.headers), -# 'params': dict(req.params), -# 'get_body': req.get_body().decode(), -# 'get_json': req.get_json() -# }) -# -# -# async def nested(): -# try: -# 1 / 0 -# except ZeroDivisionError: -# logger.error('and another error', exc_info=True) -# -# -# @app.route(route="async_logging") -# async def async_logging(req: Request): -# logger.info('hello %s', 'info') -# -# await asyncio.sleep(0.1) -# -# # Create a nested task to check if invocation_id is still -# # logged correctly. -# await asyncio.ensure_future(nested()) -# -# await asyncio.sleep(0.1) -# -# return 'OK-async' -# -# -# @app.route(route="async_return_str") -# async def async_return_str(req: Request): -# await asyncio.sleep(0.1) -# return 'Hello Async World!' -# -# -# @app.route(route="debug_logging") -# def debug_logging(req: Request): -# logging.critical('logging critical', exc_info=True) -# logging.info('logging info', exc_info=True) -# logging.warning('logging warning', exc_info=True) -# logging.debug('logging debug', exc_info=True) -# logging.error('logging error', exc_info=True) -# return 'OK-debug' -# -# -# @app.route(route="debug_user_logging") -# def debug_user_logging(req: Request): -# logger.setLevel(logging.DEBUG) -# -# logging.critical('logging critical', exc_info=True) -# logger.info('logging info', exc_info=True) -# logger.warning('logging warning', exc_info=True) -# logger.debug('logging debug', exc_info=True) -# logger.error('logging error', exc_info=True) -# return 'OK-user-debug' -# -# -# # Attempt to log info into system log from customer code -# disguised_logger = logging.getLogger('azure_functions_worker') -# -# -# async def parallelly_print(): -# await asyncio.sleep(0.1) -# print('parallelly_print') -# -# -# async def parallelly_log_info(): -# await asyncio.sleep(0.2) -# logging.info('parallelly_log_info at root logger') -# -# -# async def parallelly_log_warning(): -# await asyncio.sleep(0.3) -# logging.warning('parallelly_log_warning at root logger') -# -# -# async def parallelly_log_error(): -# await asyncio.sleep(0.4) -# logging.error('parallelly_log_error at root logger') -# -# -# async def parallelly_log_exception(): -# await asyncio.sleep(0.5) -# try: -# raise Exception('custom exception') -# except Exception: -# logging.exception('parallelly_log_exception at root logger', -# exc_info=sys.exc_info()) -# -# -# async def parallelly_log_custom(): -# await asyncio.sleep(0.6) -# logger.info('parallelly_log_custom at custom_logger') -# -# -# async def parallelly_log_system(): -# await asyncio.sleep(0.7) -# disguised_logger.info('parallelly_log_system at disguised_logger') -# -# -# @app.route(route="hijack_current_event_loop") -# async def hijack_current_event_loop(req: Request) -> Response: -# loop = asyncio.get_event_loop() -# -# # Create multiple tasks and schedule it into one asyncio.wait blocker -# task_print: asyncio.Task = loop.create_task(parallelly_print()) -# task_info: asyncio.Task = loop.create_task(parallelly_log_info()) -# task_warning: asyncio.Task = loop.create_task(parallelly_log_warning()) -# task_error: asyncio.Task = loop.create_task(parallelly_log_error()) -# task_exception: asyncio.Task = loop.create_task(parallelly_log_exception()) -# task_custom: asyncio.Task = loop.create_task(parallelly_log_custom()) -# task_disguise: asyncio.Task = loop.create_task(parallelly_log_system()) -# -# # Create an awaitable future and occupy the current event loop resource -# future = loop.create_future() -# loop.call_soon_threadsafe(future.set_result, 'callsoon_log') -# -# # WaitAll -# await asyncio.wait([task_print, task_info, task_warning, task_error, -# task_exception, task_custom, task_disguise, future]) -# -# # Log asyncio low-level future result -# logging.info(future.result()) -# -# return 'OK-hijack-current-event-loop' -# -# -# @app.route(route="no_return") -# def no_return(req: Request): -# logger.info('hi') -# -# -# @app.route(route="no_return_returns") -# def no_return_returns(req): -# return 'ABC' -# -# -# @app.route(route="print_logging") -# def print_logging(req: Request): -# flush_required = False -# is_console_log = False -# is_stderr = False -# message = req.params.get('message', '') -# -# if req.params.get('flush') == 'true': -# flush_required = True -# if req.params.get('console') == 'true': -# is_console_log = True -# if req.params.get('is_stderr') == 'true': -# is_stderr = True -# -# # Adding LanguageWorkerConsoleLog will make function host to treat -# # this as system log and will be propagated to kusto -# prefix = 'LanguageWorkerConsoleLog' if is_console_log else '' -# print(f'{prefix} {message}'.strip(), -# file=sys.stderr if is_stderr else sys.stdout, -# flush=flush_required) -# -# return 'OK-print-logging' -# -# -# @app.route(route="raw_body_bytes") -# def raw_body_bytes(req: Request) -> Response: -# body = req.get_body() -# body_len = str(len(body)) -# -# headers = {'body-len': body_len} -# return Response(body=body, status_code=200, headers=headers) -# -# -# @app.route(route="remapped_context") -# def remapped_context(req: Request): -# return req.method -# -# -# @app.route(route="return_bytes") -# def return_bytes(req: Request): -# # This function will fail, as we don't auto-convert "bytes" to "http". -# return b'Hello World!' -# -# -# @app.route(route="return_context") -# def return_context(req: Request, context: func.Context): -# return json.dumps({ -# 'method': req.method, -# 'ctx_func_name': context.function_name, -# 'ctx_func_dir': context.function_directory, -# 'ctx_invocation_id': context.invocation_id, -# 'ctx_trace_context_Traceparent': context.trace_context.Traceparent, -# 'ctx_trace_context_Tracestate': context.trace_context.Tracestate, -# }) -# -# -# @app.route(route="return_http") -# def return_http(req: Request): -# return Response('

Hello World™

', -# mimetype='text/html') -# -# -# @app.route(route="return_http_404") -# def return_http_404(req: Request): -# return Response('bye', status_code=404) -# -# -# @app.route(route="return_http_auth_admin", auth_level=func.AuthLevel.ADMIN) -# def return_http_auth_admin(req: Request): -# return Response('

Hello World™

', -# mimetype='text/html') -# -# -# @app.route(route="return_http_no_body") -# def return_http_no_body(req: Request): -# return Response() -# -# -# @app.route(route="return_http_redirect") -# def return_http_redirect(req: Request): -# location = 'return_http?code={}'.format(req.params['code']) -# return Response( -# status_code=302, -# headers={'location': location}) -# -# -# @app.route(route="return_out", binding_arg_name="foo") -# def return_out(req: Request, foo: func.Out[Response]): -# foo.set(Response(body='hello', status_code=201)) -# -# -# @app.route(route="return_request") -# def return_request(req: Request): -# params = dict(req.params) -# params.pop('code', None) -# body = req.get_body() -# return json.dumps({ -# 'method': req.method, -# 'url': req.url, -# 'headers': dict(req.headers), -# 'params': params, -# 'get_body': body.decode(), -# 'body_hash': hashlib.sha256(body).hexdigest(), -# }) -# -# -# @app.route(route="return_route_params/{param1}/{param2}") -# def return_route_params(req: Request) -> str: -# return json.dumps(dict(req.route_params)) -# -# -# @app.route(route="sync_logging") -# def main(req: Request): -# try: -# 1 / 0 -# except ZeroDivisionError: -# logger.error('a gracefully handled error', exc_info=True) -# logger.error('a gracefully handled critical error', exc_info=True) -# time.sleep(0.05) -# return 'OK-sync' -# -# -# @app.route(route="unhandled_error") -# def unhandled_error(req: Request): -# 1 / 0 -# -# -# @app.route(route="unhandled_urllib_error") -# def unhandled_urllib_error(req: Request) -> str: -# image_url = req.params.get('img') -# urlopen(image_url).read() -# -# -# class UnserializableException(Exception): -# def __str__(self): -# raise RuntimeError('cannot serialize me') -# -# -# @app.route(route="unhandled_unserializable_error") -# def unhandled_unserializable_error(req: Request) -> str: -# raise UnserializableException('foo') -# -# -# async def try_log(): -# logger.info("try_log") -# -# -# @app.route(route="user_event_loop") -# def user_event_loop(req: Request) -> Response: -# loop = asyncio.SelectorEventLoop() -# asyncio.set_event_loop(loop) -# -# # This line should throws an asyncio RuntimeError exception -# loop.run_until_complete(try_log()) -# loop.close() -# return 'OK-user-event-loop' -# -# -# @app.route(route="multiple_set_cookie_resp_headers") -# def multiple_set_cookie_resp_headers( -# req: Request) -> Response: -# logging.info('Python HTTP trigger function processed a request.') -# resp = Response( -# "This HTTP triggered function executed successfully.") -# -# resp.headers.add("Set-Cookie", -# 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 ' -# '13:55:08 GMT; Path=/; Max-Age=10000000; Secure; ' -# 'HttpOnly') -# resp.headers.add("Set-Cookie", -# 'foo3=43; Domain=example.com; Expires=Thu, 12-Jan-2018 ' -# '13:55:08 GMT; Path=/; Max-Age=10000000; Secure; ' -# 'HttpOnly') -# resp.headers.add("HELLO", 'world') -# -# return resp -# -# -# @app.route(route="response_cookie_header_nullable_bool_err") -# def response_cookie_header_nullable_bool_err( -# req: Request) -> Response: -# logging.info('Python HTTP trigger function processed a request.') -# resp = Response( -# "This HTTP triggered function executed successfully.") -# -# resp.headers.add("Set-Cookie", -# 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 ' -# '13:55:08 GMT; Path=/; Max-Age=10000000; SecureFalse; ' -# 'HttpOnly') -# -# return resp -# -# -# @app.route(route="response_cookie_header_nullable_double_err") -# def response_cookie_header_nullable_double_err( -# req: Request) -> Response: -# logging.info('Python HTTP trigger function processed a request.') -# resp = Response( -# "This HTTP triggered function executed successfully.") -# -# resp.headers.add("Set-Cookie", -# 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 ' -# '13:55:08 GMT; Path=/; Max-Age=Dummy; SecureFalse; ' -# 'HttpOnly') -# -# return resp -# -# -# @app.route(route="response_cookie_header_nullable_timestamp_err") -# def response_cookie_header_nullable_timestamp_err( -# req: Request) -> Response: -# logging.info('Python HTTP trigger function processed a request.') -# resp = Response( -# "This HTTP triggered function executed successfully.") -# -# resp.headers.add("Set-Cookie", 'foo=bar; Domain=123; Expires=Dummy') -# -# return resp -# -# -# @app.route(route="set_cookie_resp_header_default_values") -# def set_cookie_resp_header_default_values( -# req: Request) -> Response: -# logging.info('Python HTTP trigger function processed a request.') -# resp = Response( -# "This HTTP triggered function executed successfully.") -# -# resp.headers.add("Set-Cookie", 'foo=bar') -# -# return resp -# -# -# @app.route(route="set_cookie_resp_header_empty") -# def set_cookie_resp_header_empty( -# req: Request) -> Response: -# logging.info('Python HTTP trigger function processed a request.') -# resp = Response( -# "This HTTP triggered function executed successfully.") -# -# resp.headers.add("Set-Cookie", '') -# -# return resp diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py index 344ffdce8..50d7d6c65 100644 --- a/tests/unittests/test_http_functions.py +++ b/tests/unittests/test_http_functions.py @@ -462,8 +462,3 @@ def test_no_return_returns(self): r = self.webhost.request('GET', 'no_return_returns') self.assertEqual(r.status_code, 200) -class TestHttpFunctionsV2(TestHttpFunctions): - @classmethod - def get_script_dir(cls): - return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \ - 'http_v2_functions' diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py new file mode 100644 index 000000000..7c5adcd5d --- /dev/null +++ b/tests/unittests/test_http_functions_v2.py @@ -0,0 +1,457 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import filecmp +import hashlib +import os +import pathlib +import sys +import typing +from unittest import skipIf +from unittest.mock import patch + +from azure_functions_worker.constants import PYTHON_ENABLE_INIT_INDEXING +from tests.utils import testutils + + +class TestHttpFunctionsV2FastApi(testutils.WebHostTestCase): + @classmethod + def setUpClass(cls): + os_environ = os.environ.copy() + # Turn on feature flag + os_environ[PYTHON_ENABLE_INIT_INDEXING] = '1' + cls._patch_environ = patch.dict('os.environ', os_environ) + cls._patch_environ.start() + + super().setUpClass() + + @classmethod + def tearDownClass(cls): + cls._patch_environ.stop() + super().tearDownClass() + + @classmethod + def get_script_dir(cls): + return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \ + 'http_v2_functions' / \ + 'fastapi' + + def test_return_bytes(self): + r = self.webhost.request('GET', 'return_bytes') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.content, b'"Hello World"') + self.assertEqual(r.headers['content-type'], 'application/json') + + def test_return_http_200(self): + r = self.webhost.request('GET', 'return_http') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '

Hello World™

') + self.assertEqual(r.headers['content-type'], 'text/html; charset=utf-8') + + def test_return_http_no_body(self): + r = self.webhost.request('GET', 'return_http_no_body') + self.assertEqual(r.text, '') + self.assertEqual(r.status_code, 200) + + def test_return_http_auth_level_admin(self): + r = self.webhost.request('GET', 'return_http_auth_admin', + params={'code': 'testMasterKey'}) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '

Hello World™

') + self.assertEqual(r.headers['content-type'], 'text/html; charset=utf-8') + + def test_return_http_404(self): + r = self.webhost.request('GET', 'return_http_404') + self.assertEqual(r.status_code, 404) + self.assertEqual(r.text, 'bye') + + def test_return_http_redirect(self): + r = self.webhost.request('GET', 'return_http_redirect') + self.assertEqual(r.text, '

Hello World™

') + self.assertEqual(r.status_code, 200) + + r = self.webhost.request('GET', 'return_http_redirect', + allow_redirects=False) + self.assertEqual(r.status_code, 302) + + def test_async_return_str(self): + r = self.webhost.request('GET', 'async_return_str') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '"Hello Async World!"') + + def test_async_logging(self): + # Test that logging doesn't *break* things. + r = self.webhost.request('GET', 'async_logging') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '"OK-async"') + + def check_log_async_logging(self, host_out: typing.List[str]): + # Host out only contains user logs + self.assertIn('hello info', host_out) + self.assertIn('and another error', host_out) + + def test_debug_logging(self): + r = self.webhost.request('GET', 'debug_logging') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '"OK-debug"') + + def check_log_debug_logging(self, host_out: typing.List[str]): + self.assertIn('logging info', host_out) + self.assertIn('logging warning', host_out) + self.assertIn('logging error', host_out) + self.assertNotIn('logging debug', host_out) + + def test_debug_with_user_logging(self): + r = self.webhost.request('GET', 'debug_user_logging') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '"OK-user-debug"') + + def check_log_debug_with_user_logging(self, host_out: typing.List[str]): + self.assertIn('logging info', host_out) + self.assertIn('logging warning', host_out) + self.assertIn('logging debug', host_out) + self.assertIn('logging error', host_out) + + def test_sync_logging(self): + # Test that logging doesn't *break* things. + r = self.webhost.request('GET', 'sync_logging') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '"OK-sync"') + + def check_log_sync_logging(self, host_out: typing.List[str]): + # Host out only contains user logs + self.assertIn('a gracefully handled error', host_out) + + def test_return_context(self): + r = self.webhost.request('GET', 'return_context') + self.assertEqual(r.status_code, 200) + + data = r.json() + + self.assertEqual(data['method'], 'GET') + self.assertEqual(data['ctx_func_name'], 'return_context') + self.assertIn('ctx_invocation_id', data) + self.assertIn('ctx_trace_context_Tracestate', data) + self.assertIn('ctx_trace_context_Traceparent', data) + + def test_remapped_context(self): + r = self.webhost.request('GET', 'remapped_context') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '"GET"') + + def test_return_request(self): + r = self.webhost.request( + 'GET', 'return_request', + params={'a': 1, 'b': ':%)'}, + headers={'xxx': 'zzz', 'Max-Forwards': '10'}) + + self.assertEqual(r.status_code, 200) + + req = r.json() + + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['params'], {'a': '1', 'b': ':%)'}) + self.assertEqual(req['headers']['xxx'], 'zzz') + self.assertEqual(req['headers']['max-forwards'], '10') + + self.assertIn('return_request', req['url']) + + def test_post_return_request(self): + r = self.webhost.request( + 'POST', 'return_request', + params={'a': 1, 'b': ':%)'}, + headers={'xxx': 'zzz'}, + data={'key': 'value'}) + + self.assertEqual(r.status_code, 200) + + req = r.json() + + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['params'], {'a': '1', 'b': ':%)'}) + self.assertEqual(req['headers']['xxx'], 'zzz') + + self.assertIn('return_request', req['url']) + + self.assertEqual(req['body'], 'key=value') + + def test_post_json_request_is_untouched(self): + body = b'{"foo": "bar", "two": 4}' + body_hash = hashlib.sha256(body).hexdigest() + r = self.webhost.request( + 'POST', 'return_request', + headers={'Content-Type': 'application/json'}, + data=body) + + self.assertEqual(r.status_code, 200) + req = r.json() + self.assertEqual(req['body_hash'], body_hash) + + def test_accept_json(self): + r = self.webhost.request( + 'GET', 'accept_json', + json={'a': 'abc', 'd': 42}) + + self.assertEqual(r.status_code, 200) + r_json = r.json() + self.assertEqual(r_json, {'a': 'abc', 'd': 42}) + self.assertEqual(r.headers['content-type'], 'application/json') + + def test_unhandled_error(self): + r = self.webhost.request('GET', 'unhandled_error') + self.assertEqual(r.status_code, 500) + # https://github.com/Azure/azure-functions-host/issues/2706 + # self.assertIn('Exception: ZeroDivisionError', r.text) + + def check_log_unhandled_error(self, + host_out: typing.List[str]): + self.assertIn('Exception: ZeroDivisionError: division by zero', + host_out) + + def test_unhandled_urllib_error(self): + r = self.webhost.request( + 'GET', 'unhandled_urllib_error', + params={'img': 'http://example.com/nonexistent.jpg'}) + self.assertEqual(r.status_code, 500) + + def test_unhandled_unserializable_error(self): + r = self.webhost.request( + 'GET', 'unhandled_unserializable_error') + self.assertEqual(r.status_code, 500) + + def test_return_route_params(self): + r = self.webhost.request('GET', 'return_route_params/foo/bar') + self.assertEqual(r.status_code, 200) + resp = r.json() + self.assertEqual(resp, {'param1': 'foo', 'param2': 'bar'}) + + def test_raw_body_bytes(self): + parent_dir = pathlib.Path(__file__).parent + image_file = parent_dir / 'resources/functions.png' + with open(image_file, 'rb') as image: + img = image.read() + img_len = len(img) + r = self.webhost.request('POST', 'raw_body_bytes', data=img) + + received_body_len = int(r.headers['body-len']) + self.assertEqual(received_body_len, img_len) + + body = r.content + try: + received_img_file = parent_dir / 'received_img.png' + with open(received_img_file, 'wb') as received_img: + received_img.write(body) + self.assertTrue(filecmp.cmp(received_img_file, image_file)) + finally: + if (os.path.exists(received_img_file)): + os.remove(received_img_file) + + def test_image_png_content_type(self): + parent_dir = pathlib.Path(__file__).parent + image_file = parent_dir / 'resources/functions.png' + with open(image_file, 'rb') as image: + img = image.read() + img_len = len(img) + r = self.webhost.request( + 'POST', 'raw_body_bytes', + headers={'Content-Type': 'image/png'}, + data=img) + + received_body_len = int(r.headers['body-len']) + self.assertEqual(received_body_len, img_len) + + body = r.content + try: + received_img_file = parent_dir / 'received_img.png' + with open(received_img_file, 'wb') as received_img: + received_img.write(body) + self.assertTrue(filecmp.cmp(received_img_file, image_file)) + finally: + if (os.path.exists(received_img_file)): + os.remove(received_img_file) + + def test_application_octet_stream_content_type(self): + parent_dir = pathlib.Path(__file__).parent + image_file = parent_dir / 'resources/functions.png' + with open(image_file, 'rb') as image: + img = image.read() + img_len = len(img) + r = self.webhost.request( + 'POST', 'raw_body_bytes', + headers={'Content-Type': 'application/octet-stream'}, + data=img) + + received_body_len = int(r.headers['body-len']) + self.assertEqual(received_body_len, img_len) + + body = r.content + try: + received_img_file = parent_dir / 'received_img.png' + with open(received_img_file, 'wb') as received_img: + received_img.write(body) + self.assertTrue(filecmp.cmp(received_img_file, image_file)) + finally: + if (os.path.exists(received_img_file)): + os.remove(received_img_file) + + def test_user_event_loop_error(self): + # User event loop is not supported in HTTP trigger + r = self.webhost.request('GET', 'user_event_loop/') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '"OK-user-event-loop"') + + def check_log_user_event_loop_error(self, host_out: typing.List[str]): + self.assertIn('try_log', host_out) + + def check_log_import_module_troubleshooting_url(self, + host_out: typing.List[str]): + passed = False + exception_message = "Exception: ModuleNotFoundError: "\ + "No module named 'does_not_exist'. "\ + "Cannot find module. "\ + "Please check the requirements.txt file for the "\ + "missing module. For more info, please refer the "\ + "troubleshooting guide: "\ + "https://aka.ms/functions-modulenotfound. "\ + "Current sys.path: " + for log in host_out: + if exception_message in log: + passed = True + self.assertTrue(passed) + + def test_print_logging_no_flush(self): + r = self.webhost.request('GET', 'print_logging?message=Secret42') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '"OK-print-logging"') + + def check_log_print_logging_no_flush(self, host_out: typing.List[str]): + self.assertIn('Secret42', host_out) + + def test_print_logging_with_flush(self): + r = self.webhost.request('GET', + 'print_logging?flush=true&message=Secret42') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '"OK-print-logging"') + + def check_log_print_logging_with_flush(self, host_out: typing.List[str]): + self.assertIn('Secret42', host_out) + + def test_print_to_console_stdout(self): + r = self.webhost.request('GET', + 'print_logging?console=true&message=Secret42') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '"OK-print-logging"') + + @skipIf(sys.version_info < (3, 8, 0), + "Skip the tests for Python 3.7 and below") + def test_multiple_cookie_header_in_response(self): + r = self.webhost.request('GET', 'multiple_set_cookie_resp_headers') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers.get( + 'Set-Cookie'), + "foo3=42; Domain=example.com; expires=Thu, 12 Jan 2017 13:55:08" + " GMT; HttpOnly; Max-Age=10000000; Path=/; SameSite=Lax; Secure," + " foo3=43; Domain=example.com; expires=Fri, 12 Jan 2018 13:55:08" + " GMT; HttpOnly; Max-Age=10000000; Path=/; SameSite=Lax; Secure") + + @skipIf(sys.version_info < (3, 8, 0), + "Skip the tests for Python 3.7 and below") + def test_set_cookie_header_in_response_default_value(self): + r = self.webhost.request('GET', + 'set_cookie_resp_header_default_values') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers.get('Set-Cookie'), + 'foo3=42; Path=/; SameSite=lax') + + @skipIf(sys.version_info < (3, 8, 0), + "Skip the tests for Python 3.7 and below") + def test_response_cookie_header_nullable_timestamp_err(self): + r = self.webhost.request( + 'GET', + 'response_cookie_header_nullable_timestamp_err') + self.assertEqual(r.status_code, 200) + + + @skipIf(sys.version_info < (3, 8, 0), + "Skip the tests for Python 3.7 and below") + def test_response_cookie_header_nullable_bool_err(self): + r = self.webhost.request( + 'GET', + 'response_cookie_header_nullable_bool_err') + self.assertEqual(r.status_code, 200) + self.assertTrue("Set-Cookie" in r.headers) + + + def test_print_to_console_stderr(self): + r = self.webhost.request('GET', 'print_logging?console=true' + '&message=Secret42&is_stderr=true') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '"OK-print-logging"') + + def check_log_print_to_console_stderr(self, host_out: typing.List[str], ): + # System logs stderr should not exist in host_out + self.assertNotIn('Secret42', host_out) + + def test_hijack_current_event_loop(self): + r = self.webhost.request('GET', 'hijack_current_event_loop/') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '"OK-hijack-current-event-loop"') + + def check_log_hijack_current_event_loop(self, host_out: typing.List[str]): + # User logs should exist in host_out + self.assertIn('parallelly_print', host_out) + self.assertIn('parallelly_log_info at root logger', host_out) + self.assertIn('parallelly_log_warning at root logger', host_out) + self.assertIn('parallelly_log_error at root logger', host_out) + self.assertIn('parallelly_log_exception at root logger', host_out) + self.assertIn('parallelly_log_custom at custom_logger', host_out) + self.assertIn('callsoon_log', host_out) + + # System logs should not exist in host_out + self.assertNotIn('parallelly_log_system at disguised_logger', host_out) + + def test_no_type_hint(self): + r = self.webhost.request('GET', 'no_type_hint') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '"no_type_hint"') + + def test_return_int(self): + r = self.webhost.request('GET', 'return_int') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '1000') + + def test_return_float(self): + r = self.webhost.request('GET', 'return_float') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, '1000.0') + + def test_return_bool(self): + r = self.webhost.request('GET', 'return_bool') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, 'true') + + def test_return_dict(self): + r = self.webhost.request('GET', 'return_dict') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json(), {'key': 'value'}) + + def test_return_list(self): + r = self.webhost.request('GET', 'return_list') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json(), ["value1", "value2"]) + + def test_return_pydantic_model(self): + r = self.webhost.request('GET', 'return_pydantic_model') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json(), {'description': 'description1', + 'name': 'item1'}) + + def test_return_pydantic_model_with_missing_fields(self): + r = self.webhost.request('GET', + 'return_pydantic_model_with_missing_fields') + self.assertEqual(r.status_code, 500) + + def check_return_pydantic_model_with_missing_fields(self, + host_out: + typing.List[str]): + self.assertIn("Field required [type=missing, input_value={'name': " + "'item1'}, input_type=dict]", host_out) \ No newline at end of file From dffc81f6125c9fcec51793a3ac2475366c44f5f1 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 4 Apr 2024 22:59:30 -0700 Subject: [PATCH 003/101] fix ppl --- .github/workflows/ci_e2e_workflow.yml | 1 + .github/workflows/ci_ut_workflow.yml | 3 ++- setup.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml index 38c04946d..b92822ab5 100644 --- a/.github/workflows/ci_e2e_workflow.yml +++ b/.github/workflows/ci_e2e_workflow.yml @@ -65,6 +65,7 @@ jobs: python -m pip install --upgrade pip python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] # Retry a couple times to avoid certificate issue retry 5 python setup.py build diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index 557f6a0bb..9cbe59fc1 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -59,7 +59,8 @@ jobs: python -m pip install --upgrade pip python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] - + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-deferred-bindings] + # Retry a couple times to avoid certificate issue retry 5 python setup.py build retry 5 python setup.py webhost --branch-name=dev diff --git a/setup.py b/setup.py index a4f3095a0..5308ae768 100644 --- a/setup.py +++ b/setup.py @@ -110,7 +110,7 @@ "numpy", "pre-commit" ], - "http-v2": ["azure-functions-extension-fastapi", "ujson", "orjson"] + "test-http-v2": ["azure-functions-extension-fastapi", "ujson", "orjson"] } From fa52c76db7b93b8fc46e06474555962f6376e0e0 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 4 Apr 2024 23:29:50 -0700 Subject: [PATCH 004/101] revert --- python/test/worker.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/test/worker.config.json b/python/test/worker.config.json index d530908fc..3fc2a9236 100644 --- a/python/test/worker.config.json +++ b/python/test/worker.config.json @@ -2,7 +2,7 @@ "description":{ "language":"python", "extensions":[".py"], - "defaultExecutablePath":"E:\\projects\\AzureFunctionsPythonWorker\\.venv_3.9_debug\\Scripts\\python.exe", + "defaultExecutablePath":"python", "defaultWorkerPath":"worker.py", "workerIndexing": "true" } From de028b6353cedbb6060ce41ca96fe8cf6aed3223 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 4 Apr 2024 23:34:44 -0700 Subject: [PATCH 005/101] fix --- .github/workflows/ci_consumption_workflow.yml | 1 + .github/workflows/ci_ut_workflow.yml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml index 907e3de4c..e62d6b1b6 100644 --- a/.github/workflows/ci_consumption_workflow.yml +++ b/.github/workflows/ci_consumption_workflow.yml @@ -34,6 +34,7 @@ jobs: run: | python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] python setup.py build - name: Running 3.7 Tests if: matrix.python-version == 3.7 diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index 53a62dfa8..1e3547955 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -59,8 +59,8 @@ jobs: python -m pip install --upgrade pip python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] - python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-deferred-bindings] - + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] + # Retry a couple times to avoid certificate issue retry 5 python setup.py build retry 5 python setup.py webhost --branch-name=dev From d5987cf49f5709de45cd03393aa0a1b76671f233 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 01:19:52 -0700 Subject: [PATCH 006/101] fix --- azure_functions_worker/dispatcher.py | 2 +- azure_functions_worker/{http_proxy.py => http_v2.py} | 0 azure_functions_worker/logging.py | 2 -- 3 files changed, 1 insertion(+), 3 deletions(-) rename azure_functions_worker/{http_proxy.py => http_v2.py} (100%) diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 641b2a25e..b13c5511d 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -35,7 +35,7 @@ X_MS_INVOCATION_ID, LOCAL_HOST, METADATA_PROPERTIES_WORKER_INDEXED) from .extension import ExtensionManager -from .http_proxy import http_coordinator +from .http_v2 import http_coordinator from .logging import disable_console_logging, enable_console_logging from .logging import (logger, error_logger, is_system_log_category, CONSOLE_LOG_PREFIX, format_exception) diff --git a/azure_functions_worker/http_proxy.py b/azure_functions_worker/http_v2.py similarity index 100% rename from azure_functions_worker/http_proxy.py rename to azure_functions_worker/http_v2.py diff --git a/azure_functions_worker/logging.py b/azure_functions_worker/logging.py index ddc5a7faf..9f733c651 100644 --- a/azure_functions_worker/logging.py +++ b/azure_functions_worker/logging.py @@ -20,8 +20,6 @@ handler: Optional[logging.Handler] = None error_handler: Optional[logging.Handler] = None -# local_handler = logging.FileHandler("E:/projects/AzureFunctionsPythonWorker/log.txt") -# logger.addHandler(local_handler) def format_exception(exception: Exception) -> str: msg = str(exception) + "\n" From 2410771c9dbb1afde33f6324298d8b4b91940e21 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:00:12 -0700 Subject: [PATCH 007/101] fix --- azure_functions_worker/functions.py | 2 +- azure_functions_worker/http_v2.py | 33 +++++--- tests/unittests/test_http_v2.py | 127 ++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 tests/unittests/test_http_v2.py diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index 0200c01ae..52c79847c 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -33,7 +33,7 @@ class FunctionInfo(typing.NamedTuple): output_types: typing.Mapping[str, ParamTypeInfo] return_type: typing.Optional[ParamTypeInfo] - trigger_metadata: typing.Dict[str, typing.Any] + trigger_metadata: typing.Optional[Dict[str, typing.Any]] class FunctionLoadError(RuntimeError): diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index b56213485..8209ea80c 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -2,8 +2,11 @@ import asyncio from typing import Dict + class BaseContextReference(abc.ABC): - def __init__(self, event_class, http_request=None, http_response=None, function=None, fi_context=None, args=None, http_trigger_param_name=None): + def __init__(self, event_class, http_request=None, http_response=None, + function=None, fi_context=None, args=None, + http_trigger_param_name=None): self._http_request = http_request self._http_response = http_response self._function = function @@ -78,8 +81,10 @@ def rpc_invocation_ready_event(self): class AsyncContextReference(BaseContextReference): - def __init__(self, http_request=None, http_response=None, function=None, fi_context=None, args=None): - super().__init__(event_class=asyncio.Event, http_request=http_request, http_response=http_response, + def __init__(self, http_request=None, http_response=None, function=None, + fi_context=None, args=None): + super().__init__(event_class=asyncio.Event, http_request=http_request, + http_response=http_response, function=function, fi_context=fi_context, args=args) self.is_async = True @@ -96,7 +101,7 @@ def __call__(cls, *args, **kwargs): class HttpCoordinator(metaclass=SingletonMeta): def __init__(self): self._context_references: Dict[str, BaseContextReference] = {} - + def set_http_request(self, invoc_id, http_request): if invoc_id not in self._context_references: self._context_references[invoc_id] = AsyncContextReference() @@ -105,32 +110,36 @@ def set_http_request(self, invoc_id, http_request): def set_http_response(self, invoc_id, http_response): if invoc_id not in self._context_references: - raise Exception("No context reference found for invocation %s", invoc_id) + raise Exception("No context reference found for invocation %s", + invoc_id) context_ref = self._context_references.get(invoc_id) context_ref.http_response = http_response async def get_http_request_async(self, invoc_id): if invoc_id not in self._context_references: self._context_references[invoc_id] = AsyncContextReference() - + await asyncio.sleep(0) - await self._context_references.get(invoc_id).http_request_available_event.wait() + await self._context_references.get( + invoc_id).http_request_available_event.wait() return self._pop_http_request(invoc_id) async def await_http_response_async(self, invoc_id): if invoc_id not in self._context_references: - raise Exception("No context reference found for invocation %s", invoc_id) + raise Exception("No context reference found for invocation %s", + invoc_id) await asyncio.sleep(0) - await self._context_references.get(invoc_id).http_response_available_event.wait() + await self._context_references.get( + invoc_id).http_response_available_event.wait() return self._pop_http_response(invoc_id) - + def _pop_http_request(self, invoc_id): context_ref = self._context_references.get(invoc_id) request = context_ref.http_request if request is not None: context_ref.http_request = None return request - + raise Exception("No http request found for invocation %s", invoc_id) def _pop_http_response(self, invoc_id): @@ -139,7 +148,7 @@ def _pop_http_response(self, invoc_id): if response is not None: context_ref.http_response = None return response - # If user does not set the response, return nothing and web server will return 200 empty response + raise Exception("No http response found for invocation %s", invoc_id) http_coordinator = HttpCoordinator() diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py new file mode 100644 index 000000000..64aefe56f --- /dev/null +++ b/tests/unittests/test_http_v2.py @@ -0,0 +1,127 @@ +import asyncio +import unittest +from unittest.mock import MagicMock + +from azure_functions_worker.http_v2 import http_coordinator + + +class MockHttpRequest: + pass + + +class MockHttpResponse: + pass + + +class TestHttpCoordinator(unittest.TestCase): + def setUp(self): + self.invoc_id = "test_invocation" + self.http_request = MockHttpRequest() + self.http_response = MockHttpResponse() + + def tearDown(self) -> None: + http_coordinator._context_references.clear() + + def test_set_http_request_new_invocation(self): + # Test setting a new HTTP request + http_coordinator.set_http_request(self.invoc_id, self.http_request) + context_ref = http_coordinator._context_references.get(self.invoc_id) + self.assertIsNotNone(context_ref) + self.assertEqual(context_ref.http_request, self.http_request) + + def test_set_http_request_existing_invocation(self): + # Test updating an existing HTTP request + new_http_request = MagicMock() + http_coordinator.set_http_request(self.invoc_id, self.http_request) + http_coordinator.set_http_request(self.invoc_id, new_http_request) + context_ref = http_coordinator._context_references.get(self.invoc_id) + self.assertIsNotNone(context_ref) + self.assertEqual(context_ref.http_request, new_http_request) + + def test_set_http_response_context_ref_null(self): + with self.assertRaises(Exception) as cm: + http_coordinator.set_http_response(self.invoc_id, + self.http_response) + self.assertEqual(cm.exception.args[0], + "No context reference found for invocation %s") + + def test_set_http_response(self): + http_coordinator.set_http_request(self.invoc_id, self.http_request) + http_coordinator.set_http_response(self.invoc_id, self.http_response) + context_ref = http_coordinator._context_references[self.invoc_id] + self.assertEqual(context_ref.http_response, self.http_response) + + def test_get_http_request_async_existing_invocation(self): + # Test retrieving an existing HTTP request + http_coordinator.set_http_request(self.invoc_id, + self.http_request) + loop = asyncio.get_event_loop() + retrieved_request = loop.run_until_complete( + http_coordinator.get_http_request_async(self.invoc_id)) + self.assertEqual(retrieved_request, self.http_request) + + def test_get_http_request_async_wait_for_request(self): + # Test waiting for an HTTP request to become available + async def set_request_after_delay(): + await asyncio.sleep(1) + http_coordinator.set_http_request(self.invoc_id, + self.http_request) + + loop = asyncio.get_event_loop() + loop.create_task(set_request_after_delay()) + retrieved_request = loop.run_until_complete( + http_coordinator.get_http_request_async(self.invoc_id)) + self.assertEqual(retrieved_request, self.http_request) + + def test_get_http_request_async_wait_forever(self): + # Test handling error when invoc_id is not found + invalid_invoc_id = "invalid_invocation" + loop = asyncio.get_event_loop() + with self.assertRaises(asyncio.TimeoutError): + loop.run_until_complete( + asyncio.wait_for( + http_coordinator.get_http_request_async(invalid_invoc_id), + timeout=1 + ) + ) + + async def test_await_http_response_async_valid_invocation(self): + invoc_id = "valid_invocation" + expected_response = self.http_response + + context_ref = {} + context_ref.http_response = expected_response + + # Add the mock context reference to the coordinator + http_coordinator._context_references[invoc_id] = context_ref + + # Call the method and verify the returned response + response = await http_coordinator.await_http_response_async(invoc_id) + self.assertEqual(response, expected_response) + self.assertTrue( + http_coordinator._context_references.get( + invoc_id).http_response is None) + + async def test_await_http_response_async_invalid_invocation(self): + # Test handling error when invoc_id is not found + invalid_invoc_id = "invalid_invocation" + with self.assertRaises(Exception) as context: + await http_coordinator.await_http_response_async(invalid_invoc_id) + self.assertEqual(str(context.exception), + f"No context reference found for invocation " + f"{invalid_invoc_id}") + + async def test_await_http_response_async_response_not_set(self): + invoc_id = "invocation_with_no_response" + # Set up a mock context reference without setting the response + context_ref = {} + context_ref.http_response = None + + # Add the mock context reference to the coordinator + http_coordinator._context_references[invoc_id] = context_ref + + # Call the method and verify that it raises an exception + with self.assertRaises(Exception) as context: + await http_coordinator.await_http_response_async(invoc_id) + self.assertEqual(str(context.exception), + f"No http response found for invocation {invoc_id}") From b78e549e6aa5ff11f476a1a0dba0b2eaa8fa8c51 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:26:47 -0700 Subject: [PATCH 008/101] fix --- azure_functions_worker/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index 52c79847c..1e94a39c0 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -33,7 +33,7 @@ class FunctionInfo(typing.NamedTuple): output_types: typing.Mapping[str, ParamTypeInfo] return_type: typing.Optional[ParamTypeInfo] - trigger_metadata: typing.Optional[Dict[str, typing.Any]] + trigger_metadata: typing.Optional[typing.Dict[str, typing.Any]] class FunctionLoadError(RuntimeError): From 58e89f08a292fbeae9d94559390bd6295bb25594 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:46:57 -0700 Subject: [PATCH 009/101] flake8 --- azure_functions_worker/dispatcher.py | 65 +++++++++++++++------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index b13c5511d..bc3787267 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -146,7 +146,7 @@ async def connect(cls, host: str, port: int, worker_id: str, async def dispatch_forever(self): # sourcery skip: swap-if-expression if DispatcherMeta.__current_dispatcher__ is not None: raise RuntimeError('there can be only one running dispatcher per ' - 'process') + 'process') self._old_task_factory = self._loop.get_task_factory() @@ -174,10 +174,9 @@ async def dispatch_forever(self): # sourcery skip: swap-if-expression logging_handler = AsyncLoggingHandler() root_logger = logging.getLogger() - log_level = logging.INFO if not is_envvar_true( PYTHON_ENABLE_DEBUG_LOGGING) else logging.DEBUG - + root_logger.setLevel(log_level) root_logger.addHandler(logging_handler) logger.info('Switched to gRPC logging.') @@ -285,20 +284,19 @@ async def _dispatch_grpc_request(self, request): async def _handle__worker_init_request(self, request): try: logger.info('Received WorkerInitRequest, ' - 'python version %s, ' - 'worker version %s, ' - 'request ID %s. ' - 'App Settings state: %s. ' - 'To enable debug level logging, please refer to ' - 'https://aka.ms/python-enable-debug-logging', - sys.version, - VERSION, - self.request_id, - get_python_appsetting_state() - ) + 'python version %s, ' + 'worker version %s, ' + 'request ID %s. ' + 'App Settings state: %s. ' + 'To enable debug level logging, please refer to ' + 'https://aka.ms/python-enable-debug-logging', + sys.version, + VERSION, + self.request_id, + get_python_appsetting_state() + ) worker_init_request = request.worker_init_request - directory = worker_init_request.function_app_directory host_capabilities = worker_init_request.capabilities if constants.FUNCTION_DATA_CACHE in host_capabilities: val = host_capabilities[constants.FUNCTION_DATA_CACHE] @@ -332,10 +330,11 @@ async def _handle__worker_init_request(self, request): self._function_metadata_exception = ex if self._has_http_func: - from azure.functions.extension.base import HttpV2FeatureChecker + from azure.functions.extension.base import HttpV2FeatureChecker if HttpV2FeatureChecker.http_v2_enabled(): - capabilities[constants.HTTP_URI] = await self._initialize_http_server() + capabilities[constants.HTTP_URI] = \ + await self._initialize_http_server() return protos.StreamingMessage( request_id=self.request_id, @@ -386,7 +385,6 @@ def load_function_metadata(self, function_app_directory, caller_info): self.index_functions(function_path)) \ if os.path.exists(function_path) else None - async def _handle__functions_metadata_request(self, request): metadata_request = request.functions_metadata_request function_app_directory = metadata_request.function_app_directory @@ -555,7 +553,7 @@ async def _handle__invocation_request(self, request): trigger_metadata = None if bindings.is_trigger_binding(pb_type_info.binding_name): trigger_metadata = invoc_request.trigger_metadata - + args[pb.name] = bindings.from_incoming_proto( pb_type_info.binding_name, pb, trigger_metadata=trigger_metadata, @@ -570,11 +568,14 @@ async def _handle__invocation_request(self, request): if http_v2_enabled: http_request = await http_coordinator.get_http_request_async( invocation_id) - + from azure.functions.extension.base import RequestTrackerMeta - route_params = {key: item.string for key, item in trigger_metadata.items() if key not in ['Headers', 'Query']} + route_params = {key: item.string for key, item + in trigger_metadata.items() if key not in [ + 'Headers', 'Query']} - RequestTrackerMeta.get_synchronizer().sync_route_params(http_request, route_params) + (RequestTrackerMeta.get_synchronizer() + .sync_route_params(http_request, route_params)) args[fi.trigger_metadata.get('param_name')] = http_request fi_context = self._get_context(invoc_request, fi.name, fi.directory) @@ -599,16 +600,18 @@ async def _handle__invocation_request(self, request): self._sync_call_tp, self._run_sync_func, invocation_id, fi_context, fi.func, args) - + if call_result is not None and not fi.has_return: raise RuntimeError(f'function {fi.name!r} without a $return ' - 'binding returned a non-None value') + 'binding returned a non-None value') except Exception as e: call_error = e raise finally: if http_v2_enabled: - http_coordinator.set_http_response(invocation_id, call_result if call_result is not None else call_error) + http_coordinator.set_http_response( + invocation_id, call_result + if call_result is not None else call_error) output_data = [] cache_enabled = self._function_data_cache_enabled @@ -656,7 +659,6 @@ async def _handle__invocation_request(self, request): status=protos.StatusResult.Failure, exception=self._serialize_exception(ex)))) - async def _handle__function_environment_reload_request(self, request): """Only runs on Linux Consumption placeholder specialization. This is called only when placeholder mode is true. On worker restarts @@ -713,12 +715,13 @@ async def _handle__function_environment_reload_request(self, request): caller_info="environment_reload_request") except Exception as ex: self._function_metadata_exception = ex - + if self._has_http_func: from azure.functions.extension.base import HttpV2FeatureChecker if HttpV2FeatureChecker.http_v2_enabled(): - capabilities[constants.HTTP_URI] = await self._initialize_http_server() + capabilities[constants.HTTP_URI] = \ + await self._initialize_http_server() # Change function app directory if getattr(func_env_reload_request, @@ -748,7 +751,7 @@ async def _handle__function_environment_reload_request(self, request): async def _initialize_http_server(self): from azure.functions.extension.base import ModuleTrackerMeta, RequestTrackerMeta - + web_extension_mod_name = ModuleTrackerMeta.get_module() extension_module = importlib.import_module(web_extension_mod_name) web_app_class = extension_module.WebApp @@ -760,7 +763,7 @@ async def _initialize_http_server(self): request_type = RequestTrackerMeta.get_request_type() @app.route - async def catch_all(request: request_type): # type: ignore + async def catch_all(request: request_type): # type: ignore invoc_id = request.headers.get(X_MS_INVOCATION_ID) if invoc_id is None: raise ValueError(f"Header {X_MS_INVOCATION_ID} not found") @@ -772,7 +775,7 @@ async def catch_all(request: request_type): # type: ignore # if http_resp is an python exception, raise it if isinstance(http_resp, Exception): raise http_resp - + return http_resp web_server = web_server_class(LOCAL_HOST, unused_port, app) From 0883bb8f2dc02038b5db11576fdab42555964f24 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:56:56 -0700 Subject: [PATCH 010/101] flake8 --- azure_functions_worker/dispatcher.py | 31 ++++++++++++++++++---------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index bc3787267..a2078f128 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -330,7 +330,8 @@ async def _handle__worker_init_request(self, request): self._function_metadata_exception = ex if self._has_http_func: - from azure.functions.extension.base import HttpV2FeatureChecker + from azure.functions.extension.base \ + import HttpV2FeatureChecker if HttpV2FeatureChecker.http_v2_enabled(): capabilities[constants.HTTP_URI] = \ @@ -341,7 +342,8 @@ async def _handle__worker_init_request(self, request): worker_init_response=protos.WorkerInitResponse( capabilities=capabilities, worker_metadata=self.get_worker_metadata(), - result=protos.StatusResult(status=protos.StatusResult.Success), + result=protos.StatusResult( + status=protos.StatusResult.Success), ), ) except Exception as e: @@ -349,8 +351,9 @@ async def _handle__worker_init_request(self, request): return protos.StreamingMessage( request_id=self.request_id, worker_init_response=protos.WorkerInitResponse( - result=protos.StatusResult(status=protos.StatusResult.Failure, - exception=self._serialize_exception(e)) + result=protos.StatusResult( + status=protos.StatusResult.Failure, + exception=self._serialize_exception(e)) ), ) @@ -594,7 +597,8 @@ async def _handle__invocation_request(self, request): call_error = None try: if fi.is_async: - call_result = await self._run_async_func(fi_context, fi.func, args) + call_result = \ + await self._run_async_func(fi_context, fi.func, args) else: call_result = await self._loop.run_in_executor( self._sync_call_tp, @@ -602,8 +606,9 @@ async def _handle__invocation_request(self, request): invocation_id, fi_context, fi.func, args) if call_result is not None and not fi.has_return: - raise RuntimeError(f'function {fi.name!r} without a $return ' - 'binding returned a non-None value') + raise RuntimeError( + f'function {fi.name!r} without a $return binding' + 'returned a non-None value') except Exception as e: call_error = e raise @@ -717,7 +722,8 @@ async def _handle__function_environment_reload_request(self, request): self._function_metadata_exception = ex if self._has_http_func: - from azure.functions.extension.base import HttpV2FeatureChecker + from azure.functions.extension.base \ + import HttpV2FeatureChecker if HttpV2FeatureChecker.http_v2_enabled(): capabilities[constants.HTTP_URI] = \ @@ -750,7 +756,8 @@ async def _handle__function_environment_reload_request(self, request): function_environment_reload_response=failure_response) async def _initialize_http_server(self): - from azure.functions.extension.base import ModuleTrackerMeta, RequestTrackerMeta + from azure.functions.extension.base \ + import ModuleTrackerMeta, RequestTrackerMeta web_extension_mod_name = ModuleTrackerMeta.get_module() extension_module = importlib.import_module(web_extension_mod_name) @@ -769,7 +776,8 @@ async def catch_all(request: request_type): # type: ignore raise ValueError(f"Header {X_MS_INVOCATION_ID} not found") logger.info('Received HTTP request for invocation %s', invoc_id) http_coordinator.set_http_request(invoc_id, request) - http_resp = await http_coordinator.await_http_response_async(invoc_id) + http_resp = \ + await http_coordinator.await_http_response_async(invoc_id) logger.info('Sending HTTP response for invocation %s', invoc_id) # if http_resp is an python exception, raise it @@ -801,7 +809,8 @@ def index_functions(self, function_path: str): indexed_function_logs: List[str] = [] for func in indexed_functions: - self._has_http_func = self._has_http_func or func.is_http_function() + self._has_http_func = self._has_http_func or \ + func.is_http_function() function_log = "Function Name: {}, Function Binding: {}" \ .format(func.get_function_name(), [(binding.type, binding.name) for binding in From 54bd579519523f8becb2fa37e7045a10cede5ee9 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 12:12:05 -0700 Subject: [PATCH 011/101] flake8 --- azure_functions_worker/bindings/meta.py | 12 ++++++++---- azure_functions_worker/functions.py | 5 +++-- azure_functions_worker/logging.py | 1 + 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index ccfaa8200..60d22a7c2 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -15,19 +15,21 @@ PB_TYPE_RPC_SHARED_MEMORY = 'rpc_shared_memory' BINDING_REGISTRY = None + def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: ext_base = sys.modules.get('azure.functions.extension.base') if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.RequestTrackerMeta.check_type(pytype) - + binding = get_binding(bind_name) return binding.check_input_type_annotation(pytype) + def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool: ext_base = sys.modules.get('azure.functions.extension.base') if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.ResponseTrackerMeta.check_type(pytype) - + binding = get_binding(bind_name) return binding.check_output_type_annotation(pytype) @@ -40,6 +42,7 @@ def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool: HTTP: _check_http_output_type_annotation } + def load_binding_registry() -> None: func = sys.modules.get('azure.functions') @@ -71,15 +74,16 @@ def check_input_type_annotation(bind_name: str, pytype: type) -> bool: global INPUT_TYPE_CHECK_OVERRIDE_MAP if bind_name in INPUT_TYPE_CHECK_OVERRIDE_MAP: return INPUT_TYPE_CHECK_OVERRIDE_MAP[bind_name](bind_name, pytype) - + binding = get_binding(bind_name) return binding.check_input_type_annotation(pytype) + def check_output_type_annotation(bind_name: str, pytype: type) -> bool: global OUTPUT_TYPE_CHECK_OVERRIDE_MAP if bind_name in OUTPUT_TYPE_CHECK_OVERRIDE_MAP: return OUTPUT_TYPE_CHECK_OVERRIDE_MAP[bind_name](bind_name, pytype) - + binding = get_binding(bind_name) return binding.check_output_type_annotation(pytype) diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index 1e94a39c0..ba2b8509e 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -35,6 +35,7 @@ class FunctionInfo(typing.NamedTuple): trigger_metadata: typing.Optional[typing.Dict[str, typing.Any]] + class FunctionLoadError(RuntimeError): def __init__(self, function_name: str, msg: str) -> None: @@ -301,7 +302,8 @@ def add_func_to_registry_and_return_funcinfo(self, function, return_type: str): http_trigger_param_name = next( - (input_type for input_type, type_info in input_types.items() if type_info.binding_name == HTTP_TRIGGER), + (input_type for input_type, type_info in input_types.items() + if type_info.binding_name == HTTP_TRIGGER), None ) @@ -323,7 +325,6 @@ def add_func_to_registry_and_return_funcinfo(self, function, output_types=output_types, return_type=return_type, trigger_metadata=trigger_metadata) - self._functions[function_id] = function_info return function_info diff --git a/azure_functions_worker/logging.py b/azure_functions_worker/logging.py index 9f733c651..adb5ff294 100644 --- a/azure_functions_worker/logging.py +++ b/azure_functions_worker/logging.py @@ -21,6 +21,7 @@ handler: Optional[logging.Handler] = None error_handler: Optional[logging.Handler] = None + def format_exception(exception: Exception) -> str: msg = str(exception) + "\n" if (sys.version_info.major, sys.version_info.minor) < (3, 10): From eb26157e666bc742035510c93376063e73b7e487 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:10:47 -0700 Subject: [PATCH 012/101] only run for 3.8+ --- .github/workflows/ci_e2e_workflow.yml | 6 ++++-- .github/workflows/ci_ut_workflow.yml | 6 ++++-- azure_functions_worker/bindings/meta.py | 14 +++++++------ azure_functions_worker/constants.py | 6 +++++- azure_functions_worker/dispatcher.py | 28 ++++++++++++++++--------- 5 files changed, 39 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml index 9e81be1d1..2ab4ea3d2 100644 --- a/.github/workflows/ci_e2e_workflow.yml +++ b/.github/workflows/ci_e2e_workflow.yml @@ -61,8 +61,10 @@ jobs: python -m pip install --upgrade pip python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] - python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] - + # Conditionally install test dependencies for Python 3.8 and later + if [[ "${{ matrix.python-version }}" != "3.7" ]]; then + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] + fi # Retry a couple times to avoid certificate issue retry 5 python setup.py build retry 5 python setup.py webhost --branch-name=dev diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index 1e3547955..9b5edec1a 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -59,8 +59,10 @@ jobs: python -m pip install --upgrade pip python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] - python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] - + # Conditionally install test dependencies for Python 3.8 and later + if [[ "${{ matrix.python-version }}" != "3.7" ]]; then + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] + fi # Retry a couple times to avoid certificate issue retry 5 python setup.py build retry 5 python setup.py webhost --branch-name=dev diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index 60d22a7c2..ab8fa8666 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -17,18 +17,20 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: - ext_base = sys.modules.get('azure.functions.extension.base') - if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled(): - return ext_base.RequestTrackerMeta.check_type(pytype) + if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION: + ext_base = sys.modules.get('azure.functions.extension.base') + if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled(): + return ext_base.RequestTrackerMeta.check_type(pytype) binding = get_binding(bind_name) return binding.check_input_type_annotation(pytype) def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool: - ext_base = sys.modules.get('azure.functions.extension.base') - if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled(): - return ext_base.ResponseTrackerMeta.check_type(pytype) + if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION: + ext_base = sys.modules.get('azure.functions.extension.base') + if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled(): + return ext_base.ResponseTrackerMeta.check_type(pytype) binding = get_binding(bind_name) return binding.check_output_type_annotation(pytype) diff --git a/azure_functions_worker/constants.py b/azure_functions_worker/constants.py index b38794017..eea0193ec 100644 --- a/azure_functions_worker/constants.py +++ b/azure_functions_worker/constants.py @@ -55,7 +55,8 @@ RETRY_POLICY = "retry_policy" # Paths -CUSTOMER_PACKAGES_PATH = "/home/site/wwwroot/.python_packages/lib/site-packages" +CUSTOMER_PACKAGES_PATH = "/home/site/wwwroot/.python_packages/lib/site" \ + "-packages" # Flag to index functions in handle init request PYTHON_ENABLE_INIT_INDEXING = "PYTHON_ENABLE_INIT_INDEXING" @@ -73,3 +74,6 @@ # Output Names HTTP = "http" + +# Base extension supported Python minor version +BASE_EXT_SUPPORTED_PY_MINOR_VERSION = 8 diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index a2078f128..f6881cb49 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -33,7 +33,8 @@ PYTHON_SCRIPT_FILE_NAME_DEFAULT, PYTHON_LANGUAGE_RUNTIME, PYTHON_ENABLE_INIT_INDEXING, X_MS_INVOCATION_ID, LOCAL_HOST, - METADATA_PROPERTIES_WORKER_INDEXED) + METADATA_PROPERTIES_WORKER_INDEXED, + BASE_EXT_SUPPORTED_PY_MINOR_VERSION) from .extension import ExtensionManager from .http_v2 import http_coordinator from .logging import disable_console_logging, enable_console_logging @@ -129,7 +130,8 @@ def get_sync_tp_workers_set(self): 3.9 scenarios (as we'll start passing only None by default), and we need to get that information. - Ref: concurrent.futures.thread.ThreadPoolExecutor.__init__._max_workers + Ref: concurrent.futures.thread.ThreadPoolExecutor.__init__ + ._max_workers """ return self._sync_call_tp._max_workers @@ -208,7 +210,8 @@ def stop(self) -> None: self._stop_sync_call_tp() - def on_logging(self, record: logging.LogRecord, formatted_msg: str) -> None: + def on_logging(self, record: logging.LogRecord, + formatted_msg: str) -> None: if record.levelno >= logging.CRITICAL: log_level = protos.RpcLog.Critical elif record.levelno >= logging.ERROR: @@ -506,7 +509,6 @@ async def _handle__function_load_request(self, request): status=protos.StatusResult.Success))) except Exception as ex: - logging.error(ex) return protos.StreamingMessage( request_id=self.request_id, function_load_response=protos.FunctionLoadResponse( @@ -564,7 +566,9 @@ async def _handle__invocation_request(self, request): shmem_mgr=self._shmem_mgr) http_v2_enabled = False - if fi.trigger_metadata.get('type') == HTTP_TRIGGER: + if sys.version_info.minor >= \ + BASE_EXT_SUPPORTED_PY_MINOR_VERSION \ + and fi.trigger_metadata.get('type') == HTTP_TRIGGER: from azure.functions.extension.base import HttpV2FeatureChecker http_v2_enabled = HttpV2FeatureChecker.http_v2_enabled() @@ -578,10 +582,11 @@ async def _handle__invocation_request(self, request): 'Headers', 'Query']} (RequestTrackerMeta.get_synchronizer() - .sync_route_params(http_request, route_params)) + .sync_route_params(http_request, route_params)) args[fi.trigger_metadata.get('param_name')] = http_request - fi_context = self._get_context(invoc_request, fi.name, fi.directory) + fi_context = self._get_context(invoc_request, fi.name, + fi.directory) # Use local thread storage to store the invocation ID # for a customer's threads @@ -721,7 +726,9 @@ async def _handle__function_environment_reload_request(self, request): except Exception as ex: self._function_metadata_exception = ex - if self._has_http_func: + if sys.version_info.minor >= \ + BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \ + self._has_http_func: from azure.functions.extension.base \ import HttpV2FeatureChecker @@ -810,7 +817,7 @@ def index_functions(self, function_path: str): indexed_function_logs: List[str] = [] for func in indexed_functions: self._has_http_func = self._has_http_func or \ - func.is_http_function() + func.is_http_function() function_log = "Function Name: {}, Function Binding: {}" \ .format(func.get_function_name(), [(binding.type, binding.name) for binding in @@ -861,7 +868,8 @@ async def _handle__close_shared_memory_resources_request(self, request): @staticmethod def _get_context(invoc_request: protos.InvocationRequest, name: str, directory: str) -> bindings.Context: - """ For more information refer: https://aka.ms/azfunc-invocation-context + """ For more information refer: + https://aka.ms/azfunc-invocation-context """ trace_context = bindings.TraceContext( invoc_request.trace_context.trace_parent, From 2b99faa005f0903e6bd7727a81fd563dbbc09054 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:13:27 -0700 Subject: [PATCH 013/101] FIX --- .github/workflows/ci_consumption_workflow.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml index e62d6b1b6..e1af76497 100644 --- a/.github/workflows/ci_consumption_workflow.yml +++ b/.github/workflows/ci_consumption_workflow.yml @@ -34,7 +34,9 @@ jobs: run: | python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] - python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] + if [[ "${{ matrix.python-version }}" != "3.7" ]]; then + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] + fi python setup.py build - name: Running 3.7 Tests if: matrix.python-version == 3.7 From 5d255104d2d1188f8132dde2b43fc5c4a60174ee Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:23:59 -0700 Subject: [PATCH 014/101] skip tests for 3.7- --- tests/endtoend/test_http_functions.py | 3 +++ tests/unittests/test_http_functions.py | 10 ++++++++++ tests/unittests/test_http_functions_v2.py | 2 ++ tests/unittests/test_http_v2.py | 2 ++ 4 files changed, 17 insertions(+) diff --git a/tests/endtoend/test_http_functions.py b/tests/endtoend/test_http_functions.py index 8d7061878..1d4abf44e 100644 --- a/tests/endtoend/test_http_functions.py +++ b/tests/endtoend/test_http_functions.py @@ -2,7 +2,9 @@ # Licensed under the MIT License. import concurrent import os +import sys import typing +import unittest from concurrent.futures import ThreadPoolExecutor from unittest.mock import patch @@ -222,6 +224,7 @@ def tearDownClass(cls): super().tearDownClass() +@unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7") class TestHttpFunctionsV2FastApiWithInitIndexing(TestHttpFunctionsWithInitIndexing): @classmethod def get_script_dir(cls): diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py index 7ccd55101..344ffdce8 100644 --- a/tests/unittests/test_http_functions.py +++ b/tests/unittests/test_http_functions.py @@ -108,6 +108,11 @@ def check_log_debug_logging(self, host_out: typing.List[str]): self.assertIn('logging error', host_out) self.assertNotIn('logging debug', host_out) + def test_debug_with_user_logging(self): + r = self.webhost.request('GET', 'debug_user_logging') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.text, 'OK-user-debug') + def check_log_debug_with_user_logging(self, host_out: typing.List[str]): self.assertIn('logging info', host_out) self.assertIn('logging warning', host_out) @@ -457,3 +462,8 @@ def test_no_return_returns(self): r = self.webhost.request('GET', 'no_return_returns') self.assertEqual(r.status_code, 200) +class TestHttpFunctionsV2(TestHttpFunctions): + @classmethod + def get_script_dir(cls): + return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \ + 'http_v2_functions' diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py index 7c5adcd5d..eb6951f50 100644 --- a/tests/unittests/test_http_functions_v2.py +++ b/tests/unittests/test_http_functions_v2.py @@ -6,6 +6,7 @@ import pathlib import sys import typing +import unittest from unittest import skipIf from unittest.mock import patch @@ -13,6 +14,7 @@ from tests.utils import testutils +@unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7") class TestHttpFunctionsV2FastApi(testutils.WebHostTestCase): @classmethod def setUpClass(cls): diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py index 64aefe56f..048f84095 100644 --- a/tests/unittests/test_http_v2.py +++ b/tests/unittests/test_http_v2.py @@ -1,4 +1,5 @@ import asyncio +import sys import unittest from unittest.mock import MagicMock @@ -13,6 +14,7 @@ class MockHttpResponse: pass +@unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7") class TestHttpCoordinator(unittest.TestCase): def setUp(self): self.invoc_id = "test_invocation" From 4cea83a1b206b179af73a398189ce0f889871851 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:59:18 -0700 Subject: [PATCH 015/101] fix --- azure_functions_worker/bindings/meta.py | 1 + 1 file changed, 1 insertion(+) diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index ab8fa8666..286e1dd72 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -9,6 +9,7 @@ from . import datumdef from . import generic from .shared_memory_data_transfer import SharedMemoryManager +from ..constants import BASE_EXT_SUPPORTED_PY_MINOR_VERSION PB_TYPE = 'rpc_data' PB_TYPE_DATA = 'data' From a9414d86528c0d6c376dc18627b271ea57f87456 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:16:11 -0700 Subject: [PATCH 016/101] fix --- .github/workflows/ci_consumption_workflow.yml | 1 - .github/workflows/ci_e2e_workflow.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml index e1af76497..172e19f7e 100644 --- a/.github/workflows/ci_consumption_workflow.yml +++ b/.github/workflows/ci_consumption_workflow.yml @@ -12,7 +12,6 @@ on: push: branches: [ dev, main, release/* ] pull_request: - branches: [ dev, main, release/* ] jobs: build: diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml index 2ab4ea3d2..6cbff4704 100644 --- a/.github/workflows/ci_e2e_workflow.yml +++ b/.github/workflows/ci_e2e_workflow.yml @@ -13,7 +13,6 @@ on: push: branches: [dev, main, release/*] pull_request: - branches: [dev, main, release/*] schedule: # Monday to Thursday 3 AM CST build # * is a special character in YAML so you have to quote this string From e20b8bb1e96163fca9df606542f2db23eb20e4eb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:18:13 -0700 Subject: [PATCH 017/101] fix --- .github/workflows/ci_ut_workflow.yml | 2 +- .github/workflows/linter.yml | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index 9b5edec1a..1c3777e7a 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -15,8 +15,8 @@ on: # * is a special character in YAML so you have to quote this string - cron: "0 8 * * 1,2,3,4" push: - pull_request: branches: [ dev, main, release/* ] + pull_request: jobs: build: diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 83e6f572f..d0923a8d5 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -15,8 +15,11 @@ name: Lint Code Base ############################# # Start the job on all push # ############################# -on: [ push, pull_request, workflow_dispatch ] - +on: + workflow_dispatch: + push: + branches: [ dev, main, release/* ] + pull_request: ############### # Set the Job # ############### From 64d9e18defb4ac1d4ca2e979a7dfed0be363663a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:38:15 -0700 Subject: [PATCH 018/101] fix --- azure_functions_worker/bindings/meta.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index 286e1dd72..097d00d94 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -20,7 +20,8 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION: ext_base = sys.modules.get('azure.functions.extension.base') - if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled(): + if ext_base is not None and \ + ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.RequestTrackerMeta.check_type(pytype) binding = get_binding(bind_name) @@ -30,7 +31,8 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool: if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION: ext_base = sys.modules.get('azure.functions.extension.base') - if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled(): + if ext_base is not None and \ + ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.ResponseTrackerMeta.check_type(pytype) binding = get_binding(bind_name) From f1c780a42180dd77ae7c9015b77d375032262cc4 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:02:56 -0700 Subject: [PATCH 019/101] fix styles --- azure_functions_worker/dispatcher.py | 2 +- .../test_linux_consumption.py | 13 +- .../function_app.py | 6 +- .../get_eventhub_batch_triggered/__init__.py | 3 +- .../fastapi/file_name/main.py | 4 +- .../http_functions_v2/fastapi/function_app.py | 13 +- tests/endtoend/test_http_functions.py | 275 +++++++++--------- tests/endtoend/test_servicebus_functions.py | 22 +- .../http_v2_functions/fastapi/function_app.py | 9 +- tests/unittests/test_dispatcher.py | 42 +-- tests/unittests/test_http_functions.py | 2 + tests/unittests/test_http_functions_v2.py | 6 +- tests/utils/testutils.py | 3 +- 13 files changed, 210 insertions(+), 190 deletions(-) diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index f6881cb49..992c6c639 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -817,7 +817,7 @@ def index_functions(self, function_path: str): indexed_function_logs: List[str] = [] for func in indexed_functions: self._has_http_func = self._has_http_func or \ - func.is_http_function() + func.is_http_function() function_log = "Function Name: {}, Function Binding: {}" \ .format(func.get_function_name(), [(binding.type, binding.name) for binding in diff --git a/tests/consumption_tests/test_linux_consumption.py b/tests/consumption_tests/test_linux_consumption.py index 3c7232366..9c5be090f 100644 --- a/tests/consumption_tests/test_linux_consumption.py +++ b/tests/consumption_tests/test_linux_consumption.py @@ -342,12 +342,12 @@ def test_http_v2_fastapi_streaming_upload_download(self): """ A function app with init indexing enabled """ - import random as rand 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("HttpV2FastApiStreaming"), + "SCM_RUN_FROM_PACKAGE": + self._get_blob_url("HttpV2FastApiStreaming"), PYTHON_ENABLE_INIT_INDEXING: "true", PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1" }) @@ -360,16 +360,19 @@ def generate_random_bytes_stream(): yield b'is' yield b'returned' - req = Request('POST', f'{ctrl.url}/api/http_v2_fastapi_streaming', data=generate_random_bytes_stream()) + req = Request('POST', + f'{ctrl.url}/api/http_v2_fastapi_streaming', + data=generate_random_bytes_stream()) resp = ctrl.send_request(req) self.assertEqual(resp.status_code, 200) streamed_data = b'' for chunk in resp.iter_content(chunk_size=1024): if chunk: - streamed_data+= chunk + streamed_data += chunk - self.assertEqual(streamed_data, b'streamingtestingresponseisreturned') + self.assertEqual( + streamed_data, b'streamingtestingresponseisreturned') def _get_blob_url(self, scenario_name: str) -> str: return ( diff --git a/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py b/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py index 093d69228..30fe94cdf 100644 --- a/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py +++ b/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py @@ -50,8 +50,10 @@ def eventhub_output_batch(req: func.HttpRequest, out: func.Out[str]) -> str: @app.blob_input(arg_name="testEntities", path="python-worker-tests/test-eventhub-batch-triggered.txt", connection="AzureWebJobsStorage") -def get_eventhub_batch_triggered(req: func.HttpRequest, testEntities: func.InputStream): - return func.HttpResponse(status_code=200, body=testEntities.read().decode('utf-8')) +def get_eventhub_batch_triggered(req: func.HttpRequest, + testEntities: func.InputStream): + return func.HttpResponse(status_code=200, + body=testEntities.read().decode('utf-8')) # Retrieve the event data from storage blob and return it as Http response diff --git a/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py b/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py index 153829b31..feca352fb 100644 --- a/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py +++ b/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py @@ -5,4 +5,5 @@ # Retrieve the event data from storage blob and return it as Http response def main(req: func.HttpRequest, testEntities: func.InputStream): - return func.HttpResponse(status_code=200, body=testEntities.read().decode('utf-8')) + return func.HttpResponse(status_code=200, + body=testEntities.read().decode('utf-8')) diff --git a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py index ad2831f0a..c9718fef5 100644 --- a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py +++ b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py @@ -7,9 +7,7 @@ import azure.functions as func -from azure.functions.extension.fastapi import Request, Response, StreamingResponse, \ - HTMLResponse, PlainTextResponse, HTMLResponse, JSONResponse, \ - UJSONResponse, ORJSONResponse, RedirectResponse, FileResponse +from azure.functions.extension.fastapi import Request, Response app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) diff --git a/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py b/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py index 1870767da..b82e0baee 100644 --- a/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py +++ b/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py @@ -1,14 +1,13 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import asyncio from datetime import datetime import logging import time import azure.functions as func -from azure.functions.extension.fastapi import Request, Response, StreamingResponse, \ - HTMLResponse, PlainTextResponse, HTMLResponse, JSONResponse, \ - UJSONResponse, ORJSONResponse, RedirectResponse, FileResponse +from azure.functions.extension.fastapi import Request, Response, \ + StreamingResponse, HTMLResponse, \ + UJSONResponse, ORJSONResponse, FileResponse app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) @@ -73,19 +72,23 @@ async def content(): yield b"Second chunk\n" return StreamingResponse(content()) + @app.route(route="return_html") def return_html(req: Request) -> HTMLResponse: html_content = "

Hello, World!

" return HTMLResponse(content=html_content, status_code=200) + @app.route(route="return_ujson") def return_ujson(req: Request) -> UJSONResponse: return UJSONResponse(content={"message": "Hello, World!"}, status_code=200) + @app.route(route="return_orjson") def return_orjson(req: Request) -> ORJSONResponse: return ORJSONResponse(content={"message": "Hello, World!"}, status_code=200) + @app.route(route="return_file") def return_file(req: Request) -> FileResponse: - return FileResponse("function_app.py") \ No newline at end of file + return FileResponse("function_app.py") diff --git a/tests/endtoend/test_http_functions.py b/tests/endtoend/test_http_functions.py index 1d4abf44e..c12e91ab6 100644 --- a/tests/endtoend/test_http_functions.py +++ b/tests/endtoend/test_http_functions.py @@ -225,22 +225,43 @@ def tearDownClass(cls): @unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7") -class TestHttpFunctionsV2FastApiWithInitIndexing(TestHttpFunctionsWithInitIndexing): - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ - 'http_functions_v2' / \ - 'fastapi' - - @testutils.retryable_test(3, 5) - def test_return_streaming(self): - """Test if the return_streaming function returns a streaming - response""" - root_url = self.webhost._addr - streaming_url = f'{root_url}/api/return_streaming' - r = requests.get(streaming_url, timeout=REQUEST_TIMEOUT_SEC, stream=True) +class TestHttpFunctionsV2FastApiWithInitIndexing( + TestHttpFunctionsWithInitIndexing): + @classmethod + def get_script_dir(cls): + return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ + 'http_functions_v2' / \ + 'fastapi' + + @testutils.retryable_test(3, 5) + def test_return_streaming(self): + """Test if the return_streaming function returns a streaming + response""" + root_url = self.webhost._addr + streaming_url = f'{root_url}/api/return_streaming' + r = requests.get( + streaming_url, timeout=REQUEST_TIMEOUT_SEC, stream=True) + self.assertTrue(r.ok) + # Validate streaming content + expected_content = [b"First chunk\n", b"Second chunk\n"] + received_content = [] + for chunk in r.iter_content(chunk_size=1024): + if chunk: + received_content.append(chunk) + self.assertEqual(received_content, expected_content) + + @testutils.retryable_test(3, 5) + def test_return_streaming_concurrently(self): + """Test if the return_streaming function returns a streaming + response concurrently""" + root_url = self.webhost._addr + streaming_url = f'{root_url}/return_streaming' + + # Function to make a streaming request and validate content + def make_request(): + r = requests.get(streaming_url, timeout=REQUEST_TIMEOUT_SEC, + stream=True) self.assertTrue(r.ok) - # Validate streaming content expected_content = [b"First chunk\n", b"Second chunk\n"] received_content = [] for chunk in r.iter_content(chunk_size=1024): @@ -248,134 +269,116 @@ def test_return_streaming(self): received_content.append(chunk) self.assertEqual(received_content, expected_content) - @testutils.retryable_test(3, 5) - def test_return_streaming_concurrently(self): - """Test if the return_streaming function returns a streaming - response concurrently""" - root_url = self.webhost._addr - streaming_url = f'{root_url}/return_streaming' - - # Function to make a streaming request and validate content - def make_request(): - r = requests.get(streaming_url, timeout=REQUEST_TIMEOUT_SEC, - stream=True) - self.assertTrue(r.ok) - expected_content = [b"First chunk\n", b"Second chunk\n"] - received_content = [] - for chunk in r.iter_content(chunk_size=1024): - if chunk: - received_content.append(chunk) - self.assertEqual(received_content, expected_content) - - # Make concurrent requests - with ThreadPoolExecutor(max_workers=2) as executor: - executor.map(make_request, range(2)) - - @testutils.retryable_test(3, 5) - def test_return_html(self): - """Test if the return_html function returns an HTML response""" - root_url = self.webhost._addr - html_url = f'{root_url}/api/return_html' - r = requests.get(html_url, timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - self.assertEqual(r.headers['content-type'], - 'text/html; charset=utf-8') - # Validate HTML content - expected_html = "

Hello, World!

" - self.assertEqual(r.text, expected_html) - - @testutils.retryable_test(3, 5) - def test_return_ujson(self): - """Test if the return_ujson function returns a UJSON response""" - root_url = self.webhost._addr - ujson_url = f'{root_url}/api/return_ujson' - r = requests.get(ujson_url, timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - self.assertEqual(r.headers['content-type'],'application/json') - self.assertEqual(r.text, '{"message":"Hello, World!"}') - - @testutils.retryable_test(3, 5) - def test_return_orjson(self): - """Test if the return_orjson function returns an ORJSON response""" - root_url = self.webhost._addr - orjson_url = f'{root_url}/api/return_orjson' - r = requests.get(orjson_url, timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - self.assertEqual(r.headers['content-type'], 'application/json') - self.assertEqual(r.text, '{"message":"Hello, World!"}') - - @testutils.retryable_test(3, 5) - def test_return_file(self): - """Test if the return_file function returns a file response""" - root_url = self.webhost._addr - file_url = f'{root_url}/api/return_file' - r = requests.get(file_url, timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - self.assertIn('@app.route(route="default_template")', r.text) + # Make concurrent requests + with ThreadPoolExecutor(max_workers=2) as executor: + executor.map(make_request, range(2)) - @testutils.retryable_test(3, 5) - def test_upload_data_stream(self): - """Test if the upload_data_stream function receives streaming data - and returns the complete data""" - root_url = self.webhost._addr - upload_url = f'{root_url}/api/upload_data_stream' + @testutils.retryable_test(3, 5) + def test_return_html(self): + """Test if the return_html function returns an HTML response""" + root_url = self.webhost._addr + html_url = f'{root_url}/api/return_html' + r = requests.get(html_url, timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual(r.headers['content-type'], + 'text/html; charset=utf-8') + # Validate HTML content + expected_html = "

Hello, World!

" + self.assertEqual(r.text, expected_html) - # Define the streaming data - data_chunks = [b"First chunk\n", b"Second chunk\n"] + @testutils.retryable_test(3, 5) + def test_return_ujson(self): + """Test if the return_ujson function returns a UJSON response""" + root_url = self.webhost._addr + ujson_url = f'{root_url}/api/return_ujson' + r = requests.get(ujson_url, timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual(r.headers['content-type'], 'application/json') + self.assertEqual(r.text, '{"message":"Hello, World!"}') - # Define a function to simulate streaming by reading from an - # iterator - def stream_data(data_chunks): - for chunk in data_chunks: - yield chunk + @testutils.retryable_test(3, 5) + def test_return_orjson(self): + """Test if the return_orjson function returns an ORJSON response""" + root_url = self.webhost._addr + orjson_url = f'{root_url}/api/return_orjson' + r = requests.get(orjson_url, timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual(r.headers['content-type'], 'application/json') + self.assertEqual(r.text, '{"message":"Hello, World!"}') - # Send a POST request with streaming data - r = requests.post(upload_url, data=stream_data(data_chunks)) + @testutils.retryable_test(3, 5) + def test_return_file(self): + """Test if the return_file function returns a file response""" + root_url = self.webhost._addr + file_url = f'{root_url}/api/return_file' + r = requests.get(file_url, timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertIn('@app.route(route="default_template")', r.text) - # Assert that the request was successful - self.assertTrue(r.ok) + @testutils.retryable_test(3, 5) + def test_upload_data_stream(self): + """Test if the upload_data_stream function receives streaming data + and returns the complete data""" + root_url = self.webhost._addr + upload_url = f'{root_url}/api/upload_data_stream' + + # Define the streaming data + data_chunks = [b"First chunk\n", b"Second chunk\n"] + + # Define a function to simulate streaming by reading from an + # iterator + def stream_data(data_chunks): + for chunk in data_chunks: + yield chunk + + # Send a POST request with streaming data + r = requests.post(upload_url, data=stream_data(data_chunks)) - # Assert that the response content matches the concatenation of - # all data chunks + # Assert that the request was successful + self.assertTrue(r.ok) + + # Assert that the response content matches the concatenation of + # all data chunks + complete_data = b"".join(data_chunks) + self.assertEqual(r.content, complete_data) + + @testutils.retryable_test(3, 5) + def test_upload_data_stream_concurrently(self): + """Test if the upload_data_stream function receives streaming data + and returns the complete data""" + root_url = self.webhost._addr + upload_url = f'{root_url}/api/upload_data_stream' + + # Define the streaming data + data_chunks = [b"First chunk\n", b"Second chunk\n"] + + # Define a function to simulate streaming by reading from an + # iterator + def stream_data(data_chunks): + for chunk in data_chunks: + yield chunk + + # Define the number of concurrent requests + num_requests = 5 + + # Define a function to send a single request + def send_request(): + r = requests.post(upload_url, data=stream_data(data_chunks)) + return r.ok, r.content + + # Send multiple requests concurrently + with concurrent.futures.ThreadPoolExecutor() as executor: + futures = [executor.submit(send_request) for _ in + range(num_requests)] + + # Assert that all requests were successful and the response + # contents are correct + for future in concurrent.futures.as_completed(futures): + ok, content = future.result() + self.assertTrue(ok) complete_data = b"".join(data_chunks) - self.assertEqual(r.content, complete_data) - - @testutils.retryable_test(3, 5) - def test_upload_data_stream_concurrently(self): - """Test if the upload_data_stream function receives streaming data - and returns the complete data""" - root_url = self.webhost._addr - upload_url = f'{root_url}/api/upload_data_stream' - - # Define the streaming data - data_chunks = [b"First chunk\n", b"Second chunk\n"] - - # Define a function to simulate streaming by reading from an - # iterator - def stream_data(data_chunks): - for chunk in data_chunks: - yield chunk - - # Define the number of concurrent requests - num_requests = 5 - - # Define a function to send a single request - def send_request(): - r = requests.post(upload_url, data=stream_data(data_chunks)) - return r.ok, r.content - - # Send multiple requests concurrently - with concurrent.futures.ThreadPoolExecutor() as executor: - futures = [executor.submit(send_request) for _ in - range(num_requests)] - - # Assert that all requests were successful and the response - # contents are correct - for future in concurrent.futures.as_completed(futures): - ok, content = future.result() - self.assertTrue(ok) - complete_data = b"".join(data_chunks) - self.assertEqual(content, complete_data) + self.assertEqual(content, complete_data) + class TestUserThreadLoggingHttpFunctions(testutils.WebHostTestCase): """Test the Http trigger that contains logging with user threads. diff --git a/tests/endtoend/test_servicebus_functions.py b/tests/endtoend/test_servicebus_functions.py index 34f51c5bc..48d71392e 100644 --- a/tests/endtoend/test_servicebus_functions.py +++ b/tests/endtoend/test_servicebus_functions.py @@ -37,16 +37,18 @@ def test_servicebus_basic(self): self.assertEqual(r.status_code, 200) msg = r.json() self.assertEqual(msg['body'], data) - for attr in {'message_id', 'body', 'content_type', 'delivery_count', - 'expiration_time', 'label', 'partition_key', 'reply_to', - 'reply_to_session_id', 'scheduled_enqueue_time', - 'session_id', 'time_to_live', 'to', 'user_properties', - 'application_properties', 'correlation_id', - 'dead_letter_error_description', 'dead_letter_reason', - 'dead_letter_source', 'enqueued_sequence_number', - 'enqueued_time_utc', 'expires_at_utc', 'locked_until', - 'lock_token', 'sequence_number', 'state', 'subject', - 'transaction_partition_key'}: + for attr in { + 'message_id', 'body', 'content_type', 'delivery_count', + 'expiration_time', 'label', 'partition_key', 'reply_to', + 'reply_to_session_id', 'scheduled_enqueue_time', + 'session_id', 'time_to_live', 'to', 'user_properties', + 'application_properties', 'correlation_id', + 'dead_letter_error_description', 'dead_letter_reason', + 'dead_letter_source', 'enqueued_sequence_number', + 'enqueued_time_utc', 'expires_at_utc', 'locked_until', + 'lock_token', 'sequence_number', 'state', 'subject', + 'transaction_partition_key' + }: self.assertIn(attr, msg) except (AssertionError, json.JSONDecodeError): if try_no == max_retries - 1: diff --git a/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py b/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py index 25accf853..9830f572e 100644 --- a/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py +++ b/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py @@ -2,13 +2,12 @@ # Licensed under the MIT License. import asyncio import hashlib -import json import logging import sys import time from urllib.request import urlopen from azure.functions.extension.fastapi import Request, Response, \ - PlainTextResponse, HTMLResponse, RedirectResponse + HTMLResponse, RedirectResponse import azure.functions as func from pydantic import BaseModel @@ -273,11 +272,11 @@ def return_http_redirect(req: Request): async def return_request(req: Request): params = dict(req.query_params) params.pop('code', None) # Remove 'code' parameter if present - + # Get the body content and calculate its hash body = await req.body() body_hash = hashlib.sha256(body).hexdigest() if body else None - + # Return a dictionary containing request information return { 'method': req.method, @@ -431,4 +430,4 @@ def set_cookie_resp_header_default_values( value='42' ) - return resp \ No newline at end of file + return resp diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 37f23ea5f..ba9670e24 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -9,13 +9,14 @@ from unittest.mock import patch from azure_functions_worker import protos -from azure_functions_worker.constants import (PYTHON_THREADPOOL_THREAD_COUNT, - PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT, - PYTHON_THREADPOOL_THREAD_COUNT_MAX_37, - PYTHON_THREADPOOL_THREAD_COUNT_MIN, - PYTHON_ENABLE_INIT_INDEXING, - METADATA_PROPERTIES_WORKER_INDEXED, - PYTHON_ENABLE_DEBUG_LOGGING) +from azure_functions_worker.constants import ( + PYTHON_THREADPOOL_THREAD_COUNT, + PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT, + PYTHON_THREADPOOL_THREAD_COUNT_MAX_37, + PYTHON_THREADPOOL_THREAD_COUNT_MIN, + PYTHON_ENABLE_INIT_INDEXING, + METADATA_PROPERTIES_WORKER_INDEXED, + PYTHON_ENABLE_DEBUG_LOGGING) from azure_functions_worker.dispatcher import Dispatcher from azure_functions_worker.version import VERSION from tests.utils import testutils @@ -682,9 +683,11 @@ async def test_dispatcher_load_azfunc_in_init(self): 1 ) self.assertEqual( - len([log for log in r.logs if log.message.startswith( - "Received WorkerMetadataRequest from _handle__worker_init_request" - )]), + len([log for log in r.logs if + log.message.startswith( + "Received WorkerMetadataRequest from" + "_handle__worker_init_request" + )]), 0 ) self.assertIn("azure.functions", sys.modules) @@ -844,11 +847,14 @@ def test_functions_metadata_request_with_init_indexing_enabled(self): self.assertEqual(init_response.worker_init_response.result.status, protos.StatusResult.Success) - metadata_response = self.loop.run_until_complete( - self.dispatcher._handle__functions_metadata_request(metadata_request)) + metadata_response = \ + self.loop.run_until_complete( + self.dispatcher._handle__functions_metadata_request( + metadata_request)) - self.assertEqual(metadata_response.function_metadata_response.result.status, - protos.StatusResult.Success) + self.assertEqual( + metadata_response.function_metadata_response.result.status, + protos.StatusResult.Success) self.assertIsNotNone(self.dispatcher._function_metadata_result) self.assertIsNone(self.dispatcher._function_metadata_exception) @@ -875,10 +881,12 @@ def test_functions_metadata_request_with_init_indexing_disabled(self): self.assertIsNone(self.dispatcher._function_metadata_exception) metadata_response = self.loop.run_until_complete( - self.dispatcher._handle__functions_metadata_request(metadata_request)) + self.dispatcher._handle__functions_metadata_request( + metadata_request)) - self.assertEqual(metadata_response.function_metadata_response.result.status, - protos.StatusResult.Success) + self.assertEqual( + metadata_response.function_metadata_response.result.status, + protos.StatusResult.Success) self.assertIsNotNone(self.dispatcher._function_metadata_result) self.assertIsNone(self.dispatcher._function_metadata_exception) diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py index 344ffdce8..f21a5aff9 100644 --- a/tests/unittests/test_http_functions.py +++ b/tests/unittests/test_http_functions.py @@ -10,6 +10,7 @@ from tests.utils import testutils + class TestHttpFunctions(testutils.WebHostTestCase): @classmethod @@ -462,6 +463,7 @@ def test_no_return_returns(self): r = self.webhost.request('GET', 'no_return_returns') self.assertEqual(r.status_code, 200) + class TestHttpFunctionsV2(TestHttpFunctions): @classmethod def get_script_dir(cls): diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py index eb6951f50..202dce14d 100644 --- a/tests/unittests/test_http_functions_v2.py +++ b/tests/unittests/test_http_functions_v2.py @@ -35,7 +35,7 @@ def tearDownClass(cls): def get_script_dir(cls): return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \ 'http_v2_functions' / \ - 'fastapi' + 'fastapi' def test_return_bytes(self): r = self.webhost.request('GET', 'return_bytes') @@ -372,7 +372,6 @@ def test_response_cookie_header_nullable_timestamp_err(self): 'response_cookie_header_nullable_timestamp_err') self.assertEqual(r.status_code, 200) - @skipIf(sys.version_info < (3, 8, 0), "Skip the tests for Python 3.7 and below") def test_response_cookie_header_nullable_bool_err(self): @@ -382,7 +381,6 @@ def test_response_cookie_header_nullable_bool_err(self): self.assertEqual(r.status_code, 200) self.assertTrue("Set-Cookie" in r.headers) - def test_print_to_console_stderr(self): r = self.webhost.request('GET', 'print_logging?console=true' '&message=Secret42&is_stderr=true') @@ -456,4 +454,4 @@ def check_return_pydantic_model_with_missing_fields(self, host_out: typing.List[str]): self.assertIn("Field required [type=missing, input_value={'name': " - "'item1'}, input_type=dict]", host_out) \ No newline at end of file + "'item1'}, input_type=dict]", host_out) diff --git a/tests/utils/testutils.py b/tests/utils/testutils.py index 57946f1eb..86467f0c7 100644 --- a/tests/utils/testutils.py +++ b/tests/utils/testutils.py @@ -249,7 +249,8 @@ def setUpClass(cls): cls.host_stdout_logger.error(error_message) raise RuntimeError(error_message) except Exception as ex: - cls.host_stdout_logger.error(f"WebHost is not started correctly. {ex}") + cls.host_stdout_logger.error( + f"WebHost is not started correctly. {ex}") cls.tearDownClass() raise From bb8e77a4581ff8d577ea612b58d5070d19b200be Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:44:51 -0700 Subject: [PATCH 020/101] fix --- tests/unittests/test_http_functions.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py index f21a5aff9..9819430d1 100644 --- a/tests/unittests/test_http_functions.py +++ b/tests/unittests/test_http_functions.py @@ -461,11 +461,4 @@ def test_no_return(self): def test_no_return_returns(self): r = self.webhost.request('GET', 'no_return_returns') - self.assertEqual(r.status_code, 200) - - -class TestHttpFunctionsV2(TestHttpFunctions): - @classmethod - def get_script_dir(cls): - return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \ - 'http_v2_functions' + self.assertEqual(r.status_code, 200) \ No newline at end of file From 1e66de647e1f769f9014b548ce77d0e1da76d9aa Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:50:44 -0700 Subject: [PATCH 021/101] s --- .../function_app.py | 23 ++++++++------- .../get_eventhub_batch_triggered/__init__.py | 5 ++-- tests/endtoend/test_servicebus_functions.py | 28 +++++++------------ 3 files changed, 23 insertions(+), 33 deletions(-) diff --git a/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py b/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py index 30fe94cdf..bccc29d27 100644 --- a/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py +++ b/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py @@ -18,10 +18,10 @@ connection="AzureWebJobsEventHubConnectionString", data_type="string", cardinality="many") -@app.blob_output(arg_name="$return", - path="python-worker-tests/test-eventhub-batch-triggered.txt", - connection="AzureWebJobsStorage") -def eventhub_multiple(events) -> str: +@app.table_output(arg_name="$return", + connection="AzureWebJobsStorage", + table_name="EventHubBatchTest") +def eventhub_multiple(events): table_entries = [] for event in events: json_entry = event.get_body() @@ -46,14 +46,13 @@ def eventhub_output_batch(req: func.HttpRequest, out: func.Out[str]) -> str: # Retrieve the event data from storage blob and return it as Http response @app.function_name(name="get_eventhub_batch_triggered") -@app.route(route="get_eventhub_batch_triggered") -@app.blob_input(arg_name="testEntities", - path="python-worker-tests/test-eventhub-batch-triggered.txt", - connection="AzureWebJobsStorage") -def get_eventhub_batch_triggered(req: func.HttpRequest, - testEntities: func.InputStream): - return func.HttpResponse(status_code=200, - body=testEntities.read().decode('utf-8')) +@app.route(route="get_eventhub_batch_triggered/{id}") +@app.table_input(arg_name="testEntities", + connection="AzureWebJobsStorage", + table_name="EventHubBatchTest", + partition_key="{id}") +def get_eventhub_batch_triggered(req: func.HttpRequest, testEntities): + return func.HttpResponse(status_code=200, body=testEntities) # Retrieve the event data from storage blob and return it as Http response diff --git a/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py b/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py index feca352fb..8eccb90ee 100644 --- a/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py +++ b/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py @@ -4,6 +4,5 @@ # Retrieve the event data from storage blob and return it as Http response -def main(req: func.HttpRequest, testEntities: func.InputStream): - return func.HttpResponse(status_code=200, - body=testEntities.read().decode('utf-8')) +def main(req: func.HttpRequest, testEntities): + return func.HttpResponse(status_code=200, body=testEntities) diff --git a/tests/endtoend/test_servicebus_functions.py b/tests/endtoend/test_servicebus_functions.py index 48d71392e..aaacd76d6 100644 --- a/tests/endtoend/test_servicebus_functions.py +++ b/tests/endtoend/test_servicebus_functions.py @@ -2,16 +2,10 @@ # Licensed under the MIT License. import json import time -from unittest import skipIf -from azure_functions_worker.utils.common import is_envvar_true from tests.utils import testutils -from tests.utils.constants import DEDICATED_DOCKER_TEST, CONSUMPTION_DOCKER_TEST -@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST) - or is_envvar_true(CONSUMPTION_DOCKER_TEST), - "Skipping SB tests till docker image is updated with host 4.33") class TestServiceBusFunctions(testutils.WebHostTestCase): @classmethod @@ -37,18 +31,16 @@ def test_servicebus_basic(self): self.assertEqual(r.status_code, 200) msg = r.json() self.assertEqual(msg['body'], data) - for attr in { - 'message_id', 'body', 'content_type', 'delivery_count', - 'expiration_time', 'label', 'partition_key', 'reply_to', - 'reply_to_session_id', 'scheduled_enqueue_time', - 'session_id', 'time_to_live', 'to', 'user_properties', - 'application_properties', 'correlation_id', - 'dead_letter_error_description', 'dead_letter_reason', - 'dead_letter_source', 'enqueued_sequence_number', - 'enqueued_time_utc', 'expires_at_utc', 'locked_until', - 'lock_token', 'sequence_number', 'state', 'subject', - 'transaction_partition_key' - }: + for attr in {'message_id', 'body', 'content_type', 'delivery_count', + 'expiration_time', 'label', 'partition_key', 'reply_to', + 'reply_to_session_id', 'scheduled_enqueue_time', + 'session_id', 'time_to_live', 'to', 'user_properties', + 'application_properties', 'correlation_id', + 'dead_letter_error_description', 'dead_letter_reason', + 'dead_letter_source', 'enqueued_sequence_number', + 'enqueued_time_utc', 'expires_at_utc', 'locked_until', + 'lock_token', 'sequence_number', 'state', 'subject', + 'transaction_partition_key'}: self.assertIn(attr, msg) except (AssertionError, json.JSONDecodeError): if try_no == max_retries - 1: From 42d7a3dace025373b723232a44cb298de992b8ad Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 19:31:37 -0700 Subject: [PATCH 022/101] fix --- tests/unittests/test_dispatcher.py | 42 +++++++++++--------------- tests/unittests/test_http_functions.py | 9 ++++-- tests/utils/testutils.py | 3 +- 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index ba9670e24..37f23ea5f 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -9,14 +9,13 @@ from unittest.mock import patch from azure_functions_worker import protos -from azure_functions_worker.constants import ( - PYTHON_THREADPOOL_THREAD_COUNT, - PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT, - PYTHON_THREADPOOL_THREAD_COUNT_MAX_37, - PYTHON_THREADPOOL_THREAD_COUNT_MIN, - PYTHON_ENABLE_INIT_INDEXING, - METADATA_PROPERTIES_WORKER_INDEXED, - PYTHON_ENABLE_DEBUG_LOGGING) +from azure_functions_worker.constants import (PYTHON_THREADPOOL_THREAD_COUNT, + PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT, + PYTHON_THREADPOOL_THREAD_COUNT_MAX_37, + PYTHON_THREADPOOL_THREAD_COUNT_MIN, + PYTHON_ENABLE_INIT_INDEXING, + METADATA_PROPERTIES_WORKER_INDEXED, + PYTHON_ENABLE_DEBUG_LOGGING) from azure_functions_worker.dispatcher import Dispatcher from azure_functions_worker.version import VERSION from tests.utils import testutils @@ -683,11 +682,9 @@ async def test_dispatcher_load_azfunc_in_init(self): 1 ) self.assertEqual( - len([log for log in r.logs if - log.message.startswith( - "Received WorkerMetadataRequest from" - "_handle__worker_init_request" - )]), + len([log for log in r.logs if log.message.startswith( + "Received WorkerMetadataRequest from _handle__worker_init_request" + )]), 0 ) self.assertIn("azure.functions", sys.modules) @@ -847,14 +844,11 @@ def test_functions_metadata_request_with_init_indexing_enabled(self): self.assertEqual(init_response.worker_init_response.result.status, protos.StatusResult.Success) - metadata_response = \ - self.loop.run_until_complete( - self.dispatcher._handle__functions_metadata_request( - metadata_request)) + metadata_response = self.loop.run_until_complete( + self.dispatcher._handle__functions_metadata_request(metadata_request)) - self.assertEqual( - metadata_response.function_metadata_response.result.status, - protos.StatusResult.Success) + self.assertEqual(metadata_response.function_metadata_response.result.status, + protos.StatusResult.Success) self.assertIsNotNone(self.dispatcher._function_metadata_result) self.assertIsNone(self.dispatcher._function_metadata_exception) @@ -881,12 +875,10 @@ def test_functions_metadata_request_with_init_indexing_disabled(self): self.assertIsNone(self.dispatcher._function_metadata_exception) metadata_response = self.loop.run_until_complete( - self.dispatcher._handle__functions_metadata_request( - metadata_request)) + self.dispatcher._handle__functions_metadata_request(metadata_request)) - self.assertEqual( - metadata_response.function_metadata_response.result.status, - protos.StatusResult.Success) + self.assertEqual(metadata_response.function_metadata_response.result.status, + protos.StatusResult.Success) self.assertIsNotNone(self.dispatcher._function_metadata_result) self.assertIsNone(self.dispatcher._function_metadata_exception) diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py index 9819430d1..344ffdce8 100644 --- a/tests/unittests/test_http_functions.py +++ b/tests/unittests/test_http_functions.py @@ -10,7 +10,6 @@ from tests.utils import testutils - class TestHttpFunctions(testutils.WebHostTestCase): @classmethod @@ -461,4 +460,10 @@ def test_no_return(self): def test_no_return_returns(self): r = self.webhost.request('GET', 'no_return_returns') - self.assertEqual(r.status_code, 200) \ No newline at end of file + self.assertEqual(r.status_code, 200) + +class TestHttpFunctionsV2(TestHttpFunctions): + @classmethod + def get_script_dir(cls): + return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \ + 'http_v2_functions' diff --git a/tests/utils/testutils.py b/tests/utils/testutils.py index 86467f0c7..57946f1eb 100644 --- a/tests/utils/testutils.py +++ b/tests/utils/testutils.py @@ -249,8 +249,7 @@ def setUpClass(cls): cls.host_stdout_logger.error(error_message) raise RuntimeError(error_message) except Exception as ex: - cls.host_stdout_logger.error( - f"WebHost is not started correctly. {ex}") + cls.host_stdout_logger.error(f"WebHost is not started correctly. {ex}") cls.tearDownClass() raise From 21589e543e17fce28c63a84ccea1fa69082abb7f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 19:36:14 -0700 Subject: [PATCH 023/101] fix --- .../function_app.py | 21 +++++++++---------- .../get_eventhub_batch_triggered/__init__.py | 4 ++-- tests/endtoend/test_servicebus_functions.py | 6 ++++++ tests/unittests/test_http_functions.py | 12 +---------- 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py b/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py index bccc29d27..093d69228 100644 --- a/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py +++ b/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py @@ -18,10 +18,10 @@ connection="AzureWebJobsEventHubConnectionString", data_type="string", cardinality="many") -@app.table_output(arg_name="$return", - connection="AzureWebJobsStorage", - table_name="EventHubBatchTest") -def eventhub_multiple(events): +@app.blob_output(arg_name="$return", + path="python-worker-tests/test-eventhub-batch-triggered.txt", + connection="AzureWebJobsStorage") +def eventhub_multiple(events) -> str: table_entries = [] for event in events: json_entry = event.get_body() @@ -46,13 +46,12 @@ def eventhub_output_batch(req: func.HttpRequest, out: func.Out[str]) -> str: # Retrieve the event data from storage blob and return it as Http response @app.function_name(name="get_eventhub_batch_triggered") -@app.route(route="get_eventhub_batch_triggered/{id}") -@app.table_input(arg_name="testEntities", - connection="AzureWebJobsStorage", - table_name="EventHubBatchTest", - partition_key="{id}") -def get_eventhub_batch_triggered(req: func.HttpRequest, testEntities): - return func.HttpResponse(status_code=200, body=testEntities) +@app.route(route="get_eventhub_batch_triggered") +@app.blob_input(arg_name="testEntities", + path="python-worker-tests/test-eventhub-batch-triggered.txt", + connection="AzureWebJobsStorage") +def get_eventhub_batch_triggered(req: func.HttpRequest, testEntities: func.InputStream): + return func.HttpResponse(status_code=200, body=testEntities.read().decode('utf-8')) # Retrieve the event data from storage blob and return it as Http response diff --git a/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py b/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py index 8eccb90ee..153829b31 100644 --- a/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py +++ b/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py @@ -4,5 +4,5 @@ # Retrieve the event data from storage blob and return it as Http response -def main(req: func.HttpRequest, testEntities): - return func.HttpResponse(status_code=200, body=testEntities) +def main(req: func.HttpRequest, testEntities: func.InputStream): + return func.HttpResponse(status_code=200, body=testEntities.read().decode('utf-8')) diff --git a/tests/endtoend/test_servicebus_functions.py b/tests/endtoend/test_servicebus_functions.py index aaacd76d6..34f51c5bc 100644 --- a/tests/endtoend/test_servicebus_functions.py +++ b/tests/endtoend/test_servicebus_functions.py @@ -2,10 +2,16 @@ # Licensed under the MIT License. import json import time +from unittest import skipIf +from azure_functions_worker.utils.common import is_envvar_true from tests.utils import testutils +from tests.utils.constants import DEDICATED_DOCKER_TEST, CONSUMPTION_DOCKER_TEST +@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST) + or is_envvar_true(CONSUMPTION_DOCKER_TEST), + "Skipping SB tests till docker image is updated with host 4.33") class TestServiceBusFunctions(testutils.WebHostTestCase): @classmethod diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py index 344ffdce8..109694d71 100644 --- a/tests/unittests/test_http_functions.py +++ b/tests/unittests/test_http_functions.py @@ -10,6 +10,7 @@ from tests.utils import testutils + class TestHttpFunctions(testutils.WebHostTestCase): @classmethod @@ -108,11 +109,6 @@ def check_log_debug_logging(self, host_out: typing.List[str]): self.assertIn('logging error', host_out) self.assertNotIn('logging debug', host_out) - def test_debug_with_user_logging(self): - r = self.webhost.request('GET', 'debug_user_logging') - self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'OK-user-debug') - def check_log_debug_with_user_logging(self, host_out: typing.List[str]): self.assertIn('logging info', host_out) self.assertIn('logging warning', host_out) @@ -461,9 +457,3 @@ def test_no_return(self): def test_no_return_returns(self): r = self.webhost.request('GET', 'no_return_returns') self.assertEqual(r.status_code, 200) - -class TestHttpFunctionsV2(TestHttpFunctions): - @classmethod - def get_script_dir(cls): - return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \ - 'http_v2_functions' From 69ed92d271bb7d7f7fd041a455c582172be2ef79 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 19:37:44 -0700 Subject: [PATCH 024/101] revert --- azure_functions_worker/loader.py | 53 +++++++++++++++----------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index e90888e3c..938fde64c 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -122,34 +122,31 @@ def build_variable_interval_retry(retry, max_retry_count, retry_strategy): def process_indexed_function(functions_registry: functions.Registry, indexed_functions): - try: - fx_metadata_results = [] - for indexed_function in indexed_functions: - function_info = functions_registry.add_indexed_function( - function=indexed_function) - - binding_protos = build_binding_protos(indexed_function) - retry_protos = build_retry_protos(indexed_function) - - function_metadata = protos.RpcFunctionMetadata( - name=function_info.name, - function_id=function_info.function_id, - managed_dependency_enabled=False, # only enabled for PowerShell - directory=function_info.directory, - script_file=indexed_function.function_script_file, - entry_point=function_info.name, - is_proxy=False, # not supported in V4 - language=PYTHON_LANGUAGE_RUNTIME, - bindings=binding_protos, - raw_bindings=indexed_function.get_raw_bindings(), - retry_options=retry_protos, - properties={METADATA_PROPERTIES_WORKER_INDEXED: "True"}) - - fx_metadata_results.append(function_metadata) - return fx_metadata_results - except Exception as e: - logger.error(f'Error in process_indexed_function. {e}', exc_info=True) - raise e + fx_metadata_results = [] + for indexed_function in indexed_functions: + function_info = functions_registry.add_indexed_function( + function=indexed_function) + + binding_protos = build_binding_protos(indexed_function) + retry_protos = build_retry_protos(indexed_function) + + function_metadata = protos.RpcFunctionMetadata( + name=function_info.name, + function_id=function_info.function_id, + managed_dependency_enabled=False, # only enabled for PowerShell + directory=function_info.directory, + script_file=indexed_function.function_script_file, + entry_point=function_info.name, + is_proxy=False, # not supported in V4 + language=PYTHON_LANGUAGE_RUNTIME, + bindings=binding_protos, + raw_bindings=indexed_function.get_raw_bindings(), + retry_options=retry_protos, + properties={METADATA_PROPERTIES_WORKER_INDEXED: "True"}) + + fx_metadata_results.append(function_metadata) + + return fx_metadata_results @attach_message_to_exception( From 43114e78b46a224f5218992a8eca68772fcc2522 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 20:43:18 -0700 Subject: [PATCH 025/101] fix --- azure_functions_worker/dispatcher.py | 1 + azure_functions_worker/functions.py | 1 + 2 files changed, 2 insertions(+) diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 992c6c639..8cd3d0d67 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -568,6 +568,7 @@ async def _handle__invocation_request(self, request): http_v2_enabled = False if sys.version_info.minor >= \ BASE_EXT_SUPPORTED_PY_MINOR_VERSION \ + and fi.trigger_metadata is not None \ and fi.trigger_metadata.get('type') == HTTP_TRIGGER: from azure.functions.extension.base import HttpV2FeatureChecker http_v2_enabled = HttpV2FeatureChecker.http_v2_enabled() diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index ba2b8509e..c5f00e040 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -307,6 +307,7 @@ def add_func_to_registry_and_return_funcinfo(self, function, None ) + trigger_metadata = None if http_trigger_param_name is not None: trigger_metadata = { "type": HTTP_TRIGGER, From 8923a442bff78dd9bc3f764d0adfd744f7cbc99c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 21:18:12 -0700 Subject: [PATCH 026/101] fix --- setup.py | 3 +- tests/unittests/test_http_v2.py | 70 ++++++++++++++++++++------------- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/setup.py b/setup.py index 5308ae768..e82b5eb33 100644 --- a/setup.py +++ b/setup.py @@ -79,7 +79,8 @@ ) else: INSTALL_REQUIRES.extend( - ("protobuf~=4.22.0", "grpcio-tools~=1.54.2", "grpcio~=1.54.2", "azure-functions-extension-base") + ("protobuf~=4.22.0", "grpcio-tools~=1.54.2", "grpcio~=1.54.2", + "azure-functions-extension-base") ) EXTRA_REQUIRES = { diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py index 048f84095..e9b77f2f6 100644 --- a/tests/unittests/test_http_v2.py +++ b/tests/unittests/test_http_v2.py @@ -24,68 +24,82 @@ def setUp(self): def tearDown(self) -> None: http_coordinator._context_references.clear() - def test_set_http_request_new_invocation(self): + async def test_set_http_request_new_invocation(self): # Test setting a new HTTP request http_coordinator.set_http_request(self.invoc_id, self.http_request) context_ref = http_coordinator._context_references.get(self.invoc_id) self.assertIsNotNone(context_ref) self.assertEqual(context_ref.http_request, self.http_request) - def test_set_http_request_existing_invocation(self): + async def test_set_http_request_existing_invocation(self): # Test updating an existing HTTP request new_http_request = MagicMock() - http_coordinator.set_http_request(self.invoc_id, self.http_request) http_coordinator.set_http_request(self.invoc_id, new_http_request) context_ref = http_coordinator._context_references.get(self.invoc_id) self.assertIsNotNone(context_ref) self.assertEqual(context_ref.http_request, new_http_request) - def test_set_http_response_context_ref_null(self): + async def test_set_http_response_context_ref_null(self): with self.assertRaises(Exception) as cm: http_coordinator.set_http_response(self.invoc_id, self.http_response) self.assertEqual(cm.exception.args[0], "No context reference found for invocation %s") - def test_set_http_response(self): + async def test_set_http_response(self): http_coordinator.set_http_request(self.invoc_id, self.http_request) http_coordinator.set_http_response(self.invoc_id, self.http_response) context_ref = http_coordinator._context_references[self.invoc_id] self.assertEqual(context_ref.http_response, self.http_response) - def test_get_http_request_async_existing_invocation(self): + async def test_get_http_request_async_existing_invocation(self): # Test retrieving an existing HTTP request http_coordinator.set_http_request(self.invoc_id, self.http_request) - loop = asyncio.get_event_loop() - retrieved_request = loop.run_until_complete( - http_coordinator.get_http_request_async(self.invoc_id)) - self.assertEqual(retrieved_request, self.http_request) + loop = asyncio.new_event_loop() + + try: + retrieved_request = loop.run_until_complete( + http_coordinator.get_http_request_async(self.invoc_id)) + self.assertEqual(retrieved_request, self.http_request) + finally: + loop.close() - def test_get_http_request_async_wait_for_request(self): + async def test_get_http_request_async_wait_for_request(self): # Test waiting for an HTTP request to become available async def set_request_after_delay(): await asyncio.sleep(1) - http_coordinator.set_http_request(self.invoc_id, - self.http_request) - - loop = asyncio.get_event_loop() - loop.create_task(set_request_after_delay()) - retrieved_request = loop.run_until_complete( - http_coordinator.get_http_request_async(self.invoc_id)) - self.assertEqual(retrieved_request, self.http_request) - - def test_get_http_request_async_wait_forever(self): + http_coordinator.set_http_request(self.invoc_id, self.http_request) + + # Create a new event loop in the main thread + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + # Run the test + try: + loop.run_until_complete(set_request_after_delay()) + retrieved_request = loop.run_until_complete( + http_coordinator.get_http_request_async(self.invoc_id)) + self.assertEqual(retrieved_request, self.http_request) + finally: + loop.close() # Close the event loop when done + + async def test_get_http_request_async_wait_forever(self): # Test handling error when invoc_id is not found invalid_invoc_id = "invalid_invocation" - loop = asyncio.get_event_loop() - with self.assertRaises(asyncio.TimeoutError): - loop.run_until_complete( - asyncio.wait_for( - http_coordinator.get_http_request_async(invalid_invoc_id), - timeout=1 + # Create a new event loop in the main thread + loop = asyncio.new_event_loop() + try: + with self.assertRaises(asyncio.TimeoutError): + loop.run_until_complete( + asyncio.wait_for( + http_coordinator.get_http_request_async( + invalid_invoc_id), + timeout=1 + ) ) - ) + finally: + loop.close() async def test_await_http_response_async_valid_invocation(self): invoc_id = "valid_invocation" From d90a2f8cc0de8daa5a430067d6ad3153b77d809b Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 22:25:30 -0700 Subject: [PATCH 027/101] fix --- tests/endtoend/test_http_functions.py | 2 +- tests/unittests/test_http_functions_v2.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/endtoend/test_http_functions.py b/tests/endtoend/test_http_functions.py index c12e91ab6..f3a8a6e07 100644 --- a/tests/endtoend/test_http_functions.py +++ b/tests/endtoend/test_http_functions.py @@ -224,7 +224,7 @@ def tearDownClass(cls): super().tearDownClass() -@unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7") +@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7") class TestHttpFunctionsV2FastApiWithInitIndexing( TestHttpFunctionsWithInitIndexing): @classmethod diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py index 202dce14d..49f931f92 100644 --- a/tests/unittests/test_http_functions_v2.py +++ b/tests/unittests/test_http_functions_v2.py @@ -14,7 +14,7 @@ from tests.utils import testutils -@unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7") +@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7") class TestHttpFunctionsV2FastApi(testutils.WebHostTestCase): @classmethod def setUpClass(cls): From f44a28fc5aba39159c70b9409fbb88dd992395d3 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 23:45:39 -0700 Subject: [PATCH 028/101] fix --- azure_functions_worker/bindings/meta.py | 7 +- azure_functions_worker/dispatcher.py | 147 ++++++++++-------------- azure_functions_worker/http_v2.py | 14 +++ 3 files changed, 78 insertions(+), 90 deletions(-) diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index 097d00d94..7d79b0de4 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -19,7 +19,7 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION: - ext_base = sys.modules.get('azure.functions.extension.base') + import azure.functions.extension.base as ext_base if ext_base is not None and \ ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.RequestTrackerMeta.check_type(pytype) @@ -30,9 +30,8 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool: if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION: - ext_base = sys.modules.get('azure.functions.extension.base') - if ext_base is not None and \ - ext_base.HttpV2FeatureChecker.http_v2_enabled(): + import azure.functions.extension.base as ext_base + if ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.ResponseTrackerMeta.check_type(pytype) binding = get_binding(bind_name) diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 8cd3d0d67..1175349fe 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -20,7 +20,6 @@ from datetime import datetime import grpc -import socket from . import bindings, constants, functions, loader, protos from .bindings.shared_memory_data_transfer import SharedMemoryManager from .constants import (HTTP_TRIGGER, PYTHON_ROLLBACK_CWD_PATH, @@ -36,7 +35,7 @@ METADATA_PROPERTIES_WORKER_INDEXED, BASE_EXT_SUPPORTED_PY_MINOR_VERSION) from .extension import ExtensionManager -from .http_v2 import http_coordinator +from .http_v2 import http_coordinator, get_unused_tcp_port from .logging import disable_console_logging, enable_console_logging from .logging import (logger, error_logger, is_system_log_category, CONSOLE_LOG_PREFIX, format_exception) @@ -62,19 +61,6 @@ def current(mcls): return disp -def get_unused_tcp_port(): - # Create a TCP socket - tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - # Bind it to a free port provided by the OS - tcp_socket.bind(("", 0)) - # Get the port number - port = tcp_socket.getsockname()[1] - # Close the socket - tcp_socket.close() - # Return the port number - return port - - class Dispatcher(metaclass=DispatcherMeta): _GRPC_STOP_RESPONSE = object() @@ -91,7 +77,6 @@ def __init__(self, loop: BaseEventLoop, host: str, port: int, self._functions = functions.Registry() self._shmem_mgr = SharedMemoryManager() self._old_task_factory = None - self.function_metadata_result = None self._has_http_func = False # Used to store metadata returns @@ -285,80 +270,70 @@ async def _dispatch_grpc_request(self, request): self._grpc_resp_queue.put_nowait(resp) async def _handle__worker_init_request(self, request): - try: - logger.info('Received WorkerInitRequest, ' - 'python version %s, ' - 'worker version %s, ' - 'request ID %s. ' - 'App Settings state: %s. ' - 'To enable debug level logging, please refer to ' - 'https://aka.ms/python-enable-debug-logging', - sys.version, - VERSION, - self.request_id, - get_python_appsetting_state() - ) - - worker_init_request = request.worker_init_request - host_capabilities = worker_init_request.capabilities - if constants.FUNCTION_DATA_CACHE in host_capabilities: - val = host_capabilities[constants.FUNCTION_DATA_CACHE] - self._function_data_cache_enabled = val == _TRUE - - capabilities = { - constants.RAW_HTTP_BODY_BYTES: _TRUE, - constants.TYPED_DATA_COLLECTION: _TRUE, - constants.RPC_HTTP_BODY_ONLY: _TRUE, - constants.WORKER_STATUS: _TRUE, - constants.RPC_HTTP_TRIGGER_METADATA_REMOVED: _TRUE, - constants.SHARED_MEMORY_DATA_TRANSFER: _TRUE, - } - - if DependencyManager.should_load_cx_dependencies(): - DependencyManager.prioritize_customer_dependencies() - - if DependencyManager.is_in_linux_consumption(): - import azure.functions # NoQA - - # loading bindings registry and saving results to a static - # dictionary which will be later used in the invocation request - bindings.load_binding_registry() + logger.info('Received WorkerInitRequest, ' + 'python version %s, ' + 'worker version %s, ' + 'request ID %s. ' + 'App Settings state: %s. ' + 'To enable debug level logging, please refer to ' + 'https://aka.ms/python-enable-debug-logging', + sys.version, + VERSION, + self.request_id, + get_python_appsetting_state() + ) + + worker_init_request = request.worker_init_request + host_capabilities = worker_init_request.capabilities + if constants.FUNCTION_DATA_CACHE in host_capabilities: + val = host_capabilities[constants.FUNCTION_DATA_CACHE] + self._function_data_cache_enabled = val == _TRUE + + capabilities = { + constants.RAW_HTTP_BODY_BYTES: _TRUE, + constants.TYPED_DATA_COLLECTION: _TRUE, + constants.RPC_HTTP_BODY_ONLY: _TRUE, + constants.WORKER_STATUS: _TRUE, + constants.RPC_HTTP_TRIGGER_METADATA_REMOVED: _TRUE, + constants.SHARED_MEMORY_DATA_TRANSFER: _TRUE, + } + + if DependencyManager.should_load_cx_dependencies(): + DependencyManager.prioritize_customer_dependencies() + + if DependencyManager.is_in_linux_consumption(): + import azure.functions # NoQA + + # loading bindings registry and saving results to a static + # dictionary which will be later used in the invocation request + bindings.load_binding_registry() + + if is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): + try: + self.load_function_metadata( + worker_init_request.function_app_directory, + caller_info="worker_init_request") + except Exception as ex: + self._function_metadata_exception = ex - if is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): - try: - self.load_function_metadata( - worker_init_request.function_app_directory, - caller_info="worker_init_request") - except Exception as ex: - self._function_metadata_exception = ex + if self._has_http_func: + from azure.functions.extension.base \ + import HttpV2FeatureChecker - if self._has_http_func: - from azure.functions.extension.base \ - import HttpV2FeatureChecker + if HttpV2FeatureChecker.http_v2_enabled(): + capabilities[constants.HTTP_URI] = \ + await self._initialize_http_server() - if HttpV2FeatureChecker.http_v2_enabled(): - capabilities[constants.HTTP_URI] = \ - await self._initialize_http_server() + return protos.StreamingMessage( + request_id=self.request_id, + worker_init_response=protos.WorkerInitResponse( + capabilities=capabilities, + worker_metadata=self.get_worker_metadata(), + result=protos.StatusResult( + status=protos.StatusResult.Success), + ), + ) - return protos.StreamingMessage( - request_id=self.request_id, - worker_init_response=protos.WorkerInitResponse( - capabilities=capabilities, - worker_metadata=self.get_worker_metadata(), - result=protos.StatusResult( - status=protos.StatusResult.Success), - ), - ) - except Exception as e: - logger.error("Error handling WorkerInitRequest: %s", str(e)) - return protos.StreamingMessage( - request_id=self.request_id, - worker_init_response=protos.WorkerInitResponse( - result=protos.StatusResult( - status=protos.StatusResult.Failure, - exception=self._serialize_exception(e)) - ), - ) async def _handle__worker_status_request(self, request): # Logging is not necessary in this request since the response is used diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index 8209ea80c..2ad7e4643 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -1,5 +1,6 @@ import abc import asyncio +import socket from typing import Dict @@ -151,4 +152,17 @@ def _pop_http_response(self, invoc_id): raise Exception("No http response found for invocation %s", invoc_id) +def get_unused_tcp_port(): + # Create a TCP socket + tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # Bind it to a free port provided by the OS + tcp_socket.bind(("", 0)) + # Get the port number + port = tcp_socket.getsockname()[1] + # Close the socket + tcp_socket.close() + # Return the port number + return port + + http_coordinator = HttpCoordinator() From b807e780c0ab946dc41a6262f225740eece4e4c0 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 5 Apr 2024 23:49:31 -0700 Subject: [PATCH 029/101] fix --- azure_functions_worker/dispatcher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 1175349fe..905a45af0 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -316,7 +316,8 @@ async def _handle__worker_init_request(self, request): except Exception as ex: self._function_metadata_exception = ex - if self._has_http_func: + if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION \ + and self._has_http_func: from azure.functions.extension.base \ import HttpV2FeatureChecker @@ -334,7 +335,6 @@ async def _handle__worker_init_request(self, request): ), ) - async def _handle__worker_status_request(self, request): # Logging is not necessary in this request since the response is used # for host to judge scale decisions of out-of-proc languages. From ffc7a1f0c469c6e8765b2981dda76cccab75b87f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 6 Apr 2024 01:16:39 -0700 Subject: [PATCH 030/101] fix --- azure_functions_worker/bindings/meta.py | 3 +-- tests/unittests/test_dispatcher.py | 10 ---------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index 7d79b0de4..778f85a33 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -20,8 +20,7 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION: import azure.functions.extension.base as ext_base - if ext_base is not None and \ - ext_base.HttpV2FeatureChecker.http_v2_enabled(): + if ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.RequestTrackerMeta.check_type(pytype) binding = get_binding(bind_name) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 37f23ea5f..52117dc21 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -590,16 +590,6 @@ class TestDispatcherStein(testutils.AsyncTestCase): def setUp(self): self._ctrl = testutils.start_mockhost( script_root=DISPATCHER_STEIN_FUNCTIONS_DIR) - self._pre_env = dict(os.environ) - self.mock_version_info = patch( - 'azure_functions_worker.dispatcher.sys.version_info', - SysVersionInfo(3, 9, 0, 'final', 0)) - self.mock_version_info.start() - - def tearDown(self): - os.environ.clear() - os.environ.update(self._pre_env) - self.mock_version_info.stop() async def test_dispatcher_functions_metadata_request(self): """Test if the functions metadata response will be sent correctly From f53ddac1246a7cf6966fbdc4b12dd65281dce25a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 6 Apr 2024 01:34:00 -0700 Subject: [PATCH 031/101] fix --- tests/unittests/test_enable_debug_logging_functions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unittests/test_enable_debug_logging_functions.py b/tests/unittests/test_enable_debug_logging_functions.py index 120d54dfe..6f3739809 100644 --- a/tests/unittests/test_enable_debug_logging_functions.py +++ b/tests/unittests/test_enable_debug_logging_functions.py @@ -65,6 +65,7 @@ class TestDebugLoggingDisabledFunctions(testutils.WebHostTestCase): """ @classmethod def setUpClass(cls): + cls._pre_env = dict(os.environ) os_environ = os.environ.copy() os_environ[PYTHON_ENABLE_DEBUG_LOGGING] = '0' cls._patch_environ = patch.dict('os.environ', os_environ) @@ -73,8 +74,9 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - os.environ.pop(PYTHON_ENABLE_DEBUG_LOGGING) super().tearDownClass() + os.environ.clear() + os.environ.update(cls._pre_env) cls._patch_environ.stop() @classmethod From d7d537be2ad636590cb3fb53e76057b08d7f99f3 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 6 Apr 2024 01:45:02 -0700 Subject: [PATCH 032/101] fix --- tests/unittests/test_http_functions_v2.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py index 49f931f92..cf0e2a166 100644 --- a/tests/unittests/test_http_functions_v2.py +++ b/tests/unittests/test_http_functions_v2.py @@ -18,6 +18,7 @@ class TestHttpFunctionsV2FastApi(testutils.WebHostTestCase): @classmethod def setUpClass(cls): + cls._pre_env = dict(os.environ) os_environ = os.environ.copy() # Turn on feature flag os_environ[PYTHON_ENABLE_INIT_INDEXING] = '1' @@ -28,6 +29,8 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): + os.environ.clear() + os.environ.update(cls._pre_env) cls._patch_environ.stop() super().tearDownClass() From 9682363d8235e5af34be241176d87a08ac0c08e1 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 6 Apr 2024 12:39:19 -0700 Subject: [PATCH 033/101] tests --- azure_functions_worker/http_v2.py | 5 -- tests/unittests/test_http_v2.py | 97 ++++++++++++++++++++++++++++++- 2 files changed, 96 insertions(+), 6 deletions(-) diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index 2ad7e4643..d04c198a3 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -16,7 +16,6 @@ def __init__(self, event_class, http_request=None, http_response=None, self._http_trigger_param_name = http_trigger_param_name self._http_request_available_event = event_class() self._http_response_available_event = event_class() - self._rpc_invocation_ready_event = event_class() @property def http_request(self): @@ -76,10 +75,6 @@ def http_request_available_event(self): def http_response_available_event(self): return self._http_response_available_event - @property - def rpc_invocation_ready_event(self): - return self._rpc_invocation_ready_event - class AsyncContextReference(BaseContextReference): def __init__(self, http_request=None, http_response=None, function=None, diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py index e9b77f2f6..4b9e91137 100644 --- a/tests/unittests/test_http_v2.py +++ b/tests/unittests/test_http_v2.py @@ -3,7 +3,8 @@ import unittest from unittest.mock import MagicMock -from azure_functions_worker.http_v2 import http_coordinator +from azure_functions_worker.http_v2 import http_coordinator, \ + BaseContextReference, AsyncContextReference, SingletonMeta class MockHttpRequest: @@ -141,3 +142,97 @@ async def test_await_http_response_async_response_not_set(self): await http_coordinator.await_http_response_async(invoc_id) self.assertEqual(str(context.exception), f"No http response found for invocation {invoc_id}") + +@unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7") +class TestAsyncContextReference(unittest.TestCase): + + def test_init(self): + ref = AsyncContextReference() + self.assertIsInstance(ref, AsyncContextReference) + self.assertTrue(ref.is_async) + + def test_http_request_property(self): + ref = AsyncContextReference() + ref.http_request = object() + self.assertIsNotNone(ref.http_request) + + def test_http_response_property(self): + ref = AsyncContextReference() + ref.http_response = object() + self.assertIsNotNone(ref.http_response) + + def test_function_property(self): + ref = AsyncContextReference() + ref.function = object() + self.assertIsNotNone(ref.function) + + def test_fi_context_property(self): + ref = AsyncContextReference() + ref.fi_context = object() + self.assertIsNotNone(ref.fi_context) + + def test_http_trigger_param_name_property(self): + ref = AsyncContextReference() + ref.http_trigger_param_name = object() + self.assertIsNotNone(ref.http_trigger_param_name) + + def test_args_property(self): + ref = AsyncContextReference() + ref.args = object() + self.assertIsNotNone(ref.args) + + def test_http_request_available_event_property(self): + ref = AsyncContextReference() + self.assertIsNotNone(ref.http_request_available_event) + + def test_http_response_available_event_property(self): + ref = AsyncContextReference() + self.assertIsNotNone(ref.http_response_available_event) + + def test_full_args(self): + ref = AsyncContextReference(http_request=object(), + http_response=object(), + function=object(), + fi_context=object(), + args=object()) + self.assertIsNotNone(ref.http_request) + self.assertIsNotNone(ref.http_response) + self.assertIsNotNone(ref.function) + self.assertIsNotNone(ref.fi_context) + self.assertIsNotNone(ref.args) + + +class TestSingletonMeta(unittest.TestCase): + + def test_singleton_instance(self): + class TestClass(metaclass=SingletonMeta): + pass + + obj1 = TestClass() + obj2 = TestClass() + + self.assertIs(obj1, obj2) + + def test_singleton_with_arguments(self): + class TestClass(metaclass=SingletonMeta): + def __init__(self, arg): + self.arg = arg + + obj1 = TestClass(1) + obj2 = TestClass(2) + + self.assertEqual(obj1.arg, 1) + self.assertEqual(obj2.arg, + 1) # Should still refer to the same instance + + def test_singleton_with_kwargs(self): + class TestClass(metaclass=SingletonMeta): + def __init__(self, **kwargs): + self.kwargs = kwargs + + obj1 = TestClass(a=1) + obj2 = TestClass(b=2) + + self.assertEqual(obj1.kwargs, {'a': 1}) + self.assertEqual(obj2.kwargs, + {'a': 1}) # Should still refer to the same instance \ No newline at end of file From a7505a0368d45bc89670b83d915e08227c1da8aa Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 6 Apr 2024 12:50:15 -0700 Subject: [PATCH 034/101] fix --- tests/unittests/test_http_v2.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py index 4b9e91137..89cd25ced 100644 --- a/tests/unittests/test_http_v2.py +++ b/tests/unittests/test_http_v2.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock from azure_functions_worker.http_v2 import http_coordinator, \ - BaseContextReference, AsyncContextReference, SingletonMeta + AsyncContextReference, SingletonMeta class MockHttpRequest: @@ -143,6 +143,7 @@ async def test_await_http_response_async_response_not_set(self): self.assertEqual(str(context.exception), f"No http response found for invocation {invoc_id}") + @unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7") class TestAsyncContextReference(unittest.TestCase): @@ -189,7 +190,7 @@ def test_http_response_available_event_property(self): ref = AsyncContextReference() self.assertIsNotNone(ref.http_response_available_event) - def test_full_args(self): + def test_full_args(self): ref = AsyncContextReference(http_request=object(), http_response=object(), function=object(), @@ -235,4 +236,4 @@ def __init__(self, **kwargs): self.assertEqual(obj1.kwargs, {'a': 1}) self.assertEqual(obj2.kwargs, - {'a': 1}) # Should still refer to the same instance \ No newline at end of file + {'a': 1}) # Should still refer to the same instance From 5f3565e982598cc463b1247d0936ad289ad288af Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 6 Apr 2024 14:15:26 -0700 Subject: [PATCH 035/101] test --- .../http_v2/fastapi/function_app.py | 10 ++++++++++ tests/unittests/test_dispatcher.py | 12 ++++++++++++ tests/unittests/test_http_v2.py | 4 ++++ 3 files changed, 26 insertions(+) create mode 100644 tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py diff --git a/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py b/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py new file mode 100644 index 000000000..f202890de --- /dev/null +++ b/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py @@ -0,0 +1,10 @@ +from azure.functions.extension.fastapi import Request, Response +import azure.functions as func + + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +@app.route(route="http_trigger") +def http_trigger(req: Request) -> Response: + return Response("ok") diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 52117dc21..6ed2ee306 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -27,6 +27,10 @@ DISPATCHER_STEIN_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / \ 'dispatcher_functions' / \ 'dispatcher_functions_stein' +DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / \ + 'dispatcher_functions' / \ + 'http_v2' / \ + 'fastapi' FUNCTION_APP_DIRECTORY = UNIT_TESTS_ROOT / 'dispatcher_functions' / \ 'dispatcher_functions_stein' @@ -616,6 +620,14 @@ async def test_dispatcher_functions_metadata_request_with_retry(self): protos.StatusResult.Success) +@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7") +class TestDispatcherHttpV2(TestDispatcherStein): + + def setUp(self): + self._ctrl = testutils.start_mockhost( + script_root=DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR) + + class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase): def setUp(self): diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py index 89cd25ced..f64291561 100644 --- a/tests/unittests/test_http_v2.py +++ b/tests/unittests/test_http_v2.py @@ -147,6 +147,10 @@ async def test_await_http_response_async_response_not_set(self): @unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7") class TestAsyncContextReference(unittest.TestCase): + def setUp(self): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + def test_init(self): ref = AsyncContextReference() self.assertIsInstance(ref, AsyncContextReference) From b702aaf5a5143e8f8878da7594a27cd547af4df9 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 6 Apr 2024 16:04:06 -0700 Subject: [PATCH 036/101] tests --- azure_functions_worker/bindings/meta.py | 9 ++++++--- tests/unittests/test_dispatcher.py | 22 ++++++++++++++++++++ tests/unittests/test_http_v2.py | 27 +++++++++++++++++++++++-- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index 778f85a33..8c4627ed9 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -9,7 +9,8 @@ from . import datumdef from . import generic from .shared_memory_data_transfer import SharedMemoryManager -from ..constants import BASE_EXT_SUPPORTED_PY_MINOR_VERSION +from ..constants import BASE_EXT_SUPPORTED_PY_MINOR_VERSION, \ + PYTHON_ENABLE_INIT_INDEXING PB_TYPE = 'rpc_data' PB_TYPE_DATA = 'data' @@ -18,7 +19,8 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: - if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION: + if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \ + is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): import azure.functions.extension.base as ext_base if ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.RequestTrackerMeta.check_type(pytype) @@ -28,7 +30,8 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool: - if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION: + if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \ + is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): import azure.functions.extension.base as ext_base if ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.ResponseTrackerMeta.check_type(pytype) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 6ed2ee306..eb512af1b 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -627,6 +627,28 @@ def setUp(self): self._ctrl = testutils.start_mockhost( script_root=DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR) + async def test_dispatcher_index_without_init_should_fail(self): + """Test if the functions metadata response will be sent correctly + when a functions metadata request is received + """ + async with self._ctrl as host: + await host.init_worker() + r = await host.get_functions_metadata() + self.assertIsInstance(r.response, protos.FunctionMetadataResponse) + self.assertFalse(r.response.use_default_metadata_indexing) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) + + @patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: 'true'}) + async def test_dispatcher_invoke_function(self): + async with self._ctrl as host: + await host.init_worker() + r = await host.get_functions_metadata() + self.assertIsInstance(r.response, protos.FunctionMetadataResponse) + self.assertFalse(r.response.use_default_metadata_indexing) + self.assertEqual(r.response.result.status, + protos.StatusResult.Success) + class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase): diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py index f64291561..1df51b992 100644 --- a/tests/unittests/test_http_v2.py +++ b/tests/unittests/test_http_v2.py @@ -1,10 +1,11 @@ import asyncio +import socket import sys import unittest -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from azure_functions_worker.http_v2 import http_coordinator, \ - AsyncContextReference, SingletonMeta + AsyncContextReference, SingletonMeta, get_unused_tcp_port class MockHttpRequest: @@ -241,3 +242,25 @@ def __init__(self, **kwargs): self.assertEqual(obj1.kwargs, {'a': 1}) self.assertEqual(obj2.kwargs, {'a': 1}) # Should still refer to the same instance + + +class TestGetUnusedTCPPort(unittest.TestCase): + + @patch('socket.socket') + def test_get_unused_tcp_port(self, mock_socket): + # Mock the socket object and its methods + mock_socket_instance = mock_socket.return_value + mock_socket_instance.getsockname.return_value = ('localhost', 12345) + + # Call the function + port = get_unused_tcp_port() + + # Assert that socket.socket was called with the correct arguments + mock_socket.assert_called_once_with(socket.AF_INET, socket.SOCK_STREAM) + + # Assert that bind and close methods were called on the socket instance + mock_socket_instance.bind.assert_called_once_with(('', 0)) + mock_socket_instance.close.assert_called_once() + + # Assert that the returned port matches the expected value + self.assertEqual(port, 12345) From 98df5dee2294939a114ee240f996f69ceeee5232 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 6 Apr 2024 16:08:11 -0700 Subject: [PATCH 037/101] fix --- azure_functions_worker/bindings/meta.py | 1 + 1 file changed, 1 insertion(+) diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index 8c4627ed9..b7daf5666 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -11,6 +11,7 @@ from .shared_memory_data_transfer import SharedMemoryManager from ..constants import BASE_EXT_SUPPORTED_PY_MINOR_VERSION, \ PYTHON_ENABLE_INIT_INDEXING +from ..utils.common import is_envvar_true PB_TYPE = 'rpc_data' PB_TYPE_DATA = 'data' From 462fad4857ad819813c53a64b62bcfb1e68ba682 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 6 Apr 2024 16:30:03 -0700 Subject: [PATCH 038/101] fix --- tests/unittests/test_dispatcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index eb512af1b..9b197454f 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -621,7 +621,7 @@ async def test_dispatcher_functions_metadata_request_with_retry(self): @unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7") -class TestDispatcherHttpV2(TestDispatcherStein): +class TestDispatcherHttpV2(testutils.AsyncTestCase): def setUp(self): self._ctrl = testutils.start_mockhost( From c76c42cd68613c2431f190f298561e94b9b8b6f9 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 6 Apr 2024 16:42:16 -0700 Subject: [PATCH 039/101] fix --- tests/unittests/test_dispatcher.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 9b197454f..34dcfe2e9 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -631,23 +631,16 @@ async def test_dispatcher_index_without_init_should_fail(self): """Test if the functions metadata response will be sent correctly when a functions metadata request is received """ - async with self._ctrl as host: - await host.init_worker() - r = await host.get_functions_metadata() - self.assertIsInstance(r.response, protos.FunctionMetadataResponse) - self.assertFalse(r.response.use_default_metadata_indexing) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - @patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: 'true'}) - async def test_dispatcher_invoke_function(self): - async with self._ctrl as host: - await host.init_worker() - r = await host.get_functions_metadata() - self.assertIsInstance(r.response, protos.FunctionMetadataResponse) - self.assertFalse(r.response.use_default_metadata_indexing) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) + env = {PYTHON_ENABLE_INIT_INDEXING: "0"} + with patch.dict(os.environ, env): + async with self._ctrl as host: + await host.init_worker() + r = await host.get_functions_metadata() + self.assertIsInstance(r.response, + protos.FunctionMetadataResponse) + self.assertFalse(r.response.use_default_metadata_indexing) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase): From 90a7db637c10deadb8e43bcd1b89f4f5d05453bb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 6 Apr 2024 16:49:09 -0700 Subject: [PATCH 040/101] move init server --- azure_functions_worker/dispatcher.py | 48 ++-------------------------- azure_functions_worker/http_v2.py | 45 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 45 deletions(-) diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 905a45af0..5af143eee 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -7,7 +7,6 @@ import asyncio import concurrent.futures -import importlib import logging import os import platform @@ -31,11 +30,10 @@ PYTHON_SCRIPT_FILE_NAME, PYTHON_SCRIPT_FILE_NAME_DEFAULT, PYTHON_LANGUAGE_RUNTIME, PYTHON_ENABLE_INIT_INDEXING, - X_MS_INVOCATION_ID, LOCAL_HOST, METADATA_PROPERTIES_WORKER_INDEXED, BASE_EXT_SUPPORTED_PY_MINOR_VERSION) from .extension import ExtensionManager -from .http_v2 import http_coordinator, get_unused_tcp_port +from .http_v2 import http_coordinator, initialize_http_server from .logging import disable_console_logging, enable_console_logging from .logging import (logger, error_logger, is_system_log_category, CONSOLE_LOG_PREFIX, format_exception) @@ -323,7 +321,7 @@ async def _handle__worker_init_request(self, request): if HttpV2FeatureChecker.http_v2_enabled(): capabilities[constants.HTTP_URI] = \ - await self._initialize_http_server() + await initialize_http_server() return protos.StreamingMessage( request_id=self.request_id, @@ -710,7 +708,7 @@ async def _handle__function_environment_reload_request(self, request): if HttpV2FeatureChecker.http_v2_enabled(): capabilities[constants.HTTP_URI] = \ - await self._initialize_http_server() + await initialize_http_server() # Change function app directory if getattr(func_env_reload_request, @@ -738,46 +736,6 @@ async def _handle__function_environment_reload_request(self, request): request_id=self.request_id, function_environment_reload_response=failure_response) - async def _initialize_http_server(self): - from azure.functions.extension.base \ - import ModuleTrackerMeta, RequestTrackerMeta - - web_extension_mod_name = ModuleTrackerMeta.get_module() - extension_module = importlib.import_module(web_extension_mod_name) - web_app_class = extension_module.WebApp - web_server_class = extension_module.WebServer - - unused_port = get_unused_tcp_port() - - app = web_app_class() - request_type = RequestTrackerMeta.get_request_type() - - @app.route - async def catch_all(request: request_type): # type: ignore - invoc_id = request.headers.get(X_MS_INVOCATION_ID) - if invoc_id is None: - raise ValueError(f"Header {X_MS_INVOCATION_ID} not found") - logger.info('Received HTTP request for invocation %s', invoc_id) - http_coordinator.set_http_request(invoc_id, request) - http_resp = \ - await http_coordinator.await_http_response_async(invoc_id) - - logger.info('Sending HTTP response for invocation %s', invoc_id) - # if http_resp is an python exception, raise it - if isinstance(http_resp, Exception): - raise http_resp - - return http_resp - - web_server = web_server_class(LOCAL_HOST, unused_port, app) - web_server_run_task = web_server.serve() - - loop = asyncio.get_event_loop() - loop.create_task(web_server_run_task) - logger.info('HTTP server starting on %s:%s', LOCAL_HOST, unused_port) - - return f"http://{LOCAL_HOST}:{unused_port}" - def index_functions(self, function_path: str): indexed_functions = loader.index_function_app(function_path) logger.info( diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index d04c198a3..788b0e8f0 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -1,8 +1,12 @@ import abc import asyncio +import importlib import socket from typing import Dict +from azure_functions_worker.constants import X_MS_INVOCATION_ID, LOCAL_HOST +from azure_functions_worker.logging import logger + class BaseContextReference(abc.ABC): def __init__(self, event_class, http_request=None, http_response=None, @@ -160,4 +164,45 @@ def get_unused_tcp_port(): return port +async def initialize_http_server(): + from azure.functions.extension.base \ + import ModuleTrackerMeta, RequestTrackerMeta + + web_extension_mod_name = ModuleTrackerMeta.get_module() + extension_module = importlib.import_module(web_extension_mod_name) + web_app_class = extension_module.WebApp + web_server_class = extension_module.WebServer + + unused_port = get_unused_tcp_port() + + app = web_app_class() + request_type = RequestTrackerMeta.get_request_type() + + @app.route + async def catch_all(request: request_type): # type: ignore + invoc_id = request.headers.get(X_MS_INVOCATION_ID) + if invoc_id is None: + raise ValueError(f"Header {X_MS_INVOCATION_ID} not found") + logger.info('Received HTTP request for invocation %s', invoc_id) + http_coordinator.set_http_request(invoc_id, request) + http_resp = \ + await http_coordinator.await_http_response_async(invoc_id) + + logger.info('Sending HTTP response for invocation %s', invoc_id) + # if http_resp is an python exception, raise it + if isinstance(http_resp, Exception): + raise http_resp + + return http_resp + + web_server = web_server_class(LOCAL_HOST, unused_port, app) + web_server_run_task = web_server.serve() + + loop = asyncio.get_event_loop() + loop.create_task(web_server_run_task) + logger.info('HTTP server starting on %s:%s', LOCAL_HOST, unused_port) + + return f"http://{LOCAL_HOST}:{unused_port}" + + http_coordinator = HttpCoordinator() From 988f9ca0e733b4d8b87c8a438ef78b894bd5b5eb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 6 Apr 2024 17:14:14 -0700 Subject: [PATCH 041/101] fix --- tests/unittests/test_dispatcher.py | 18 +++++++++++++++--- tests/utils/testutils.py | 7 +++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 34dcfe2e9..cfe7cfdca 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -624,13 +624,12 @@ async def test_dispatcher_functions_metadata_request_with_retry(self): class TestDispatcherHttpV2(testutils.AsyncTestCase): def setUp(self): + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) self._ctrl = testutils.start_mockhost( script_root=DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR) async def test_dispatcher_index_without_init_should_fail(self): - """Test if the functions metadata response will be sent correctly - when a functions metadata request is received - """ env = {PYTHON_ENABLE_INIT_INDEXING: "0"} with patch.dict(os.environ, env): async with self._ctrl as host: @@ -642,6 +641,19 @@ async def test_dispatcher_index_without_init_should_fail(self): self.assertEqual(r.response.result.status, protos.StatusResult.Failure) + async def test_dispatcher_index_with_init_should_pass(self): + env = {PYTHON_ENABLE_INIT_INDEXING: "1"} + sys.path.append(str(DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR)) + with patch.dict(os.environ, env): + async with self._ctrl as host: + await host.init_worker(include_func_app_dir=True) + r = await host.get_functions_metadata() + self.assertIsInstance(r.response, + protos.FunctionMetadataResponse) + self.assertFalse(r.response.use_default_metadata_indexing) + self.assertEqual(r.response.result.status, + protos.StatusResult.Success) + class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase): diff --git a/tests/utils/testutils.py b/tests/utils/testutils.py index 57946f1eb..ff76a5e21 100644 --- a/tests/utils/testutils.py +++ b/tests/utils/testutils.py @@ -529,11 +529,14 @@ def worker_id(self): def request_id(self): return self._request_id - async def init_worker(self, host_version: str = '4.28.0'): + async def init_worker(self, host_version: str = '4.28.0', **kwargs): + include_func_app_dir = kwargs.get('include_func_app_dir', False) r = await self.communicate( protos.StreamingMessage( worker_init_request=protos.WorkerInitRequest( - host_version=host_version + host_version=host_version, + function_app_directory= + str(self._scripts_dir) if include_func_app_dir else None, ) ), wait_for='worker_init_response' From 67f679dceedb89844969ff5060a5227118a9a741 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 6 Apr 2024 17:26:51 -0700 Subject: [PATCH 042/101] test --- tests/unittests/test_dispatcher.py | 14 ++++++++++++++ tests/utils/testutils.py | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index cfe7cfdca..69dff67fa 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -654,6 +654,20 @@ async def test_dispatcher_index_with_init_should_pass(self): self.assertEqual(r.response.result.status, protos.StatusResult.Success) + async def test_dispatcher_environment_reload_with_init_should_pass(self): + async with self._ctrl as host: + # Reload environment variable on specialization + r = await host.reload_environment( + environment={PYTHON_ENABLE_INIT_INDEXING: "1"}) + self.assertIsInstance(r.response, + protos.FunctionEnvironmentReloadResponse) + self.assertIsInstance(r.response.worker_metadata, + protos.WorkerMetadata) + self.assertEquals(r.response.worker_metadata.runtime_name, + "python") + self.assertEquals(r.response.worker_metadata.worker_version, + VERSION) + class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase): diff --git a/tests/utils/testutils.py b/tests/utils/testutils.py index ff76a5e21..bdc305291 100644 --- a/tests/utils/testutils.py +++ b/tests/utils/testutils.py @@ -535,8 +535,8 @@ async def init_worker(self, host_version: str = '4.28.0', **kwargs): protos.StreamingMessage( worker_init_request=protos.WorkerInitRequest( host_version=host_version, - function_app_directory= - str(self._scripts_dir) if include_func_app_dir else None, + function_app_directory=str( + self._scripts_dir) if include_func_app_dir else None, ) ), wait_for='worker_init_response' From c41db1630bad12d8193adf477cc3678041a97d72 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 00:10:23 -0700 Subject: [PATCH 043/101] update ppls --- .github/workflows/ci_consumption_workflow.yml | 13 ++++++++ .github/workflows/ci_e2e_workflow.yml | 29 ++++++++++++----- .github/workflows/ci_ut_workflow.yml | 31 +++++++++++++------ 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml index 172e19f7e..d4580c7f1 100644 --- a/.github/workflows/ci_consumption_workflow.yml +++ b/.github/workflows/ci_consumption_workflow.yml @@ -29,13 +29,26 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + - name: Get Date + id: get-date + run: | + echo "todayDate=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_ENV + shell: bash + - uses: actions/cache@v4 + id: cache-pip + with: + path: ${{ env.pythonLocation }} + key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}-${{ env.todayDate }}-${{ matrix.python-version }} - name: Install dependencies + if: steps.cache-pip.outputs.cache-hit != 'true' run: | python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] if [[ "${{ matrix.python-version }}" != "3.7" ]]; then python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] fi + - name: Install worker + run: | python setup.py build - name: Running 3.7 Tests if: matrix.python-version == 3.7 diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml index 6cbff4704..bbb20c88c 100644 --- a/.github/workflows/ci_e2e_workflow.yml +++ b/.github/workflows/ci_e2e_workflow.yml @@ -38,7 +38,27 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: "8.0.x" - - name: Install dependencies and the worker + - name: Get Date + id: get-date + run: | + echo "todayDate=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_ENV + shell: bash + - uses: actions/cache@v4 + id: cache-pip + with: + path: ${{ env.pythonLocation }} + key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}-${{ env.todayDate }}-${{ matrix.python-version }} + - name: Install dependencies + if: steps.cache-pip.outputs.cache-hit != 'true' + run: | + python -m pip install --upgrade pip + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre + python -m pip install -U -e .[dev] + # Conditionally install test dependencies for Python 3.8 and later + if [[ "${{ matrix.python-version }}" != "3.7" ]]; then + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] + fi + - name: Install worker run: | retry() { local -r -i max_attempts="$1"; shift @@ -57,13 +77,6 @@ jobs: done } - python -m pip install --upgrade pip - python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre - python -m pip install -U -e .[dev] - # Conditionally install test dependencies for Python 3.8 and later - if [[ "${{ matrix.python-version }}" != "3.7" ]]; then - python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] - fi # Retry a couple times to avoid certificate issue retry 5 python setup.py build retry 5 python setup.py webhost --branch-name=dev diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index 1c3777e7a..cb92a14f9 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -37,7 +37,27 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: "8.0.x" - - name: Install dependencies and the worker + - name: Get Date + id: get-date + run: | + echo "todayDate=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_ENV + shell: bash + - uses: actions/cache@v4 + id: cache-pip + with: + path: ${{ env.pythonLocation }} + key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}-${{ env.todayDate }}-${{ matrix.python-version }} + - name: Install dependencies + if: steps.cache-pip.outputs.cache-hit != 'true' + run: | + python -m pip install --upgrade pip + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre + python -m pip install -U -e .[dev] + # Conditionally install test dependencies for Python 3.8 and later + if [[ "${{ matrix.python-version }}" != "3.7" ]]; then + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] + fi + - name: Install the worker run: | retry() { local -r -i max_attempts="$1"; shift @@ -55,14 +75,7 @@ jobs: fi done } - - python -m pip install --upgrade pip - python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre - python -m pip install -U -e .[dev] - # Conditionally install test dependencies for Python 3.8 and later - if [[ "${{ matrix.python-version }}" != "3.7" ]]; then - python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] - fi + # Retry a couple times to avoid certificate issue retry 5 python setup.py build retry 5 python setup.py webhost --branch-name=dev From 3f0bfb620820a45af75f35afd65b2cfe3e678885 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 00:30:21 -0700 Subject: [PATCH 044/101] fix test --- tests/unittests/test_dispatcher.py | 12 ++++++------ tests/unittests/test_http_v2.py | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 69dff67fa..52bc1e8ce 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -71,9 +71,9 @@ async def test_dispatcher_initialize_worker(self): self.assertIsInstance(r.response, protos.WorkerInitResponse) self.assertIsInstance(r.response.worker_metadata, protos.WorkerMetadata) - self.assertEquals(r.response.worker_metadata.runtime_name, + self.assertEqual(r.response.worker_metadata.runtime_name, "python") - self.assertEquals(r.response.worker_metadata.worker_version, + self.assertEqual(r.response.worker_metadata.worker_version, VERSION) async def test_dispatcher_environment_reload(self): @@ -86,9 +86,9 @@ async def test_dispatcher_environment_reload(self): protos.FunctionEnvironmentReloadResponse) self.assertIsInstance(r.response.worker_metadata, protos.WorkerMetadata) - self.assertEquals(r.response.worker_metadata.runtime_name, + self.assertEqual(r.response.worker_metadata.runtime_name, "python") - self.assertEquals(r.response.worker_metadata.worker_version, + self.assertEqual(r.response.worker_metadata.worker_version, VERSION) async def test_dispatcher_initialize_worker_logging(self): @@ -663,9 +663,9 @@ async def test_dispatcher_environment_reload_with_init_should_pass(self): protos.FunctionEnvironmentReloadResponse) self.assertIsInstance(r.response.worker_metadata, protos.WorkerMetadata) - self.assertEquals(r.response.worker_metadata.runtime_name, + self.assertEqual(r.response.worker_metadata.runtime_name, "python") - self.assertEquals(r.response.worker_metadata.worker_version, + self.assertEqual(r.response.worker_metadata.worker_version, VERSION) diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py index 1df51b992..e149e0144 100644 --- a/tests/unittests/test_http_v2.py +++ b/tests/unittests/test_http_v2.py @@ -26,14 +26,14 @@ def setUp(self): def tearDown(self) -> None: http_coordinator._context_references.clear() - async def test_set_http_request_new_invocation(self): + def test_set_http_request_new_invocation(self): # Test setting a new HTTP request http_coordinator.set_http_request(self.invoc_id, self.http_request) context_ref = http_coordinator._context_references.get(self.invoc_id) self.assertIsNotNone(context_ref) self.assertEqual(context_ref.http_request, self.http_request) - async def test_set_http_request_existing_invocation(self): + def test_set_http_request_existing_invocation(self): # Test updating an existing HTTP request new_http_request = MagicMock() http_coordinator.set_http_request(self.invoc_id, new_http_request) @@ -41,20 +41,20 @@ async def test_set_http_request_existing_invocation(self): self.assertIsNotNone(context_ref) self.assertEqual(context_ref.http_request, new_http_request) - async def test_set_http_response_context_ref_null(self): + def test_set_http_response_context_ref_null(self): with self.assertRaises(Exception) as cm: http_coordinator.set_http_response(self.invoc_id, self.http_response) self.assertEqual(cm.exception.args[0], "No context reference found for invocation %s") - async def test_set_http_response(self): + def test_set_http_response(self): http_coordinator.set_http_request(self.invoc_id, self.http_request) http_coordinator.set_http_response(self.invoc_id, self.http_response) context_ref = http_coordinator._context_references[self.invoc_id] self.assertEqual(context_ref.http_response, self.http_response) - async def test_get_http_request_async_existing_invocation(self): + def test_get_http_request_async_existing_invocation(self): # Test retrieving an existing HTTP request http_coordinator.set_http_request(self.invoc_id, self.http_request) @@ -86,7 +86,7 @@ async def set_request_after_delay(): finally: loop.close() # Close the event loop when done - async def test_get_http_request_async_wait_forever(self): + def test_get_http_request_async_wait_forever(self): # Test handling error when invoc_id is not found invalid_invoc_id = "invalid_invocation" # Create a new event loop in the main thread @@ -103,7 +103,7 @@ async def test_get_http_request_async_wait_forever(self): finally: loop.close() - async def test_await_http_response_async_valid_invocation(self): + def test_await_http_response_async_valid_invocation(self): invoc_id = "valid_invocation" expected_response = self.http_response From 66f9edd52e17dcb77140f621ca72d8fc8b6078b3 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 01:13:14 -0700 Subject: [PATCH 045/101] fix tests --- azure_functions_worker/http_v2.py | 22 +++++----- tests/unittests/test_dispatcher.py | 12 +++--- tests/unittests/test_http_v2.py | 69 +++++++++++++----------------- 3 files changed, 48 insertions(+), 55 deletions(-) diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index 788b0e8f0..e1030248b 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -110,8 +110,8 @@ def set_http_request(self, invoc_id, http_request): def set_http_response(self, invoc_id, http_response): if invoc_id not in self._context_references: - raise Exception("No context reference found for invocation %s", - invoc_id) + raise Exception("No context reference found for invocation " + f"{invoc_id}") context_ref = self._context_references.get(invoc_id) context_ref.http_response = http_response @@ -126,8 +126,8 @@ async def get_http_request_async(self, invoc_id): async def await_http_response_async(self, invoc_id): if invoc_id not in self._context_references: - raise Exception("No context reference found for invocation %s", - invoc_id) + raise Exception("No context reference found for invocation " + f"{invoc_id}") await asyncio.sleep(0) await self._context_references.get( invoc_id).http_response_available_event.wait() @@ -140,7 +140,7 @@ def _pop_http_request(self, invoc_id): context_ref.http_request = None return request - raise Exception("No http request found for invocation %s", invoc_id) + raise Exception(f"No http request found for invocation {invoc_id}") def _pop_http_response(self, invoc_id): context_ref = self._context_references.get(invoc_id) @@ -148,7 +148,7 @@ def _pop_http_response(self, invoc_id): if response is not None: context_ref.http_response = None return response - raise Exception("No http response found for invocation %s", invoc_id) + raise Exception(f"No http response found for invocation {invoc_id}") def get_unused_tcp_port(): @@ -183,12 +183,12 @@ async def catch_all(request: request_type): # type: ignore invoc_id = request.headers.get(X_MS_INVOCATION_ID) if invoc_id is None: raise ValueError(f"Header {X_MS_INVOCATION_ID} not found") - logger.info('Received HTTP request for invocation %s', invoc_id) + logger.info(f'Received HTTP request for invocation {invoc_id}') http_coordinator.set_http_request(invoc_id, request) http_resp = \ await http_coordinator.await_http_response_async(invoc_id) - logger.info('Sending HTTP response for invocation %s', invoc_id) + logger.info(f'Sending HTTP response for invocation {invoc_id}') # if http_resp is an python exception, raise it if isinstance(http_resp, Exception): raise http_resp @@ -200,9 +200,11 @@ async def catch_all(request: request_type): # type: ignore loop = asyncio.get_event_loop() loop.create_task(web_server_run_task) - logger.info('HTTP server starting on %s:%s', LOCAL_HOST, unused_port) - return f"http://{LOCAL_HOST}:{unused_port}" + web_server_address = f"http://{LOCAL_HOST}:{unused_port}" + logger.info(f'HTTP server starting on {web_server_address}') + + return web_server_address http_coordinator = HttpCoordinator() diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 52bc1e8ce..2be8bf587 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -72,9 +72,9 @@ async def test_dispatcher_initialize_worker(self): self.assertIsInstance(r.response.worker_metadata, protos.WorkerMetadata) self.assertEqual(r.response.worker_metadata.runtime_name, - "python") + "python") self.assertEqual(r.response.worker_metadata.worker_version, - VERSION) + VERSION) async def test_dispatcher_environment_reload(self): """Test function environment reload response @@ -87,9 +87,9 @@ async def test_dispatcher_environment_reload(self): self.assertIsInstance(r.response.worker_metadata, protos.WorkerMetadata) self.assertEqual(r.response.worker_metadata.runtime_name, - "python") + "python") self.assertEqual(r.response.worker_metadata.worker_version, - VERSION) + VERSION) async def test_dispatcher_initialize_worker_logging(self): """Test if the dispatcher's log can be flushed out during worker @@ -664,9 +664,9 @@ async def test_dispatcher_environment_reload_with_init_should_pass(self): self.assertIsInstance(r.response.worker_metadata, protos.WorkerMetadata) self.assertEqual(r.response.worker_metadata.runtime_name, - "python") + "python") self.assertEqual(r.response.worker_metadata.worker_version, - VERSION) + VERSION) class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase): diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py index e149e0144..ab141120b 100644 --- a/tests/unittests/test_http_v2.py +++ b/tests/unittests/test_http_v2.py @@ -22,9 +22,12 @@ def setUp(self): self.invoc_id = "test_invocation" self.http_request = MockHttpRequest() self.http_response = MockHttpResponse() + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) def tearDown(self) -> None: http_coordinator._context_references.clear() + self.loop.close() def test_set_http_request_new_invocation(self): # Test setting a new HTTP request @@ -46,7 +49,8 @@ def test_set_http_response_context_ref_null(self): http_coordinator.set_http_response(self.invoc_id, self.http_response) self.assertEqual(cm.exception.args[0], - "No context reference found for invocation %s") + "No context reference found for invocation " + f"{self.invoc_id}") def test_set_http_response(self): http_coordinator.set_http_request(self.invoc_id, self.http_request) @@ -58,14 +62,9 @@ def test_get_http_request_async_existing_invocation(self): # Test retrieving an existing HTTP request http_coordinator.set_http_request(self.invoc_id, self.http_request) - loop = asyncio.new_event_loop() - - try: - retrieved_request = loop.run_until_complete( - http_coordinator.get_http_request_async(self.invoc_id)) - self.assertEqual(retrieved_request, self.http_request) - finally: - loop.close() + retrieved_request = self.loop.run_until_complete( + http_coordinator.get_http_request_async(self.invoc_id)) + self.assertEqual(retrieved_request, self.http_request) async def test_get_http_request_async_wait_for_request(self): # Test waiting for an HTTP request to become available @@ -73,58 +72,49 @@ async def set_request_after_delay(): await asyncio.sleep(1) http_coordinator.set_http_request(self.invoc_id, self.http_request) - # Create a new event loop in the main thread - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - # Run the test - try: - loop.run_until_complete(set_request_after_delay()) - retrieved_request = loop.run_until_complete( - http_coordinator.get_http_request_async(self.invoc_id)) - self.assertEqual(retrieved_request, self.http_request) - finally: - loop.close() # Close the event loop when done + self.loop.run_until_complete(set_request_after_delay()) + retrieved_request = self.loop.run_until_complete( + http_coordinator.get_http_request_async(self.invoc_id)) + self.assertEqual(retrieved_request, self.http_request) def test_get_http_request_async_wait_forever(self): # Test handling error when invoc_id is not found invalid_invoc_id = "invalid_invocation" - # Create a new event loop in the main thread - loop = asyncio.new_event_loop() - try: - with self.assertRaises(asyncio.TimeoutError): - loop.run_until_complete( - asyncio.wait_for( - http_coordinator.get_http_request_async( - invalid_invoc_id), - timeout=1 - ) + + with self.assertRaises(asyncio.TimeoutError): + self.loop.run_until_complete( + asyncio.wait_for( + http_coordinator.get_http_request_async( + invalid_invoc_id), + timeout=1 ) - finally: - loop.close() + ) def test_await_http_response_async_valid_invocation(self): invoc_id = "valid_invocation" expected_response = self.http_response - context_ref = {} - context_ref.http_response = expected_response + context_ref = AsyncContextReference(http_response=expected_response) # Add the mock context reference to the coordinator http_coordinator._context_references[invoc_id] = context_ref + http_coordinator.set_http_response(invoc_id, expected_response) + # Call the method and verify the returned response - response = await http_coordinator.await_http_response_async(invoc_id) + response = self.loop.run_until_complete( + http_coordinator.await_http_response_async(invoc_id)) self.assertEqual(response, expected_response) self.assertTrue( http_coordinator._context_references.get( invoc_id).http_response is None) - async def test_await_http_response_async_invalid_invocation(self): + def test_await_http_response_async_invalid_invocation(self): # Test handling error when invoc_id is not found invalid_invoc_id = "invalid_invocation" with self.assertRaises(Exception) as context: - await http_coordinator.await_http_response_async(invalid_invoc_id) + self.loop.run_until_complete( + http_coordinator.await_http_response_async(invalid_invoc_id)) self.assertEqual(str(context.exception), f"No context reference found for invocation " f"{invalid_invoc_id}") @@ -140,7 +130,8 @@ async def test_await_http_response_async_response_not_set(self): # Call the method and verify that it raises an exception with self.assertRaises(Exception) as context: - await http_coordinator.await_http_response_async(invoc_id) + self.loop.run_until_complete( + http_coordinator.await_http_response_async(invoc_id)) self.assertEqual(str(context.exception), f"No http response found for invocation {invoc_id}") From ddc5f390bcb1980146aa12786e7a031b80b2ae55 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 01:52:43 -0700 Subject: [PATCH 046/101] fix --- tests/unittests/test_http_v2.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py index ab141120b..1ab7af5e7 100644 --- a/tests/unittests/test_http_v2.py +++ b/tests/unittests/test_http_v2.py @@ -66,17 +66,6 @@ def test_get_http_request_async_existing_invocation(self): http_coordinator.get_http_request_async(self.invoc_id)) self.assertEqual(retrieved_request, self.http_request) - async def test_get_http_request_async_wait_for_request(self): - # Test waiting for an HTTP request to become available - async def set_request_after_delay(): - await asyncio.sleep(1) - http_coordinator.set_http_request(self.invoc_id, self.http_request) - - self.loop.run_until_complete(set_request_after_delay()) - retrieved_request = self.loop.run_until_complete( - http_coordinator.get_http_request_async(self.invoc_id)) - self.assertEqual(retrieved_request, self.http_request) - def test_get_http_request_async_wait_forever(self): # Test handling error when invoc_id is not found invalid_invoc_id = "invalid_invocation" @@ -119,15 +108,15 @@ def test_await_http_response_async_invalid_invocation(self): f"No context reference found for invocation " f"{invalid_invoc_id}") - async def test_await_http_response_async_response_not_set(self): + def test_await_http_response_async_response_not_set(self): invoc_id = "invocation_with_no_response" # Set up a mock context reference without setting the response - context_ref = {} - context_ref.http_response = None + context_ref = AsyncContextReference() # Add the mock context reference to the coordinator http_coordinator._context_references[invoc_id] = context_ref + http_coordinator.set_http_response(invoc_id, None) # Call the method and verify that it raises an exception with self.assertRaises(Exception) as context: self.loop.run_until_complete( @@ -140,8 +129,11 @@ async def test_await_http_response_async_response_not_set(self): class TestAsyncContextReference(unittest.TestCase): def setUp(self): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + def tearDown(self) -> None: + self.loop.close() def test_init(self): ref = AsyncContextReference() From 5ce390aee30949e9f5ca3764e4b374931aefff30 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 01:55:18 -0700 Subject: [PATCH 047/101] fix --- tests/unittests/test_dispatcher.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 2be8bf587..113534a30 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -629,6 +629,9 @@ def setUp(self): self._ctrl = testutils.start_mockhost( script_root=DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR) + def tearDown(self): + self.loop.close() + async def test_dispatcher_index_without_init_should_fail(self): env = {PYTHON_ENABLE_INIT_INDEXING: "0"} with patch.dict(os.environ, env): From bc9ca7f02436a52d958ac68c8510fbeaacaf2923 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 01:57:33 -0700 Subject: [PATCH 048/101] fix --- azure_functions_worker/bindings/meta.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index b7daf5666..14d7c132c 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -20,8 +20,7 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: - if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \ - is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): + if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION: import azure.functions.extension.base as ext_base if ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.RequestTrackerMeta.check_type(pytype) @@ -31,8 +30,7 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool: - if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \ - is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): + if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION: import azure.functions.extension.base as ext_base if ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.ResponseTrackerMeta.check_type(pytype) From 680ed45a3550c114cd763b0e97289af38b8b3e14 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 02:03:57 -0700 Subject: [PATCH 049/101] Revert "fix" This reverts commit bc9ca7f02436a52d958ac68c8510fbeaacaf2923. --- azure_functions_worker/bindings/meta.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index 14d7c132c..b7daf5666 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -20,7 +20,8 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: - if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION: + if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \ + is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): import azure.functions.extension.base as ext_base if ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.RequestTrackerMeta.check_type(pytype) @@ -30,7 +31,8 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool: - if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION: + if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \ + is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): import azure.functions.extension.base as ext_base if ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.ResponseTrackerMeta.check_type(pytype) From 526765f74fa9f8ffe7fb894e93255d96c738ca94 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 11:24:15 -0700 Subject: [PATCH 050/101] skip --- tests/unittests/test_dispatcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 113534a30..5d2da0050 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -620,7 +620,7 @@ async def test_dispatcher_functions_metadata_request_with_retry(self): protos.StatusResult.Success) -@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7") +@unittest.skip class TestDispatcherHttpV2(testutils.AsyncTestCase): def setUp(self): From 45217415ae380a128c18824ad617750a398531aa Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 13:31:44 -0700 Subject: [PATCH 051/101] fix tests --- azure_functions_worker/dispatcher.py | 4 +-- azure_functions_worker/http_v2.py | 2 +- tests/unittests/test_dispatcher.py | 42 ++++++++++++++++++---------- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 5af143eee..6899a7929 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -321,7 +321,7 @@ async def _handle__worker_init_request(self, request): if HttpV2FeatureChecker.http_v2_enabled(): capabilities[constants.HTTP_URI] = \ - await initialize_http_server() + initialize_http_server() return protos.StreamingMessage( request_id=self.request_id, @@ -708,7 +708,7 @@ async def _handle__function_environment_reload_request(self, request): if HttpV2FeatureChecker.http_v2_enabled(): capabilities[constants.HTTP_URI] = \ - await initialize_http_server() + initialize_http_server() # Change function app directory if getattr(func_env_reload_request, diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index e1030248b..91758d0a2 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -164,7 +164,7 @@ def get_unused_tcp_port(): return port -async def initialize_http_server(): +def initialize_http_server(): from azure.functions.extension.base \ import ModuleTrackerMeta, RequestTrackerMeta diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 5d2da0050..24484a0a5 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -21,6 +21,7 @@ from tests.utils import testutils from tests.utils.testutils import UNIT_TESTS_ROOT + SysVersionInfo = col.namedtuple("VersionInfo", ["major", "minor", "micro", "releaselevel", "serial"]) DISPATCHER_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / 'dispatcher_functions' @@ -620,8 +621,10 @@ async def test_dispatcher_functions_metadata_request_with_retry(self): protos.StatusResult.Success) -@unittest.skip +@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7") class TestDispatcherHttpV2(testutils.AsyncTestCase): + def return_mock_url(*args, **kwargs): + return 'http://1.0.0.0' def setUp(self): self.loop = asyncio.new_event_loop() @@ -644,9 +647,13 @@ async def test_dispatcher_index_without_init_should_fail(self): self.assertEqual(r.response.result.status, protos.StatusResult.Failure) - async def test_dispatcher_index_with_init_should_pass(self): + @patch('azure_functions_worker.dispatcher.initialize_http_server') + async def test_dispatcher_index_with_init_should_pass( + self, mock_initiate_http_server): + + mock_initiate_http_server.side_effect = self.return_mock_url env = {PYTHON_ENABLE_INIT_INDEXING: "1"} - sys.path.append(str(DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR)) + with patch.dict(os.environ, env): async with self._ctrl as host: await host.init_worker(include_func_app_dir=True) @@ -657,11 +664,15 @@ async def test_dispatcher_index_with_init_should_pass(self): self.assertEqual(r.response.result.status, protos.StatusResult.Success) - async def test_dispatcher_environment_reload_with_init_should_pass(self): + @patch('azure_functions_worker.dispatcher.initialize_http_server') + async def test_dispatcher_environment_reload_with_init_should_pass( + self, mock_initiate_http_server): + mock_initiate_http_server.side_effect = self.return_mock_url async with self._ctrl as host: # Reload environment variable on specialization r = await host.reload_environment( - environment={PYTHON_ENABLE_INIT_INDEXING: "1"}) + environment={PYTHON_ENABLE_INIT_INDEXING: "1"}, + function_project_path=str(host._scripts_dir)) self.assertIsInstance(r.response, protos.FunctionEnvironmentReloadResponse) self.assertIsInstance(r.response.worker_metadata, @@ -729,7 +740,8 @@ async def test_dispatcher_load_azfunc_in_init(self): ) self.assertEqual( len([log for log in r.logs if log.message.startswith( - "Received WorkerMetadataRequest from _handle__worker_init_request" + "Received WorkerMetadataRequest from " + "_handle__worker_init_request" )]), 0 ) @@ -822,7 +834,6 @@ def tearDown(self): @patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: 'true'}) def test_worker_init_request_with_indexing_enabled(self): - request = protos.StreamingMessage( worker_init_request=protos.WorkerInitRequest( host_version="2.3.4", @@ -891,10 +902,12 @@ def test_functions_metadata_request_with_init_indexing_enabled(self): protos.StatusResult.Success) metadata_response = self.loop.run_until_complete( - self.dispatcher._handle__functions_metadata_request(metadata_request)) + self.dispatcher._handle__functions_metadata_request( + metadata_request)) - self.assertEqual(metadata_response.function_metadata_response.result.status, - protos.StatusResult.Success) + self.assertEqual( + metadata_response.function_metadata_response.result.status, + protos.StatusResult.Success) self.assertIsNotNone(self.dispatcher._function_metadata_result) self.assertIsNone(self.dispatcher._function_metadata_exception) @@ -921,10 +934,12 @@ def test_functions_metadata_request_with_init_indexing_disabled(self): self.assertIsNone(self.dispatcher._function_metadata_exception) metadata_response = self.loop.run_until_complete( - self.dispatcher._handle__functions_metadata_request(metadata_request)) + self.dispatcher._handle__functions_metadata_request( + metadata_request)) - self.assertEqual(metadata_response.function_metadata_response.result.status, - protos.StatusResult.Success) + self.assertEqual( + metadata_response.function_metadata_response.result.status, + protos.StatusResult.Success) self.assertIsNotNone(self.dispatcher._function_metadata_result) self.assertIsNone(self.dispatcher._function_metadata_exception) @@ -933,7 +948,6 @@ def test_functions_metadata_request_with_init_indexing_disabled(self): def test_functions_metadata_request_with_indexing_exception( self, mock_index_functions): - mock_index_functions.side_effect = Exception("Mocked Exception") request = protos.StreamingMessage( From f5fff784b4721417d1911ab5441d04e98f6ad4df Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 13:41:36 -0700 Subject: [PATCH 052/101] style --- azure_functions_worker/dispatcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 6899a7929..161a74284 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -321,7 +321,7 @@ async def _handle__worker_init_request(self, request): if HttpV2FeatureChecker.http_v2_enabled(): capabilities[constants.HTTP_URI] = \ - initialize_http_server() + initialize_http_server() return protos.StreamingMessage( request_id=self.request_id, From 3e0075da484cc430b2590dc3dee4ea4ae33bb735 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 13:47:27 -0700 Subject: [PATCH 053/101] test --- tests/unittests/test_dispatcher.py | 56 +++++++++++++++--------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 24484a0a5..8474bcf00 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -635,34 +635,34 @@ def setUp(self): def tearDown(self): self.loop.close() - async def test_dispatcher_index_without_init_should_fail(self): - env = {PYTHON_ENABLE_INIT_INDEXING: "0"} - with patch.dict(os.environ, env): - async with self._ctrl as host: - await host.init_worker() - r = await host.get_functions_metadata() - self.assertIsInstance(r.response, - protos.FunctionMetadataResponse) - self.assertFalse(r.response.use_default_metadata_indexing) - self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - @patch('azure_functions_worker.dispatcher.initialize_http_server') - async def test_dispatcher_index_with_init_should_pass( - self, mock_initiate_http_server): - - mock_initiate_http_server.side_effect = self.return_mock_url - env = {PYTHON_ENABLE_INIT_INDEXING: "1"} - - with patch.dict(os.environ, env): - async with self._ctrl as host: - await host.init_worker(include_func_app_dir=True) - r = await host.get_functions_metadata() - self.assertIsInstance(r.response, - protos.FunctionMetadataResponse) - self.assertFalse(r.response.use_default_metadata_indexing) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) + # async def test_dispatcher_index_without_init_should_fail(self): + # env = {PYTHON_ENABLE_INIT_INDEXING: "0"} + # with patch.dict(os.environ, env): + # async with self._ctrl as host: + # await host.init_worker() + # r = await host.get_functions_metadata() + # self.assertIsInstance(r.response, + # protos.FunctionMetadataResponse) + # self.assertFalse(r.response.use_default_metadata_indexing) + # self.assertEqual(r.response.result.status, + # protos.StatusResult.Failure) + + # @patch('azure_functions_worker.dispatcher.initialize_http_server') + # async def test_dispatcher_index_with_init_should_pass( + # self, mock_initiate_http_server): + # + # mock_initiate_http_server.side_effect = self.return_mock_url + # env = {PYTHON_ENABLE_INIT_INDEXING: "1"} + # + # with patch.dict(os.environ, env): + # async with self._ctrl as host: + # await host.init_worker(include_func_app_dir=True) + # r = await host.get_functions_metadata() + # self.assertIsInstance(r.response, + # protos.FunctionMetadataResponse) + # self.assertFalse(r.response.use_default_metadata_indexing) + # self.assertEqual(r.response.result.status, + # protos.StatusResult.Success) @patch('azure_functions_worker.dispatcher.initialize_http_server') async def test_dispatcher_environment_reload_with_init_should_pass( From b96f74d443873ecd2fa4136ace8a28a0cbf45efc Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 13:55:26 -0700 Subject: [PATCH 054/101] test --- tests/unittests/test_dispatcher.py | 56 +++++++++++++++--------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 8474bcf00..6958a0bd6 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -635,17 +635,17 @@ def setUp(self): def tearDown(self): self.loop.close() - # async def test_dispatcher_index_without_init_should_fail(self): - # env = {PYTHON_ENABLE_INIT_INDEXING: "0"} - # with patch.dict(os.environ, env): - # async with self._ctrl as host: - # await host.init_worker() - # r = await host.get_functions_metadata() - # self.assertIsInstance(r.response, - # protos.FunctionMetadataResponse) - # self.assertFalse(r.response.use_default_metadata_indexing) - # self.assertEqual(r.response.result.status, - # protos.StatusResult.Failure) + async def test_dispatcher_index_without_init_should_fail(self): + env = {PYTHON_ENABLE_INIT_INDEXING: "0"} + with patch.dict(os.environ, env): + async with self._ctrl as host: + await host.init_worker() + r = await host.get_functions_metadata() + self.assertIsInstance(r.response, + protos.FunctionMetadataResponse) + self.assertFalse(r.response.use_default_metadata_indexing) + self.assertEqual(r.response.result.status, + protos.StatusResult.Failure) # @patch('azure_functions_worker.dispatcher.initialize_http_server') # async def test_dispatcher_index_with_init_should_pass( @@ -664,23 +664,23 @@ def tearDown(self): # self.assertEqual(r.response.result.status, # protos.StatusResult.Success) - @patch('azure_functions_worker.dispatcher.initialize_http_server') - async def test_dispatcher_environment_reload_with_init_should_pass( - self, mock_initiate_http_server): - mock_initiate_http_server.side_effect = self.return_mock_url - async with self._ctrl as host: - # Reload environment variable on specialization - r = await host.reload_environment( - environment={PYTHON_ENABLE_INIT_INDEXING: "1"}, - function_project_path=str(host._scripts_dir)) - self.assertIsInstance(r.response, - protos.FunctionEnvironmentReloadResponse) - self.assertIsInstance(r.response.worker_metadata, - protos.WorkerMetadata) - self.assertEqual(r.response.worker_metadata.runtime_name, - "python") - self.assertEqual(r.response.worker_metadata.worker_version, - VERSION) + # @patch('azure_functions_worker.dispatcher.initialize_http_server') + # async def test_dispatcher_environment_reload_with_init_should_pass( + # self, mock_initiate_http_server): + # mock_initiate_http_server.side_effect = self.return_mock_url + # async with self._ctrl as host: + # # Reload environment variable on specialization + # r = await host.reload_environment( + # environment={PYTHON_ENABLE_INIT_INDEXING: "1"}, + # function_project_path=str(host._scripts_dir)) + # self.assertIsInstance(r.response, + # protos.FunctionEnvironmentReloadResponse) + # self.assertIsInstance(r.response.worker_metadata, + # protos.WorkerMetadata) + # self.assertEqual(r.response.worker_metadata.runtime_name, + # "python") + # self.assertEqual(r.response.worker_metadata.worker_version, + # VERSION) class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase): From e68b41b1a9029cb6fb4339b9d9550a585416430c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 14:07:38 -0700 Subject: [PATCH 055/101] ff --- tests/unittests/test_dispatcher.py | 67 +++++++++++++----------------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 6958a0bd6..ef702cd5f 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -635,52 +635,41 @@ def setUp(self): def tearDown(self): self.loop.close() - async def test_dispatcher_index_without_init_should_fail(self): - env = {PYTHON_ENABLE_INIT_INDEXING: "0"} + + @patch('azure_functions_worker.dispatcher.initialize_http_server') + async def test_dispatcher_index_with_init_should_pass( + self, mock_initiate_http_server): + + mock_initiate_http_server.side_effect = self.return_mock_url + env = {PYTHON_ENABLE_INIT_INDEXING: "1"} + with patch.dict(os.environ, env): async with self._ctrl as host: - await host.init_worker() + await host.init_worker(include_func_app_dir=True) r = await host.get_functions_metadata() self.assertIsInstance(r.response, protos.FunctionMetadataResponse) self.assertFalse(r.response.use_default_metadata_indexing) self.assertEqual(r.response.result.status, - protos.StatusResult.Failure) - - # @patch('azure_functions_worker.dispatcher.initialize_http_server') - # async def test_dispatcher_index_with_init_should_pass( - # self, mock_initiate_http_server): - # - # mock_initiate_http_server.side_effect = self.return_mock_url - # env = {PYTHON_ENABLE_INIT_INDEXING: "1"} - # - # with patch.dict(os.environ, env): - # async with self._ctrl as host: - # await host.init_worker(include_func_app_dir=True) - # r = await host.get_functions_metadata() - # self.assertIsInstance(r.response, - # protos.FunctionMetadataResponse) - # self.assertFalse(r.response.use_default_metadata_indexing) - # self.assertEqual(r.response.result.status, - # protos.StatusResult.Success) - - # @patch('azure_functions_worker.dispatcher.initialize_http_server') - # async def test_dispatcher_environment_reload_with_init_should_pass( - # self, mock_initiate_http_server): - # mock_initiate_http_server.side_effect = self.return_mock_url - # async with self._ctrl as host: - # # Reload environment variable on specialization - # r = await host.reload_environment( - # environment={PYTHON_ENABLE_INIT_INDEXING: "1"}, - # function_project_path=str(host._scripts_dir)) - # self.assertIsInstance(r.response, - # protos.FunctionEnvironmentReloadResponse) - # self.assertIsInstance(r.response.worker_metadata, - # protos.WorkerMetadata) - # self.assertEqual(r.response.worker_metadata.runtime_name, - # "python") - # self.assertEqual(r.response.worker_metadata.worker_version, - # VERSION) + protos.StatusResult.Success) + + @patch('azure_functions_worker.dispatcher.initialize_http_server') + async def test_dispatcher_environment_reload_with_init_should_pass( + self, mock_initiate_http_server): + mock_initiate_http_server.side_effect = self.return_mock_url + async with self._ctrl as host: + # Reload environment variable on specialization + r = await host.reload_environment( + environment={PYTHON_ENABLE_INIT_INDEXING: "1"}, + function_project_path=str(host._scripts_dir)) + self.assertIsInstance(r.response, + protos.FunctionEnvironmentReloadResponse) + self.assertIsInstance(r.response.worker_metadata, + protos.WorkerMetadata) + self.assertEqual(r.response.worker_metadata.runtime_name, + "python") + self.assertEqual(r.response.worker_metadata.worker_version, + VERSION) class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase): From a58268840ee9dfc475a30bcf11e105a2308371e6 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 14:45:17 -0700 Subject: [PATCH 056/101] test --- tests/unittests/test_dispatcher.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index ef702cd5f..be04066e1 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -627,14 +627,9 @@ def return_mock_url(*args, **kwargs): return 'http://1.0.0.0' def setUp(self): - self.loop = asyncio.new_event_loop() - asyncio.set_event_loop(self.loop) self._ctrl = testutils.start_mockhost( script_root=DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR) - def tearDown(self): - self.loop.close() - @patch('azure_functions_worker.dispatcher.initialize_http_server') async def test_dispatcher_index_with_init_should_pass( From d9bd32c36373bfcc7c641977e0b424c1be3b4e3f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 14:45:32 -0700 Subject: [PATCH 057/101] style --- tests/unittests/test_dispatcher.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index be04066e1..91cbab44e 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -630,7 +630,6 @@ def setUp(self): self._ctrl = testutils.start_mockhost( script_root=DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR) - @patch('azure_functions_worker.dispatcher.initialize_http_server') async def test_dispatcher_index_with_init_should_pass( self, mock_initiate_http_server): From d664f655db6e0f6955e107871ea04d8288874dcd Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 15:28:26 -0700 Subject: [PATCH 058/101] skip --- tests/unittests/test_dispatcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 91cbab44e..5fa0b1719 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -621,7 +621,7 @@ async def test_dispatcher_functions_metadata_request_with_retry(self): protos.StatusResult.Success) -@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7") +@unittest.skip class TestDispatcherHttpV2(testutils.AsyncTestCase): def return_mock_url(*args, **kwargs): return 'http://1.0.0.0' From 7bd566c125ae81d0a489a51b0c643d98f9e48956 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 15:47:05 -0700 Subject: [PATCH 059/101] test --- .github/workflows/ci_ut_workflow.yml | 3 ++- tests/unittests/test_http_functions_v2.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index cb92a14f9..b86dcd53e 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -86,7 +86,8 @@ jobs: AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString310 }} # needed for installing azure-functions-durable while running setup.py ARCHIVE_WEBHOST_LOGS: ${{ github.event.inputs.archive_webhost_logging }} run: | - python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests + python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch -m "not serial" tests/unittests + python -m pytest -q --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch -m "serial" tests/unittests - name: Codecov uses: codecov/codecov-action@v3 with: diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py index cf0e2a166..380d0b21c 100644 --- a/tests/unittests/test_http_functions_v2.py +++ b/tests/unittests/test_http_functions_v2.py @@ -12,7 +12,7 @@ from azure_functions_worker.constants import PYTHON_ENABLE_INIT_INDEXING from tests.utils import testutils - +import pytest @unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7") class TestHttpFunctionsV2FastApi(testutils.WebHostTestCase): @@ -201,6 +201,7 @@ def test_accept_json(self): self.assertEqual(r_json, {'a': 'abc', 'd': 42}) self.assertEqual(r.headers['content-type'], 'application/json') + @pytest.mark.serial def test_unhandled_error(self): r = self.webhost.request('GET', 'unhandled_error') self.assertEqual(r.status_code, 500) From 863f82bae99f24d6c6b5476cf8aa1fc08fb9e97b Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 15:53:08 -0700 Subject: [PATCH 060/101] fix --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..56e02198d --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +markers = + serial: mark test as a serial test From e220fc912e5a2e040c3f9e0b145636f5b8797b11 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 15:54:16 -0700 Subject: [PATCH 061/101] fix --- tests/unittests/test_http_functions_v2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py index 380d0b21c..ef4e9a073 100644 --- a/tests/unittests/test_http_functions_v2.py +++ b/tests/unittests/test_http_functions_v2.py @@ -14,6 +14,7 @@ from tests.utils import testutils import pytest + @unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7") class TestHttpFunctionsV2FastApi(testutils.WebHostTestCase): @classmethod From 0ea9207ebadb26dfde13398d3465276e770f7166 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 16:04:45 -0700 Subject: [PATCH 062/101] fix --- .github/workflows/ci_ut_workflow.yml | 3 +-- tests/unittests/test_http_functions_v2.py | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index b86dcd53e..cb92a14f9 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -86,8 +86,7 @@ jobs: AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString310 }} # needed for installing azure-functions-durable while running setup.py ARCHIVE_WEBHOST_LOGS: ${{ github.event.inputs.archive_webhost_logging }} run: | - python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch -m "not serial" tests/unittests - python -m pytest -q --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch -m "serial" tests/unittests + python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests - name: Codecov uses: codecov/codecov-action@v3 with: diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py index ef4e9a073..be2eeb136 100644 --- a/tests/unittests/test_http_functions_v2.py +++ b/tests/unittests/test_http_functions_v2.py @@ -12,7 +12,6 @@ from azure_functions_worker.constants import PYTHON_ENABLE_INIT_INDEXING from tests.utils import testutils -import pytest @unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7") @@ -202,7 +201,6 @@ def test_accept_json(self): self.assertEqual(r_json, {'a': 'abc', 'd': 42}) self.assertEqual(r.headers['content-type'], 'application/json') - @pytest.mark.serial def test_unhandled_error(self): r = self.webhost.request('GET', 'unhandled_error') self.assertEqual(r.status_code, 500) @@ -211,7 +209,7 @@ def test_unhandled_error(self): def check_log_unhandled_error(self, host_out: typing.List[str]): - self.assertIn('Exception: ZeroDivisionError: division by zero', + self.assertIn('ZeroDivisionError: division by zero', host_out) def test_unhandled_urllib_error(self): From 7e895282a943e1c79c6fc3d173cdc3f7327bde1a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 16:40:42 -0700 Subject: [PATCH 063/101] replay --- .github/workflows/ci_ut_workflow.yml | 8 +++++++- setup.py | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index cb92a14f9..dd567466a 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -86,7 +86,7 @@ jobs: AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString310 }} # needed for installing azure-functions-durable while running setup.py ARCHIVE_WEBHOST_LOGS: ${{ github.event.inputs.archive_webhost_logging }} run: | - python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests + python -m pytest -q -n auto --replay-record-dir=build/tests/replay --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests - name: Codecov uses: codecov/codecov-action@v3 with: @@ -101,3 +101,9 @@ jobs: name: Test WebHost Logs ${{ github.run_id }} ${{ matrix.python-version }} path: logs/*.log if-no-files-found: ignore + - name: Publish replays to Artifact + uses: actions/upload-artifact@v4 + with: + name: Test Replays ${{ github.run_id }} ${{ matrix.python-version }} + path: build/tests/replay + if-no-files-found: ignore diff --git a/setup.py b/setup.py index e82b5eb33..bf60c456a 100644 --- a/setup.py +++ b/setup.py @@ -102,6 +102,7 @@ "pytest-randomly", "pytest-instafail", "pytest-rerunfailures", + "pytest-replay", "ptvsd", "python-dotenv", "plotly", From 43b346cb97b38c945a91e5d602fea257c1ef862d Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 16:55:51 -0700 Subject: [PATCH 064/101] f --- .github/workflows/ci_ut_workflow.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index dd567466a..0fe952d1a 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -102,6 +102,7 @@ jobs: path: logs/*.log if-no-files-found: ignore - name: Publish replays to Artifact + if: failure() uses: actions/upload-artifact@v4 with: name: Test Replays ${{ github.run_id }} ${{ matrix.python-version }} From 60553788f4ff658a5a11f4465b2132301149249b Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 7 Apr 2024 21:32:22 -0700 Subject: [PATCH 065/101] fix --- tests/unittests/test_http_functions_v2.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py index be2eeb136..45428a6b7 100644 --- a/tests/unittests/test_http_functions_v2.py +++ b/tests/unittests/test_http_functions_v2.py @@ -209,8 +209,13 @@ def test_unhandled_error(self): def check_log_unhandled_error(self, host_out: typing.List[str]): - self.assertIn('ZeroDivisionError: division by zero', - host_out) + error_substring = 'ZeroDivisionError: division by zero' + for item in host_out: + if error_substring in item: + break + else: + self.fail( + f"{error_substring}' not found in host log.") def test_unhandled_urllib_error(self): r = self.webhost.request( From 0a8f6dfa05d1ac5bfd5fd2f7f6f625a969cb10b3 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 8 Apr 2024 00:59:16 -0700 Subject: [PATCH 066/101] codecov fix --- .github/workflows/ci_ut_workflow.yml | 3 +- tests/unittests/test_dispatcher.py | 45 ---------------------------- 2 files changed, 2 insertions(+), 46 deletions(-) diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index 0fe952d1a..78e93eea4 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -88,12 +88,13 @@ jobs: run: | python -m pytest -q -n auto --replay-record-dir=build/tests/replay --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests - name: Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml # optional flags: unittests # optional name: codecov # optional fail_ci_if_error: false # optional (default = false) + token: ${{ secrets.CODECOV_TOKEN }} - name: Publish Logs to Artifact if: failure() uses: actions/upload-artifact@v4 diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 5fa0b1719..dc62acb8e 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -621,51 +621,6 @@ async def test_dispatcher_functions_metadata_request_with_retry(self): protos.StatusResult.Success) -@unittest.skip -class TestDispatcherHttpV2(testutils.AsyncTestCase): - def return_mock_url(*args, **kwargs): - return 'http://1.0.0.0' - - def setUp(self): - self._ctrl = testutils.start_mockhost( - script_root=DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR) - - @patch('azure_functions_worker.dispatcher.initialize_http_server') - async def test_dispatcher_index_with_init_should_pass( - self, mock_initiate_http_server): - - mock_initiate_http_server.side_effect = self.return_mock_url - env = {PYTHON_ENABLE_INIT_INDEXING: "1"} - - with patch.dict(os.environ, env): - async with self._ctrl as host: - await host.init_worker(include_func_app_dir=True) - r = await host.get_functions_metadata() - self.assertIsInstance(r.response, - protos.FunctionMetadataResponse) - self.assertFalse(r.response.use_default_metadata_indexing) - self.assertEqual(r.response.result.status, - protos.StatusResult.Success) - - @patch('azure_functions_worker.dispatcher.initialize_http_server') - async def test_dispatcher_environment_reload_with_init_should_pass( - self, mock_initiate_http_server): - mock_initiate_http_server.side_effect = self.return_mock_url - async with self._ctrl as host: - # Reload environment variable on specialization - r = await host.reload_environment( - environment={PYTHON_ENABLE_INIT_INDEXING: "1"}, - function_project_path=str(host._scripts_dir)) - self.assertIsInstance(r.response, - protos.FunctionEnvironmentReloadResponse) - self.assertIsInstance(r.response.worker_metadata, - protos.WorkerMetadata) - self.assertEqual(r.response.worker_metadata.runtime_name, - "python") - self.assertEqual(r.response.worker_metadata.worker_version, - VERSION) - - class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase): def setUp(self): From 265c821c21c163e7ecf4eaceeb9c95ac04829dd4 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 8 Apr 2024 15:52:20 -0700 Subject: [PATCH 067/101] Mount base extension to fix consumption test failures --- tests/utils/testutils_lc.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/utils/testutils_lc.py b/tests/utils/testutils_lc.py index 10c5079a9..19c81f15d 100644 --- a/tests/utils/testutils_lc.py +++ b/tests/utils/testutils_lc.py @@ -32,7 +32,8 @@ "/archive/refs/heads/dev.zip" _FUNC_FILE_NAME = "azure-functions-python-library-dev" _CUSTOM_IMAGE = "CUSTOM_IMAGE" - +_EXTENSION_BASE_ZIP = 'https://github.com/Azure/azure-functions-python-' \ + 'extensions/archive/refs/heads/dev.zip' class LinuxConsumptionWebHostController: """A controller for spawning mesh Docker container and apply multiple @@ -151,6 +152,15 @@ def _download_azure_functions() -> str: with ZipFile(BytesIO(zipresp.read())) as zfile: zfile.extractall(tempfile.gettempdir()) + @staticmethod + def _download_extensions() -> str: + folder = tempfile.gettempdir() + with urlopen(_EXTENSION_BASE_ZIP) as zipresp: + with ZipFile(BytesIO(zipresp.read())) as zfile: + zfile.extractall(folder) + + return folder + def spawn_container(self, image: str, env: Dict[str, str] = {}) -> int: @@ -163,11 +173,24 @@ def spawn_container(self, # TODO: Mount library in docker container # self._download_azure_functions() + # Download python extension base package + ext_folder = self._download_extensions() + container_worker_path = ( f"/azure-functions-host/workers/python/{self._py_version}/" "LINUX/X64/azure_functions_worker" ) + base_ext_container_path = ( + f"/azure-functions-host/workers/python/{self._py_version}/" + "LINUX/X64/azure/functions/extension/base" + ) + + base_ext_local_path = ( + f'{ext_folder}\\azure-functions-python' + f'-extensions-dev\\azure-functions-extension-base' + f'\\azure\\functions\\extension\\base' + ) run_cmd = [] run_cmd.extend([self._docker_cmd, "run", "-p", "0:80", "-d"]) run_cmd.extend(["--name", self._uuid, "--privileged"]) @@ -177,6 +200,8 @@ def spawn_container(self, run_cmd.extend(["-e", f"CONTAINER_ENCRYPTION_KEY={_DUMMY_CONT_KEY}"]) run_cmd.extend(["-e", "WEBSITE_PLACEHOLDER_MODE=1"]) run_cmd.extend(["-v", f'{worker_path}:{container_worker_path}']) + run_cmd.extend(["-v", + f'{base_ext_local_path}:{base_ext_container_path}']) for key, value in env.items(): run_cmd.extend(["-e", f"{key}={value}"]) From 68c2d901f4d94a8f78430e467d5d3ba534d0e46d Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 8 Apr 2024 15:59:45 -0700 Subject: [PATCH 068/101] style --- tests/utils/testutils_lc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/utils/testutils_lc.py b/tests/utils/testutils_lc.py index 19c81f15d..84311a6c1 100644 --- a/tests/utils/testutils_lc.py +++ b/tests/utils/testutils_lc.py @@ -35,6 +35,7 @@ _EXTENSION_BASE_ZIP = 'https://github.com/Azure/azure-functions-python-' \ 'extensions/archive/refs/heads/dev.zip' + class LinuxConsumptionWebHostController: """A controller for spawning mesh Docker container and apply multiple test cases on it. From 23bd8ab28a47054a62d729c2b1eb1259fdf477f4 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 8 Apr 2024 16:49:02 -0700 Subject: [PATCH 069/101] revert --- pytest.ini | 3 --- setup.py | 1 - tests/utils/testutils.py | 7 ++----- 3 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 56e02198d..000000000 --- a/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -markers = - serial: mark test as a serial test diff --git a/setup.py b/setup.py index bf60c456a..e82b5eb33 100644 --- a/setup.py +++ b/setup.py @@ -102,7 +102,6 @@ "pytest-randomly", "pytest-instafail", "pytest-rerunfailures", - "pytest-replay", "ptvsd", "python-dotenv", "plotly", diff --git a/tests/utils/testutils.py b/tests/utils/testutils.py index bdc305291..57946f1eb 100644 --- a/tests/utils/testutils.py +++ b/tests/utils/testutils.py @@ -529,14 +529,11 @@ def worker_id(self): def request_id(self): return self._request_id - async def init_worker(self, host_version: str = '4.28.0', **kwargs): - include_func_app_dir = kwargs.get('include_func_app_dir', False) + async def init_worker(self, host_version: str = '4.28.0'): r = await self.communicate( protos.StreamingMessage( worker_init_request=protos.WorkerInitRequest( - host_version=host_version, - function_app_directory=str( - self._scripts_dir) if include_func_app_dir else None, + host_version=host_version ) ), wait_for='worker_init_response' From aac033d4280d7e1ae8c2a0944eadd32b1b93401d Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 8 Apr 2024 16:53:55 -0700 Subject: [PATCH 070/101] revert --- .github/workflows/ci_ut_workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index 78e93eea4..8e95a0a4e 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -86,7 +86,7 @@ jobs: AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString310 }} # needed for installing azure-functions-durable while running setup.py ARCHIVE_WEBHOST_LOGS: ${{ github.event.inputs.archive_webhost_logging }} run: | - python -m pytest -q -n auto --replay-record-dir=build/tests/replay --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests + python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests - name: Codecov uses: codecov/codecov-action@v4 with: From 259ec03f632ab0461de3f25c9d4aec6f3301b833 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:33:31 -0700 Subject: [PATCH 071/101] revert ppls --- .github/workflows/ci_consumption_workflow.yml | 1 + .github/workflows/ci_e2e_workflow.yml | 1 + .github/workflows/ci_ut_workflow.yml | 1 + .github/workflows/linter.yml | 7 ++----- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml index d4580c7f1..34ef6eb51 100644 --- a/.github/workflows/ci_consumption_workflow.yml +++ b/.github/workflows/ci_consumption_workflow.yml @@ -12,6 +12,7 @@ on: push: branches: [ dev, main, release/* ] pull_request: + branches: [ dev, main, release/* ] jobs: build: diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml index bbb20c88c..55862f50d 100644 --- a/.github/workflows/ci_e2e_workflow.yml +++ b/.github/workflows/ci_e2e_workflow.yml @@ -13,6 +13,7 @@ on: push: branches: [dev, main, release/*] pull_request: + branches: [ dev, main, release/* ] schedule: # Monday to Thursday 3 AM CST build # * is a special character in YAML so you have to quote this string diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index 8e95a0a4e..a54ce22a2 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -17,6 +17,7 @@ on: push: branches: [ dev, main, release/* ] pull_request: + branches: [ dev, main, release/* ] jobs: build: diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index d0923a8d5..83e6f572f 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -15,11 +15,8 @@ name: Lint Code Base ############################# # Start the job on all push # ############################# -on: - workflow_dispatch: - push: - branches: [ dev, main, release/* ] - pull_request: +on: [ push, pull_request, workflow_dispatch ] + ############### # Set the Job # ############### From 41427678136db0d6fbb66f08bb6a055abb98764e Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Tue, 9 Apr 2024 13:18:40 -0500 Subject: [PATCH 072/101] test --- azure_functions_worker/bindings/meta.py | 4 +- azure_functions_worker/dispatcher.py | 9 ++-- azure_functions_worker/http_v2.py | 2 +- azure_functions_worker/logging.py | 4 +- mylog.txt | 66 +++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 mylog.txt diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index b7daf5666..cfdd2b73e 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -22,7 +22,7 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \ is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): - import azure.functions.extension.base as ext_base + import azurefunctions.extension.base as ext_base if ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.RequestTrackerMeta.check_type(pytype) @@ -33,7 +33,7 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool: if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \ is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): - import azure.functions.extension.base as ext_base + import azurefunctions.extension.base as ext_base if ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.ResponseTrackerMeta.check_type(pytype) diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 161a74284..e38f8d427 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -316,7 +316,7 @@ async def _handle__worker_init_request(self, request): if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION \ and self._has_http_func: - from azure.functions.extension.base \ + from azurefunctions.extension.base \ import HttpV2FeatureChecker if HttpV2FeatureChecker.http_v2_enabled(): @@ -482,6 +482,7 @@ async def _handle__function_load_request(self, request): status=protos.StatusResult.Success))) except Exception as ex: + logger.warning("VICTORIA Error: {}", ex) return protos.StreamingMessage( request_id=self.request_id, function_load_response=protos.FunctionLoadResponse( @@ -543,14 +544,14 @@ async def _handle__invocation_request(self, request): BASE_EXT_SUPPORTED_PY_MINOR_VERSION \ and fi.trigger_metadata is not None \ and fi.trigger_metadata.get('type') == HTTP_TRIGGER: - from azure.functions.extension.base import HttpV2FeatureChecker + from azurefunctions.extension.base import HttpV2FeatureChecker http_v2_enabled = HttpV2FeatureChecker.http_v2_enabled() if http_v2_enabled: http_request = await http_coordinator.get_http_request_async( invocation_id) - from azure.functions.extension.base import RequestTrackerMeta + from azurefunctions.extension.base import RequestTrackerMeta route_params = {key: item.string for key, item in trigger_metadata.items() if key not in [ 'Headers', 'Query']} @@ -703,7 +704,7 @@ async def _handle__function_environment_reload_request(self, request): if sys.version_info.minor >= \ BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \ self._has_http_func: - from azure.functions.extension.base \ + from azurefunctions.extension.base \ import HttpV2FeatureChecker if HttpV2FeatureChecker.http_v2_enabled(): diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index 91758d0a2..313fd9a3c 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -165,7 +165,7 @@ def get_unused_tcp_port(): def initialize_http_server(): - from azure.functions.extension.base \ + from azurefunctions.extension.base \ import ModuleTrackerMeta, RequestTrackerMeta web_extension_mod_name = ModuleTrackerMeta.get_module() diff --git a/azure_functions_worker/logging.py b/azure_functions_worker/logging.py index adb5ff294..12bc2e976 100644 --- a/azure_functions_worker/logging.py +++ b/azure_functions_worker/logging.py @@ -13,7 +13,7 @@ SDK_LOG_PREFIX = "azure.functions" SYSTEM_ERROR_LOG_PREFIX = "azure_functions_worker_errors" - +local_handler = logging.FileHandler("C:\\Users\\victoriahall\\Documents\\repos\\azure-functions-python-worker\\mylog.txt") logger: logging.Logger = logging.getLogger(SYSTEM_LOG_PREFIX) error_logger: logging.Logger = ( logging.getLogger(SYSTEM_ERROR_LOG_PREFIX)) @@ -78,6 +78,8 @@ def setup(log_level, log_destination): error_logger.addHandler(error_handler) error_logger.setLevel(getattr(logging, log_level)) + logger.addHandler(local_handler) + def disable_console_logging() -> None: # We should only remove the sys.stdout stream, as error_logger is used for diff --git a/mylog.txt b/mylog.txt new file mode 100644 index 000000000..101cc22e0 --- /dev/null +++ b/mylog.txt @@ -0,0 +1,66 @@ +Starting Azure Functions Python Worker. +Worker ID: 6dc9b2d4-fb85-4d7b-8da4-e154805df978, Request ID: 02e1a775-3c01-4879-a822-082610e9fe4a, Host Address: 127.0.0.1:56694 +Successfully opened gRPC channel to 127.0.0.1:56694 +Detaching console logging. +Switched to gRPC logging. +Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 02e1a775-3c01-4879-a822-082610e9fe4a. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging +Indexed function app and found 8 functions +Received WorkerMetadataRequest, request ID 02e1a775-3c01-4879-a822-082610e9fe4a, function_path: C:\Users\victoriahall\Documents\repos\azure-functions-python-worker\tests\endtoend\http_functions\http_functions_v2\fastapi\function_app.py +Starting Azure Functions Python Worker. +Worker ID: e01fe27f-1bc0-4614-a0c7-8a04f5875953, Request ID: d4a05a61-3069-433d-806e-a1ae7b5ae34f, Host Address: 127.0.0.1:56934 +Successfully opened gRPC channel to 127.0.0.1:56934 +Detaching console logging. +Switched to gRPC logging. +Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID d4a05a61-3069-433d-806e-a1ae7b5ae34f. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging +Received WorkerMetadataRequest, request ID d4a05a61-3069-433d-806e-a1ae7b5ae34f, function_path: C:\Users\victoriahall\Downloads\streaming_flask\streaming_flask\function_app.py +Indexed function app and found 1 functions +Starting Azure Functions Python Worker. +Worker ID: 8dcce15c-568d-4692-aba6-e5c82f0f84d5, Request ID: 1c00731f-108f-4d91-993c-485114c64174, Host Address: 127.0.0.1:56982 +Successfully opened gRPC channel to 127.0.0.1:56982 +Detaching console logging. +Switched to gRPC logging. +Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 1c00731f-108f-4d91-993c-485114c64174. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging +Received WorkerMetadataRequest, request ID 1c00731f-108f-4d91-993c-485114c64174, function_path: C:\Users\victoriahall\Downloads\streaming_flask\streaming_flask\function_app.py +Indexed function app and found 1 functions +Starting Azure Functions Python Worker. +Worker ID: 257d0ff5-8812-4187-986e-1eecc77e78f7, Request ID: f583915d-e9ed-4b28-9ae0-1965499bc9a2, Host Address: 127.0.0.1:57026 +Successfully opened gRPC channel to 127.0.0.1:57026 +Detaching console logging. +Switched to gRPC logging. +Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID f583915d-e9ed-4b28-9ae0-1965499bc9a2. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging +Received WorkerMetadataRequest, request ID f583915d-e9ed-4b28-9ae0-1965499bc9a2, function_path: C:\Users\victoriahall\Downloads\streaming_flask\streaming_flask\function_app.py +Indexed function app and found 1 functions +Starting Azure Functions Python Worker. +Worker ID: f5151493-68f1-4eaa-ae15-83ba85af70d5, Request ID: 060d3d04-4570-4fb8-9339-be58790c570c, Host Address: 127.0.0.1:57080 +Successfully opened gRPC channel to 127.0.0.1:57080 +Detaching console logging. +Switched to gRPC logging. +Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 060d3d04-4570-4fb8-9339-be58790c570c. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging +Indexed function app and found 1 functions +Starting Azure Functions Python Worker. +Worker ID: f5f6e191-ae39-4968-bc17-5a813ada1c38, Request ID: c14a058e-19cd-4ebc-a4a9-da6f4fb2f377, Host Address: 127.0.0.1:57116 +Successfully opened gRPC channel to 127.0.0.1:57116 +Detaching console logging. +Switched to gRPC logging. +Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID c14a058e-19cd-4ebc-a4a9-da6f4fb2f377. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging +Indexed function app and found 1 functions +Starting Azure Functions Python Worker. +Worker ID: 35186346-38de-491c-b07d-65525818a2a1, Request ID: 60aea45d-b492-436d-b695-75ec7d99b16a, Host Address: 127.0.0.1:57434 +Successfully opened gRPC channel to 127.0.0.1:57434 +Detaching console logging. +Switched to gRPC logging. +Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 60aea45d-b492-436d-b695-75ec7d99b16a. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging +Indexed function app and found 1 functions +Starting Azure Functions Python Worker. +Worker ID: ad247ea9-b5d4-46a6-a5a5-b52271648e00, Request ID: 612075e4-49d1-47e1-902e-a83b86bfa1da, Host Address: 127.0.0.1:57547 +Successfully opened gRPC channel to 127.0.0.1:57547 +Detaching console logging. +Switched to gRPC logging. +Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 612075e4-49d1-47e1-902e-a83b86bfa1da. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging +Starting Azure Functions Python Worker. +Worker ID: 0780177e-191a-4211-a1a3-1cca83e42d4a, Request ID: ec727f1f-4cde-4225-a53f-44fa09247368, Host Address: 127.0.0.1:57776 +Successfully opened gRPC channel to 127.0.0.1:57776 +Detaching console logging. +Switched to gRPC logging. +Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID ec727f1f-4cde-4225-a53f-44fa09247368. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging +Indexed function app and found 1 functions From 3915493a03ea4a5a15b904816370ff346e7921ad Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Tue, 9 Apr 2024 13:22:52 -0500 Subject: [PATCH 073/101] Revert "test" This reverts commit 41427678136db0d6fbb66f08bb6a055abb98764e. --- azure_functions_worker/bindings/meta.py | 4 +- azure_functions_worker/dispatcher.py | 9 ++-- azure_functions_worker/http_v2.py | 2 +- azure_functions_worker/logging.py | 4 +- mylog.txt | 66 ------------------------- 5 files changed, 8 insertions(+), 77 deletions(-) delete mode 100644 mylog.txt diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index cfdd2b73e..b7daf5666 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -22,7 +22,7 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \ is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): - import azurefunctions.extension.base as ext_base + import azure.functions.extension.base as ext_base if ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.RequestTrackerMeta.check_type(pytype) @@ -33,7 +33,7 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool: if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \ is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): - import azurefunctions.extension.base as ext_base + import azure.functions.extension.base as ext_base if ext_base.HttpV2FeatureChecker.http_v2_enabled(): return ext_base.ResponseTrackerMeta.check_type(pytype) diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index e38f8d427..161a74284 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -316,7 +316,7 @@ async def _handle__worker_init_request(self, request): if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION \ and self._has_http_func: - from azurefunctions.extension.base \ + from azure.functions.extension.base \ import HttpV2FeatureChecker if HttpV2FeatureChecker.http_v2_enabled(): @@ -482,7 +482,6 @@ async def _handle__function_load_request(self, request): status=protos.StatusResult.Success))) except Exception as ex: - logger.warning("VICTORIA Error: {}", ex) return protos.StreamingMessage( request_id=self.request_id, function_load_response=protos.FunctionLoadResponse( @@ -544,14 +543,14 @@ async def _handle__invocation_request(self, request): BASE_EXT_SUPPORTED_PY_MINOR_VERSION \ and fi.trigger_metadata is not None \ and fi.trigger_metadata.get('type') == HTTP_TRIGGER: - from azurefunctions.extension.base import HttpV2FeatureChecker + from azure.functions.extension.base import HttpV2FeatureChecker http_v2_enabled = HttpV2FeatureChecker.http_v2_enabled() if http_v2_enabled: http_request = await http_coordinator.get_http_request_async( invocation_id) - from azurefunctions.extension.base import RequestTrackerMeta + from azure.functions.extension.base import RequestTrackerMeta route_params = {key: item.string for key, item in trigger_metadata.items() if key not in [ 'Headers', 'Query']} @@ -704,7 +703,7 @@ async def _handle__function_environment_reload_request(self, request): if sys.version_info.minor >= \ BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \ self._has_http_func: - from azurefunctions.extension.base \ + from azure.functions.extension.base \ import HttpV2FeatureChecker if HttpV2FeatureChecker.http_v2_enabled(): diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index 313fd9a3c..91758d0a2 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -165,7 +165,7 @@ def get_unused_tcp_port(): def initialize_http_server(): - from azurefunctions.extension.base \ + from azure.functions.extension.base \ import ModuleTrackerMeta, RequestTrackerMeta web_extension_mod_name = ModuleTrackerMeta.get_module() diff --git a/azure_functions_worker/logging.py b/azure_functions_worker/logging.py index 12bc2e976..adb5ff294 100644 --- a/azure_functions_worker/logging.py +++ b/azure_functions_worker/logging.py @@ -13,7 +13,7 @@ SDK_LOG_PREFIX = "azure.functions" SYSTEM_ERROR_LOG_PREFIX = "azure_functions_worker_errors" -local_handler = logging.FileHandler("C:\\Users\\victoriahall\\Documents\\repos\\azure-functions-python-worker\\mylog.txt") + logger: logging.Logger = logging.getLogger(SYSTEM_LOG_PREFIX) error_logger: logging.Logger = ( logging.getLogger(SYSTEM_ERROR_LOG_PREFIX)) @@ -78,8 +78,6 @@ def setup(log_level, log_destination): error_logger.addHandler(error_handler) error_logger.setLevel(getattr(logging, log_level)) - logger.addHandler(local_handler) - def disable_console_logging() -> None: # We should only remove the sys.stdout stream, as error_logger is used for diff --git a/mylog.txt b/mylog.txt deleted file mode 100644 index 101cc22e0..000000000 --- a/mylog.txt +++ /dev/null @@ -1,66 +0,0 @@ -Starting Azure Functions Python Worker. -Worker ID: 6dc9b2d4-fb85-4d7b-8da4-e154805df978, Request ID: 02e1a775-3c01-4879-a822-082610e9fe4a, Host Address: 127.0.0.1:56694 -Successfully opened gRPC channel to 127.0.0.1:56694 -Detaching console logging. -Switched to gRPC logging. -Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 02e1a775-3c01-4879-a822-082610e9fe4a. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging -Indexed function app and found 8 functions -Received WorkerMetadataRequest, request ID 02e1a775-3c01-4879-a822-082610e9fe4a, function_path: C:\Users\victoriahall\Documents\repos\azure-functions-python-worker\tests\endtoend\http_functions\http_functions_v2\fastapi\function_app.py -Starting Azure Functions Python Worker. -Worker ID: e01fe27f-1bc0-4614-a0c7-8a04f5875953, Request ID: d4a05a61-3069-433d-806e-a1ae7b5ae34f, Host Address: 127.0.0.1:56934 -Successfully opened gRPC channel to 127.0.0.1:56934 -Detaching console logging. -Switched to gRPC logging. -Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID d4a05a61-3069-433d-806e-a1ae7b5ae34f. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging -Received WorkerMetadataRequest, request ID d4a05a61-3069-433d-806e-a1ae7b5ae34f, function_path: C:\Users\victoriahall\Downloads\streaming_flask\streaming_flask\function_app.py -Indexed function app and found 1 functions -Starting Azure Functions Python Worker. -Worker ID: 8dcce15c-568d-4692-aba6-e5c82f0f84d5, Request ID: 1c00731f-108f-4d91-993c-485114c64174, Host Address: 127.0.0.1:56982 -Successfully opened gRPC channel to 127.0.0.1:56982 -Detaching console logging. -Switched to gRPC logging. -Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 1c00731f-108f-4d91-993c-485114c64174. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging -Received WorkerMetadataRequest, request ID 1c00731f-108f-4d91-993c-485114c64174, function_path: C:\Users\victoriahall\Downloads\streaming_flask\streaming_flask\function_app.py -Indexed function app and found 1 functions -Starting Azure Functions Python Worker. -Worker ID: 257d0ff5-8812-4187-986e-1eecc77e78f7, Request ID: f583915d-e9ed-4b28-9ae0-1965499bc9a2, Host Address: 127.0.0.1:57026 -Successfully opened gRPC channel to 127.0.0.1:57026 -Detaching console logging. -Switched to gRPC logging. -Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID f583915d-e9ed-4b28-9ae0-1965499bc9a2. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging -Received WorkerMetadataRequest, request ID f583915d-e9ed-4b28-9ae0-1965499bc9a2, function_path: C:\Users\victoriahall\Downloads\streaming_flask\streaming_flask\function_app.py -Indexed function app and found 1 functions -Starting Azure Functions Python Worker. -Worker ID: f5151493-68f1-4eaa-ae15-83ba85af70d5, Request ID: 060d3d04-4570-4fb8-9339-be58790c570c, Host Address: 127.0.0.1:57080 -Successfully opened gRPC channel to 127.0.0.1:57080 -Detaching console logging. -Switched to gRPC logging. -Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 060d3d04-4570-4fb8-9339-be58790c570c. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging -Indexed function app and found 1 functions -Starting Azure Functions Python Worker. -Worker ID: f5f6e191-ae39-4968-bc17-5a813ada1c38, Request ID: c14a058e-19cd-4ebc-a4a9-da6f4fb2f377, Host Address: 127.0.0.1:57116 -Successfully opened gRPC channel to 127.0.0.1:57116 -Detaching console logging. -Switched to gRPC logging. -Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID c14a058e-19cd-4ebc-a4a9-da6f4fb2f377. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging -Indexed function app and found 1 functions -Starting Azure Functions Python Worker. -Worker ID: 35186346-38de-491c-b07d-65525818a2a1, Request ID: 60aea45d-b492-436d-b695-75ec7d99b16a, Host Address: 127.0.0.1:57434 -Successfully opened gRPC channel to 127.0.0.1:57434 -Detaching console logging. -Switched to gRPC logging. -Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 60aea45d-b492-436d-b695-75ec7d99b16a. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging -Indexed function app and found 1 functions -Starting Azure Functions Python Worker. -Worker ID: ad247ea9-b5d4-46a6-a5a5-b52271648e00, Request ID: 612075e4-49d1-47e1-902e-a83b86bfa1da, Host Address: 127.0.0.1:57547 -Successfully opened gRPC channel to 127.0.0.1:57547 -Detaching console logging. -Switched to gRPC logging. -Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 612075e4-49d1-47e1-902e-a83b86bfa1da. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging -Starting Azure Functions Python Worker. -Worker ID: 0780177e-191a-4211-a1a3-1cca83e42d4a, Request ID: ec727f1f-4cde-4225-a53f-44fa09247368, Host Address: 127.0.0.1:57776 -Successfully opened gRPC channel to 127.0.0.1:57776 -Detaching console logging. -Switched to gRPC logging. -Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID ec727f1f-4cde-4225-a53f-44fa09247368. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging -Indexed function app and found 1 functions From ec42efcd6c7e17251dfc5c5413acc0e345deb061 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 9 Apr 2024 17:50:37 -0700 Subject: [PATCH 074/101] address feedback --- azure_functions_worker/bindings/meta.py | 19 +--- azure_functions_worker/constants.py | 3 - azure_functions_worker/dispatcher.py | 106 ++++++++---------- azure_functions_worker/functions.py | 12 ++ azure_functions_worker/http_v2.py | 60 ++++++++-- .../test_enable_debug_logging_functions.py | 4 +- 6 files changed, 116 insertions(+), 88 deletions(-) diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index b7daf5666..9d70160bf 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -9,9 +9,7 @@ from . import datumdef from . import generic from .shared_memory_data_transfer import SharedMemoryManager -from ..constants import BASE_EXT_SUPPORTED_PY_MINOR_VERSION, \ - PYTHON_ENABLE_INIT_INDEXING -from ..utils.common import is_envvar_true +from ..http_v2 import HttpV2Registry PB_TYPE = 'rpc_data' PB_TYPE_DATA = 'data' @@ -20,22 +18,17 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool: - if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \ - is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): - import azure.functions.extension.base as ext_base - if ext_base.HttpV2FeatureChecker.http_v2_enabled(): - return ext_base.RequestTrackerMeta.check_type(pytype) + if HttpV2Registry.http_v2_enabled(): + return HttpV2Registry.ext_base().RequestTrackerMeta \ + .check_type(pytype) binding = get_binding(bind_name) return binding.check_input_type_annotation(pytype) def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool: - if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \ - is_envvar_true(PYTHON_ENABLE_INIT_INDEXING): - import azure.functions.extension.base as ext_base - if ext_base.HttpV2FeatureChecker.http_v2_enabled(): - return ext_base.ResponseTrackerMeta.check_type(pytype) + if HttpV2Registry.http_v2_enabled(): + return HttpV2Registry.ext_base().ResponseTrackerMeta.check_type(pytype) binding = get_binding(bind_name) return binding.check_output_type_annotation(pytype) diff --git a/azure_functions_worker/constants.py b/azure_functions_worker/constants.py index eea0193ec..8820b4e98 100644 --- a/azure_functions_worker/constants.py +++ b/azure_functions_worker/constants.py @@ -63,9 +63,6 @@ METADATA_PROPERTIES_WORKER_INDEXED = "worker_indexed" -# HostNames -LOCAL_HOST = "127.0.0.1" - # Header names X_MS_INVOCATION_ID = "x-ms-invocation-id" diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 161a74284..65c9ebd57 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -21,7 +21,7 @@ import grpc from . import bindings, constants, functions, loader, protos from .bindings.shared_memory_data_transfer import SharedMemoryManager -from .constants import (HTTP_TRIGGER, PYTHON_ROLLBACK_CWD_PATH, +from .constants import (PYTHON_ROLLBACK_CWD_PATH, PYTHON_THREADPOOL_THREAD_COUNT, PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT, PYTHON_THREADPOOL_THREAD_COUNT_MAX_37, @@ -30,10 +30,9 @@ PYTHON_SCRIPT_FILE_NAME, PYTHON_SCRIPT_FILE_NAME_DEFAULT, PYTHON_LANGUAGE_RUNTIME, PYTHON_ENABLE_INIT_INDEXING, - METADATA_PROPERTIES_WORKER_INDEXED, - BASE_EXT_SUPPORTED_PY_MINOR_VERSION) + METADATA_PROPERTIES_WORKER_INDEXED) from .extension import ExtensionManager -from .http_v2 import http_coordinator, initialize_http_server +from .http_v2 import http_coordinator, initialize_http_server, HttpV2Registry from .logging import disable_console_logging, enable_console_logging from .logging import (logger, error_logger, is_system_log_category, CONSOLE_LOG_PREFIX, format_exception) @@ -75,7 +74,6 @@ def __init__(self, loop: BaseEventLoop, host: str, port: int, self._functions = functions.Registry() self._shmem_mgr = SharedMemoryManager() self._old_task_factory = None - self._has_http_func = False # Used to store metadata returns self._function_metadata_result = None @@ -314,14 +312,19 @@ async def _handle__worker_init_request(self, request): except Exception as ex: self._function_metadata_exception = ex - if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION \ - and self._has_http_func: - from azure.functions.extension.base \ - import HttpV2FeatureChecker - - if HttpV2FeatureChecker.http_v2_enabled(): + try: + if HttpV2Registry.http_v2_enabled(self._functions): capabilities[constants.HTTP_URI] = \ - initialize_http_server() + initialize_http_server(self._host) + except Exception as ex: + return protos.StreamingMessage( + request_id=self.request_id, + worker_init_response=protos.WorkerInitResponse( + result=protos.StatusResult( + status=protos.StatusResult.Failure, + exception=self._serialize_exception(ex)) + ) + ) return protos.StreamingMessage( request_id=self.request_id, @@ -493,7 +496,6 @@ async def _handle__function_load_request(self, request): async def _handle__invocation_request(self, request): invocation_time = datetime.utcnow() invoc_request = request.invocation_request - trigger_metadata = invoc_request.trigger_metadata invocation_id = invoc_request.invocation_id function_id = invoc_request.function_id @@ -528,9 +530,10 @@ async def _handle__invocation_request(self, request): for pb in invoc_request.input_data: pb_type_info = fi.input_types[pb.name] - trigger_metadata = None if bindings.is_trigger_binding(pb_type_info.binding_name): trigger_metadata = invoc_request.trigger_metadata + else: + trigger_metadata = None args[pb.name] = bindings.from_incoming_proto( pb_type_info.binding_name, pb, @@ -538,24 +541,20 @@ async def _handle__invocation_request(self, request): pytype=pb_type_info.pytype, shmem_mgr=self._shmem_mgr) - http_v2_enabled = False - if sys.version_info.minor >= \ - BASE_EXT_SUPPORTED_PY_MINOR_VERSION \ - and fi.trigger_metadata is not None \ - and fi.trigger_metadata.get('type') == HTTP_TRIGGER: - from azure.functions.extension.base import HttpV2FeatureChecker - http_v2_enabled = HttpV2FeatureChecker.http_v2_enabled() + http_v2_enabled = self._functions.get_function(function_id) \ + .is_http_func and \ + HttpV2Registry.http_v2_enabled() if http_v2_enabled: http_request = await http_coordinator.get_http_request_async( invocation_id) - from azure.functions.extension.base import RequestTrackerMeta route_params = {key: item.string for key, item - in trigger_metadata.items() if key not in [ - 'Headers', 'Query']} + in invoc_request.trigger_metadata.items() + if key not in ['Headers', 'Query']} - (RequestTrackerMeta.get_synchronizer() + (HttpV2Registry.ext_base().RequestTrackerMeta + .get_synchronizer() .sync_route_params(http_request, route_params)) args[fi.trigger_metadata.get('param_name')] = http_request @@ -572,30 +571,22 @@ async def _handle__invocation_request(self, request): for name in fi.output_types: args[name] = bindings.Out() - call_result = None - call_error = None - try: - if fi.is_async: - call_result = \ - await self._run_async_func(fi_context, fi.func, args) - else: - call_result = await self._loop.run_in_executor( - self._sync_call_tp, - self._run_sync_func, - invocation_id, fi_context, fi.func, args) - - if call_result is not None and not fi.has_return: - raise RuntimeError( - f'function {fi.name!r} without a $return binding' - 'returned a non-None value') - except Exception as e: - call_error = e - raise - finally: - if http_v2_enabled: - http_coordinator.set_http_response( - invocation_id, call_result - if call_result is not None else call_error) + if fi.is_async: + call_result = \ + await self._run_async_func(fi_context, fi.func, args) + else: + call_result = await self._loop.run_in_executor( + self._sync_call_tp, + self._run_sync_func, + invocation_id, fi_context, fi.func, args) + + if call_result is not None and not fi.has_return: + raise RuntimeError( + f'function {fi.name!r} without a $return binding' + 'returned a non-None value') + + if http_v2_enabled: + http_coordinator.set_http_response(invocation_id, call_result) output_data = [] cache_enabled = self._function_data_cache_enabled @@ -635,6 +626,9 @@ async def _handle__invocation_request(self, request): output_data=output_data)) except Exception as ex: + if http_v2_enabled: + http_coordinator.set_http_response(invocation_id, ex) + return protos.StreamingMessage( request_id=self.request_id, invocation_response=protos.InvocationResponse( @@ -700,15 +694,9 @@ async def _handle__function_environment_reload_request(self, request): except Exception as ex: self._function_metadata_exception = ex - if sys.version_info.minor >= \ - BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \ - self._has_http_func: - from azure.functions.extension.base \ - import HttpV2FeatureChecker - - if HttpV2FeatureChecker.http_v2_enabled(): - capabilities[constants.HTTP_URI] = \ - initialize_http_server() + if HttpV2Registry.http_v2_enabled(self._functions): + capabilities[constants.HTTP_URI] = \ + initialize_http_server(self._host) # Change function app directory if getattr(func_env_reload_request, @@ -750,8 +738,6 @@ def index_functions(self, function_path: str): indexed_function_logs: List[str] = [] for func in indexed_functions: - self._has_http_func = self._has_http_func or \ - func.is_http_function() function_log = "Function Name: {}, Function Binding: {}" \ .format(func.get_function_name(), [(binding.type, binding.name) for binding in diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index c5f00e040..acdb4f30b 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -28,6 +28,7 @@ class FunctionInfo(typing.NamedTuple): requires_context: bool is_async: bool has_return: bool + is_http_func: bool input_types: typing.Mapping[str, ParamTypeInfo] output_types: typing.Mapping[str, ParamTypeInfo] @@ -45,6 +46,7 @@ def __init__(self, function_name: str, msg: str) -> None: class Registry: _functions: typing.MutableMapping[str, FunctionInfo] + _has_http_func: bool = False def __init__(self) -> None: self._functions = {} @@ -55,6 +57,9 @@ def get_function(self, function_id: str) -> FunctionInfo: return None + def has_http_func(self) -> bool: + return self._has_http_func + @staticmethod def get_explicit_and_implicit_return(binding_name: str, binding: BindingInfo, @@ -308,11 +313,13 @@ def add_func_to_registry_and_return_funcinfo(self, function, ) trigger_metadata = None + is_http_func = False if http_trigger_param_name is not None: trigger_metadata = { "type": HTTP_TRIGGER, "param_name": http_trigger_param_name } + is_http_func = True function_info = FunctionInfo( func=function, @@ -322,12 +329,17 @@ def add_func_to_registry_and_return_funcinfo(self, function, requires_context=requires_context, is_async=inspect.iscoroutinefunction(function), has_return=has_explicit_return or has_implicit_return, + is_http_func=is_http_func, input_types=input_types, output_types=output_types, return_type=return_type, trigger_metadata=trigger_metadata) self._functions[function_id] = function_info + + if not self._has_http_func: + self._has_http_func = function_info.is_http_func + return function_info def add_function(self, function_id: str, diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index 91758d0a2..52e993735 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -2,10 +2,13 @@ import asyncio import importlib import socket +import sys from typing import Dict -from azure_functions_worker.constants import X_MS_INVOCATION_ID, LOCAL_HOST +from azure_functions_worker.constants import X_MS_INVOCATION_ID, \ + BASE_EXT_SUPPORTED_PY_MINOR_VERSION, PYTHON_ENABLE_INIT_INDEXING from azure_functions_worker.logging import logger +from azure_functions_worker.utils.common import is_envvar_false class BaseContextReference(abc.ABC): @@ -164,11 +167,9 @@ def get_unused_tcp_port(): return port -def initialize_http_server(): - from azure.functions.extension.base \ - import ModuleTrackerMeta, RequestTrackerMeta - - web_extension_mod_name = ModuleTrackerMeta.get_module() +def initialize_http_server(host_addr, **kwargs): + ext_base = HttpV2Registry.ext_base() + web_extension_mod_name = ext_base.ModuleTrackerMeta.get_module() extension_module = importlib.import_module(web_extension_mod_name) web_app_class = extension_module.WebApp web_server_class = extension_module.WebServer @@ -176,7 +177,7 @@ def initialize_http_server(): unused_port = get_unused_tcp_port() app = web_app_class() - request_type = RequestTrackerMeta.get_request_type() + request_type = ext_base.RequestTrackerMeta.get_request_type() @app.route async def catch_all(request: request_type): # type: ignore @@ -195,16 +196,57 @@ async def catch_all(request: request_type): # type: ignore return http_resp - web_server = web_server_class(LOCAL_HOST, unused_port, app) + web_server = web_server_class(host_addr, unused_port, app) web_server_run_task = web_server.serve() loop = asyncio.get_event_loop() loop.create_task(web_server_run_task) - web_server_address = f"http://{LOCAL_HOST}:{unused_port}" + web_server_address = f"http://{host_addr}:{unused_port}" logger.info(f'HTTP server starting on {web_server_address}') return web_server_address +class HttpV2Registry: + _http_v2_enabled = False + _ext_base = None + _http_v2_enabled_checked = False + + @classmethod + def http_v2_enabled(cls, functions=None, **kwargs): + # Check if HTTP/2 enablement has already been checked + if not cls._http_v2_enabled_checked: + # If not checked yet, mark as checked + cls._http_v2_enabled_checked = True + + # Check if there are functions provided and if any of them has + # HTTP triggers + cls._http_v2_enabled = functions is not None and \ + functions.has_http_func() + + # If HTTP functions are present, perform additional checks + if cls._http_v2_enabled: + # Check if HTTP/2 is enabled + cls._http_v2_enabled = cls._check_http_v2_enabled() + + # Return the result of HTTP/2 enablement + return cls._http_v2_enabled + + @classmethod + def ext_base(cls): + return cls._ext_base + + @classmethod + def _check_http_v2_enabled(cls): + if sys.version_info.minor < BASE_EXT_SUPPORTED_PY_MINOR_VERSION or \ + is_envvar_false(PYTHON_ENABLE_INIT_INDEXING): + return False + + import azure.functions.extension.base as ext_base + cls._ext_base = ext_base + + return cls._ext_base.HttpV2FeatureChecker.http_v2_enabled() + + http_coordinator = HttpCoordinator() diff --git a/tests/unittests/test_enable_debug_logging_functions.py b/tests/unittests/test_enable_debug_logging_functions.py index 6f3739809..120d54dfe 100644 --- a/tests/unittests/test_enable_debug_logging_functions.py +++ b/tests/unittests/test_enable_debug_logging_functions.py @@ -65,7 +65,6 @@ class TestDebugLoggingDisabledFunctions(testutils.WebHostTestCase): """ @classmethod def setUpClass(cls): - cls._pre_env = dict(os.environ) os_environ = os.environ.copy() os_environ[PYTHON_ENABLE_DEBUG_LOGGING] = '0' cls._patch_environ = patch.dict('os.environ', os_environ) @@ -74,9 +73,8 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): + os.environ.pop(PYTHON_ENABLE_DEBUG_LOGGING) super().tearDownClass() - os.environ.clear() - os.environ.update(cls._pre_env) cls._patch_environ.stop() @classmethod From 7333a025768b03beee62f4362d4133e012e3c096 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 9 Apr 2024 23:46:21 -0700 Subject: [PATCH 075/101] revert ut ppl --- .github/workflows/ci_ut_workflow.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index a54ce22a2..01aa36070 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -89,13 +89,12 @@ jobs: run: | python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests - name: Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3 with: file: ./coverage.xml # optional flags: unittests # optional name: codecov # optional fail_ci_if_error: false # optional (default = false) - token: ${{ secrets.CODECOV_TOKEN }} - name: Publish Logs to Artifact if: failure() uses: actions/upload-artifact@v4 @@ -103,10 +102,4 @@ jobs: name: Test WebHost Logs ${{ github.run_id }} ${{ matrix.python-version }} path: logs/*.log if-no-files-found: ignore - - name: Publish replays to Artifact - if: failure() - uses: actions/upload-artifact@v4 - with: - name: Test Replays ${{ github.run_id }} ${{ matrix.python-version }} - path: build/tests/replay - if-no-files-found: ignore + From 99a47a210c9ac1d76270957f7226c94625ceac36 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 01:07:50 -0700 Subject: [PATCH 076/101] skip checking has http func --- azure_functions_worker/functions.py | 6 ------ azure_functions_worker/http_v2.py | 10 +--------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index acdb4f30b..10bf70968 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -46,7 +46,6 @@ def __init__(self, function_name: str, msg: str) -> None: class Registry: _functions: typing.MutableMapping[str, FunctionInfo] - _has_http_func: bool = False def __init__(self) -> None: self._functions = {} @@ -57,8 +56,6 @@ def get_function(self, function_id: str) -> FunctionInfo: return None - def has_http_func(self) -> bool: - return self._has_http_func @staticmethod def get_explicit_and_implicit_return(binding_name: str, @@ -337,9 +334,6 @@ def add_func_to_registry_and_return_funcinfo(self, function, self._functions[function_id] = function_info - if not self._has_http_func: - self._has_http_func = function_info.is_http_func - return function_info def add_function(self, function_id: str, diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index 52e993735..5d00ecd6a 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -220,15 +220,7 @@ def http_v2_enabled(cls, functions=None, **kwargs): # If not checked yet, mark as checked cls._http_v2_enabled_checked = True - # Check if there are functions provided and if any of them has - # HTTP triggers - cls._http_v2_enabled = functions is not None and \ - functions.has_http_func() - - # If HTTP functions are present, perform additional checks - if cls._http_v2_enabled: - # Check if HTTP/2 is enabled - cls._http_v2_enabled = cls._check_http_v2_enabled() + cls._http_v2_enabled = cls._check_http_v2_enabled() # Return the result of HTTP/2 enablement return cls._http_v2_enabled From 9221846ad42e45b79beedc0634350df539bf6053 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 01:08:00 -0700 Subject: [PATCH 077/101] fix test --- tests/endtoend/test_http_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/endtoend/test_http_functions.py b/tests/endtoend/test_http_functions.py index f3a8a6e07..f6e6e3a76 100644 --- a/tests/endtoend/test_http_functions.py +++ b/tests/endtoend/test_http_functions.py @@ -243,9 +243,9 @@ def test_return_streaming(self): streaming_url, timeout=REQUEST_TIMEOUT_SEC, stream=True) self.assertTrue(r.ok) # Validate streaming content - expected_content = [b"First chunk\n", b"Second chunk\n"] + expected_content = [b'First', b' chun', b'k\nSec', b'ond c', b'hunk\n'] received_content = [] - for chunk in r.iter_content(chunk_size=1024): + for chunk in r.iter_content(chunk_size=5): if chunk: received_content.append(chunk) self.assertEqual(received_content, expected_content) From a70ca752940f4d9662c047fbbbc1881ab6feace4 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 01:19:52 -0700 Subject: [PATCH 078/101] style --- azure_functions_worker/functions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index 10bf70968..00296c441 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -56,7 +56,6 @@ def get_function(self, function_id: str) -> FunctionInfo: return None - @staticmethod def get_explicit_and_implicit_return(binding_name: str, binding: BindingInfo, From f7e136bdf473856a4635d481be3513219c3c3c1b Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 10:14:37 -0700 Subject: [PATCH 079/101] revert cache ppl --- .github/workflows/ci_consumption_workflow.yml | 15 +-------------- .github/workflows/ci_e2e_workflow.yml | 15 +-------------- .github/workflows/ci_ut_workflow.yml | 16 +--------------- 3 files changed, 3 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml index 34ef6eb51..14ee1b5e3 100644 --- a/.github/workflows/ci_consumption_workflow.yml +++ b/.github/workflows/ci_consumption_workflow.yml @@ -30,26 +30,13 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - name: Get Date - id: get-date - run: | - echo "todayDate=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_ENV - shell: bash - - uses: actions/cache@v4 - id: cache-pip - with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}-${{ env.todayDate }}-${{ matrix.python-version }} - - name: Install dependencies - if: steps.cache-pip.outputs.cache-hit != 'true' + - name: Install dependencies and the worker run: | python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] if [[ "${{ matrix.python-version }}" != "3.7" ]]; then python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] fi - - name: Install worker - run: | python setup.py build - name: Running 3.7 Tests if: matrix.python-version == 3.7 diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml index 55862f50d..58b69d131 100644 --- a/.github/workflows/ci_e2e_workflow.yml +++ b/.github/workflows/ci_e2e_workflow.yml @@ -39,18 +39,7 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: "8.0.x" - - name: Get Date - id: get-date - run: | - echo "todayDate=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_ENV - shell: bash - - uses: actions/cache@v4 - id: cache-pip - with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}-${{ env.todayDate }}-${{ matrix.python-version }} - - name: Install dependencies - if: steps.cache-pip.outputs.cache-hit != 'true' + - name: Install dependencies and the worker run: | python -m pip install --upgrade pip python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre @@ -59,8 +48,6 @@ jobs: if [[ "${{ matrix.python-version }}" != "3.7" ]]; then python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] fi - - name: Install worker - run: | retry() { local -r -i max_attempts="$1"; shift local -r cmd="$@" diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index 01aa36070..8f17cdbb9 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -38,19 +38,7 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: "8.0.x" - - name: Get Date - id: get-date - run: | - echo "todayDate=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_ENV - shell: bash - - uses: actions/cache@v4 - id: cache-pip - with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}-${{ env.todayDate }}-${{ matrix.python-version }} - - name: Install dependencies - if: steps.cache-pip.outputs.cache-hit != 'true' - run: | + - name: Install dependencies and the worker python -m pip install --upgrade pip python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] @@ -58,8 +46,6 @@ jobs: if [[ "${{ matrix.python-version }}" != "3.7" ]]; then python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] fi - - name: Install the worker - run: | retry() { local -r -i max_attempts="$1"; shift local -r cmd="$@" From 16945e5fe80e6187ad79c5c65e0e8578fbe261e5 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 10:19:20 -0700 Subject: [PATCH 080/101] revert ppl --- .github/workflows/ci_consumption_workflow.yml | 4 ++-- .github/workflows/ci_e2e_workflow.yml | 16 ++++++++-------- .github/workflows/ci_ut_workflow.yml | 19 +++++++++---------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml index 14ee1b5e3..48704ef3c 100644 --- a/.github/workflows/ci_consumption_workflow.yml +++ b/.github/workflows/ci_consumption_workflow.yml @@ -30,12 +30,12 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies and the worker + - name: Install dependencies run: | python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] if [[ "${{ matrix.python-version }}" != "3.7" ]]; then - python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] fi python setup.py build - name: Running 3.7 Tests diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml index 58b69d131..3d795261b 100644 --- a/.github/workflows/ci_e2e_workflow.yml +++ b/.github/workflows/ci_e2e_workflow.yml @@ -13,7 +13,7 @@ on: push: branches: [dev, main, release/*] pull_request: - branches: [ dev, main, release/* ] + branches: [dev, main, release/*] schedule: # Monday to Thursday 3 AM CST build # * is a special character in YAML so you have to quote this string @@ -41,13 +41,6 @@ jobs: dotnet-version: "8.0.x" - name: Install dependencies and the worker run: | - python -m pip install --upgrade pip - python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre - python -m pip install -U -e .[dev] - # Conditionally install test dependencies for Python 3.8 and later - if [[ "${{ matrix.python-version }}" != "3.7" ]]; then - python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] - fi retry() { local -r -i max_attempts="$1"; shift local -r cmd="$@" @@ -65,6 +58,13 @@ jobs: done } + python -m pip install --upgrade pip + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre + python -m pip install -U -e .[dev] + if [[ "${{ matrix.python-version }}" != "3.7" ]]; then + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] + fi + # Retry a couple times to avoid certificate issue retry 5 python setup.py build retry 5 python setup.py webhost --branch-name=dev diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index 8f17cdbb9..0fd99d3a1 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -15,7 +15,6 @@ on: # * is a special character in YAML so you have to quote this string - cron: "0 8 * * 1,2,3,4" push: - branches: [ dev, main, release/* ] pull_request: branches: [ dev, main, release/* ] @@ -39,13 +38,7 @@ jobs: with: dotnet-version: "8.0.x" - name: Install dependencies and the worker - python -m pip install --upgrade pip - python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre - python -m pip install -U -e .[dev] - # Conditionally install test dependencies for Python 3.8 and later - if [[ "${{ matrix.python-version }}" != "3.7" ]]; then - python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] - fi + run: | retry() { local -r -i max_attempts="$1"; shift local -r cmd="$@" @@ -62,7 +55,14 @@ jobs: fi done } - + + python -m pip install --upgrade pip + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre + python -m pip install -U -e .[dev] + if [[ "${{ matrix.python-version }}" != "3.7" ]]; then + python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2] + fi + # Retry a couple times to avoid certificate issue retry 5 python setup.py build retry 5 python setup.py webhost --branch-name=dev @@ -88,4 +88,3 @@ jobs: name: Test WebHost Logs ${{ github.run_id }} ${{ matrix.python-version }} path: logs/*.log if-no-files-found: ignore - From 0d9b5ca702960a8f87d9bb0165ffed9cd729684e Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 11:02:43 -0700 Subject: [PATCH 081/101] pip fix --- .github/workflows/ci_consumption_workflow.yml | 1 + .github/workflows/ci_e2e_workflow.yml | 2 +- .github/workflows/ci_ut_workflow.yml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml index 48704ef3c..c610347fe 100644 --- a/.github/workflows/ci_consumption_workflow.yml +++ b/.github/workflows/ci_consumption_workflow.yml @@ -32,6 +32,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | + python -m pip install --upgrade pip==23.0 python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] if [[ "${{ matrix.python-version }}" != "3.7" ]]; then diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml index 3d795261b..5c90cfdb8 100644 --- a/.github/workflows/ci_e2e_workflow.yml +++ b/.github/workflows/ci_e2e_workflow.yml @@ -58,7 +58,7 @@ jobs: done } - python -m pip install --upgrade pip + python -m pip install --upgrade pip==23.0 python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] if [[ "${{ matrix.python-version }}" != "3.7" ]]; then diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index 0fd99d3a1..a24a0675e 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -56,7 +56,7 @@ jobs: done } - python -m pip install --upgrade pip + python -m pip install --upgrade pip==23.0 python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] if [[ "${{ matrix.python-version }}" != "3.7" ]]; then From efbeff35db288a47783e2c5026e4b4e0e246a733 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:39:20 -0700 Subject: [PATCH 082/101] try fix 3.7 ppl --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e82b5eb33..d2ff37724 100644 --- a/setup.py +++ b/setup.py @@ -109,7 +109,8 @@ "opencv-python", "pandas", "numpy", - "pre-commit" + "pre-commit", + "importlib-metadata" ], "test-http-v2": ["azure-functions-extension-fastapi", "ujson", "orjson"] } From b49fc06b72e4890bbc0bf2c9367b9035787cdbeb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:43:37 -0700 Subject: [PATCH 083/101] try --- .github/workflows/ci_ut_workflow.yml | 1 + setup.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index a24a0675e..fda117d02 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -57,6 +57,7 @@ jobs: } python -m pip install --upgrade pip==23.0 + python -m pip install importlib-metadata python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] if [[ "${{ matrix.python-version }}" != "3.7" ]]; then diff --git a/setup.py b/setup.py index d2ff37724..e82b5eb33 100644 --- a/setup.py +++ b/setup.py @@ -109,8 +109,7 @@ "opencv-python", "pandas", "numpy", - "pre-commit", - "importlib-metadata" + "pre-commit" ], "test-http-v2": ["azure-functions-extension-fastapi", "ujson", "orjson"] } From 5b8af9e2113feeca11088132ea31095d211338ad Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:51:13 -0700 Subject: [PATCH 084/101] oh meta --- .github/workflows/ci_ut_workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index fda117d02..0dcb72106 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -57,7 +57,7 @@ jobs: } python -m pip install --upgrade pip==23.0 - python -m pip install importlib-metadata + python -m pip install importlib_metadata python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] if [[ "${{ matrix.python-version }}" != "3.7" ]]; then From ae97beab07853b89a874bbd3d8f25a7ae23b34db Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:07:37 -0700 Subject: [PATCH 085/101] fix --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index e82b5eb33..d90438021 100644 --- a/setup.py +++ b/setup.py @@ -85,6 +85,7 @@ EXTRA_REQUIRES = { "dev": [ + "importlib_metadata", "azure-eventhub~=5.7.0", # Used for EventHub E2E tests "azure-functions-durable", # Used for Durable E2E tests "flask", From 6f01bcb9eaefb5b5eef5e450a4e7977ac4e12ae1 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:39:46 -0700 Subject: [PATCH 086/101] fix --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index d90438021..8c78e8813 100644 --- a/setup.py +++ b/setup.py @@ -92,6 +92,7 @@ "fastapi~=0.85.0", # Used for ASGIMiddleware test "pydantic", "pycryptodome~=3.10.1", + "cmake==3.29.0.1" "flake8~=4.0.1", "mypy", "pytest", From 797f7167a3caad4783aa6e259f77d332dac132b3 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:08:31 -0700 Subject: [PATCH 087/101] fix --- .github/workflows/ci_ut_workflow.yml | 2 +- setup.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index 0dcb72106..e86e32e65 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -57,7 +57,7 @@ jobs: } python -m pip install --upgrade pip==23.0 - python -m pip install importlib_metadata + python -m pip install pipdeptree python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] if [[ "${{ matrix.python-version }}" != "3.7" ]]; then diff --git a/setup.py b/setup.py index 8c78e8813..e82b5eb33 100644 --- a/setup.py +++ b/setup.py @@ -85,14 +85,12 @@ EXTRA_REQUIRES = { "dev": [ - "importlib_metadata", "azure-eventhub~=5.7.0", # Used for EventHub E2E tests "azure-functions-durable", # Used for Durable E2E tests "flask", "fastapi~=0.85.0", # Used for ASGIMiddleware test "pydantic", "pycryptodome~=3.10.1", - "cmake==3.29.0.1" "flake8~=4.0.1", "mypy", "pytest", From de0cc9830b44d4bd14e5cd8081589d153b4b62f9 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:35:48 -0700 Subject: [PATCH 088/101] dont pin fastapi to fix cmake err --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e82b5eb33..6fde7ed5b 100644 --- a/setup.py +++ b/setup.py @@ -88,7 +88,7 @@ "azure-eventhub~=5.7.0", # Used for EventHub E2E tests "azure-functions-durable", # Used for Durable E2E tests "flask", - "fastapi~=0.85.0", # Used for ASGIMiddleware test + "fastapi", # Used for ASGIMiddleware test "pydantic", "pycryptodome~=3.10.1", "flake8~=4.0.1", From e61734094b4bd8427caf63e686eb7691fe9f557a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:40:15 -0700 Subject: [PATCH 089/101] fix --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6fde7ed5b..b55f460a0 100644 --- a/setup.py +++ b/setup.py @@ -85,7 +85,7 @@ EXTRA_REQUIRES = { "dev": [ - "azure-eventhub~=5.7.0", # Used for EventHub E2E tests + "azure-eventhub", # Used for EventHub E2E tests "azure-functions-durable", # Used for Durable E2E tests "flask", "fastapi", # Used for ASGIMiddleware test From e28d76925c813e17f7a9449d309a4c900005b9ae Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:48:13 -0700 Subject: [PATCH 090/101] fix --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b55f460a0..80afab519 100644 --- a/setup.py +++ b/setup.py @@ -88,7 +88,7 @@ "azure-eventhub", # Used for EventHub E2E tests "azure-functions-durable", # Used for Durable E2E tests "flask", - "fastapi", # Used for ASGIMiddleware test + "fastapi~=0.85.0", # Used for ASGIMiddleware test "pydantic", "pycryptodome~=3.10.1", "flake8~=4.0.1", From 7449d0db5349820a29ef029f9f0624c22995131c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 19:18:13 -0700 Subject: [PATCH 091/101] update azfunc base --- azure_functions_worker/http_v2.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index 5d00ecd6a..8c20d8b32 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -235,7 +235,7 @@ def _check_http_v2_enabled(cls): is_envvar_false(PYTHON_ENABLE_INIT_INDEXING): return False - import azure.functions.extension.base as ext_base + import azurefunctions.extensions.base as ext_base cls._ext_base = ext_base return cls._ext_base.HttpV2FeatureChecker.http_v2_enabled() diff --git a/setup.py b/setup.py index 80afab519..7b518151f 100644 --- a/setup.py +++ b/setup.py @@ -80,7 +80,7 @@ else: INSTALL_REQUIRES.extend( ("protobuf~=4.22.0", "grpcio-tools~=1.54.2", "grpcio~=1.54.2", - "azure-functions-extension-base") + "azurefunctions-extensions-base") ) EXTRA_REQUIRES = { From 450b43e5fc2196698ba4a7f05c6335c4e3302f77 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 10 Apr 2024 19:47:07 -0700 Subject: [PATCH 092/101] update --- setup.py | 6 +++++- .../http_functions_v2/fastapi/file_name/main.py | 2 +- .../http_functions_v2/fastapi/function_app.py | 2 +- .../dispatcher_functions/http_v2/fastapi/function_app.py | 2 +- .../http_v2_functions/fastapi/function_app.py | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 7b518151f..85d6941c9 100644 --- a/setup.py +++ b/setup.py @@ -111,7 +111,11 @@ "numpy", "pre-commit" ], - "test-http-v2": ["azure-functions-extension-fastapi", "ujson", "orjson"] + "test-http-v2": [ + "azurefunctions-extensions-http-fastapi", + "ujson", + "orjson" + ] } diff --git a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py index c9718fef5..65ae5525b 100644 --- a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py +++ b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py @@ -7,7 +7,7 @@ import azure.functions as func -from azure.functions.extension.fastapi import Request, Response +from azurefunctions.extensions.http.fastapi import Request, Response app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) diff --git a/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py b/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py index b82e0baee..355de98a8 100644 --- a/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py +++ b/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py @@ -5,7 +5,7 @@ import time import azure.functions as func -from azure.functions.extension.fastapi import Request, Response, \ +from azurefunctions.extensions.http.fastapi import Request, Response, \ StreamingResponse, HTMLResponse, \ UJSONResponse, ORJSONResponse, FileResponse diff --git a/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py b/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py index f202890de..a2c15d419 100644 --- a/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py +++ b/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py @@ -1,4 +1,4 @@ -from azure.functions.extension.fastapi import Request, Response +from azurefunctions.extensions.http.fastapi import Request, Response import azure.functions as func diff --git a/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py b/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py index 9830f572e..b4018466d 100644 --- a/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py +++ b/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py @@ -6,7 +6,7 @@ import sys import time from urllib.request import urlopen -from azure.functions.extension.fastapi import Request, Response, \ +from azurefunctions.extensions.http.fastapi import Request, Response, \ HTMLResponse, RedirectResponse import azure.functions as func from pydantic import BaseModel From 3ab30f79e9c97ec20d4637975852c0c76eb592d1 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:18:30 -0700 Subject: [PATCH 093/101] feedback --- azure_functions_worker/dispatcher.py | 11 +++-------- azure_functions_worker/functions.py | 14 +++++++++----- azure_functions_worker/http_v2.py | 10 ++++++++++ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 65c9ebd57..d31c10b2f 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -32,7 +32,8 @@ PYTHON_LANGUAGE_RUNTIME, PYTHON_ENABLE_INIT_INDEXING, METADATA_PROPERTIES_WORKER_INDEXED) from .extension import ExtensionManager -from .http_v2 import http_coordinator, initialize_http_server, HttpV2Registry +from .http_v2 import http_coordinator, initialize_http_server, HttpV2Registry, \ + sync_http_request from .logging import disable_console_logging, enable_console_logging from .logging import (logger, error_logger, is_system_log_category, CONSOLE_LOG_PREFIX, format_exception) @@ -549,13 +550,7 @@ async def _handle__invocation_request(self, request): http_request = await http_coordinator.get_http_request_async( invocation_id) - route_params = {key: item.string for key, item - in invoc_request.trigger_metadata.items() - if key not in ['Headers', 'Query']} - - (HttpV2Registry.ext_base().RequestTrackerMeta - .get_synchronizer() - .sync_route_params(http_request, route_params)) + await sync_http_request(http_request, invoc_request) args[fi.trigger_metadata.get('param_name')] = http_request fi_context = self._get_context(invoc_request, fi.name, diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index 00296c441..e529f37ee 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -302,11 +302,7 @@ def add_func_to_registry_and_return_funcinfo(self, function, str, ParamTypeInfo], return_type: str): - http_trigger_param_name = next( - (input_type for input_type, type_info in input_types.items() - if type_info.binding_name == HTTP_TRIGGER), - None - ) + http_trigger_param_name = self._get_http_trigger_param_name(input_types) trigger_metadata = None is_http_func = False @@ -335,6 +331,14 @@ def add_func_to_registry_and_return_funcinfo(self, function, return function_info + def _get_http_trigger_param_name(self, input_types): + http_trigger_param_name = next( + (input_type for input_type, type_info in input_types.items() + if type_info.binding_name == HTTP_TRIGGER), + None + ) + return http_trigger_param_name + def add_function(self, function_id: str, func: typing.Callable, metadata: protos.RpcFunctionMetadata): diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index 8c20d8b32..d75548922 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -208,6 +208,16 @@ async def catch_all(request: request_type): # type: ignore return web_server_address +async def sync_http_request(http_request, invoc_request): + # Sync http request route params from invoc_request to http_request + route_params = {key: item.string for key, item + in invoc_request.trigger_metadata.items() + if key not in ['Headers', 'Query']} + (HttpV2Registry.ext_base().RequestTrackerMeta + .get_synchronizer() + .sync_route_params(http_request, route_params)) + + class HttpV2Registry: _http_v2_enabled = False _ext_base = None From e85ea9e2fe6f93121008ccf7d5c94df3134c5b05 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:41:51 -0700 Subject: [PATCH 094/101] feedback --- .github/workflows/ci_consumption_workflow.yml | 2 +- .github/workflows/ci_e2e_workflow.yml | 2 +- .github/workflows/ci_ut_workflow.yml | 3 +-- azure_functions_worker/dispatcher.py | 4 ++-- azure_functions_worker/http_v2.py | 13 +++++++------ 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml index c610347fe..3a280cdb1 100644 --- a/.github/workflows/ci_consumption_workflow.yml +++ b/.github/workflows/ci_consumption_workflow.yml @@ -32,7 +32,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip==23.0 + python -m pip install --upgrade pip python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] if [[ "${{ matrix.python-version }}" != "3.7" ]]; then diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml index 5c90cfdb8..3d795261b 100644 --- a/.github/workflows/ci_e2e_workflow.yml +++ b/.github/workflows/ci_e2e_workflow.yml @@ -58,7 +58,7 @@ jobs: done } - python -m pip install --upgrade pip==23.0 + python -m pip install --upgrade pip python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] if [[ "${{ matrix.python-version }}" != "3.7" ]]; then diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml index e86e32e65..0fd99d3a1 100644 --- a/.github/workflows/ci_ut_workflow.yml +++ b/.github/workflows/ci_ut_workflow.yml @@ -56,8 +56,7 @@ jobs: done } - python -m pip install --upgrade pip==23.0 - python -m pip install pipdeptree + python -m pip install --upgrade pip python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre python -m pip install -U -e .[dev] if [[ "${{ matrix.python-version }}" != "3.7" ]]; then diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index d31c10b2f..a981fe049 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -314,7 +314,7 @@ async def _handle__worker_init_request(self, request): self._function_metadata_exception = ex try: - if HttpV2Registry.http_v2_enabled(self._functions): + if HttpV2Registry.http_v2_enabled(): capabilities[constants.HTTP_URI] = \ initialize_http_server(self._host) except Exception as ex: @@ -689,7 +689,7 @@ async def _handle__function_environment_reload_request(self, request): except Exception as ex: self._function_metadata_exception = ex - if HttpV2Registry.http_v2_enabled(self._functions): + if HttpV2Registry.http_v2_enabled(): capabilities[constants.HTTP_URI] = \ initialize_http_server(self._host) diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index d75548922..b95a60afd 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -143,7 +143,7 @@ def _pop_http_request(self, invoc_id): context_ref.http_request = None return request - raise Exception(f"No http request found for invocation {invoc_id}") + raise ValueError(f"No http request found for invocation {invoc_id}") def _pop_http_response(self, invoc_id): context_ref = self._context_references.get(invoc_id) @@ -151,7 +151,8 @@ def _pop_http_response(self, invoc_id): if response is not None: context_ref.http_response = None return response - raise Exception(f"No http response found for invocation {invoc_id}") + + raise ValueError(f"No http response found for invocation {invoc_id}") def get_unused_tcp_port(): @@ -184,12 +185,12 @@ async def catch_all(request: request_type): # type: ignore invoc_id = request.headers.get(X_MS_INVOCATION_ID) if invoc_id is None: raise ValueError(f"Header {X_MS_INVOCATION_ID} not found") - logger.info(f'Received HTTP request for invocation {invoc_id}') + logger.info('Received HTTP request for invocation %s', invoc_id) http_coordinator.set_http_request(invoc_id, request) http_resp = \ await http_coordinator.await_http_response_async(invoc_id) - logger.info(f'Sending HTTP response for invocation {invoc_id}') + logger.info('Sending HTTP response for invocation %s', invoc_id) # if http_resp is an python exception, raise it if isinstance(http_resp, Exception): raise http_resp @@ -203,7 +204,7 @@ async def catch_all(request: request_type): # type: ignore loop.create_task(web_server_run_task) web_server_address = f"http://{host_addr}:{unused_port}" - logger.info(f'HTTP server starting on {web_server_address}') + logger.info('HTTP server starting on %s', web_server_address) return web_server_address @@ -224,7 +225,7 @@ class HttpV2Registry: _http_v2_enabled_checked = False @classmethod - def http_v2_enabled(cls, functions=None, **kwargs): + def http_v2_enabled(cls, **kwargs): # Check if HTTP/2 enablement has already been checked if not cls._http_v2_enabled_checked: # If not checked yet, mark as checked From a695ba3fce92c4e16a181d8eabacdeb9ca6fb1c2 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 11 Apr 2024 11:29:55 -0700 Subject: [PATCH 095/101] feedback --- azure_functions_worker/dispatcher.py | 33 ++++------ azure_functions_worker/exceptions.py | 9 +++ azure_functions_worker/http_v2.py | 99 +++++++++++++++------------- 3 files changed, 75 insertions(+), 66 deletions(-) create mode 100644 azure_functions_worker/exceptions.py diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index a981fe049..4a7c1b5c1 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -31,6 +31,7 @@ PYTHON_SCRIPT_FILE_NAME_DEFAULT, PYTHON_LANGUAGE_RUNTIME, PYTHON_ENABLE_INIT_INDEXING, METADATA_PROPERTIES_WORKER_INDEXED) +from .exceptions import HttpServerInitError from .extension import ExtensionManager from .http_v2 import http_coordinator, initialize_http_server, HttpV2Registry, \ sync_http_request @@ -112,8 +113,7 @@ def get_sync_tp_workers_set(self): 3.9 scenarios (as we'll start passing only None by default), and we need to get that information. - Ref: concurrent.futures.thread.ThreadPoolExecutor.__init__ - ._max_workers + Ref: concurrent.futures.thread.ThreadPoolExecutor.__init__._max_workers """ return self._sync_call_tp._max_workers @@ -310,22 +310,15 @@ async def _handle__worker_init_request(self, request): self.load_function_metadata( worker_init_request.function_app_directory, caller_info="worker_init_request") - except Exception as ex: - self._function_metadata_exception = ex - try: if HttpV2Registry.http_v2_enabled(): capabilities[constants.HTTP_URI] = \ initialize_http_server(self._host) + + except HttpServerInitError: + raise except Exception as ex: - return protos.StreamingMessage( - request_id=self.request_id, - worker_init_response=protos.WorkerInitResponse( - result=protos.StatusResult( - status=protos.StatusResult.Failure, - exception=self._serialize_exception(ex)) - ) - ) + self._function_metadata_exception = ex return protos.StreamingMessage( request_id=self.request_id, @@ -333,9 +326,7 @@ async def _handle__worker_init_request(self, request): capabilities=capabilities, worker_metadata=self.get_worker_metadata(), result=protos.StatusResult( - status=protos.StatusResult.Success), - ), - ) + status=protos.StatusResult.Success))) async def _handle__worker_status_request(self, request): # Logging is not necessary in this request since the response is used @@ -686,13 +677,15 @@ async def _handle__function_environment_reload_request(self, request): self.load_function_metadata( directory, caller_info="environment_reload_request") + + if HttpV2Registry.http_v2_enabled(): + capabilities[constants.HTTP_URI] = \ + initialize_http_server(self._host) + except HttpServerInitError: + raise except Exception as ex: self._function_metadata_exception = ex - if HttpV2Registry.http_v2_enabled(): - capabilities[constants.HTTP_URI] = \ - initialize_http_server(self._host) - # Change function app directory if getattr(func_env_reload_request, 'function_app_directory', None): diff --git a/azure_functions_worker/exceptions.py b/azure_functions_worker/exceptions.py new file mode 100644 index 000000000..910d0ab73 --- /dev/null +++ b/azure_functions_worker/exceptions.py @@ -0,0 +1,9 @@ +# http v2 exception types +class HttpServerInitError(Exception): + """Exception raised when there is an error during HTTP server + initialization.""" + + +class MissingHeaderError(ValueError): + """Exception raised when a required header is missing in the + HTTP request.""" diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index b95a60afd..93262b430 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -7,6 +7,8 @@ from azure_functions_worker.constants import X_MS_INVOCATION_ID, \ BASE_EXT_SUPPORTED_PY_MINOR_VERSION, PYTHON_ENABLE_INIT_INDEXING +from azure_functions_worker.exceptions import MissingHeaderError, \ + HttpServerInitError from azure_functions_worker.logging import logger from azure_functions_worker.utils.common import is_envvar_false @@ -113,8 +115,8 @@ def set_http_request(self, invoc_id, http_request): def set_http_response(self, invoc_id, http_response): if invoc_id not in self._context_references: - raise Exception("No context reference found for invocation " - f"{invoc_id}") + raise KeyError("No context reference found for invocation %s" + % invoc_id) context_ref = self._context_references.get(invoc_id) context_ref.http_response = http_response @@ -122,16 +124,15 @@ async def get_http_request_async(self, invoc_id): if invoc_id not in self._context_references: self._context_references[invoc_id] = AsyncContextReference() - await asyncio.sleep(0) await self._context_references.get( invoc_id).http_request_available_event.wait() return self._pop_http_request(invoc_id) async def await_http_response_async(self, invoc_id): if invoc_id not in self._context_references: - raise Exception("No context reference found for invocation " - f"{invoc_id}") - await asyncio.sleep(0) + raise KeyError("No context reference found for invocation %s" + % invoc_id) + await self._context_references.get( invoc_id).http_response_available_event.wait() return self._pop_http_response(invoc_id) @@ -143,7 +144,7 @@ def _pop_http_request(self, invoc_id): context_ref.http_request = None return request - raise ValueError(f"No http request found for invocation {invoc_id}") + raise ValueError("No http request found for invocation %s" % invoc_id) def _pop_http_response(self, invoc_id): context_ref = self._context_references.get(invoc_id) @@ -152,7 +153,7 @@ def _pop_http_response(self, invoc_id): context_ref.http_response = None return response - raise ValueError(f"No http response found for invocation {invoc_id}") + raise ValueError("No http response found for invocation %s" % invoc_id) def get_unused_tcp_port(): @@ -169,44 +170,50 @@ def get_unused_tcp_port(): def initialize_http_server(host_addr, **kwargs): - ext_base = HttpV2Registry.ext_base() - web_extension_mod_name = ext_base.ModuleTrackerMeta.get_module() - extension_module = importlib.import_module(web_extension_mod_name) - web_app_class = extension_module.WebApp - web_server_class = extension_module.WebServer - - unused_port = get_unused_tcp_port() - - app = web_app_class() - request_type = ext_base.RequestTrackerMeta.get_request_type() - - @app.route - async def catch_all(request: request_type): # type: ignore - invoc_id = request.headers.get(X_MS_INVOCATION_ID) - if invoc_id is None: - raise ValueError(f"Header {X_MS_INVOCATION_ID} not found") - logger.info('Received HTTP request for invocation %s', invoc_id) - http_coordinator.set_http_request(invoc_id, request) - http_resp = \ - await http_coordinator.await_http_response_async(invoc_id) - - logger.info('Sending HTTP response for invocation %s', invoc_id) - # if http_resp is an python exception, raise it - if isinstance(http_resp, Exception): - raise http_resp - - return http_resp - - web_server = web_server_class(host_addr, unused_port, app) - web_server_run_task = web_server.serve() - - loop = asyncio.get_event_loop() - loop.create_task(web_server_run_task) - - web_server_address = f"http://{host_addr}:{unused_port}" - logger.info('HTTP server starting on %s', web_server_address) - - return web_server_address + try: + ext_base = HttpV2Registry.ext_base() + web_extension_mod_name = ext_base.ModuleTrackerMeta.get_module() + extension_module = importlib.import_module(web_extension_mod_name) + web_app_class = extension_module.WebApp + web_server_class = extension_module.WebServer + + unused_port = get_unused_tcp_port() + + app = web_app_class() + request_type = ext_base.RequestTrackerMeta.get_request_type() + + @app.route + async def catch_all(request: request_type): # type: ignore + invoc_id = request.headers.get(X_MS_INVOCATION_ID) + if invoc_id is None: + raise MissingHeaderError("Header %s not found" % + X_MS_INVOCATION_ID) + logger.info('Received HTTP request for invocation %s', invoc_id) + http_coordinator.set_http_request(invoc_id, request) + http_resp = \ + await http_coordinator.await_http_response_async(invoc_id) + + logger.info('Sending HTTP response for invocation %s', invoc_id) + # if http_resp is an python exception, raise it + if isinstance(http_resp, Exception): + raise http_resp + + return http_resp + + web_server = web_server_class(host_addr, unused_port, app) + web_server_run_task = web_server.serve() + + loop = asyncio.get_event_loop() + loop.create_task(web_server_run_task) + + web_server_address = f"http://{host_addr}:{unused_port}" + logger.info('HTTP server starting on %s', web_server_address) + + return web_server_address + + except Exception as e: + raise HttpServerInitError("Error initializing HTTP server: %s" % e) \ + from e async def sync_http_request(http_request, invoc_request): From 62d7baceba19312d6615f7cbe8abc9d007fb42d3 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 11 Apr 2024 11:53:30 -0700 Subject: [PATCH 096/101] add docs --- azure_functions_worker/http_v2.py | 18 ++++++++++++++++++ tests/unittests/test_http_v2.py | 4 ++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index 93262b430..9892c6a9a 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -14,6 +14,9 @@ class BaseContextReference(abc.ABC): + """ + Base class for context references. + """ def __init__(self, event_class, http_request=None, http_response=None, function=None, fi_context=None, args=None, http_trigger_param_name=None): @@ -86,6 +89,9 @@ def http_response_available_event(self): class AsyncContextReference(BaseContextReference): + """ + Asynchronous context reference class. + """ def __init__(self, http_request=None, http_response=None, function=None, fi_context=None, args=None): super().__init__(event_class=asyncio.Event, http_request=http_request, @@ -95,6 +101,9 @@ def __init__(self, http_request=None, http_response=None, function=None, class SingletonMeta(type): + """ + Metaclass for implementing the singleton pattern. + """ _instances = {} def __call__(cls, *args, **kwargs): @@ -104,6 +113,9 @@ def __call__(cls, *args, **kwargs): class HttpCoordinator(metaclass=SingletonMeta): + """ + HTTP coordinator class for managing HTTP v2 requests and responses. + """ def __init__(self): self._context_references: Dict[str, BaseContextReference] = {} @@ -170,6 +182,9 @@ def get_unused_tcp_port(): def initialize_http_server(host_addr, **kwargs): + """ + Initialize HTTP v2 server for handling HTTP requests. + """ try: ext_base = HttpV2Registry.ext_base() web_extension_mod_name = ext_base.ModuleTrackerMeta.get_module() @@ -227,6 +242,9 @@ async def sync_http_request(http_request, invoc_request): class HttpV2Registry: + """ + HTTP v2 registry class for managing HTTP v2 states. + """ _http_v2_enabled = False _ext_base = None _http_v2_enabled_checked = False diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py index 1ab7af5e7..90d4d9b46 100644 --- a/tests/unittests/test_http_v2.py +++ b/tests/unittests/test_http_v2.py @@ -105,8 +105,8 @@ def test_await_http_response_async_invalid_invocation(self): self.loop.run_until_complete( http_coordinator.await_http_response_async(invalid_invoc_id)) self.assertEqual(str(context.exception), - f"No context reference found for invocation " - f"{invalid_invoc_id}") + f"'No context reference found for invocation " + f"{invalid_invoc_id}'") def test_await_http_response_async_response_not_set(self): invoc_id = "invocation_with_no_response" From 142b5cecd3861e0feebefa820522d640c2ef1bd5 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 11 Apr 2024 17:13:03 -0700 Subject: [PATCH 097/101] merge fix --- azure_functions_worker/bindings/meta.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index 2571f9a8e..4202b87a7 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -27,7 +27,8 @@ deferred_bindings_cache = {} -def _check_http_input_type_annotation(bind_name: str, pytype: type, is_deferred_binding: bool) -> bool: +def _check_http_input_type_annotation(bind_name: str, pytype: type, + is_deferred_binding: bool) -> bool: if HttpV2Registry.http_v2_enabled(): return HttpV2Registry.ext_base().RequestTrackerMeta \ .check_type(pytype) @@ -115,10 +116,13 @@ def is_trigger_binding(bind_name: str) -> bool: return binding.has_trigger_support() -def check_input_type_annotation(bind_name: str, pytype: type) -> bool: +def check_input_type_annotation(bind_name: str, + pytype: type, + is_deferred_binding: bool) -> bool: global INPUT_TYPE_CHECK_OVERRIDE_MAP if bind_name in INPUT_TYPE_CHECK_OVERRIDE_MAP: - return INPUT_TYPE_CHECK_OVERRIDE_MAP[bind_name](bind_name, pytype, is_deferred_binding) + return INPUT_TYPE_CHECK_OVERRIDE_MAP[bind_name](bind_name, pytype, + is_deferred_binding) binding = get_binding(bind_name, is_deferred_binding) From 3608c23da90d0793023e791e2acc4e1bccba7111 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 12 Apr 2024 10:45:01 -0700 Subject: [PATCH 098/101] feedback --- azure_functions_worker/bindings/meta.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index 4202b87a7..c1ff6e2ee 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -4,7 +4,6 @@ import sys import typing -from azure_functions_worker.constants import HTTP, HTTP_TRIGGER from .. import protos from . import datumdef @@ -12,7 +11,7 @@ from .shared_memory_data_transfer import SharedMemoryManager from ..http_v2 import HttpV2Registry -from ..constants import CUSTOMER_PACKAGES_PATH +from ..constants import CUSTOMER_PACKAGES_PATH, HTTP, HTTP_TRIGGER from ..logging import logger From a20ff5c2af102be4075ddedacc4f6029bf8e9a06 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:56:05 -0700 Subject: [PATCH 099/101] feedback --- azure_functions_worker/dispatcher.py | 3 +-- azure_functions_worker/exceptions.py | 9 --------- azure_functions_worker/http_v2.py | 16 ++++++++++++++-- .../consumption_tests/test_linux_consumption.py | 3 ++- .../http_functions_v2/fastapi/file_name/main.py | 3 --- 5 files changed, 17 insertions(+), 17 deletions(-) delete mode 100644 azure_functions_worker/exceptions.py diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 5c0ec106f..6938eb917 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -31,10 +31,9 @@ PYTHON_SCRIPT_FILE_NAME_DEFAULT, PYTHON_LANGUAGE_RUNTIME, PYTHON_ENABLE_INIT_INDEXING, METADATA_PROPERTIES_WORKER_INDEXED) -from .exceptions import HttpServerInitError from .extension import ExtensionManager from .http_v2 import http_coordinator, initialize_http_server, HttpV2Registry, \ - sync_http_request + sync_http_request, HttpServerInitError from .logging import disable_console_logging, enable_console_logging from .logging import (logger, error_logger, is_system_log_category, CONSOLE_LOG_PREFIX, format_exception) diff --git a/azure_functions_worker/exceptions.py b/azure_functions_worker/exceptions.py deleted file mode 100644 index 910d0ab73..000000000 --- a/azure_functions_worker/exceptions.py +++ /dev/null @@ -1,9 +0,0 @@ -# http v2 exception types -class HttpServerInitError(Exception): - """Exception raised when there is an error during HTTP server - initialization.""" - - -class MissingHeaderError(ValueError): - """Exception raised when a required header is missing in the - HTTP request.""" diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py index 9892c6a9a..d7fff59b6 100644 --- a/azure_functions_worker/http_v2.py +++ b/azure_functions_worker/http_v2.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + import abc import asyncio import importlib @@ -7,12 +10,21 @@ from azure_functions_worker.constants import X_MS_INVOCATION_ID, \ BASE_EXT_SUPPORTED_PY_MINOR_VERSION, PYTHON_ENABLE_INIT_INDEXING -from azure_functions_worker.exceptions import MissingHeaderError, \ - HttpServerInitError from azure_functions_worker.logging import logger from azure_functions_worker.utils.common import is_envvar_false +# Http V2 Exceptions +class HttpServerInitError(Exception): + """Exception raised when there is an error during HTTP server + initialization.""" + + +class MissingHeaderError(ValueError): + """Exception raised when a required header is missing in the + HTTP request.""" + + class BaseContextReference(abc.ABC): """ Base class for context references. diff --git a/tests/consumption_tests/test_linux_consumption.py b/tests/consumption_tests/test_linux_consumption.py index 9c5be090f..fbfd3bcea 100644 --- a/tests/consumption_tests/test_linux_consumption.py +++ b/tests/consumption_tests/test_linux_consumption.py @@ -340,7 +340,8 @@ def test_reload_variables_after_oom_error(self): "This is testing only for python310") def test_http_v2_fastapi_streaming_upload_download(self): """ - A function app with init indexing enabled + A function app using http v2 fastapi extension with streaming upload and + download """ with LinuxConsumptionWebHostController(_DEFAULT_HOST_VERSION, self._py_version) as ctrl: diff --git a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py index 65ae5525b..b149c1f78 100644 --- a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py +++ b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py @@ -1,6 +1,3 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - from datetime import datetime import logging import time From 15ba6b44977ab62c70cb5cc7bc9114ef67182e2b Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 12 Apr 2024 12:43:58 -0700 Subject: [PATCH 100/101] feedback --- azure_functions_worker/bindings/meta.py | 5 ++--- azure_functions_worker/functions.py | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index c1ff6e2ee..de3d69db6 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -11,15 +11,14 @@ from .shared_memory_data_transfer import SharedMemoryManager from ..http_v2 import HttpV2Registry -from ..constants import CUSTOMER_PACKAGES_PATH, HTTP, HTTP_TRIGGER +from ..constants import CUSTOMER_PACKAGES_PATH, HTTP, HTTP_TRIGGER, \ + BASE_EXT_SUPPORTED_PY_MINOR_VERSION from ..logging import logger PB_TYPE = 'rpc_data' PB_TYPE_DATA = 'data' PB_TYPE_RPC_SHARED_MEMORY = 'rpc_shared_memory' -# Base extension supported Python minor version -BASE_EXT_SUPPORTED_PY_MINOR_VERSION = 8 BINDING_REGISTRY = None DEFERRED_BINDING_REGISTRY = None diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index 258a24ab9..cbfef029c 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -6,8 +6,7 @@ import typing import uuid -from azure_functions_worker.constants import HTTP_TRIGGER - +from .constants import HTTP_TRIGGER from . import bindings as bindings_utils from . import protos from ._thirdparty import typing_inspect From 87399e7fa3d5ffcbee20e19167db5ce419417e53 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 12 Apr 2024 12:47:43 -0700 Subject: [PATCH 101/101] feedback --- .../fastapi/file_name/main.py | 43 ------------------- 1 file changed, 43 deletions(-) delete mode 100644 tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py diff --git a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py deleted file mode 100644 index b149c1f78..000000000 --- a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py +++ /dev/null @@ -1,43 +0,0 @@ -from datetime import datetime -import logging -import time - -import azure.functions as func - -from azurefunctions.extensions.http.fastapi import Request, Response - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.route(route="default_template") -async def default_template(req: Request) -> Response: - logging.info('Python HTTP trigger function processed a request.') - - name = req.query_params.get('name') - if not name: - try: - req_body = await req.json() - except ValueError: - pass - else: - name = req_body.get('name') - - if name: - return Response( - f"Hello, {name}. This HTTP triggered function " - f"executed successfully.") - else: - return Response( - "This HTTP triggered function executed successfully. " - "Pass a name in the query string or in the request body for a" - " personalized response.", - status_code=200 - ) - - -@app.route(route="http_func") -def http_func(req: Request) -> Response: - time.sleep(1) - - current_time = datetime.now().strftime("%H:%M:%S") - return Response(f"{current_time}")