From b389091915e0be8df35ca255cf8c10f16aa32341 Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Thu, 4 May 2023 14:31:26 -0500 Subject: [PATCH 01/12] Retry policies --- .../bindings/retrycontext.py | 29 +++-------- azure_functions_worker/functions.py | 8 ++- azure_functions_worker/loader.py | 11 +++++ azure_functions_worker/protos/__init__.py | 3 +- .../protos/_src/src/proto/FunctionRpc.proto | 49 +++++++++++++++++++ 5 files changed, 76 insertions(+), 24 deletions(-) diff --git a/azure_functions_worker/bindings/retrycontext.py b/azure_functions_worker/bindings/retrycontext.py index 04481ff9..36e7c682 100644 --- a/azure_functions_worker/bindings/retrycontext.py +++ b/azure_functions_worker/bindings/retrycontext.py @@ -1,31 +1,16 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from dataclasses import dataclass from . import rpcexception +@dataclass class RetryContext: - """Check https://docs.microsoft.com/en-us/azure/azure-functions/ - functions-bindings-error-pages?tabs=python#retry-policies-preview""" + """Gets the current retry count from retry-context""" + retry_count: int - def __init__(self, - retry_count: int, - max_retry_count: int, - rpc_exception: rpcexception.RpcException) -> None: - self.__retry_count = retry_count - self.__max_retry_count = max_retry_count - self.__rpc_exception = rpc_exception + """Gets the max retry count from retry-context""" + max_retry_count: int - @property - def retry_count(self) -> int: - """Gets the current retry count from retry-context""" - return self.__retry_count - - @property - def max_retry_count(self) -> int: - """Gets the max retry count from retry-context""" - return self.__max_retry_count - - @property - def exception(self) -> rpcexception.RpcException: - return self.__rpc_exception + rpc_exception: rpcexception.RpcException diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index f6d59122..3876312d 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -255,6 +255,10 @@ def validate_function_params(params: dict, bound_params: dict, else: input_types[param.name] = param_type_info return input_types, output_types + + @staticmethod + def get_retry_polies(binding): + pass @staticmethod def get_function_return_type(annotations: dict, has_explicit_return: bool, @@ -397,6 +401,8 @@ def add_indexed_function(self, function): self.get_explicit_and_implicit_return( binding.name, binding, has_explicit_return, has_implicit_return, bound_params) + + retry_policy = self.get_retry_polies(binding) return_binding_name = self.get_return_binding(binding.name, binding.type, @@ -417,7 +423,7 @@ def add_indexed_function(self, function): has_implicit_return, return_binding_name, func_name) - + return \ self.add_func_to_registry_and_return_funcinfo(func, func_name, function_id, diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index 8f545691..fadb7cb6 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. """Python functions loader.""" +import datetime import importlib import importlib.machinery import importlib.util @@ -11,6 +12,8 @@ from os import PathLike, fspath from typing import Optional, Dict +from google.protobuf.duration_pb2 import Duration + from . import protos, functions from .constants import MODULE_NOT_FOUND_TS_URL, SCRIPT_FILE_NAME, \ PYTHON_LANGUAGE_RUNTIME @@ -62,12 +65,19 @@ def build_binding_protos(indexed_function) -> Dict: def process_indexed_function(functions_registry: functions.Registry, indexed_functions): + td = datetime.timedelta(seconds=2) + duration = Duration() + duration.FromTimedelta(td) + 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_options = protos.RetryOptions(retryStrategy="fixedDelay", + max_retry_count=10, + delay_interval=duration) function_metadata = protos.RpcFunctionMetadata( name=function_info.name, @@ -80,6 +90,7 @@ def process_indexed_function(functions_registry: functions.Registry, language=PYTHON_LANGUAGE_RUNTIME, bindings=binding_protos, raw_bindings=indexed_function.get_raw_bindings(), + retry=retry_options, properties={"worker_indexed": "True"}) fx_metadata_results.append(function_metadata) diff --git a/azure_functions_worker/protos/__init__.py b/azure_functions_worker/protos/__init__.py index 8e74b5c6..6e8ae0f7 100644 --- a/azure_functions_worker/protos/__init__.py +++ b/azure_functions_worker/protos/__init__.py @@ -32,7 +32,8 @@ CloseSharedMemoryResourcesResponse, FunctionsMetadataRequest, FunctionMetadataResponse, - WorkerMetadata) + WorkerMetadata, + RetryOptions) from .shared.NullableTypes_pb2 import ( NullableString, diff --git a/azure_functions_worker/protos/_src/src/proto/FunctionRpc.proto b/azure_functions_worker/protos/_src/src/proto/FunctionRpc.proto index f5cc48b3..c3cc8bd8 100644 --- a/azure_functions_worker/protos/_src/src/proto/FunctionRpc.proto +++ b/azure_functions_worker/protos/_src/src/proto/FunctionRpc.proto @@ -10,6 +10,7 @@ option go_package ="github.com/Azure/azure-functions-go-worker/internal/rpc"; package AzureFunctionsRpcMessages; import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; import "identity/ClaimsIdentityRpc.proto"; import "shared/NullableTypes.proto"; @@ -86,6 +87,13 @@ message StreamingMessage { // Host gets the list of function load responses FunctionLoadResponseCollection function_load_response_collection = 32; + + // Host sends required metadata to worker to warmup the worker + WorkerWarmupRequest worker_warmup_request = 33; + + // Worker responds after warming up with the warmup result + WorkerWarmupResponse worker_warmup_response = 34; + } } @@ -330,10 +338,15 @@ message RpcFunctionMetadata { // A flag indicating if managed dependency is enabled or not bool managed_dependency_enabled = 14; + // The optional function execution retry strategy to use on invocation failures. + RetryOptions retry = 15; + // Properties for function metadata // They're usually specific to a worker and largely passed along to the controller API for use // outside the host map properties = 16; + + string raw_retry_options = 17; } // Host tells worker it is ready to receive metadata @@ -423,6 +436,15 @@ message InvocationResponse { StatusResult result = 3; } +message WorkerWarmupRequest { + // Full path of worker.config.json location + string worker_directory = 1; +} + +message WorkerWarmupResponse { + StatusResult result = 1; +} + // Used to encapsulate data which could be a variety of types message TypedData { oneof data { @@ -681,4 +703,31 @@ message ModelBindingData // Used to encapsulate collection model_binding_data message CollectionModelBindingData { repeated ModelBindingData model_binding_data = 1; +} + +// Retry policy which the worker sends the host when the worker indexes +// a function. +message RetryOptions +{ + // The retry strategy to use. Valid values are fixedDelay or exponentialBackoff. + enum RetryStrategy + { + exponentialBackoff = 0; + fixedDelay = 1; + } + + // The maximum number of retries allowed per function execution. + // -1 means to retry indefinitely. + int32 max_retry_count = 2; + + // The delay that's used between retries when you're using a fixedDelay strategy. + google.protobuf.Duration delay_interval = 3; + + // The minimum retry delay when you're using an exponentialBackoff strategy + google.protobuf.Duration minimum_interval = 4; + + // The maximum retry delay when you're using an exponentialBackoff strategy + google.protobuf.Duration maximum_interval = 5; + + RetryStrategy retryStrategy = 6; } \ No newline at end of file From 50e391cf86e83e99369bd2b5dc559fa3f30a39d4 Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Mon, 5 Jun 2023 13:11:04 -0500 Subject: [PATCH 02/12] Additional changes for retry policy --- azure_functions_worker/dispatcher.py | 2 +- azure_functions_worker/functions.py | 4 ++- azure_functions_worker/loader.py | 22 +++++++++------ azure_functions_worker/protos/__init__.py | 2 +- .../protos/_src/src/proto/FunctionRpc.proto | 27 +++++++++---------- python/test/worker.config.json | 2 +- 6 files changed, 32 insertions(+), 27 deletions(-) diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 2d5543d2..4f6c0039 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -612,7 +612,7 @@ def index_functions(self, function_path: str): indexed_function_logs: List[str] = [] for func in indexed_functions: function_log = "Function Name: {}, Function Binding: {}" \ - .format(func.get_function_name(), + .format(func.get_setting("function_name"), [(binding.type, binding.name) for binding in func.get_bindings()]) indexed_function_logs.append(function_log) diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index 3876312d..539cfcb4 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -380,7 +380,9 @@ def add_function(self, function_id: str, def add_indexed_function(self, function): func = function.get_user_function() - func_name = function.get_function_name() + func_name_setting = function.get_setting("function_name") + func_name = func_name_setting if func_name_setting else func.__name__ + func_type = function.http_type function_id = str(uuid.uuid5(namespace=uuid.NAMESPACE_OID, name=func_name)) diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index fadb7cb6..1b4092f2 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -62,22 +62,28 @@ def build_binding_protos(indexed_function) -> Dict: return binding_protos +def build_retry_protos(indexed_function) -> Dict: + retry_protos = {} + retry = indexed_function.get_setting("retry_policy") + retry_protos = protos.RpcRetryOptions(max_retry_count=int(retry.max_retry_count), + retry_strategy=retry.strategy, + delay_interval=Duration(seconds=int(retry.delay_interval or 0)), + minimum_interval=Duration(seconds=int(retry.minimum_interval or 0)), + maximum_interval=Duration(seconds=int(retry.maximum_interval or 0)), + ) + + return retry_protos + def process_indexed_function(functions_registry: functions.Registry, indexed_functions): - td = datetime.timedelta(seconds=2) - duration = Duration() - duration.FromTimedelta(td) - 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_options = protos.RetryOptions(retryStrategy="fixedDelay", - max_retry_count=10, - delay_interval=duration) + retry_protos = build_retry_protos(indexed_function) function_metadata = protos.RpcFunctionMetadata( name=function_info.name, @@ -90,7 +96,7 @@ def process_indexed_function(functions_registry: functions.Registry, language=PYTHON_LANGUAGE_RUNTIME, bindings=binding_protos, raw_bindings=indexed_function.get_raw_bindings(), - retry=retry_options, + retry_options=retry_protos, properties={"worker_indexed": "True"}) fx_metadata_results.append(function_metadata) diff --git a/azure_functions_worker/protos/__init__.py b/azure_functions_worker/protos/__init__.py index 6e8ae0f7..e9c4f239 100644 --- a/azure_functions_worker/protos/__init__.py +++ b/azure_functions_worker/protos/__init__.py @@ -33,7 +33,7 @@ FunctionsMetadataRequest, FunctionMetadataResponse, WorkerMetadata, - RetryOptions) + RpcRetryOptions) from .shared.NullableTypes_pb2 import ( NullableString, diff --git a/azure_functions_worker/protos/_src/src/proto/FunctionRpc.proto b/azure_functions_worker/protos/_src/src/proto/FunctionRpc.proto index c3cc8bd8..f48bc7bb 100644 --- a/azure_functions_worker/protos/_src/src/proto/FunctionRpc.proto +++ b/azure_functions_worker/protos/_src/src/proto/FunctionRpc.proto @@ -10,7 +10,6 @@ option go_package ="github.com/Azure/azure-functions-go-worker/internal/rpc"; package AzureFunctionsRpcMessages; import "google/protobuf/duration.proto"; -import "google/protobuf/timestamp.proto"; import "identity/ClaimsIdentityRpc.proto"; import "shared/NullableTypes.proto"; @@ -87,13 +86,13 @@ message StreamingMessage { // Host gets the list of function load responses FunctionLoadResponseCollection function_load_response_collection = 32; - + // Host sends required metadata to worker to warmup the worker WorkerWarmupRequest worker_warmup_request = 33; - + // Worker responds after warming up with the warmup result WorkerWarmupResponse worker_warmup_response = 34; - + } } @@ -339,14 +338,12 @@ message RpcFunctionMetadata { bool managed_dependency_enabled = 14; // The optional function execution retry strategy to use on invocation failures. - RetryOptions retry = 15; + RpcRetryOptions retry_options = 15; // Properties for function metadata // They're usually specific to a worker and largely passed along to the controller API for use // outside the host map properties = 16; - - string raw_retry_options = 17; } // Host tells worker it is ready to receive metadata @@ -707,27 +704,27 @@ message CollectionModelBindingData { // Retry policy which the worker sends the host when the worker indexes // a function. -message RetryOptions +message RpcRetryOptions { - // The retry strategy to use. Valid values are fixedDelay or exponentialBackoff. + // The retry strategy to use. Valid values are fixed delay or exponential backoff. enum RetryStrategy { - exponentialBackoff = 0; - fixedDelay = 1; + exponential_backoff = 0; + fixed_delay = 1; } // The maximum number of retries allowed per function execution. // -1 means to retry indefinitely. int32 max_retry_count = 2; - // The delay that's used between retries when you're using a fixedDelay strategy. + // The delay that's used between retries when you're using a fixed delay strategy. google.protobuf.Duration delay_interval = 3; - // The minimum retry delay when you're using an exponentialBackoff strategy + // The minimum retry delay when you're using an exponential backoff strategy google.protobuf.Duration minimum_interval = 4; - // The maximum retry delay when you're using an exponentialBackoff strategy + // The maximum retry delay when you're using an exponential backoff strategy google.protobuf.Duration maximum_interval = 5; - RetryStrategy retryStrategy = 6; + RetryStrategy retry_strategy = 6; } \ No newline at end of file diff --git a/python/test/worker.config.json b/python/test/worker.config.json index 3fc2a923..91b503cd 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":"D:\\Repos\\azure-functions-python-worker\\.venv310\\Scripts\\python.exe", "defaultWorkerPath":"worker.py", "workerIndexing": "true" } From 3c891d01257425d6c584d33c87db769124990e0d Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Tue, 20 Jun 2023 14:51:50 -0500 Subject: [PATCH 03/12] Added retry policy support for v2 function --- azure_functions_worker/dispatcher.py | 11 ++++++----- azure_functions_worker/functions.py | 3 +-- azure_functions_worker/loader.py | 22 ++++++++++++++-------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 4f6c0039..48f853f8 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -609,10 +609,14 @@ def index_functions(self, function_path: str): len(indexed_functions)) if indexed_functions: + fx_metadata_results = loader.process_indexed_function( + self._functions, + indexed_functions) + indexed_function_logs: List[str] = [] for func in indexed_functions: function_log = "Function Name: {}, Function Binding: {}" \ - .format(func.get_setting("function_name"), + .format(func.get_function_name(), [(binding.type, binding.name) for binding in func.get_bindings()]) indexed_function_logs.append(function_log) @@ -621,10 +625,7 @@ def index_functions(self, function_path: str): 'Successfully processed FunctionMetadataRequest for ' 'functions: %s', " ".join(indexed_function_logs)) - fx_metadata_results = loader.process_indexed_function( - self._functions, - indexed_functions) - + return fx_metadata_results async def _handle__close_shared_memory_resources_request(self, request): diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index 539cfcb4..711b4981 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -380,8 +380,7 @@ def add_function(self, function_id: str, def add_indexed_function(self, function): func = function.get_user_function() - func_name_setting = function.get_setting("function_name") - func_name = func_name_setting if func_name_setting else func.__name__ + func_name = function.get_function_name() func_type = function.http_type function_id = str(uuid.uuid5(namespace=uuid.NAMESPACE_OID, diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index 1b4092f2..a1faa0a2 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -62,15 +62,21 @@ def build_binding_protos(indexed_function) -> Dict: return binding_protos + def build_retry_protos(indexed_function) -> Dict: - retry_protos = {} - retry = indexed_function.get_setting("retry_policy") - retry_protos = protos.RpcRetryOptions(max_retry_count=int(retry.max_retry_count), - retry_strategy=retry.strategy, - delay_interval=Duration(seconds=int(retry.delay_interval or 0)), - minimum_interval=Duration(seconds=int(retry.minimum_interval or 0)), - maximum_interval=Duration(seconds=int(retry.maximum_interval or 0)), - ) + retry = indexed_function.get_settings_json("retry_policy") + if not retry: + return None + + retry_protos = protos.RpcRetryOptions( + max_retry_count=int(retry.get("maxRetryCount")), + retry_strategy=retry.get("strategy"), + delay_interval=Duration(seconds=int(retry.get("delayInterval") or 0)), + minimum_interval=Duration( + seconds=int(retry.get("minimumInterval") or 0)), + maximum_interval=Duration( + seconds=int(retry.get("maximumInterval") or 0)), + ) return retry_protos From 1431b599fc6a07c6ea651ae86dd8d1c52b8ccfac Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Wed, 21 Jun 2023 20:14:13 -0500 Subject: [PATCH 04/12] Minor updates and added e2e tests --- .../bindings/retrycontext.py | 12 ++++++++++ azure_functions_worker/dispatcher.py | 1 - azure_functions_worker/functions.py | 6 ----- azure_functions_worker/loader.py | 17 ++++++------- .../retry_policy_functions/function_app.py | 18 ++++++++++++++ tests/endtoend/test_retry_policy_functions.py | 24 +++++++++++++++++++ 6 files changed, 63 insertions(+), 15 deletions(-) create mode 100644 tests/endtoend/retry_policy_functions/function_app.py create mode 100644 tests/endtoend/test_retry_policy_functions.py diff --git a/azure_functions_worker/bindings/retrycontext.py b/azure_functions_worker/bindings/retrycontext.py index 36e7c682..da1f8c76 100644 --- a/azure_functions_worker/bindings/retrycontext.py +++ b/azure_functions_worker/bindings/retrycontext.py @@ -1,10 +1,22 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + from dataclasses import dataclass +from enum import Enum from . import rpcexception +class RetryPolicy(Enum): + """Retry policy for the function invocation""" + + MAX_RETRY_COUNT = "maxRetryCount" + STRATEGY = "retryStrategy" + DELAY_INTERVAL = "delayInterval" + MINIMUM_INTERVAL = "minimumInterval" + MAXIMUM_INTERVAL = "maximumInterval" + + @dataclass class RetryContext: """Gets the current retry count from retry-context""" diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 48f853f8..944461d3 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -625,7 +625,6 @@ def index_functions(self, function_path: str): 'Successfully processed FunctionMetadataRequest for ' 'functions: %s', " ".join(indexed_function_logs)) - return fx_metadata_results async def _handle__close_shared_memory_resources_request(self, request): diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index 711b4981..d5f9157e 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -255,10 +255,6 @@ def validate_function_params(params: dict, bound_params: dict, else: input_types[param.name] = param_type_info return input_types, output_types - - @staticmethod - def get_retry_polies(binding): - pass @staticmethod def get_function_return_type(annotations: dict, has_explicit_return: bool, @@ -402,8 +398,6 @@ def add_indexed_function(self, function): self.get_explicit_and_implicit_return( binding.name, binding, has_explicit_return, has_implicit_return, bound_params) - - retry_policy = self.get_retry_polies(binding) return_binding_name = self.get_return_binding(binding.name, binding.type, diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index a1faa0a2..40d5b931 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. """Python functions loader.""" -import datetime import importlib import importlib.machinery import importlib.util @@ -15,6 +14,7 @@ from google.protobuf.duration_pb2 import Duration from . import protos, functions +from .bindings.retrycontext import RetryPolicy from .constants import MODULE_NOT_FOUND_TS_URL, SCRIPT_FILE_NAME, \ PYTHON_LANGUAGE_RUNTIME from .utils.wrappers import attach_message_to_exception @@ -67,16 +67,17 @@ def build_retry_protos(indexed_function) -> Dict: retry = indexed_function.get_settings_json("retry_policy") if not retry: return None - + retry_protos = protos.RpcRetryOptions( - max_retry_count=int(retry.get("maxRetryCount")), - retry_strategy=retry.get("strategy"), - delay_interval=Duration(seconds=int(retry.get("delayInterval") or 0)), + max_retry_count=int(retry.get(RetryPolicy.MAX_RETRY_COUNT)), + retry_strategy=retry.get(RetryPolicy.STRATEGY), + delay_interval=Duration( + seconds=int(retry.get(RetryPolicy.DELAY_INTERVAL) or 0)), minimum_interval=Duration( - seconds=int(retry.get("minimumInterval") or 0)), + seconds=int(retry.get(RetryPolicy.MINIMUM_INTERVAL) or 0)), maximum_interval=Duration( - seconds=int(retry.get("maximumInterval") or 0)), - ) + seconds=int(retry.get(RetryPolicy.MAXIMUM_INTERVAL) or 0)), + ) return retry_protos diff --git a/tests/endtoend/retry_policy_functions/function_app.py b/tests/endtoend/retry_policy_functions/function_app.py new file mode 100644 index 00000000..9b0ffdd8 --- /dev/null +++ b/tests/endtoend/retry_policy_functions/function_app.py @@ -0,0 +1,18 @@ +from azure.functions import FunctionApp, TimerRequest, Context +import logging + +app = FunctionApp() + + +@app.timer_trigger(schedule="*/1 * * * * *", arg_name="mytimer", + run_on_startup=False, + use_monitor=False) +@app.retry(strategy="fixed_delay", max_retry_count="1", delay_interval="5") +def mytimer(mytimer: TimerRequest, context: Context) -> None: + logging.info(f'Current retry count: {context.retry_context.retry_count}') + if context.retry_context.retry_count == \ + context.retry_context.max_retry_count: + logging.info( + f"Max retries of {context.retry_context.max_retry_count} for " + f"function {context.function_name} has been reached") + raise Exception("This is a retryable exception") diff --git a/tests/endtoend/test_retry_policy_functions.py b/tests/endtoend/test_retry_policy_functions.py new file mode 100644 index 00000000..9f1fb29f --- /dev/null +++ b/tests/endtoend/test_retry_policy_functions.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import time +import typing + +from tests.utils import testutils + + +class TestRetryPolicyFunctions(testutils.WebHostTestCase): + + @classmethod + def get_script_dir(cls): + return testutils.E2E_TESTS_FOLDER / 'retry_policy_functions' + + def test_retry_policy(self): + time.sleep(1) + # Checking webhost status. + r = self.webhost.request('GET', '', no_prefix=True, + timeout=5) + self.assertTrue(r.ok) + + def check_log_retry_policy(self, host_out: typing.List[str]): + self.assertEqual(host_out.count("Current retry count:"), 3) + From fe648ee83ab83670c9b13d3769ac702a2149de1f Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Wed, 21 Jun 2023 20:21:59 -0500 Subject: [PATCH 05/12] Reverted change to worker config --- 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 91b503cd..3fc2a923 100644 --- a/python/test/worker.config.json +++ b/python/test/worker.config.json @@ -2,7 +2,7 @@ "description":{ "language":"python", "extensions":[".py"], - "defaultExecutablePath":"D:\\Repos\\azure-functions-python-worker\\.venv310\\Scripts\\python.exe", + "defaultExecutablePath":"python", "defaultWorkerPath":"worker.py", "workerIndexing": "true" } From dadbc77fcfb8da1b6b95c279ff3a09d28e74701c Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Thu, 22 Jun 2023 13:21:58 -0500 Subject: [PATCH 06/12] Added e2e tests --- azure_functions_worker/loader.py | 8 ++++---- tests/endtoend/retry_policy_functions/function_app.py | 8 ++++++-- tests/endtoend/test_retry_policy_functions.py | 7 +++++-- tests/utils/testutils.py | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index 40d5b931..c6c6f20c 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -69,14 +69,14 @@ def build_retry_protos(indexed_function) -> Dict: return None retry_protos = protos.RpcRetryOptions( - max_retry_count=int(retry.get(RetryPolicy.MAX_RETRY_COUNT)), + max_retry_count=int(retry.get(RetryPolicy.MAX_RETRY_COUNT.value)), retry_strategy=retry.get(RetryPolicy.STRATEGY), delay_interval=Duration( - seconds=int(retry.get(RetryPolicy.DELAY_INTERVAL) or 0)), + seconds=int(retry.get(RetryPolicy.DELAY_INTERVAL.value) or 0)), minimum_interval=Duration( - seconds=int(retry.get(RetryPolicy.MINIMUM_INTERVAL) or 0)), + seconds=int(retry.get(RetryPolicy.MINIMUM_INTERVAL.value) or 0)), maximum_interval=Duration( - seconds=int(retry.get(RetryPolicy.MAXIMUM_INTERVAL) or 0)), + seconds=int(retry.get(RetryPolicy.MAXIMUM_INTERVAL.value) or 0)), ) return retry_protos diff --git a/tests/endtoend/retry_policy_functions/function_app.py b/tests/endtoend/retry_policy_functions/function_app.py index 9b0ffdd8..d72fd47a 100644 --- a/tests/endtoend/retry_policy_functions/function_app.py +++ b/tests/endtoend/retry_policy_functions/function_app.py @@ -1,7 +1,7 @@ -from azure.functions import FunctionApp, TimerRequest, Context +from azure.functions import FunctionApp, TimerRequest, Context, AuthLevel import logging -app = FunctionApp() +app = FunctionApp(http_auth_level=AuthLevel.ANONYMOUS) @app.timer_trigger(schedule="*/1 * * * * *", arg_name="mytimer", @@ -10,6 +10,10 @@ @app.retry(strategy="fixed_delay", max_retry_count="1", delay_interval="5") def mytimer(mytimer: TimerRequest, context: Context) -> None: logging.info(f'Current retry count: {context.retry_context.retry_count}') + + if mytimer.past_due: + logging.info("Timer trigger is past due") + if context.retry_context.retry_count == \ context.retry_context.max_retry_count: logging.info( diff --git a/tests/endtoend/test_retry_policy_functions.py b/tests/endtoend/test_retry_policy_functions.py index 9f1fb29f..5ffbbcd8 100644 --- a/tests/endtoend/test_retry_policy_functions.py +++ b/tests/endtoend/test_retry_policy_functions.py @@ -13,12 +13,15 @@ def get_script_dir(cls): return testutils.E2E_TESTS_FOLDER / 'retry_policy_functions' def test_retry_policy(self): - time.sleep(1) # Checking webhost status. r = self.webhost.request('GET', '', no_prefix=True, timeout=5) self.assertTrue(r.ok) + time.sleep(1) def check_log_retry_policy(self, host_out: typing.List[str]): - self.assertEqual(host_out.count("Current retry count:"), 3) + self.assertEqual(host_out.count("Current retry count: 0"), 1) + self.assertEqual(host_out.count("Current retry count: 1"), 1) + self.assertEqual(host_out.count(f"Max retries of 1 for function mytimer" + f" has been reached"), 1) diff --git a/tests/utils/testutils.py b/tests/utils/testutils.py index 61cfb2d2..6f6fb76a 100644 --- a/tests/utils/testutils.py +++ b/tests/utils/testutils.py @@ -815,7 +815,7 @@ def popen_webhost(*, stdout, stderr, script_root=FUNCS_PATH, port=None): if coretools_exe: coretools_exe = coretools_exe.strip() if pathlib.Path(coretools_exe).exists(): - hostexe_args = [str(coretools_exe), 'host', 'start'] + hostexe_args = [str(coretools_exe), 'host', 'start', '--verbose'] if port is not None: hostexe_args.extend(['--port', str(port)]) From 01af6a718b1d857d8dbbd654df25bb62e06e88fd Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Wed, 5 Jul 2023 17:20:39 -0500 Subject: [PATCH 07/12] Updated according to new libary --- azure_functions_worker/bindings/retrycontext.py | 10 +++++----- azure_functions_worker/constants.py | 3 +++ azure_functions_worker/loader.py | 4 ++-- .../{ => fixed_strategy}/function_app.py | 0 tests/endtoend/test_retry_policy_functions.py | 4 ++-- 5 files changed, 12 insertions(+), 9 deletions(-) rename tests/endtoend/retry_policy_functions/{ => fixed_strategy}/function_app.py (100%) diff --git a/azure_functions_worker/bindings/retrycontext.py b/azure_functions_worker/bindings/retrycontext.py index da1f8c76..8c216638 100644 --- a/azure_functions_worker/bindings/retrycontext.py +++ b/azure_functions_worker/bindings/retrycontext.py @@ -10,11 +10,11 @@ class RetryPolicy(Enum): """Retry policy for the function invocation""" - MAX_RETRY_COUNT = "maxRetryCount" - STRATEGY = "retryStrategy" - DELAY_INTERVAL = "delayInterval" - MINIMUM_INTERVAL = "minimumInterval" - MAXIMUM_INTERVAL = "maximumInterval" + MAX_RETRY_COUNT = "max_retry_count" + STRATEGY = "strategy" + DELAY_INTERVAL = "delay_interval" + MINIMUM_INTERVAL = "minimum_interval" + MAXIMUM_INTERVAL = "maximum_interval" @dataclass diff --git a/azure_functions_worker/constants.py b/azure_functions_worker/constants.py index a0606d1f..27380bd9 100644 --- a/azure_functions_worker/constants.py +++ b/azure_functions_worker/constants.py @@ -51,3 +51,6 @@ SCRIPT_FILE_NAME = "function_app.py" PYTHON_LANGUAGE_RUNTIME = "python" + +# Settings for V2 programming model +RETRY_POLICY = "retry_policy" diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index c6c6f20c..981e1dd3 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -16,7 +16,7 @@ from . import protos, functions from .bindings.retrycontext import RetryPolicy from .constants import MODULE_NOT_FOUND_TS_URL, SCRIPT_FILE_NAME, \ - PYTHON_LANGUAGE_RUNTIME + PYTHON_LANGUAGE_RUNTIME, RETRY_POLICY from .utils.wrappers import attach_message_to_exception _AZURE_NAMESPACE = '__app__' @@ -64,7 +64,7 @@ def build_binding_protos(indexed_function) -> Dict: def build_retry_protos(indexed_function) -> Dict: - retry = indexed_function.get_settings_json("retry_policy") + retry = indexed_function.get_settings_dict(RETRY_POLICY) if not retry: return None diff --git a/tests/endtoend/retry_policy_functions/function_app.py b/tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py similarity index 100% rename from tests/endtoend/retry_policy_functions/function_app.py rename to tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py diff --git a/tests/endtoend/test_retry_policy_functions.py b/tests/endtoend/test_retry_policy_functions.py index 5ffbbcd8..c52626c4 100644 --- a/tests/endtoend/test_retry_policy_functions.py +++ b/tests/endtoend/test_retry_policy_functions.py @@ -10,7 +10,8 @@ class TestRetryPolicyFunctions(testutils.WebHostTestCase): @classmethod def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'retry_policy_functions' + return testutils.E2E_TESTS_FOLDER / 'retry_policy_functions' \ + / 'fixed_strategy' def test_retry_policy(self): # Checking webhost status. @@ -24,4 +25,3 @@ def check_log_retry_policy(self, host_out: typing.List[str]): self.assertEqual(host_out.count("Current retry count: 1"), 1) self.assertEqual(host_out.count(f"Max retries of 1 for function mytimer" f" has been reached"), 1) - From 659e76ca5e34e57c9e3fb132193b0a74da1fa0f3 Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Wed, 5 Jul 2023 18:11:03 -0500 Subject: [PATCH 08/12] Added tests --- azure_functions_worker/dispatcher.py | 4 +- azure_functions_worker/functions.py | 3 +- azure_functions_worker/loader.py | 2 +- .../exponential_strategy/function_app.py | 23 ++++++++++++ tests/endtoend/test_retry_policy_functions.py | 37 +++++++++++++++---- 5 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py index 944461d3..485d5ed1 100644 --- a/azure_functions_worker/dispatcher.py +++ b/azure_functions_worker/dispatcher.py @@ -610,8 +610,8 @@ def index_functions(self, function_path: str): if indexed_functions: fx_metadata_results = loader.process_indexed_function( - self._functions, - indexed_functions) + self._functions, + indexed_functions) indexed_function_logs: List[str] = [] for func in indexed_functions: diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py index d5f9157e..f6d59122 100644 --- a/azure_functions_worker/functions.py +++ b/azure_functions_worker/functions.py @@ -377,7 +377,6 @@ def add_function(self, function_id: str, def add_indexed_function(self, function): func = function.get_user_function() func_name = function.get_function_name() - func_type = function.http_type function_id = str(uuid.uuid5(namespace=uuid.NAMESPACE_OID, name=func_name)) @@ -418,7 +417,7 @@ def add_indexed_function(self, function): has_implicit_return, return_binding_name, func_name) - + return \ self.add_func_to_registry_and_return_funcinfo(func, func_name, function_id, diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index 981e1dd3..2a7a56da 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -79,7 +79,7 @@ def build_retry_protos(indexed_function) -> Dict: seconds=int(retry.get(RetryPolicy.MAXIMUM_INTERVAL.value) or 0)), ) - return retry_protos + return retry_protos def process_indexed_function(functions_registry: functions.Registry, diff --git a/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py b/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py new file mode 100644 index 00000000..ec866952 --- /dev/null +++ b/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py @@ -0,0 +1,23 @@ +from azure.functions import FunctionApp, TimerRequest, Context, AuthLevel +import logging + +app = FunctionApp(http_auth_level=AuthLevel.ANONYMOUS) + + +@app.timer_trigger(schedule="*/1 * * * * *", arg_name="mytimer", + run_on_startup=False, + use_monitor=False) +@app.retry(strategy="fixed_delay", max_retry_count="3", minimum_interval="1", + maximum_interval="5") +def mytimer(mytimer: TimerRequest, context: Context) -> None: + logging.info(f'Current retry count: {context.retry_context.retry_count}') + + if mytimer.past_due: + logging.info("Timer trigger is past due") + + if context.retry_context.retry_count == \ + context.retry_context.max_retry_count: + logging.info( + f"Max retries of {context.retry_context.max_retry_count} for " + f"function {context.function_name} has been reached") + raise Exception("This is a retryable exception") diff --git a/tests/endtoend/test_retry_policy_functions.py b/tests/endtoend/test_retry_policy_functions.py index c52626c4..7820fde0 100644 --- a/tests/endtoend/test_retry_policy_functions.py +++ b/tests/endtoend/test_retry_policy_functions.py @@ -6,12 +6,12 @@ from tests.utils import testutils -class TestRetryPolicyFunctions(testutils.WebHostTestCase): +class TestFixedRetryPolicyFunctions(testutils.WebHostTestCase): @classmethod def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'retry_policy_functions' \ - / 'fixed_strategy' + return testutils.E2E_TESTS_FOLDER / 'retry_policy_functions' / \ + 'fixed_strategy' def test_retry_policy(self): # Checking webhost status. @@ -21,7 +21,30 @@ def test_retry_policy(self): time.sleep(1) def check_log_retry_policy(self, host_out: typing.List[str]): - self.assertEqual(host_out.count("Current retry count: 0"), 1) - self.assertEqual(host_out.count("Current retry count: 1"), 1) - self.assertEqual(host_out.count(f"Max retries of 1 for function mytimer" - f" has been reached"), 1) + self.assertIn('Current retry count: 0', host_out) + self.assertIn('Current retry count: 1', host_out) + self.assertIn("Max retries of 1 for function mytimer" + " has been reached", host_out) + + +class TestExponentialRetryPolicyFunctions(testutils.WebHostTestCase): + + @classmethod + def get_script_dir(cls): + return testutils.E2E_TESTS_FOLDER / 'retry_policy_functions' / \ + 'exponential_strategy' + + def test_retry_policy(self): + # Checking webhost status. + r = self.webhost.request('GET', '', no_prefix=True, + timeout=5) + self.assertTrue(r.ok) + time.sleep(1) + + def check_log_retry_policy(self, host_out: typing.List[str]): + self.assertIn('Current retry count: 0', host_out) + self.assertIn('Current retry count: 1', host_out) + self.assertIn('Current retry count: 2', host_out) + self.assertNotIn('Current retry count: 3', host_out) + self.assertIn("Max retries of 1 for function mytimer" + " has been reached", host_out) From 9866fcaa59c1f20cb6a1c7332889bb982c829d1e Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Thu, 6 Jul 2023 11:02:23 -0500 Subject: [PATCH 09/12] Fixed existing tests --- .../exponential_strategy/function_app.py | 8 +++----- .../fixed_strategy/function_app.py | 10 +++++----- tests/endtoend/test_retry_policy_functions.py | 19 +++++++++---------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py b/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py index ec866952..e9f35207 100644 --- a/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py +++ b/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py @@ -8,16 +8,14 @@ run_on_startup=False, use_monitor=False) @app.retry(strategy="fixed_delay", max_retry_count="3", minimum_interval="1", - maximum_interval="5") + maximum_interval="2") def mytimer(mytimer: TimerRequest, context: Context) -> None: logging.info(f'Current retry count: {context.retry_context.retry_count}') - if mytimer.past_due: - logging.info("Timer trigger is past due") - if context.retry_context.retry_count == \ context.retry_context.max_retry_count: logging.info( f"Max retries of {context.retry_context.max_retry_count} for " f"function {context.function_name} has been reached") - raise Exception("This is a retryable exception") + else: + raise Exception("This is a retryable exception") diff --git a/tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py b/tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py index d72fd47a..bca00918 100644 --- a/tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py +++ b/tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py @@ -7,16 +7,16 @@ @app.timer_trigger(schedule="*/1 * * * * *", arg_name="mytimer", run_on_startup=False, use_monitor=False) -@app.retry(strategy="fixed_delay", max_retry_count="1", delay_interval="5") +@app.retry(strategy="fixed_delay", max_retry_count="3", + delay_interval="1") def mytimer(mytimer: TimerRequest, context: Context) -> None: logging.info(f'Current retry count: {context.retry_context.retry_count}') - if mytimer.past_due: - logging.info("Timer trigger is past due") - if context.retry_context.retry_count == \ context.retry_context.max_retry_count: logging.info( f"Max retries of {context.retry_context.max_retry_count} for " f"function {context.function_name} has been reached") - raise Exception("This is a retryable exception") + return + else: + raise Exception("This is a retryable exception") diff --git a/tests/endtoend/test_retry_policy_functions.py b/tests/endtoend/test_retry_policy_functions.py index 7820fde0..60f21f74 100644 --- a/tests/endtoend/test_retry_policy_functions.py +++ b/tests/endtoend/test_retry_policy_functions.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import time import typing +import requests from tests.utils import testutils @@ -13,17 +14,16 @@ def get_script_dir(cls): return testutils.E2E_TESTS_FOLDER / 'retry_policy_functions' / \ 'fixed_strategy' - def test_retry_policy(self): + def test_fixed_retry_policy(self): # Checking webhost status. - r = self.webhost.request('GET', '', no_prefix=True, - timeout=5) + time.sleep(5) + r = self.webhost.request('GET', '', no_prefix=True) self.assertTrue(r.ok) - time.sleep(1) - def check_log_retry_policy(self, host_out: typing.List[str]): + def check_log_fixed_retry_policy(self, host_out: typing.List[str]): self.assertIn('Current retry count: 0', host_out) self.assertIn('Current retry count: 1', host_out) - self.assertIn("Max retries of 1 for function mytimer" + self.assertIn("Max retries of 3 for function mytimer" " has been reached", host_out) @@ -38,13 +38,12 @@ def test_retry_policy(self): # Checking webhost status. r = self.webhost.request('GET', '', no_prefix=True, timeout=5) + time.sleep(5) self.assertTrue(r.ok) - time.sleep(1) def check_log_retry_policy(self, host_out: typing.List[str]): - self.assertIn('Current retry count: 0', host_out) self.assertIn('Current retry count: 1', host_out) self.assertIn('Current retry count: 2', host_out) - self.assertNotIn('Current retry count: 3', host_out) - self.assertIn("Max retries of 1 for function mytimer" + self.assertIn('Current retry count: 3', host_out) + self.assertIn("Max retries of 3 for function mytimer" " has been reached", host_out) From 4f167d6472063add9bf77673ec88bf8c8246a9a7 Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Thu, 6 Jul 2023 13:19:51 -0500 Subject: [PATCH 10/12] Added unit tests --- azure_functions_worker/loader.py | 2 +- .../exponential_strategy/function_app.py | 3 ++- .../fixed_strategy/function_app.py | 1 - tests/endtoend/test_retry_policy_functions.py | 1 - tests/unittests/test_dispatcher.py | 11 ++++++++ tests/unittests/test_loader.py | 27 +++++++++++++++++++ 6 files changed, 41 insertions(+), 4 deletions(-) diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index 2a7a56da..ce2da61a 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -70,7 +70,7 @@ def build_retry_protos(indexed_function) -> Dict: retry_protos = protos.RpcRetryOptions( max_retry_count=int(retry.get(RetryPolicy.MAX_RETRY_COUNT.value)), - retry_strategy=retry.get(RetryPolicy.STRATEGY), + retry_strategy=retry.get(RetryPolicy.STRATEGY.value), delay_interval=Duration( seconds=int(retry.get(RetryPolicy.DELAY_INTERVAL.value) or 0)), minimum_interval=Duration( diff --git a/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py b/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py index e9f35207..955b36f3 100644 --- a/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py +++ b/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py @@ -7,7 +7,8 @@ @app.timer_trigger(schedule="*/1 * * * * *", arg_name="mytimer", run_on_startup=False, use_monitor=False) -@app.retry(strategy="fixed_delay", max_retry_count="3", minimum_interval="1", +@app.retry(strategy="exponential_backoff", max_retry_count="3", + minimum_interval="1", maximum_interval="2") def mytimer(mytimer: TimerRequest, context: Context) -> None: logging.info(f'Current retry count: {context.retry_context.retry_count}') diff --git a/tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py b/tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py index bca00918..e3a32094 100644 --- a/tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py +++ b/tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py @@ -17,6 +17,5 @@ def mytimer(mytimer: TimerRequest, context: Context) -> None: logging.info( f"Max retries of {context.retry_context.max_retry_count} for " f"function {context.function_name} has been reached") - return else: raise Exception("This is a retryable exception") diff --git a/tests/endtoend/test_retry_policy_functions.py b/tests/endtoend/test_retry_policy_functions.py index 60f21f74..58851f35 100644 --- a/tests/endtoend/test_retry_policy_functions.py +++ b/tests/endtoend/test_retry_policy_functions.py @@ -2,7 +2,6 @@ # Licensed under the MIT License. import time import typing -import requests from tests.utils import testutils diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py index 02f4a31e..472f8e21 100644 --- a/tests/unittests/test_dispatcher.py +++ b/tests/unittests/test_dispatcher.py @@ -572,6 +572,17 @@ async def test_dispatcher_functions_metadata_request(self): self.assertEqual(r.response.result.status, protos.StatusResult.Success) + async def test_dispatcher_functions_metadata_request_with_retry(self): + """Test if the functions metadata response will be sent correctly + when a functions metadata request is received + """ + async with self._ctrl as host: + 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_loader.py b/tests/unittests/test_loader.py index 3f194dd7..737c0b86 100644 --- a/tests/unittests/test_loader.py +++ b/tests/unittests/test_loader.py @@ -6,11 +6,38 @@ import sys import textwrap +from azure.functions import Function +from azure.functions.decorators.retry_policy import RetryPolicy +from azure.functions.decorators.timer import TimerTrigger + +from azure_functions_worker import functions +from azure_functions_worker.loader import build_retry_protos from tests.utils import testutils class TestLoader(testutils.WebHostTestCase): + def setUp(self) -> None: + def test_function(): + return "Test" + + self.test_function = test_function + self.func = Function(self.test_function, script_file="test.py") + self.function_registry = functions.Registry() + + def test_building_retry_protos(self): + trigger = TimerTrigger(schedule="*/1 * * * * *", arg_name="mytimer", + name="mytimer") + self.func.add_trigger(trigger=trigger) + setting = RetryPolicy(strategy="fixed_delay", max_retry_count="1", + delay_interval="1") + self.func.add_setting(setting=setting) + + protos = build_retry_protos(self.func) + self.assertEqual(protos.max_retry_count, 1) + self.assertEqual(protos.retry_strategy, 1) + self.assertEqual(protos.delay_interval.seconds, 1) + @classmethod def get_script_dir(cls): return testutils.UNIT_TESTS_FOLDER / 'load_functions' From 30bd2e7fd5567a51aab2892c3750c575fb081873 Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Thu, 6 Jul 2023 15:03:38 -0500 Subject: [PATCH 11/12] Updated time format --- azure_functions_worker/loader.py | 42 ++++++++++++++----- .../exponential_strategy/function_app.py | 4 +- .../fixed_strategy/function_app.py | 2 +- tests/unittests/test_loader.py | 25 +++++++++-- 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index ce2da61a..20970a03 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -8,6 +8,8 @@ import os.path import pathlib import sys +import time +from datetime import timedelta from os import PathLike, fspath from typing import Optional, Dict @@ -48,6 +50,12 @@ def install() -> None: sys.modules[_AZURE_NAMESPACE] = ns_pkg +def convert_to_seconds(timestr: str): + x = time.strptime(timestr, '%H:%M:%S') + return int(timedelta(hours=x.tm_hour, minutes=x.tm_min, + seconds=x.tm_sec).total_seconds()) + + def uninstall() -> None: pass @@ -68,16 +76,30 @@ def build_retry_protos(indexed_function) -> Dict: if not retry: return None - retry_protos = protos.RpcRetryOptions( - max_retry_count=int(retry.get(RetryPolicy.MAX_RETRY_COUNT.value)), - retry_strategy=retry.get(RetryPolicy.STRATEGY.value), - delay_interval=Duration( - seconds=int(retry.get(RetryPolicy.DELAY_INTERVAL.value) or 0)), - minimum_interval=Duration( - seconds=int(retry.get(RetryPolicy.MINIMUM_INTERVAL.value) or 0)), - maximum_interval=Duration( - seconds=int(retry.get(RetryPolicy.MAXIMUM_INTERVAL.value) or 0)), - ) + strategy = retry.get(RetryPolicy.STRATEGY.value) + if strategy == "fixed_delay": + delay_interval = Duration( + seconds=convert_to_seconds( + retry.get(RetryPolicy.DELAY_INTERVAL.value))) + retry_protos = protos.RpcRetryOptions( + max_retry_count=int(retry.get(RetryPolicy.MAX_RETRY_COUNT.value)), + retry_strategy=retry.get(RetryPolicy.STRATEGY.value), + delay_interval=delay_interval, + ) + else: + minimum_interval = Duration( + seconds=convert_to_seconds( + retry.get(RetryPolicy.MINIMUM_INTERVAL.value))) + maximum_interval = Duration( + seconds=convert_to_seconds( + retry.get(RetryPolicy.MAXIMUM_INTERVAL.value))) + + retry_protos = protos.RpcRetryOptions( + max_retry_count=int(retry.get(RetryPolicy.MAX_RETRY_COUNT.value)), + retry_strategy=retry.get(RetryPolicy.STRATEGY.value), + minimum_interval=minimum_interval, + maximum_interval=maximum_interval + ) return retry_protos diff --git a/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py b/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py index 955b36f3..5370c8b6 100644 --- a/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py +++ b/tests/endtoend/retry_policy_functions/exponential_strategy/function_app.py @@ -8,8 +8,8 @@ run_on_startup=False, use_monitor=False) @app.retry(strategy="exponential_backoff", max_retry_count="3", - minimum_interval="1", - maximum_interval="2") + minimum_interval="00:00:01", + maximum_interval="00:00:02") def mytimer(mytimer: TimerRequest, context: Context) -> None: logging.info(f'Current retry count: {context.retry_context.retry_count}') diff --git a/tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py b/tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py index e3a32094..5d72fd18 100644 --- a/tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py +++ b/tests/endtoend/retry_policy_functions/fixed_strategy/function_app.py @@ -8,7 +8,7 @@ run_on_startup=False, use_monitor=False) @app.retry(strategy="fixed_delay", max_retry_count="3", - delay_interval="1") + delay_interval="00:00:01") def mytimer(mytimer: TimerRequest, context: Context) -> None: logging.info(f'Current retry count: {context.retry_context.retry_count}') diff --git a/tests/unittests/test_loader.py b/tests/unittests/test_loader.py index 737c0b86..96ed7e6e 100644 --- a/tests/unittests/test_loader.py +++ b/tests/unittests/test_loader.py @@ -25,18 +25,35 @@ def test_function(): self.func = Function(self.test_function, script_file="test.py") self.function_registry = functions.Registry() - def test_building_retry_protos(self): + def test_building_fixed_retry_protos(self): trigger = TimerTrigger(schedule="*/1 * * * * *", arg_name="mytimer", name="mytimer") self.func.add_trigger(trigger=trigger) setting = RetryPolicy(strategy="fixed_delay", max_retry_count="1", - delay_interval="1") + delay_interval="00:02:00") self.func.add_setting(setting=setting) protos = build_retry_protos(self.func) self.assertEqual(protos.max_retry_count, 1) - self.assertEqual(protos.retry_strategy, 1) - self.assertEqual(protos.delay_interval.seconds, 1) + self.assertEqual(protos.retry_strategy, 1) # 1 enum for fixed delay + self.assertEqual(protos.delay_interval.seconds, 120) + + def test_building_exponential_retry_protos(self): + trigger = TimerTrigger(schedule="*/1 * * * * *", arg_name="mytimer", + name="mytimer") + self.func.add_trigger(trigger=trigger) + setting = RetryPolicy(strategy="exponential_backoff", + max_retry_count="1", + minimum_interval="00:01:00", + maximum_interval="00:02:00") + self.func.add_setting(setting=setting) + + protos = build_retry_protos(self.func) + self.assertEqual(protos.max_retry_count, 1) + self.assertEqual(protos.retry_strategy, + 0) # 0 enum for exponential backoff + self.assertEqual(protos.minimum_interval.seconds, 60) + self.assertEqual(protos.maximum_interval.seconds, 120) @classmethod def get_script_dir(cls): From e3821f6a5b94d74fb15efe559df88f177b3ffc97 Mon Sep 17 00:00:00 2001 From: Gavin Aguiar Date: Thu, 6 Jul 2023 15:04:54 -0500 Subject: [PATCH 12/12] Updated time format --- azure_functions_worker/loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py index 20970a03..49782c98 100644 --- a/azure_functions_worker/loader.py +++ b/azure_functions_worker/loader.py @@ -53,7 +53,7 @@ def install() -> None: def convert_to_seconds(timestr: str): x = time.strptime(timestr, '%H:%M:%S') return int(timedelta(hours=x.tm_hour, minutes=x.tm_min, - seconds=x.tm_sec).total_seconds()) + seconds=x.tm_sec).total_seconds()) def uninstall() -> None: