Skip to content

Commit 3235036

Browse files
gavin-aguiarGavin Aguiar
and
Gavin Aguiar
authored
Retry policy support for v2 (#182)
* Retry policy * Retry policy support for v2 * Fixing existing tests * Removing retry options from timer trigger * Added new tests * Added tests for retry decorator * Removed old commented method * Updating ProgModelSpec * Added method for get_settings_json * Addressed comments * Added comment for function_name * Addressed comments * Addressed comments --------- Co-authored-by: Gavin Aguiar <gavin@GavinPC>
1 parent 5ef605e commit 3235036

12 files changed

+346
-89
lines changed

azure/functions/__init__.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
ExternalHttpFunctionApp)
1414
from ._durable_functions import OrchestrationContext, EntityContext
1515
from .decorators.function_app import (FunctionRegister, TriggerApi,
16-
BindingApi)
16+
BindingApi, SettingsApi)
1717
from .extension import (ExtensionMeta, FunctionExtensionException,
1818
FuncExtensionBase, AppExtensionBase)
1919
from ._http_wsgi import WsgiMiddleware
@@ -35,8 +35,8 @@
3535
from . import servicebus # NoQA
3636
from . import timer # NoQA
3737
from . import durable_functions # NoQA
38-
from . import sql # NoQA
39-
from . import warmup # NoQA
38+
from . import sql # NoQA
39+
from . import warmup # NoQA
4040

4141

