Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Retry policy support for v2 #182

Merged
merged 16 commits into from
Jul 5, 2023
7 changes: 4 additions & 3 deletions azure/functions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
ExternalHttpFunctionApp)
from ._durable_functions import OrchestrationContext, EntityContext
from .decorators.function_app import (FunctionRegister, TriggerApi,
BindingApi)
BindingApi, SettingsApi)
from .extension import (ExtensionMeta, FunctionExtensionException,
FuncExtensionBase, AppExtensionBase)
from ._http_wsgi import WsgiMiddleware
Expand All @@ -35,8 +35,8 @@
from . import servicebus # NoQA
from . import timer # NoQA
from . import durable_functions # NoQA
from . import sql # NoQA
from . import warmup # NoQA
from . import sql # NoQA
from . import warmup # NoQA


__all__ = (
Expand Down Expand Up @@ -85,6 +85,7 @@
'DecoratorApi',
'TriggerApi',
'BindingApi',
'SettingsApi',
'Blueprint',
'ExternalHttpFunctionApp',
'AsgiFunctionApp',
Expand Down
3 changes: 2 additions & 1 deletion azure/functions/decorators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .core import Cardinality, AccessRights
from .function_app import FunctionApp, Function, DecoratorApi, DataType, \
AuthLevel, Blueprint, ExternalHttpFunctionApp, AsgiFunctionApp, \
WsgiFunctionApp, FunctionRegister, TriggerApi, BindingApi
WsgiFunctionApp, FunctionRegister, TriggerApi, BindingApi, SettingsApi
from .http import HttpMethod

__all__ = [
Expand All @@ -13,6 +13,7 @@
'DecoratorApi',
'TriggerApi',
'BindingApi',
'SettingsApi',
'Blueprint',
'ExternalHttpFunctionApp',
'AsgiFunctionApp',
Expand Down
40 changes: 40 additions & 0 deletions azure/functions/decorators/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,43 @@ def __init__(self, name: str, data_type: Optional[DataType] = None,
type: Optional[str] = None) -> None:
super().__init__(direction=BindingDirection.OUT,
name=name, data_type=data_type, type=type)


class Setting(ABC, metaclass=ABCBuildDictMeta):
""" Abstract class for all settings of a function app.
This class represents all the decorators that cannot be
classified as bindings or triggers. e.g function_name, retry etc.
"""

EXCLUDED_INIT_PARAMS = {'self', 'kwargs', 'setting_name'}

def __init__(self, setting_name: str) -> None:
self.setting_name = setting_name
self._dict: Dict = {
"setting_name": self.setting_name
}

def get_setting_name(self) -> str:
return self.setting_name

def get_dict_repr(self) -> Dict:
"""Build a dictionary of a particular binding. The keys are camel
cased binding field names defined in `init_params` list and
:class:`Binding` class. \n
This method is invoked in function :meth:`get_raw_bindings` of class
:class:`Function` to generate json dict for each binding.

:return: Dictionary representation of the binding.
"""
params = list(dict.fromkeys(getattr(self, 'init_params', [])))
for p in params:
if p not in Setting.EXCLUDED_INIT_PARAMS:
self._dict[p] = getattr(self, p, None)

return self._dict

def get_settings_value(self, settings_attribute_key: str) -> Optional[str]:
"""
Get the value of a particular setting attribute.
"""
return self.get_dict_repr().get(settings_attribute_key)
161 changes: 123 additions & 38 deletions azure/functions/decorators/function_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from azure.functions.decorators.blob import BlobTrigger, BlobInput, BlobOutput
from azure.functions.decorators.core import Binding, Trigger, DataType, \
AuthLevel, SCRIPT_FILE_NAME, Cardinality, AccessRights
AuthLevel, SCRIPT_FILE_NAME, Cardinality, AccessRights, Setting
from azure.functions.decorators.cosmosdb import CosmosDBTrigger, \
CosmosDBOutput, CosmosDBInput, CosmosDBTriggerV3, CosmosDBInputV3, \
CosmosDBOutputV3
Expand All @@ -29,6 +29,8 @@
parse_iterable_param_to_enums, StringifyEnumJsonEncoder
from azure.functions.http import HttpRequest
from .generic import GenericInputBinding, GenericTrigger, GenericOutputBinding
from .retry_policy import RetryPolicy
from .function_name import FunctionName
from .warmup import WarmUpTrigger
from .._http_asgi import AsgiMiddleware
from .._http_wsgi import WsgiMiddleware, Context
Expand All @@ -45,11 +47,17 @@ def __init__(self, func: Callable[..., Any], script_file: str):

:param func: User defined python function instance.
:param script_file: File name indexed by worker to find function.
:param trigger: The trigger object of the function.
:param bindings: The list of binding objects of a function.
:param settings: The list of setting objects of a function.
:param http_type: Http function type.
:param is_http_function: Whether the function is a http function.
"""
self._name: str = func.__name__
self._func = func
self._trigger: Optional[Trigger] = None
self._bindings: List[Binding] = []
self._settings: List[Setting] = []
self.function_script_file = script_file
self.http_type = 'function'
self._is_http_function = False
Expand Down Expand Up @@ -83,14 +91,12 @@ def add_trigger(self, trigger: Trigger) -> None:
# function.json is complete
self._bindings.append(trigger)

def set_function_name(self, function_name: Optional[str] = None) -> None:
"""Set or update the name for the function if :param:`function_name`
is not None. If not set, function name will default to python
function name.
:param function_name: Name the function set to.
def add_setting(self, setting: Setting) -> None:
"""Add a setting instance to the function.

:param setting: The setting object to add
"""
if function_name:
self._name = function_name
self._settings.append(setting)

def set_http_type(self, http_type: str) -> None:
"""Set or update the http type for the function if :param:`http_type`
Expand All @@ -116,6 +122,36 @@ def get_bindings(self) -> List[Binding]:
"""
return self._bindings

def get_setting(self, setting_name: str) -> Optional[Setting]:
Copy link
Member

Choose a reason for hiding this comment

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

Where do we get the list of all the settings registered to the Function? where you can get the setting_name itself from? I don't see it within a Function

Copy link
Contributor Author

Choose a reason for hiding this comment

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

class Functions has a List[Settings] which should have all the settings for a function

"""Get a specific setting attached to the function.

:param setting_name: The name of the setting to search for.
:return: The setting attached to the function (or None if not found).
"""
for setting in self._settings:
if setting.setting_name == setting_name:
return setting
return None

def get_settings_dict(self, setting_name) -> Optional[Dict]:
"""Get a dictionary representation of a setting.

:param: setting_name: The name of the setting to search for.
:return: The dictionary representation of the setting (or None if not
found).
"""
setting = self.get_setting(setting_name)
return setting.get_dict_repr() if setting else None

def get_function_name(self) -> Optional[str]:
"""Get the name of the function.
:return: The name of the function.
"""
function_name_setting = \
self.get_setting("function_name")
return function_name_setting.get_settings_value("function_name") \
if function_name_setting else self._name

def get_raw_bindings(self) -> List[str]:
return [json.dumps(b.get_dict_repr(), cls=StringifyEnumJsonEncoder)
for b in self._bindings]
Expand Down Expand Up @@ -145,13 +181,6 @@ def get_user_function(self) -> Callable[..., Any]:
"""
return self._func

def get_function_name(self) -> str:
"""Get the function name.

:return: Function name.
"""
return self._name

def get_function_json(self) -> str:
"""Get the json stringified form of function.

Expand All @@ -170,11 +199,6 @@ def __init__(self, func, function_script_file):
def __call__(self, *args, **kwargs):
pass

def configure_function_name(self, function_name: str) -> 'FunctionBuilder':
self._function.set_function_name(function_name)

return self

def configure_http_type(self, http_type: str) -> 'FunctionBuilder':
self._function.set_http_type(http_type)

Expand All @@ -188,6 +212,10 @@ def add_binding(self, binding: Binding) -> 'FunctionBuilder':
self._function.add_binding(binding=binding)
return self

def add_setting(self, setting: Setting) -> 'FunctionBuilder':
self._function.add_setting(setting=setting)
return self

def _validate_function(self,
auth_level: Optional[AuthLevel] = None) -> None:
"""
Expand Down Expand Up @@ -288,23 +316,6 @@ def decorator(func):

return decorator

def function_name(self, name: str) -> Callable[..., Any]:
"""Set name of the :class:`Function` object.

:param name: Name of the function.
:return: Decorator function.
"""

@self._configure_function_builder
def wrap(fb):
def decorator():
fb.configure_function_name(name)
return fb

return decorator()

return wrap

def http_type(self, http_type: str) -> Callable[..., Any]:
"""Set http type of the :class:`Function` object.

Expand Down Expand Up @@ -1938,6 +1949,80 @@ def decorator():
return wrap


class SettingsApi(DecoratorApi, ABC):
"""Interface to extend for using existing settings decorator in
functions."""

def function_name(self, name: str,
setting_extra_fields: Dict[str, Any] = {},
) -> Callable[..., Any]:
"""Optional: Sets name of the :class:`Function` object. If not set,
it will default to the name of the method name.

:param name: Name of the function.
:param setting_extra_fields: Keyword arguments for specifying
additional setting fields
:return: Decorator function.
"""

@self._configure_function_builder
def wrap(fb):
def decorator():
fb.add_setting(setting=FunctionName(
function_name=name,
**setting_extra_fields))
return fb

return decorator()

return wrap

def retry(self,
strategy: str,
max_retry_count: str,
delay_interval: Optional[str] = None,
minimum_interval: Optional[str] = None,
maximum_interval: Optional[str] = None,
setting_extra_fields: Dict[str, Any] = {},
) -> Callable[..., Any]:
"""The retry decorator adds :class:`RetryPolicy` to the function
settings object for building :class:`Function` object used in worker
function indexing model. This is equivalent to defining RetryPolicy
in the function.json which enables function to retry on failure.
All optional fields will be given default value by function host when
they are parsed by function host.

Ref: https://aka.ms/azure_functions_retries

:param strategy: The retry strategy to use.
:param max_retry_count: The maximum number of retry attempts.
:param delay_interval: The delay interval between retry attempts.
:param minimum_interval: The minimum delay interval between retry
attempts.
:param maximum_interval: The maximum delay interval between retry
attempts.
:param setting_extra_fields: Keyword arguments for specifying
additional setting fields.
:return: Decorator function.
"""

@self._configure_function_builder
def wrap(fb):
def decorator():
fb.add_setting(setting=RetryPolicy(
strategy=strategy,
max_retry_count=max_retry_count,
minimum_interval=minimum_interval,
maximum_interval=maximum_interval,
delay_interval=delay_interval,
**setting_extra_fields))
return fb

return decorator()
Copy link
Member

Choose a reason for hiding this comment

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

Are we planning to add validation to ensure basic combinations are not missed? Better to raise it here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. Validations will be in the host. If we add a validation here it will hide the host error and only show no functions found in the logs

Copy link
Member

Choose a reason for hiding this comment

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

We should at least send a warning - better wording of the validation

Copy link
Member

Choose a reason for hiding this comment

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

Discussed this offline can validations across the library coming later.


return wrap


class FunctionRegister(DecoratorApi, HttpFunctionsAuthLevelMixin, ABC):
def __init__(self, auth_level: Union[AuthLevel, str], *args, **kwargs):
"""Interface for declaring top level function app class which will
Expand Down Expand Up @@ -1987,7 +2072,7 @@ def register_functions(self, function_container: DecoratorApi) -> None:
register_blueprint = register_functions


class FunctionApp(FunctionRegister, TriggerApi, BindingApi):
class FunctionApp(FunctionRegister, TriggerApi, BindingApi, SettingsApi):
Copy link
Member

Choose a reason for hiding this comment

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

This is becoming more complicated day by day

"""FunctionApp object used by worker function indexing model captures
user defined functions and metadata.

Expand Down
14 changes: 14 additions & 0 deletions azure/functions/decorators/function_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from azure.functions.decorators.core import Setting

FUNCTION_NAME = "function_name"


class FunctionName(Setting):

def __init__(self, function_name: str,
**kwargs):
self.function_name = function_name
super().__init__(setting_name=FUNCTION_NAME)
24 changes: 24 additions & 0 deletions azure/functions/decorators/retry_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
from typing import Optional

from azure.functions.decorators.core import Setting

RETRY_POLICY = "retry_policy"


class RetryPolicy(Setting):

def __init__(self,
strategy: str,
max_retry_count: str,
delay_interval: Optional[str] = None,
minimum_interval: Optional[str] = None,
maximum_interval: Optional[str] = None,
**kwargs):
self.strategy = strategy
self.max_retry_count = max_retry_count
self.delay_interval = delay_interval
self.minimum_interval = minimum_interval
self.maximum_interval = maximum_interval
super().__init__(setting_name=RETRY_POLICY)
Loading