Skip to content

Commit bba8884

Browse files
authored
Merge branch 'dev' into evanroman/add-thread-local-context
2 parents 4cd388e + 4e32a20 commit bba8884

File tree

12 files changed

+198
-7
lines changed

12 files changed

+198
-7
lines changed

azure/functions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from . import timer # NoQA
3636
from . import durable_functions # NoQA
3737
from . import sql # NoQA
38+
from . import warmup # NoQA
3839

3940

4041
__all__ = (
@@ -64,6 +65,7 @@
6465
'SqlRow',
6566
'SqlRowList',
6667
'TimerRequest',
68+
'WarmUpContext',
6769

6870
# Middlewares
6971
'WsgiMiddleware',

azure/functions/_http_asgi.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ async def _receive(self):
119119
}
120120

121121
async def _send(self, message):
122+
logging.debug("Received %s from ASGI worker.", message)
122123
if message["type"] == "http.response.start":
123124
self._handle_http_response_start(message)
124125
elif message["type"] == "http.response.body":
@@ -151,7 +152,6 @@ def __init__(self, app):
151152
self._usage_reported = True
152153

153154
self._app = app
154-
self._loop = asyncio.new_event_loop()
155155
self.main = self._handle
156156

157157
def handle(self, req: HttpRequest, context: Optional[Context] = None):
@@ -173,9 +173,8 @@ async def main(req, context):
173173