4242
__all__ = (
@@ -85,6 +85,7 @@
8585
'DecoratorApi',
8686
'TriggerApi',
8787
'BindingApi',
88+
'SettingsApi',
8889
'Blueprint',
8990
'ExternalHttpFunctionApp',
9091
'AsgiFunctionApp',

azure/functions/decorators/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from .core import Cardinality, AccessRights
44
from .function_app import FunctionApp, Function, DecoratorApi, DataType, \
55
AuthLevel, Blueprint, ExternalHttpFunctionApp, AsgiFunctionApp, \
6-
WsgiFunctionApp, FunctionRegister, TriggerApi, BindingApi
6+
WsgiFunctionApp, FunctionRegister, TriggerApi, BindingApi, SettingsApi
77
from .http import HttpMethod
88

99
__all__ = [
@@ -13,6 +13,7 @@
1313
'DecoratorApi',
1414
'TriggerApi',
1515
'BindingApi',
16+
'SettingsApi',
1617
'Blueprint',
1718
'ExternalHttpFunctionApp',
1819
'AsgiFunctionApp',

azure/functions/decorators/core.py

+40
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,43 @@ def __init__(self, name: str, data_type: Optional[DataType] = None,
161161
type: Optional[str] = None) -> None:
162162
super().__init__(direction=BindingDirection.OUT,
163163
name=name, data_type=data_type, type=type)
164+
165+
166+
class Setting(ABC, metaclass=ABCBuildDictMeta):
167+
""" Abstract class for all settings of a function app.
168+
This class represents all the decorators that cannot be
169+
classified as bindings or triggers. e.g function_name, retry etc.
170+
"""
171+
172+
EXCLUDED_INIT_PARAMS = {'self', 'kwargs', 'setting_name'}
173+
174+
def __init__(self, setting_name: str) -> None:
175+
self.setting_name = setting_name
176+
self._dict: Dict = {
177+
"setting_name": self.setting_name
178+
}
179+
180+
def get_setting_name(self) -> str:
181+
return self.setting_name
182+
183+
def get_dict_repr(self) -> Dict:
184+
"""Build a dictionary of a particular binding. The keys are camel
185+
cased binding field names defined in `init_params` list and
186+
:class:`Binding` class. \n
187+
This method is invoked in function :meth:`get_raw_bindings` of class
188+
:class:`Function` to generate json dict for each binding.
189+
190+
:return: Dictionary representation of the binding.
191+
"""
192+
params = list(dict.fromkeys(getattr(self, 'init_params', [])))
193+
for p in params:
194+
if p not in Setting.EXCLUDED_INIT_PARAMS:
195+
self._dict[p] = getattr(self, p, None)
196+
197+
return self._dict
198+
199+
def get_settings_value(self, settings_attribute_key: str) -> Optional[str]:
200+
"""
201+
Get the value of a particular setting attribute.
202+
"""
203+
return self.get_dict_repr().get(settings_attribute_key)

azure/functions/decorators/function_app.py

+123-38
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from azure.functions.decorators.blob import BlobTrigger, BlobInput, BlobOutput
1212
from azure.functions.decorators.core import Binding, Trigger, DataType, \
13-
AuthLevel, SCRIPT_FILE_NAME, Cardinality, AccessRights
13+
AuthLevel, SCRIPT_FILE_NAME, Cardinality, AccessRights, Setting
1414
from azure.functions.decorators.cosmosdb import CosmosDBTrigger, \
1515
CosmosDBOutput, CosmosDBInput, CosmosDBTriggerV3, CosmosDBInputV3, \
1616
CosmosDBOutputV3
@@ -29,6 +29,8 @@
2929
parse_iterable_param_to_enums, StringifyEnumJsonEncoder
3030
from azure.functions.http import HttpRequest
3131
from .generic import GenericInputBinding, GenericTrigger, GenericOutputBinding
32+
from .retry_policy import RetryPolicy
33+
from .function_name import FunctionName
3234
from .warmup import WarmUpTrigger
3335
from .._http_asgi import AsgiMiddleware
3436
from .._http_wsgi import WsgiMiddleware, Context
@@ -45,11 +47,17 @@ def __init__(self, func: Callable[..., Any], script_file: str):
4547
4648
:param func: User defined python function instance.
4749
:param script_file: File name indexed by worker to find function.
50+
:param trigger: The trigger object of the function.
51+
:param bindings: The list of binding objects of a function.
52+
:param settings: The list of setting objects of a function.
53+
:param http_type: Http function type.
54+
:param is_http_function: Whether the function is a http function.
4855
"""
4956
self._name: str = func.__name__
5057
self._func = func
5158
self._trigger: Optional[Trigger] = None
5259
self._bindings: List[Binding] = []
60+
self._settings: List[Setting] = []
5361
self.function_script_file = script_file
5462
self.http_type = 'function'
5563
self._is_http_function = False
@@ -83,14 +91,12 @@ def add_trigger(self, trigger: Trigger) -> None:
8391
# function.json is complete
8492
self._bindings.append(trigger)
8593

86-
def set_function_name(self, function_name: Optional[str] = None) -> None:
87-
"""Set or update the name for the function if :param:`function_name`
88-
is not None. If not set, function name will default to python
89-
function name.
90-
:param function_name: Name the function set to.
94+
def add_setting(self, setting: Setting) -> None:
95+
"""Add a setting instance to the function.
96+
97+
:param setting: The setting object to add
9198
"""
92-
if function_name:
93-
self._name = function_name
99+
self._settings.append(setting)
94100

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

125+
def get_setting(self, setting_name: str) -> Optional[Setting]:
126+
"""Get a specific setting attached to the function.
127+
128+
:param setting_name: The name of the setting to search for.
129+
:return: The setting attached to the function (or None if not found).
130+
"""
131+
for setting in self._settings:
132+
if setting.setting_name == setting_name:
133+
return setting
134+
return None
135+
136+
def get_settings_dict(self, setting_name) -> Optional[Dict]:
137+
"""Get a dictionary representation of a setting.
138+
139+
:param: setting_name: The name of the setting to search for.
140+
:return: The dictionary representation of the setting (or None if not
141+
found).
142+
"""
143+
setting = self.get_setting(setting_name)
144+
return setting.get_dict_repr() if setting else None
145+
146+
def get_function_name(self) -> Optional[str]:
147+
"""Get the name of the function.
148+
:return: The name of the function.
149+
"""
150+
function_name_setting = \
151+
self.get_setting("function_name")
152+
return function_name_setting.get_settings_value("function_name") \
153+
if function_name_setting else self._name
154+
119155
def get_raw_bindings(self) -> List[str]:
120156
return [json.dumps(b.get_dict_repr(), cls=StringifyEnumJsonEncoder)
121157
for b in self._bindings]
@@ -145,13 +181,6 @@ def get_user_function(self) -> Callable[..., Any]:
145181
"""
146182
return self._func
147183

148-
def get_function_name(self) -> str:
149-
"""Get the function name.
150-
151-
:return: Function name.
152-
"""
153-
return self._name
154-
155184
def get_function_json(self) -> str:
156185
"""Get the json stringified form of function.
157186
@@ -170,11 +199,6 @@ def __init__(self, func, function_script_file):
170199
def __call__(self, *args, **kwargs):
171200
pass
172201

173-
def configure_function_name(self, function_name: str) -> 'FunctionBuilder':
174-
self._function.set_function_name(function_name)
175-
176-
return self
177-
178202
def configure_http_type(self, http_type: str) -> 'FunctionBuilder':
179203
self._function.set_http_type(http_type)
180204

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

215+
def add_setting(self, setting: Setting) -> 'FunctionBuilder':
216+
self._function.add_setting(setting=setting)
217+
return self
218+
191219
def _validate_function(self,
192220
auth_level: Optional[AuthLevel] = None) -> None:
193221
"""
@@ -288,23 +316,6 @@ def decorator(func):
288316

289317
return decorator
290318

291-
def function_name(self, name: str) -> Callable[..., Any]:
292-
"""Set name of the :class:`Function` object.
293-
294-
:param name: Name of the function.
295-
:return: Decorator function.
296-
"""
297-
298-
@self._configure_function_builder
299-
def wrap(fb):
300-
def decorator():
301-
fb.configure_function_name(name)
302-
return fb
303-
304-
return decorator()
305-
306-
return wrap
307-
308319
def http_type(self, http_type: str) -> Callable[..., Any]:
309320
"""Set http type of the :class:`Function` object.
310321
@@ -1938,6 +1949,80 @@ def decorator():
19381949
return wrap
19391950

19401951

1952+
class SettingsApi(DecoratorApi, ABC):
1953+
"""Interface to extend for using existing settings decorator in
1954+
functions."""
1955+
1956+
def function_name(self, name: str,
1957+
setting_extra_fields: Dict[str, Any] = {},
1958+
) -> Callable[..., Any]:
1959+
"""Optional: Sets name of the :class:`Function` object. If not set,
1960+
it will default to the name of the method name.
1961+
1962+
:param name: Name of the function.
1963+
:param setting_extra_fields: Keyword arguments for specifying
1964+
additional setting fields
1965+
:return: Decorator function.
1966+
"""
1967+
1968+
@self._configure_function_builder
1969+
def wrap(fb):
1970+
def decorator():
1971+
fb.add_setting(setting=FunctionName(
1972+
function_name=name,
1973+
**setting_extra_fields))
1974+
return fb
1975+
1976+
return decorator()
1977+
1978+
return wrap
1979+
1980+
def retry(self,
1981+
strategy: str,
1982+
max_retry_count: str,
1983+
delay_interval: Optional[str] = None,
1984+
minimum_interval: Optional[str] = None,
1985+
maximum_interval: Optional[str] = None,
1986+
setting_extra_fields: Dict[str, Any] = {},
1987+
) -> Callable[..., Any]:
1988+
"""The retry decorator adds :class:`RetryPolicy` to the function
1989+
settings object for building :class:`Function` object used in worker
1990+
function indexing model. This is equivalent to defining RetryPolicy
1991+
in the function.json which enables function to retry on failure.
1992+
All optional fields will be given default value by function host when
1993+
they are parsed by function host.
1994+
1995+
Ref: https://aka.ms/azure_functions_retries
1996+
1997+
:param strategy: The retry strategy to use.
1998+
:param max_retry_count: The maximum number of retry attempts.
1999+
:param delay_interval: The delay interval between retry attempts.
2000+
:param minimum_interval: The minimum delay interval between retry
2001+
attempts.
2002+
:param maximum_interval: The maximum delay interval between retry
2003+
attempts.
2004+
:param setting_extra_fields: Keyword arguments for specifying
2005+
additional setting fields.
2006+
:return: Decorator function.
2007+
"""
2008+
2009+
@self._configure_function_builder
2010+
def wrap(fb):
2011+
def decorator():
2012+
fb.add_setting(setting=RetryPolicy(
2013+
strategy=strategy,
2014+
max_retry_count=max_retry_count,
2015+
minimum_interval=minimum_interval,
2016+
maximum_interval=maximum_interval,
2017+
delay_interval=delay_interval,
2018+
**setting_extra_fields))
2019+
return fb
2020+
2021+
return decorator()
2022+
2023+
return wrap
2024+
2025+
19412026
class FunctionRegister(DecoratorApi, HttpFunctionsAuthLevelMixin, ABC):
19422027
def __init__(self, auth_level: Union[AuthLevel, str], *args, **kwargs):
19432028
"""Interface for declaring top level function app class which will
@@ -1987,7 +2072,7 @@ def register_functions(self, function_container: DecoratorApi) -> None:
19872072
register_blueprint = register_functions
19882073

19892074

1990-
class FunctionApp(FunctionRegister, TriggerApi, BindingApi):
2075+
class FunctionApp(FunctionRegister, TriggerApi, BindingApi, SettingsApi):
19912076
"""FunctionApp object used by worker function indexing model captures
19922077
user defined functions and metadata.
19932078
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from azure.functions.decorators.core import Setting
5+
6+
FUNCTION_NAME = "function_name"
7+
8+
9+
class FunctionName(Setting):
10+
11+
def __init__(self, function_name: str,
12+
**kwargs):
13+
self.function_name = function_name
14+
super().__init__(setting_name=FUNCTION_NAME)
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
from typing import Optional
4+
5+
from azure.functions.decorators.core import Setting
6+
7+
RETRY_POLICY = "retry_policy"
8+
9+
10+
class RetryPolicy(Setting):
11+
12+
def __init__(self,
13+
strategy: str,
14+
max_retry_count: str,
15+
delay_interval: Optional[str] = None,
16+
minimum_interval: Optional[str] = None,
17+
maximum_interval: Optional[str] = None,
18+
**kwargs):
19+
self.strategy = strategy
20+
self.max_retry_count = max_retry_count
21+
self.delay_interval = delay_interval
22+
self.minimum_interval = minimum_interval
23+
self.maximum_interval = maximum_interval
24+
super().__init__(setting_name=RETRY_POLICY)

0 commit comments

Comments
 (0)