Skip to content

Commit fdf5fbd

Browse files
authored
Merge pull request #25 from MagicStack/poc
prohibit "inout" bindings; ensure functions without "$return" work; don't crash on unsupported messages
2 parents 47f128c + 564be40 commit fdf5fbd

File tree

12 files changed

+151
-7
lines changed

12 files changed

+151
-7
lines changed

azure/worker/dispatcher.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class FunctionInfo(typing.NamedTuple):
2929
outputs: typing.Set[str]
3030
requires_context: bool
3131
is_async: bool
32+
has_return: bool
3233

3334

3435
class DispatcherMeta(type):
@@ -74,6 +75,8 @@ def __init__(self, loop, host, port, worker_id, request_id,
7475
self._grpc_thread = threading.Thread(
7576
name='grpc-thread', target=self.__poll_grpc)
7677

78+
self._logger = logging.getLogger('python-azure-worker')
79+
7780
@classmethod
7881
async def connect(cls, host, port, worker_id, request_id,
7982
connect_timeout):
@@ -184,14 +187,25 @@ def _register_function(self, function_id: str, func: callable,
184187
bindings = {}
185188
return_binding = None
186189
for name, desc in metadata.bindings.items():
190+
if desc.direction == protos.BindingInfo.inout:
191+
raise TypeError(
192+
f'cannot load the {func_name} function: '
193+
f'"inout" bindings are not supported')
194+
187195
if name == '$return':
188196
# TODO:
189197
# * add proper gRPC->Python type reflection;
190198
# * convert the type from function.json to a Python type;
191199
# * enforce return type of a function call in Python;
192200
# * use the return type information to marshal the result into
193201
# a correct gRPC type.
194-
return_binding = desc # NoQA
202+
203+
if desc.direction != protos.BindingInfo.out:
204+
raise TypeError(
205+
f'cannot load the {func_name} function: '
206+
f'"$return" binding must have direction set to "out"')
207+
208+
return_binding = desc
195209
else:
196210
bindings[name] = desc
197211

@@ -251,14 +265,19 @@ def _register_function(self, function_id: str, func: callable,
251265
directory=metadata.directory,
252266
outputs=frozenset(outputs),
253267
requires_context=requires_context,
254-
is_async=inspect.iscoroutinefunction(func))
268+
is_async=inspect.iscoroutinefunction(func),
269+
has_return=return_binding is not None)
255270

256271
async def _dispatch_grpc_request(self, request):
257272
content_type = request.WhichOneof('content')
258273
request_handler = getattr(self, f'_handle__{content_type}', None)
259274
if request_handler is None:
260-
raise RuntimeError(
275+
# Don't crash on unknown messages. Some of them can be ignored;
276+
# and if something goes really wrong the host can always just
277+
# kill the worker's process.
278+
self._logger.error(
261279
f'unknown StreamingMessage content type {content_type}')
280+
return
262281

263282
resp = await request_handler(request)
264283
self._grpc_resp_queue.put_nowait(resp)
@@ -349,11 +368,15 @@ async def _handle__invocation_request(self, req):
349368
name=name,
350369
data=rpc_val))
351370

371+
return_value = None
372+
if fi.has_return:
373+
return_value = rpc_types.to_outgoing_proto(call_result)
374+
352375
return protos.StreamingMessage(
353376
request_id=self.request_id,
354377
invocation_response=protos.InvocationResponse(
355378
invocation_id=invocation_id,
356-
return_value=rpc_types.to_outgoing_proto(call_result),
379+
return_value=return_value,
357380
result=protos.StatusResult(
358381
status=protos.StatusResult.Success),
359382
output_data=output_data))

azure/worker/protos/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
FunctionLoadResponse,
1414
InvocationRequest,
1515
InvocationResponse,
16+
WorkerHeartbeat,
1617
BindingInfo,
1718
StatusResult,
1819
RpcException,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"scriptFile": "main.py",
3+
"disabled": false,
4+
"bindings": [
5+
{
6+
"authLevel": "anonymous",
7+
"type": "httpTrigger",
8+
"direction": "in",
9+
"name": "req"
10+
},
11+
{
12+
"type": "http",
13+
"direction": "inout",
14+
"name": "abc"
15+
}
16+
]
17+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def main(req, abc):
2+
return 'trust me, it is OK!'
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"scriptFile": "main.py",
3+
"disabled": false,
4+
"bindings": [
5+
{
6+
"authLevel": "anonymous",
7+
"type": "httpTrigger",
8+
"direction": "in",
9+
"name": "req"
10+
},
11+
{
12+
"type": "http",
13+
"direction": "in",
14+
"name": "$return"
15+
}
16+
]
17+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def main(req):
2+
return 'trust me, it is OK!'
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"scriptFile": "main.py",
3+
"disabled": false,
4+
"bindings": [
5+
{
6+
"authLevel": "anonymous",
7+
"type": "httpTrigger",
8+
"direction": "in",
9+
"name": "req"
10+
}
11+
]
12+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import logging
2+
3+
4+
logger = logging.getLogger('test')
5+
6+
7+
def main(req):
8+
logger.error('hi')

azure/worker/tests/test_broken_functions.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,33 @@ async def test_load_broken__syntax_error(self):
9393
protos.StatusResult.Failure)
9494

9595
self.assertIn('SyntaxError', r.response.result.exception.message)
96+
97+
async def test_load_broken__inout_param(self):
98+
async with testutils.start_mockhost(
99+
script_root='broken_functions') as host:
100+
101+
func_id, r = await host.load_function('inout_param')
102+
103+
self.assertEqual(r.response.function_id, func_id)
104+
self.assertEqual(r.response.result.status,
105+
protos.StatusResult.Failure)
106+
107+
self.assertRegex(
108+
r.response.result.exception.message,
109+
r'.*cannot load the inout_param function'
110+
r'.*"inout" bindings.*')
111+
112+
async def test_load_broken__return_param_in(self):
113+
async with testutils.start_mockhost(
114+
script_root='broken_functions') as host:
115+
116+
func_id, r = await host.load_function('return_param_in')
117+
118+
self.assertEqual(r.response.function_id, func_id)
119+
self.assertEqual(r.response.result.status,
120+
protos.StatusResult.Failure)
121+
122+
self.assertRegex(
123+
r.response.result.exception.message,
124+
r'.*cannot load the return_param_in function'
125+
r'.*"\$return" .* set to "out"')

azure/worker/tests/test_functions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ def test_return_str(self):
1919
self.assertEqual(r.status_code, 200)
2020
self.assertEqual(r.text, 'Hello World!')
2121

22+
def test_no_return(self):
23+
r = self.webhost.request('GET', 'no_return')
24+
self.assertEqual(r.status_code, 204)
25+
2226
def test_async_return_str(self):
2327
r = self.webhost.request('GET', 'async_return_str')
2428
self.assertEqual(r.status_code, 200)

0 commit comments

Comments
 (0)