singlestoredb 1.14.1__cp38-abi3-win32.whl → 1.15.0__cp38-abi3-win32.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.

Files changed (30) hide show
  1. _singlestoredb_accel.pyd +0 -0
  2. singlestoredb/__init__.py +14 -10
  3. singlestoredb/apps/_python_udfs.py +3 -3
  4. singlestoredb/config.py +5 -0
  5. singlestoredb/functions/decorator.py +32 -13
  6. singlestoredb/functions/ext/asgi.py +287 -27
  7. singlestoredb/functions/ext/timer.py +98 -0
  8. singlestoredb/functions/typing/numpy.py +20 -0
  9. singlestoredb/functions/typing/pandas.py +2 -0
  10. singlestoredb/functions/typing/polars.py +2 -0
  11. singlestoredb/functions/typing/pyarrow.py +2 -0
  12. singlestoredb/fusion/handler.py +17 -4
  13. singlestoredb/magics/run_personal.py +82 -1
  14. singlestoredb/magics/run_shared.py +82 -1
  15. singlestoredb/management/__init__.py +1 -0
  16. singlestoredb/management/export.py +1 -1
  17. singlestoredb/management/region.py +92 -0
  18. singlestoredb/management/workspace.py +180 -1
  19. singlestoredb/tests/ext_funcs/__init__.py +94 -55
  20. singlestoredb/tests/test.sql +22 -0
  21. singlestoredb/tests/test_ext_func.py +90 -0
  22. singlestoredb/tests/test_fusion.py +4 -1
  23. singlestoredb/tests/test_management.py +253 -20
  24. {singlestoredb-1.14.1.dist-info → singlestoredb-1.15.0.dist-info}/METADATA +3 -2
  25. {singlestoredb-1.14.1.dist-info → singlestoredb-1.15.0.dist-info}/RECORD +30 -25
  26. /singlestoredb/functions/{typing.py → typing/__init__.py} +0 -0
  27. {singlestoredb-1.14.1.dist-info → singlestoredb-1.15.0.dist-info}/LICENSE +0 -0
  28. {singlestoredb-1.14.1.dist-info → singlestoredb-1.15.0.dist-info}/WHEEL +0 -0
  29. {singlestoredb-1.14.1.dist-info → singlestoredb-1.15.0.dist-info}/entry_points.txt +0 -0
  30. {singlestoredb-1.14.1.dist-info → singlestoredb-1.15.0.dist-info}/top_level.txt +0 -0
_singlestoredb_accel.pyd CHANGED
Binary file
singlestoredb/__init__.py CHANGED
@@ -13,7 +13,7 @@ Examples
13
13
 
14
14
  """
15
15
 
16
- __version__ = '1.14.1'
16
+ __version__ = '1.15.0'
17
17
 
18
18
  from typing import Any
19
19
 
@@ -25,20 +25,24 @@ 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,
32
32
  Binary, STRING, BINARY, NUMBER, DATETIME, ROWID,
33
33
  )
34
- from .vectorstore import (
35
- vector_db, IndexInterface, IndexList, IndexModel, MatchTypedDict,
36
- Metric, IndexStatsTypedDict, NamespaceStatsTypedDict, Vector,
37
- VectorDictMetadataValue, VectorMetadataTypedDict, VectorTuple,
38
- VectorTupleWithMetadata, DeletionProtection, AndFilter, EqFilter,
39
- ExactMatchFilter, FilterTypedDict, GteFilter, GtFilter, InFilter,
40
- LteFilter, LtFilter, NeFilter, NinFilter, OrFilter, SimpleFilter,
41
- )
34
+ # These are only loaded if the singlestore-vectorstore package is available
35
+ try:
36
+ from .vectorstore import (
37
+ vector_db, IndexInterface, IndexList, IndexModel, MatchTypedDict,
38
+ Metric, IndexStatsTypedDict, NamespaceStatsTypedDict, Vector,
39
+ VectorDictMetadataValue, VectorMetadataTypedDict, VectorTuple,
40
+ VectorTupleWithMetadata, DeletionProtection, AndFilter, EqFilter,
41
+ ExactMatchFilter, FilterTypedDict, GteFilter, GtFilter, InFilter,
42
+ LteFilter, LtFilter, NeFilter, NinFilter, OrFilter, SimpleFilter,
43
+ )
44
+ except (ImportError, ModuleNotFoundError):
45
+ pass
42
46
 
43
47
 
44
48
  #
@@ -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
- app.register_functions(replace=replace_existing)
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
- ) -> Callable[..., Any]:
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: Callable[..., Any]) -> Callable[..., Any]:
122
+ def decorate(func: UDFType) -> UDFType:
119
123
 
120
- def wrapper(*args: Any, **kwargs: Any) -> Callable[..., Any]:
121
- return func(*args, **kwargs) # type: ignore
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
- wrapper._singlestoredb_attrs = _singlestoredb_attrs # type: ignore
124
-
125
- return functools.wraps(func)(wrapper)
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
- def wrapper(*args: Any, **kwargs: Any) -> Callable[..., Any]:
130
- return func(*args, **kwargs) # type: ignore
131
-
132
- wrapper._singlestoredb_attrs = _singlestoredb_attrs # type: ignore
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
- return functools.wraps(func)(wrapper)
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
- ) -> Callable[..., Any]:
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
- return row_ids, [as_tuple(x) for x in zip(func_map(func, rows))]
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
- if cols and cols[0]:
323
- out = func(*[x if m else x[0] for x, m in zip(cols, masks)])
324
- else:
325
- out = func()
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
- for i, res in zip(row_ids, func_map(func, rows)):
372
- out.extend(as_list_of_tuples(res))
373
- out_ids.extend([row_ids[i]] * (len(out)-len(out_ids)))
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
- if cols and cols[0]:
418
- res = get_dataframe_columns(
419
- func(*[x if m else x[0] for x, m in zip(cols, masks)]),
420
- )
421
- else:
422
- res = get_dataframe_columns(func())
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
- while more_body:
853
- request = await receive()
854
- data.append(request['body'])
855
- more_body = request.get('more_body', False)
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
- out = await func(
863
- *input_handler['load']( # type: ignore
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
- body = output_handler['dump'](
868
- [x[1] for x in func_info['returns']], *out, # type: ignore
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('Error in function call')
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
- out = self.body_response_dict.copy()
909
- out['body'] = body
910
- await send(out)
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, returns=returns, function_type=info['function_type'],
1262
+ args=args,
1263
+ returns=returns,
1264
+ function_type=info['function_type'],
1265
+ sql_statement=sql,
1006
1266
  )
1007
1267
 
1008
1268
  return functions