singlestoredb 1.14.2__cp38-abi3-macosx_10_9_universal2.whl → 1.15.0__cp38-abi3-macosx_10_9_universal2.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of singlestoredb might be problematic. Click here for more details.
- _singlestoredb_accel.abi3.so +0 -0
- singlestoredb/__init__.py +2 -2
- singlestoredb/apps/_python_udfs.py +3 -3
- singlestoredb/config.py +5 -0
- singlestoredb/functions/decorator.py +32 -13
- singlestoredb/functions/ext/asgi.py +287 -27
- singlestoredb/functions/ext/timer.py +98 -0
- singlestoredb/functions/typing/numpy.py +20 -0
- singlestoredb/functions/typing/pandas.py +2 -0
- singlestoredb/functions/typing/polars.py +2 -0
- singlestoredb/functions/typing/pyarrow.py +2 -0
- singlestoredb/magics/run_personal.py +82 -1
- singlestoredb/magics/run_shared.py +82 -1
- singlestoredb/management/__init__.py +1 -0
- singlestoredb/management/region.py +92 -0
- singlestoredb/management/workspace.py +180 -1
- singlestoredb/tests/ext_funcs/__init__.py +94 -55
- singlestoredb/tests/test.sql +22 -0
- singlestoredb/tests/test_ext_func.py +90 -0
- singlestoredb/tests/test_management.py +223 -0
- {singlestoredb-1.14.2.dist-info → singlestoredb-1.15.0.dist-info}/METADATA +1 -1
- {singlestoredb-1.14.2.dist-info → singlestoredb-1.15.0.dist-info}/RECORD +27 -22
- /singlestoredb/functions/{typing.py → typing/__init__.py} +0 -0
- {singlestoredb-1.14.2.dist-info → singlestoredb-1.15.0.dist-info}/LICENSE +0 -0
- {singlestoredb-1.14.2.dist-info → singlestoredb-1.15.0.dist-info}/WHEEL +0 -0
- {singlestoredb-1.14.2.dist-info → singlestoredb-1.15.0.dist-info}/entry_points.txt +0 -0
- {singlestoredb-1.14.2.dist-info → singlestoredb-1.15.0.dist-info}/top_level.txt +0 -0
_singlestoredb_accel.abi3.so
CHANGED
|
Binary file
|
singlestoredb/__init__.py
CHANGED
|
@@ -13,7 +13,7 @@ Examples
|
|
|
13
13
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
__version__ = '1.
|
|
16
|
+
__version__ = '1.15.0'
|
|
17
17
|
|
|
18
18
|
from typing import Any
|
|
19
19
|
|
|
@@ -25,7 +25,7 @@ from .exceptions import (
|
|
|
25
25
|
DataError, ManagementError,
|
|
26
26
|
)
|
|
27
27
|
from .management import (
|
|
28
|
-
manage_cluster, manage_workspaces, manage_files,
|
|
28
|
+
manage_cluster, manage_workspaces, manage_files, manage_regions,
|
|
29
29
|
)
|
|
30
30
|
from .types import (
|
|
31
31
|
Date, Time, Timestamp, DateFromTicks, TimeFromTicks, TimestampFromTicks,
|
|
@@ -15,7 +15,6 @@ _running_server: 'typing.Optional[AwaitableUvicornServer]' = None
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
async def run_udf_app(
|
|
18
|
-
replace_existing: bool,
|
|
19
18
|
log_level: str = 'error',
|
|
20
19
|
kill_existing_app_server: bool = True,
|
|
21
20
|
) -> UdfConnectionInfo:
|
|
@@ -55,8 +54,9 @@ async def run_udf_app(
|
|
|
55
54
|
)
|
|
56
55
|
_running_server = AwaitableUvicornServer(config)
|
|
57
56
|
|
|
58
|
-
# Register the functions
|
|
59
|
-
|
|
57
|
+
# Register the functions only if the app is running interactively.
|
|
58
|
+
if app_config.running_interactively:
|
|
59
|
+
app.register_functions(replace=True)
|
|
60
60
|
|
|
61
61
|
asyncio.create_task(_running_server.serve())
|
|
62
62
|
await _running_server.wait_for_startup()
|
singlestoredb/config.py
CHANGED
|
@@ -438,6 +438,11 @@ register_option(
|
|
|
438
438
|
environ=['SINGLESTOREDB_EXT_FUNC_PORT'],
|
|
439
439
|
)
|
|
440
440
|
|
|
441
|
+
register_option(
|
|
442
|
+
'external_function.timeout', 'int', check_int, 24*60*60,
|
|
443
|
+
'Specifies the timeout in seconds for processing a batch of rows.',
|
|
444
|
+
environ=['SINGLESTOREDB_EXT_FUNC_TIMEOUT'],
|
|
445
|
+
)
|
|
441
446
|
|
|
442
447
|
#
|
|
443
448
|
# Debugging options
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import functools
|
|
2
3
|
import inspect
|
|
3
4
|
from typing import Any
|
|
@@ -19,6 +20,7 @@ ParameterType = Union[
|
|
|
19
20
|
]
|
|
20
21
|
|
|
21
22
|
ReturnType = ParameterType
|
|
23
|
+
UDFType = Callable[..., Any]
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
def is_valid_type(obj: Any) -> bool:
|
|
@@ -100,7 +102,8 @@ def _func(
|
|
|
100
102
|
name: Optional[str] = None,
|
|
101
103
|
args: Optional[ParameterType] = None,
|
|
102
104
|
returns: Optional[ReturnType] = None,
|
|
103
|
-
|
|
105
|
+
timeout: Optional[int] = None,
|
|
106
|
+
) -> UDFType:
|
|
104
107
|
"""Generic wrapper for UDF and TVF decorators."""
|
|
105
108
|
|
|
106
109
|
_singlestoredb_attrs = { # type: ignore
|
|
@@ -108,6 +111,7 @@ def _func(
|
|
|
108
111
|
name=name,
|
|
109
112
|
args=expand_types(args),
|
|
110
113
|
returns=expand_types(returns),
|
|
114
|
+
timeout=timeout,
|
|
111
115
|
).items() if v is not None
|
|
112
116
|
}
|
|
113
117
|
|
|
@@ -115,23 +119,33 @@ def _func(
|
|
|
115
119
|
# called later, so the wrapper much be created with the func passed
|
|
116
120
|
# in at that time.
|
|
117
121
|
if func is None:
|
|
118
|
-
def decorate(func:
|
|
122
|
+
def decorate(func: UDFType) -> UDFType:
|
|
119
123
|
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
if asyncio.iscoroutinefunction(func):
|
|
125
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> UDFType:
|
|
126
|
+
return await func(*args, **kwargs) # type: ignore
|
|
127
|
+
async_wrapper._singlestoredb_attrs = _singlestoredb_attrs # type: ignore
|
|
128
|
+
return functools.wraps(func)(async_wrapper)
|
|
122
129
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
130
|
+
else:
|
|
131
|
+
def wrapper(*args: Any, **kwargs: Any) -> UDFType:
|
|
132
|
+
return func(*args, **kwargs) # type: ignore
|
|
133
|
+
wrapper._singlestoredb_attrs = _singlestoredb_attrs # type: ignore
|
|
134
|
+
return functools.wraps(func)(wrapper)
|
|
126
135
|
|
|
127
136
|
return decorate
|
|
128
137
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
if asyncio.iscoroutinefunction(func):
|
|
139
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> UDFType:
|
|
140
|
+
return await func(*args, **kwargs) # type: ignore
|
|
141
|
+
async_wrapper._singlestoredb_attrs = _singlestoredb_attrs # type: ignore
|
|
142
|
+
return functools.wraps(func)(async_wrapper)
|
|
133
143
|
|
|
134
|
-
|
|
144
|
+
else:
|
|
145
|
+
def wrapper(*args: Any, **kwargs: Any) -> UDFType:
|
|
146
|
+
return func(*args, **kwargs) # type: ignore
|
|
147
|
+
wrapper._singlestoredb_attrs = _singlestoredb_attrs # type: ignore
|
|
148
|
+
return functools.wraps(func)(wrapper)
|
|
135
149
|
|
|
136
150
|
|
|
137
151
|
def udf(
|
|
@@ -140,7 +154,8 @@ def udf(
|
|
|
140
154
|
name: Optional[str] = None,
|
|
141
155
|
args: Optional[ParameterType] = None,
|
|
142
156
|
returns: Optional[ReturnType] = None,
|
|
143
|
-
|
|
157
|
+
timeout: Optional[int] = None,
|
|
158
|
+
) -> UDFType:
|
|
144
159
|
"""
|
|
145
160
|
Define a user-defined function (UDF).
|
|
146
161
|
|
|
@@ -167,6 +182,9 @@ def udf(
|
|
|
167
182
|
Specifies the return data type of the function. This parameter
|
|
168
183
|
works the same way as `args`. If the function is a table-valued
|
|
169
184
|
function, the return type should be a `Table` object.
|
|
185
|
+
timeout : int, optional
|
|
186
|
+
The timeout in seconds for the UDF execution. If not specified,
|
|
187
|
+
the global default timeout is used.
|
|
170
188
|
|
|
171
189
|
Returns
|
|
172
190
|
-------
|
|
@@ -178,4 +196,5 @@ def udf(
|
|
|
178
196
|
name=name,
|
|
179
197
|
args=args,
|
|
180
198
|
returns=returns,
|
|
199
|
+
timeout=timeout,
|
|
181
200
|
)
|
|
@@ -24,7 +24,10 @@ Example
|
|
|
24
24
|
"""
|
|
25
25
|
import argparse
|
|
26
26
|
import asyncio
|
|
27
|
+
import contextvars
|
|
27
28
|
import dataclasses
|
|
29
|
+
import datetime
|
|
30
|
+
import functools
|
|
28
31
|
import importlib.util
|
|
29
32
|
import inspect
|
|
30
33
|
import io
|
|
@@ -37,8 +40,11 @@ import secrets
|
|
|
37
40
|
import sys
|
|
38
41
|
import tempfile
|
|
39
42
|
import textwrap
|
|
43
|
+
import threading
|
|
44
|
+
import time
|
|
40
45
|
import typing
|
|
41
46
|
import urllib
|
|
47
|
+
import uuid
|
|
42
48
|
import zipfile
|
|
43
49
|
import zipimport
|
|
44
50
|
from types import ModuleType
|
|
@@ -66,6 +72,7 @@ from ..signature import get_signature
|
|
|
66
72
|
from ..signature import signature_to_sql
|
|
67
73
|
from ..typing import Masked
|
|
68
74
|
from ..typing import Table
|
|
75
|
+
from .timer import Timer
|
|
69
76
|
|
|
70
77
|
try:
|
|
71
78
|
import cloudpickle
|
|
@@ -95,6 +102,15 @@ else:
|
|
|
95
102
|
func_map = itertools.starmap
|
|
96
103
|
|
|
97
104
|
|
|
105
|
+
async def to_thread(
|
|
106
|
+
func: Any, /, *args: Any, **kwargs: Dict[str, Any],
|
|
107
|
+
) -> Any:
|
|
108
|
+
loop = asyncio.get_running_loop()
|
|
109
|
+
ctx = contextvars.copy_context()
|
|
110
|
+
func_call = functools.partial(ctx.run, func, *args, **kwargs)
|
|
111
|
+
return await loop.run_in_executor(None, func_call)
|
|
112
|
+
|
|
113
|
+
|
|
98
114
|
# Use negative values to indicate unsigned ints / binary data / usec time precision
|
|
99
115
|
rowdat_1_type_map = {
|
|
100
116
|
'bool': ft.LONGLONG,
|
|
@@ -251,6 +267,32 @@ def build_tuple(x: Any) -> Any:
|
|
|
251
267
|
return tuple(x) if isinstance(x, Masked) else (x, None)
|
|
252
268
|
|
|
253
269
|
|
|
270
|
+
def cancel_on_event(
|
|
271
|
+
cancel_event: threading.Event,
|
|
272
|
+
) -> None:
|
|
273
|
+
"""
|
|
274
|
+
Cancel the function call if the cancel event is set.
|
|
275
|
+
|
|
276
|
+
Parameters
|
|
277
|
+
----------
|
|
278
|
+
cancel_event : threading.Event
|
|
279
|
+
The event to check for cancellation
|
|
280
|
+
|
|
281
|
+
Raises
|
|
282
|
+
------
|
|
283
|
+
asyncio.CancelledError
|
|
284
|
+
If the cancel event is set
|
|
285
|
+
|
|
286
|
+
"""
|
|
287
|
+
if cancel_event.is_set():
|
|
288
|
+
task = asyncio.current_task()
|
|
289
|
+
if task is not None:
|
|
290
|
+
task.cancel()
|
|
291
|
+
raise asyncio.CancelledError(
|
|
292
|
+
'Function call was cancelled by client',
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
254
296
|
def build_udf_endpoint(
|
|
255
297
|
func: Callable[..., Any],
|
|
256
298
|
returns_data_format: str,
|
|
@@ -273,12 +315,24 @@ def build_udf_endpoint(
|
|
|
273
315
|
"""
|
|
274
316
|
if returns_data_format in ['scalar', 'list']:
|
|
275
317
|
|
|
318
|
+
is_async = asyncio.iscoroutinefunction(func)
|
|
319
|
+
|
|
276
320
|
async def do_func(
|
|
321
|
+
cancel_event: threading.Event,
|
|
322
|
+
timer: Timer,
|
|
277
323
|
row_ids: Sequence[int],
|
|
278
324
|
rows: Sequence[Sequence[Any]],
|
|
279
325
|
) -> Tuple[Sequence[int], List[Tuple[Any, ...]]]:
|
|
280
326
|
'''Call function on given rows of data.'''
|
|
281
|
-
|
|
327
|
+
out = []
|
|
328
|
+
async with timer('call_function'):
|
|
329
|
+
for row in rows:
|
|
330
|
+
cancel_on_event(cancel_event)
|
|
331
|
+
if is_async:
|
|
332
|
+
out.append(await func(*row))
|
|
333
|
+
else:
|
|
334
|
+
out.append(func(*row))
|
|
335
|
+
return row_ids, list(zip(out))
|
|
282
336
|
|
|
283
337
|
return do_func
|
|
284
338
|
|
|
@@ -307,8 +361,11 @@ def build_vector_udf_endpoint(
|
|
|
307
361
|
"""
|
|
308
362
|
masks = get_masked_params(func)
|
|
309
363
|
array_cls = get_array_class(returns_data_format)
|
|
364
|
+
is_async = asyncio.iscoroutinefunction(func)
|
|
310
365
|
|
|
311
366
|
async def do_func(
|
|
367
|
+
cancel_event: threading.Event,
|
|
368
|
+
timer: Timer,
|
|
312
369
|
row_ids: Sequence[int],
|
|
313
370
|
cols: Sequence[Tuple[Sequence[Any], Optional[Sequence[bool]]]],
|
|
314
371
|
) -> Tuple[
|
|
@@ -319,10 +376,19 @@ def build_vector_udf_endpoint(
|
|
|
319
376
|
row_ids = array_cls(row_ids)
|
|
320
377
|
|
|
321
378
|
# Call the function with `cols` as the function parameters
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
379
|
+
async with timer('call_function'):
|
|
380
|
+
if cols and cols[0]:
|
|
381
|
+
if is_async:
|
|
382
|
+
out = await func(*[x if m else x[0] for x, m in zip(cols, masks)])
|
|
383
|
+
else:
|
|
384
|
+
out = func(*[x if m else x[0] for x, m in zip(cols, masks)])
|
|
385
|
+
else:
|
|
386
|
+
if is_async:
|
|
387
|
+
out = await func()
|
|
388
|
+
else:
|
|
389
|
+
out = func()
|
|
390
|
+
|
|
391
|
+
cancel_on_event(cancel_event)
|
|
326
392
|
|
|
327
393
|
# Single masked value
|
|
328
394
|
if isinstance(out, Masked):
|
|
@@ -360,7 +426,11 @@ def build_tvf_endpoint(
|
|
|
360
426
|
"""
|
|
361
427
|
if returns_data_format in ['scalar', 'list']:
|
|
362
428
|
|
|
429
|
+
is_async = asyncio.iscoroutinefunction(func)
|
|
430
|
+
|
|
363
431
|
async def do_func(
|
|
432
|
+
cancel_event: threading.Event,
|
|
433
|
+
timer: Timer,
|
|
364
434
|
row_ids: Sequence[int],
|
|
365
435
|
rows: Sequence[Sequence[Any]],
|
|
366
436
|
) -> Tuple[Sequence[int], List[Tuple[Any, ...]]]:
|
|
@@ -368,9 +438,15 @@ def build_tvf_endpoint(
|
|
|
368
438
|
out_ids: List[int] = []
|
|
369
439
|
out = []
|
|
370
440
|
# Call function on each row of data
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
441
|
+
async with timer('call_function'):
|
|
442
|
+
for i, row in zip(row_ids, rows):
|
|
443
|
+
cancel_on_event(cancel_event)
|
|
444
|
+
if is_async:
|
|
445
|
+
res = await func(*row)
|
|
446
|
+
else:
|
|
447
|
+
res = func(*row)
|
|
448
|
+
out.extend(as_list_of_tuples(res))
|
|
449
|
+
out_ids.extend([row_ids[i]] * (len(out)-len(out_ids)))
|
|
374
450
|
return out_ids, out
|
|
375
451
|
|
|
376
452
|
return do_func
|
|
@@ -402,6 +478,8 @@ def build_vector_tvf_endpoint(
|
|
|
402
478
|
array_cls = get_array_class(returns_data_format)
|
|
403
479
|
|
|
404
480
|
async def do_func(
|
|
481
|
+
cancel_event: threading.Event,
|
|
482
|
+
timer: Timer,
|
|
405
483
|
row_ids: Sequence[int],
|
|
406
484
|
cols: Sequence[Tuple[Sequence[Any], Optional[Sequence[bool]]]],
|
|
407
485
|
) -> Tuple[
|
|
@@ -413,13 +491,28 @@ def build_vector_tvf_endpoint(
|
|
|
413
491
|
# each result row, so we just have to use the same
|
|
414
492
|
# row ID for all rows in the result.
|
|
415
493
|
|
|
494
|
+
is_async = asyncio.iscoroutinefunction(func)
|
|
495
|
+
|
|
416
496
|
# Call function on each column of data
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
497
|
+
async with timer('call_function'):
|
|
498
|
+
if cols and cols[0]:
|
|
499
|
+
if is_async:
|
|
500
|
+
func_res = await func(
|
|
501
|
+
*[x if m else x[0] for x, m in zip(cols, masks)],
|
|
502
|
+
)
|
|
503
|
+
else:
|
|
504
|
+
func_res = func(
|
|
505
|
+
*[x if m else x[0] for x, m in zip(cols, masks)],
|
|
506
|
+
)
|
|
507
|
+
else:
|
|
508
|
+
if is_async:
|
|
509
|
+
func_res = await func()
|
|
510
|
+
else:
|
|
511
|
+
func_res = func()
|
|
512
|
+
|
|
513
|
+
res = get_dataframe_columns(func_res)
|
|
514
|
+
|
|
515
|
+
cancel_on_event(cancel_event)
|
|
423
516
|
|
|
424
517
|
# Generate row IDs
|
|
425
518
|
if isinstance(res[0], Masked):
|
|
@@ -458,6 +551,10 @@ def make_func(
|
|
|
458
551
|
function_type = sig.get('function_type', 'udf')
|
|
459
552
|
args_data_format = sig.get('args_data_format', 'scalar')
|
|
460
553
|
returns_data_format = sig.get('returns_data_format', 'scalar')
|
|
554
|
+
timeout = (
|
|
555
|
+
func._singlestoredb_attrs.get('timeout') or # type: ignore
|
|
556
|
+
get_option('external_function.timeout')
|
|
557
|
+
)
|
|
461
558
|
|
|
462
559
|
if function_type == 'tvf':
|
|
463
560
|
do_func = build_tvf_endpoint(func, returns_data_format)
|
|
@@ -477,6 +574,12 @@ def make_func(
|
|
|
477
574
|
# Set function type
|
|
478
575
|
info['function_type'] = function_type
|
|
479
576
|
|
|
577
|
+
# Set timeout
|
|
578
|
+
info['timeout'] = max(timeout, 1)
|
|
579
|
+
|
|
580
|
+
# Set async flag
|
|
581
|
+
info['is_async'] = asyncio.iscoroutinefunction(func)
|
|
582
|
+
|
|
480
583
|
# Setup argument types for rowdat_1 parser
|
|
481
584
|
colspec = []
|
|
482
585
|
for x in sig['args']:
|
|
@@ -498,6 +601,43 @@ def make_func(
|
|
|
498
601
|
return do_func, info
|
|
499
602
|
|
|
500
603
|
|
|
604
|
+
async def cancel_on_timeout(timeout: int) -> None:
|
|
605
|
+
"""Cancel request if it takes too long."""
|
|
606
|
+
await asyncio.sleep(timeout)
|
|
607
|
+
raise asyncio.CancelledError(
|
|
608
|
+
'Function call was cancelled due to timeout',
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
async def cancel_on_disconnect(
|
|
613
|
+
receive: Callable[..., Awaitable[Any]],
|
|
614
|
+
) -> None:
|
|
615
|
+
"""Cancel request if client disconnects."""
|
|
616
|
+
while True:
|
|
617
|
+
message = await receive()
|
|
618
|
+
if message['type'] == 'http.disconnect':
|
|
619
|
+
raise asyncio.CancelledError(
|
|
620
|
+
'Function call was cancelled by client',
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
async def cancel_all_tasks(tasks: Iterable[asyncio.Task[Any]]) -> None:
|
|
625
|
+
"""Cancel all tasks."""
|
|
626
|
+
for task in tasks:
|
|
627
|
+
task.cancel()
|
|
628
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def start_counter() -> float:
|
|
632
|
+
"""Start a timer and return the start time."""
|
|
633
|
+
return time.perf_counter()
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def end_counter(start: float) -> float:
|
|
637
|
+
"""End a timer and return the elapsed time."""
|
|
638
|
+
return time.perf_counter() - start
|
|
639
|
+
|
|
640
|
+
|
|
501
641
|
class Application(object):
|
|
502
642
|
"""
|
|
503
643
|
Create an external function application.
|
|
@@ -824,6 +964,21 @@ class Application(object):
|
|
|
824
964
|
Function to send response information
|
|
825
965
|
|
|
826
966
|
'''
|
|
967
|
+
request_id = str(uuid.uuid4())
|
|
968
|
+
|
|
969
|
+
timer = Timer(
|
|
970
|
+
id=request_id,
|
|
971
|
+
timestamp=datetime.datetime.now(
|
|
972
|
+
datetime.timezone.utc,
|
|
973
|
+
).strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
|
974
|
+
)
|
|
975
|
+
call_timer = Timer(
|
|
976
|
+
id=request_id,
|
|
977
|
+
timestamp=datetime.datetime.now(
|
|
978
|
+
datetime.timezone.utc,
|
|
979
|
+
).strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
|
980
|
+
)
|
|
981
|
+
|
|
827
982
|
assert scope['type'] == 'http'
|
|
828
983
|
|
|
829
984
|
method = scope['method']
|
|
@@ -838,6 +993,9 @@ class Application(object):
|
|
|
838
993
|
func_name = headers.get(b's2-ef-name', b'')
|
|
839
994
|
func_endpoint = self.endpoints.get(func_name)
|
|
840
995
|
|
|
996
|
+
timer.metadata['function'] = func_name.decode('utf-8') if func_name else ''
|
|
997
|
+
call_timer.metadata['function'] = timer.metadata['function']
|
|
998
|
+
|
|
841
999
|
func = None
|
|
842
1000
|
func_info: Dict[str, Any] = {}
|
|
843
1001
|
if func_endpoint is not None:
|
|
@@ -845,35 +1003,120 @@ class Application(object):
|
|
|
845
1003
|
|
|
846
1004
|
# Call the endpoint
|
|
847
1005
|
if method == 'POST' and func is not None and path == self.invoke_path:
|
|
1006
|
+
|
|
1007
|
+
logger.info(
|
|
1008
|
+
json.dumps({
|
|
1009
|
+
'type': 'function_call',
|
|
1010
|
+
'id': request_id,
|
|
1011
|
+
'name': func_name.decode('utf-8'),
|
|
1012
|
+
'content_type': content_type.decode('utf-8'),
|
|
1013
|
+
'accepts': accepts.decode('utf-8'),
|
|
1014
|
+
}),
|
|
1015
|
+
)
|
|
1016
|
+
|
|
848
1017
|
args_data_format = func_info['args_data_format']
|
|
849
1018
|
returns_data_format = func_info['returns_data_format']
|
|
850
1019
|
data = []
|
|
851
1020
|
more_body = True
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1021
|
+
with timer('receive_data'):
|
|
1022
|
+
while more_body:
|
|
1023
|
+
request = await receive()
|
|
1024
|
+
if request['type'] == 'http.disconnect':
|
|
1025
|
+
raise RuntimeError('client disconnected')
|
|
1026
|
+
data.append(request['body'])
|
|
1027
|
+
more_body = request.get('more_body', False)
|
|
856
1028
|
|
|
857
1029
|
data_version = headers.get(b's2-ef-version', b'')
|
|
858
1030
|
input_handler = self.handlers[(content_type, data_version, args_data_format)]
|
|
859
1031
|
output_handler = self.handlers[(accepts, data_version, returns_data_format)]
|
|
860
1032
|
|
|
861
1033
|
try:
|
|
862
|
-
|
|
863
|
-
|
|
1034
|
+
all_tasks = []
|
|
1035
|
+
result = []
|
|
1036
|
+
|
|
1037
|
+
cancel_event = threading.Event()
|
|
1038
|
+
|
|
1039
|
+
with timer('parse_input'):
|
|
1040
|
+
inputs = input_handler['load']( # type: ignore
|
|
864
1041
|
func_info['colspec'], b''.join(data),
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
func_task = asyncio.create_task(
|
|
1045
|
+
func(cancel_event, call_timer, *inputs)
|
|
1046
|
+
if func_info['is_async']
|
|
1047
|
+
else to_thread(
|
|
1048
|
+
lambda: asyncio.run(
|
|
1049
|
+
func(cancel_event, call_timer, *inputs),
|
|
1050
|
+
),
|
|
865
1051
|
),
|
|
866
1052
|
)
|
|
867
|
-
|
|
868
|
-
|
|
1053
|
+
disconnect_task = asyncio.create_task(
|
|
1054
|
+
cancel_on_disconnect(receive),
|
|
869
1055
|
)
|
|
1056
|
+
timeout_task = asyncio.create_task(
|
|
1057
|
+
cancel_on_timeout(func_info['timeout']),
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
all_tasks += [func_task, disconnect_task, timeout_task]
|
|
1061
|
+
|
|
1062
|
+
async with timer('function_wrapper'):
|
|
1063
|
+
done, pending = await asyncio.wait(
|
|
1064
|
+
all_tasks, return_when=asyncio.FIRST_COMPLETED,
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
await cancel_all_tasks(pending)
|
|
1068
|
+
|
|
1069
|
+
for task in done:
|
|
1070
|
+
if task is disconnect_task:
|
|
1071
|
+
cancel_event.set()
|
|
1072
|
+
raise asyncio.CancelledError(
|
|
1073
|
+
'Function call was cancelled by client disconnect',
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
elif task is timeout_task:
|
|
1077
|
+
cancel_event.set()
|
|
1078
|
+
raise asyncio.TimeoutError(
|
|
1079
|
+
'Function call was cancelled due to timeout',
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
elif task is func_task:
|
|
1083
|
+
result.extend(task.result())
|
|
1084
|
+
|
|
1085
|
+
with timer('format_output'):
|
|
1086
|
+
body = output_handler['dump'](
|
|
1087
|
+
[x[1] for x in func_info['returns']], *result, # type: ignore
|
|
1088
|
+
)
|
|
1089
|
+
|
|
870
1090
|
await send(output_handler['response'])
|
|
871
1091
|
|
|
1092
|
+
except asyncio.TimeoutError:
|
|
1093
|
+
logging.exception(
|
|
1094
|
+
'Timeout in function call: ' + func_name.decode('utf-8'),
|
|
1095
|
+
)
|
|
1096
|
+
body = (
|
|
1097
|
+
'[TimeoutError] Function call timed out after ' +
|
|
1098
|
+
str(func_info['timeout']) +
|
|
1099
|
+
' seconds'
|
|
1100
|
+
).encode('utf-8')
|
|
1101
|
+
await send(self.error_response_dict)
|
|
1102
|
+
|
|
1103
|
+
except asyncio.CancelledError:
|
|
1104
|
+
logging.exception(
|
|
1105
|
+
'Function call cancelled: ' + func_name.decode('utf-8'),
|
|
1106
|
+
)
|
|
1107
|
+
body = b'[CancelledError] Function call was cancelled'
|
|
1108
|
+
await send(self.error_response_dict)
|
|
1109
|
+
|
|
872
1110
|
except Exception as e:
|
|
873
|
-
logging.exception(
|
|
1111
|
+
logging.exception(
|
|
1112
|
+
'Error in function call: ' + func_name.decode('utf-8'),
|
|
1113
|
+
)
|
|
874
1114
|
body = f'[{type(e).__name__}] {str(e).strip()}'.encode('utf-8')
|
|
875
1115
|
await send(self.error_response_dict)
|
|
876
1116
|
|
|
1117
|
+
finally:
|
|
1118
|
+
await cancel_all_tasks(all_tasks)
|
|
1119
|
+
|
|
877
1120
|
# Handle api reflection
|
|
878
1121
|
elif method == 'GET' and path == self.show_create_function_path:
|
|
879
1122
|
host = headers.get(b'host', b'localhost:80')
|
|
@@ -905,9 +1148,15 @@ class Application(object):
|
|
|
905
1148
|
await send(self.path_not_found_response_dict)
|
|
906
1149
|
|
|
907
1150
|
# Send body
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1151
|
+
with timer('send_response'):
|
|
1152
|
+
out = self.body_response_dict.copy()
|
|
1153
|
+
out['body'] = body
|
|
1154
|
+
await send(out)
|
|
1155
|
+
|
|
1156
|
+
for k, v in call_timer.metrics.items():
|
|
1157
|
+
timer.metrics[k] = v
|
|
1158
|
+
|
|
1159
|
+
timer.finish()
|
|
911
1160
|
|
|
912
1161
|
def _create_link(
|
|
913
1162
|
self,
|
|
@@ -964,6 +1213,13 @@ class Application(object):
|
|
|
964
1213
|
functions = {}
|
|
965
1214
|
no_default = object()
|
|
966
1215
|
|
|
1216
|
+
# Generate CREATE FUNCTION SQL for each function using get_create_functions
|
|
1217
|
+
create_sqls = self.get_create_functions(replace=True)
|
|
1218
|
+
sql_map = {}
|
|
1219
|
+
for (_, info), sql in zip(self.endpoints.values(), create_sqls):
|
|
1220
|
+
sig = info['signature']
|
|
1221
|
+
sql_map[sig['name']] = sql
|
|
1222
|
+
|
|
967
1223
|
for key, (_, info) in self.endpoints.items():
|
|
968
1224
|
if not func_name or key == func_name:
|
|
969
1225
|
sig = info['signature']
|
|
@@ -1001,8 +1257,12 @@ class Application(object):
|
|
|
1001
1257
|
if a.get('default', no_default) is not no_default:
|
|
1002
1258
|
returns[-1]['default'] = a['default']
|
|
1003
1259
|
|
|
1260
|
+
sql = sql_map.get(sig['name'], '')
|
|
1004
1261
|
functions[sig['name']] = dict(
|
|
1005
|
-
args=args,
|
|
1262
|
+
args=args,
|
|
1263
|
+
returns=returns,
|
|
1264
|
+
function_type=info['function_type'],
|
|
1265
|
+
sql_statement=sql,
|
|
1006
1266
|
)
|
|
1007
1267
|
|
|
1008
1268
|
return functions
|