174174
def _handle(self, req, context):
175175
asgi_request = AsgiRequest(req, context)
176-
asyncio.set_event_loop(self._loop)
177176
scope = asgi_request.to_asgi_http_scope()
178-
asgi_response = self._loop.run_until_complete(
177+
asgi_response = asyncio.run(
179178
AsgiResponse.from_app(self._app, scope, req.get_body())
180179
)
181180

azure/functions/_http_wsgi.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
3-
43
from typing import Dict, List, Optional, Any
54
import logging
65
from io import BytesIO, StringIO
@@ -197,13 +196,16 @@ def _handle(self, req, context):
197196
wsgi_request = WsgiRequest(req, context)
198197
environ = wsgi_request.to_environ(self._wsgi_error_buffer)
199198
wsgi_response = WsgiResponse.from_app(self._app, environ)
200-
self._handle_errors()
199+
self._handle_errors(wsgi_response)
201200
return wsgi_response.to_func_response()
202201

203-
def _handle_errors(self):
202+
def _handle_errors(self, wsgi_response):
204203
if self._wsgi_error_buffer.tell() > 0:
205204
self._wsgi_error_buffer.seek(0)
206205
error_message = linesep.join(
207206
self._wsgi_error_buffer.readline()
208207
)
209208
raise Exception(error_message)
209+
210+
if wsgi_response._status_code >= 500:
211+
raise Exception(b''.join(wsgi_response._buffer))

azure/functions/decorators/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
SERVICE_BUS = "serviceBus"
1414
SERVICE_BUS_TRIGGER = "serviceBusTrigger"
1515
TIMER_TRIGGER = "timerTrigger"
16+
WARMUP_TRIGGER = "warmupTrigger"
1617
BLOB_TRIGGER = "blobTrigger"
1718
BLOB = "blob"
1819
EVENT_GRID_TRIGGER = "eventGridTrigger"

azure/functions/decorators/function_app.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
parse_iterable_param_to_enums, StringifyEnumJsonEncoder
2727
from azure.functions.http import HttpRequest
2828
from .generic import GenericInputBinding, GenericTrigger, GenericOutputBinding
29+
from .warmup import WarmUpTrigger
2930
from .._http_asgi import AsgiMiddleware
3031
from .._http_wsgi import WsgiMiddleware, Context
3132

@@ -450,6 +451,43 @@ def decorator():
450451

451452
schedule = timer_trigger
452453

454+
def warm_up_trigger(self,
455+
arg_name: str,
456+
data_type: Optional[Union[DataType, str]] = None,
457+
**kwargs) -> Callable:
458+
"""The warm up decorator adds :class:`WarmUpTrigger` to the
459+
:class:`FunctionBuilder` object
460+
for building :class:`Function` object used in worker function
461+
indexing model. This is equivalent to defining WarmUpTrigger
462+
in the function.json which enables your function be triggered on the
463+
specified schedule.
464+
All optional fields will be given default value by function host when
465+
they are parsed by function host.
466+
467+
Ref: https://aka.ms/azure-function-binding-warmup
468+
469+
:param arg_name: The name of the variable that represents the
470+
:class:`TimerRequest` object in function code.
471+
:param data_type: Defines how Functions runtime should treat the
472+
parameter value.
473+
:return: Decorator function.
474+
"""
475+
476+
@self._configure_function_builder
477+
def wrap(fb):
478+
def decorator():
479+
fb.add_trigger(
480+
trigger=WarmUpTrigger(
481+
name=arg_name,
482+
data_type=parse_singular_param_to_enum(data_type,
483+
DataType),
484+
**kwargs))
485+
return fb
486+
487+
return decorator()
488+
489+
return wrap
490+
453491
def service_bus_queue_trigger(
454492
self,
455493
arg_name: str,

azure/functions/decorators/warmup.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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.constants import WARMUP_TRIGGER
6+
from azure.functions.decorators.core import Trigger, DataType
7+
8+
9+
class WarmUpTrigger(Trigger):
10+
@staticmethod
11+
def get_binding_name() -> str:
12+
return WARMUP_TRIGGER
13+
14+
def __init__(self,
15+
name: str,
16+
data_type: Optional[DataType] = None,
17+
**kwargs) -> None:
18+
super().__init__(name=name, data_type=data_type)

azure/functions/warmup.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import typing
5+
6+
from . import meta
7+
8+
9+
class WarmUpContext:
10+
pass
11+
12+
13+
class WarmUpTriggerConverter(meta.InConverter, binding='warmupTrigger',
14+
trigger=True):
15+
16+
@classmethod
17+
def check_input_type_annotation(cls, pytype: type) -> bool:
18+
return issubclass(pytype, WarmUpContext)
19+
20+
@classmethod
21+
def decode(cls, data: meta.Datum, *, trigger_metadata) -> typing.Any:
22+
return WarmUpContext()

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
EXTRA_REQUIRES = {
88
'dev': [
99
'flake8~=4.0.1',
10+
'flake8-logging-format',
1011
'mypy',
1112
'pytest',
1213
'pytest-cov',

tests/decorators/test_decorators.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from azure.functions.decorators.constants import TIMER_TRIGGER, HTTP_TRIGGER, \
66
HTTP_OUTPUT, QUEUE, QUEUE_TRIGGER, SERVICE_BUS, SERVICE_BUS_TRIGGER, \
77
EVENT_HUB, EVENT_HUB_TRIGGER, COSMOS_DB, COSMOS_DB_TRIGGER, BLOB, \
8-
BLOB_TRIGGER, EVENT_GRID_TRIGGER, EVENT_GRID, TABLE
8+
BLOB_TRIGGER, EVENT_GRID_TRIGGER, EVENT_GRID, TABLE, WARMUP_TRIGGER
99
from azure.functions.decorators.core import DataType, AuthLevel, \
1010
BindingDirection, AccessRights, Cardinality
1111
from azure.functions.decorators.function_app import FunctionApp
@@ -220,6 +220,48 @@ def dummy():
220220
]
221221
})
222222

223+
def test_warmup_trigger_default_args(self):
224+
app = self.func_app
225+
226+
@app.warm_up_trigger(arg_name="req")
227+
def dummy_func():
228+
pass
229+
230+
func = self._get_user_function(app)
231+
self.assertEqual(func.get_function_name(), "dummy_func")
232+
assert_json(self, func, {
233+
"scriptFile": "function_app.py",
234+
"bindings": [
235+
{
236+
"name": "req",
237+
"type": WARMUP_TRIGGER,
238+
"direction": BindingDirection.IN,
239+
}
240+
]
241+
})
242+
243+
def test_warmup_trigger_full_args(self):
244+
app = self.func_app
245+
246+
@app.warm_up_trigger(arg_name="req", data_type=DataType.STRING,
247+
dummy_field='dummy')
248+
def dummy():
249+
pass
250+
251+
func = self._get_user_function(app)
252+
assert_json(self, func, {
253+
"scriptFile": "function_app.py",
254+
"bindings": [
255+
{
256+
"name": "req",
257+
"type": WARMUP_TRIGGER,
258+
"dataType": DataType.STRING,
259+
"direction": BindingDirection.IN,
260+
'dummyField': 'dummy'
261+
}
262+
]
263+
})
264+
223265
def test_queue_default_args(self):
224266
app = self.func_app
225267

tests/decorators/test_warmup.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import unittest
4+
5+
from azure.functions.decorators.constants import WARMUP_TRIGGER
6+
from azure.functions.decorators.core import BindingDirection, DataType
7+
from azure.functions.decorators.warmup import WarmUpTrigger
8+
9+
10+
class TestWarmUp(unittest.TestCase):
11+
def test_warmup_trigger_valid_creation(self):
12+
trigger = WarmUpTrigger(name="req",
13+
data_type=DataType.UNDEFINED,
14+
dummy_field="dummy")
15+
16+
self.assertEqual(trigger.get_binding_name(), "warmupTrigger")
17+
self.assertEqual(trigger.get_dict_repr(), {
18+
"type": WARMUP_TRIGGER,
19+
"direction": BindingDirection.IN,
20+
'dummyField': 'dummy',
21+
"name": "req",
22+
"dataType": DataType.UNDEFINED
23+
})

0 commit comments

Comments
 (0)