pixeltable 0.3.14__py3-none-any.whl → 0.5.7__py3-none-any.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.
- pixeltable/__init__.py +42 -8
- pixeltable/{dataframe.py → _query.py} +470 -206
- pixeltable/_version.py +1 -0
- pixeltable/catalog/__init__.py +5 -4
- pixeltable/catalog/catalog.py +1785 -432
- pixeltable/catalog/column.py +190 -113
- pixeltable/catalog/dir.py +2 -4
- pixeltable/catalog/globals.py +19 -46
- pixeltable/catalog/insertable_table.py +191 -98
- pixeltable/catalog/path.py +63 -23
- pixeltable/catalog/schema_object.py +11 -15
- pixeltable/catalog/table.py +843 -436
- pixeltable/catalog/table_metadata.py +103 -0
- pixeltable/catalog/table_version.py +978 -657
- pixeltable/catalog/table_version_handle.py +72 -16
- pixeltable/catalog/table_version_path.py +112 -43
- pixeltable/catalog/tbl_ops.py +53 -0
- pixeltable/catalog/update_status.py +191 -0
- pixeltable/catalog/view.py +134 -90
- pixeltable/config.py +134 -22
- pixeltable/env.py +471 -157
- pixeltable/exceptions.py +6 -0
- pixeltable/exec/__init__.py +4 -1
- pixeltable/exec/aggregation_node.py +7 -8
- pixeltable/exec/cache_prefetch_node.py +83 -110
- pixeltable/exec/cell_materialization_node.py +268 -0
- pixeltable/exec/cell_reconstruction_node.py +168 -0
- pixeltable/exec/component_iteration_node.py +4 -3
- pixeltable/exec/data_row_batch.py +8 -65
- pixeltable/exec/exec_context.py +16 -4
- pixeltable/exec/exec_node.py +13 -36
- pixeltable/exec/expr_eval/evaluators.py +11 -7
- pixeltable/exec/expr_eval/expr_eval_node.py +27 -12
- pixeltable/exec/expr_eval/globals.py +8 -5
- pixeltable/exec/expr_eval/row_buffer.py +1 -2
- pixeltable/exec/expr_eval/schedulers.py +106 -56
- pixeltable/exec/globals.py +35 -0
- pixeltable/exec/in_memory_data_node.py +19 -19
- pixeltable/exec/object_store_save_node.py +293 -0
- pixeltable/exec/row_update_node.py +16 -9
- pixeltable/exec/sql_node.py +351 -84
- pixeltable/exprs/__init__.py +1 -1
- pixeltable/exprs/arithmetic_expr.py +27 -22
- pixeltable/exprs/array_slice.py +3 -3
- pixeltable/exprs/column_property_ref.py +36 -23
- pixeltable/exprs/column_ref.py +213 -89
- pixeltable/exprs/comparison.py +5 -5
- pixeltable/exprs/compound_predicate.py +5 -4
- pixeltable/exprs/data_row.py +164 -54
- pixeltable/exprs/expr.py +70 -44
- pixeltable/exprs/expr_dict.py +3 -3
- pixeltable/exprs/expr_set.py +17 -10
- pixeltable/exprs/function_call.py +100 -40
- pixeltable/exprs/globals.py +2 -2
- pixeltable/exprs/in_predicate.py +4 -4
- pixeltable/exprs/inline_expr.py +18 -32
- pixeltable/exprs/is_null.py +7 -3
- pixeltable/exprs/json_mapper.py +8 -8
- pixeltable/exprs/json_path.py +56 -22
- pixeltable/exprs/literal.py +27 -5
- pixeltable/exprs/method_ref.py +2 -2
- pixeltable/exprs/object_ref.py +2 -2
- pixeltable/exprs/row_builder.py +167 -67
- pixeltable/exprs/rowid_ref.py +25 -10
- pixeltable/exprs/similarity_expr.py +58 -40
- pixeltable/exprs/sql_element_cache.py +4 -4
- pixeltable/exprs/string_op.py +5 -5
- pixeltable/exprs/type_cast.py +3 -5
- pixeltable/func/__init__.py +1 -0
- pixeltable/func/aggregate_function.py +8 -8
- pixeltable/func/callable_function.py +9 -9
- pixeltable/func/expr_template_function.py +17 -11
- pixeltable/func/function.py +18 -20
- pixeltable/func/function_registry.py +6 -7
- pixeltable/func/globals.py +2 -3
- pixeltable/func/mcp.py +74 -0
- pixeltable/func/query_template_function.py +29 -27
- pixeltable/func/signature.py +46 -19
- pixeltable/func/tools.py +31 -13
- pixeltable/func/udf.py +18 -20
- pixeltable/functions/__init__.py +16 -0
- pixeltable/functions/anthropic.py +123 -77
- pixeltable/functions/audio.py +147 -10
- pixeltable/functions/bedrock.py +13 -6
- pixeltable/functions/date.py +7 -4
- pixeltable/functions/deepseek.py +35 -43
- pixeltable/functions/document.py +81 -0
- pixeltable/functions/fal.py +76 -0
- pixeltable/functions/fireworks.py +11 -20
- pixeltable/functions/gemini.py +195 -39
- pixeltable/functions/globals.py +142 -14
- pixeltable/functions/groq.py +108 -0
- pixeltable/functions/huggingface.py +1056 -24
- pixeltable/functions/image.py +115 -57
- pixeltable/functions/json.py +1 -1
- pixeltable/functions/llama_cpp.py +28 -13
- pixeltable/functions/math.py +67 -5
- pixeltable/functions/mistralai.py +18 -55
- pixeltable/functions/net.py +70 -0
- pixeltable/functions/ollama.py +20 -13
- pixeltable/functions/openai.py +240 -226
- pixeltable/functions/openrouter.py +143 -0
- pixeltable/functions/replicate.py +4 -4
- pixeltable/functions/reve.py +250 -0
- pixeltable/functions/string.py +239 -69
- pixeltable/functions/timestamp.py +16 -16
- pixeltable/functions/together.py +24 -84
- pixeltable/functions/twelvelabs.py +188 -0
- pixeltable/functions/util.py +6 -1
- pixeltable/functions/uuid.py +30 -0
- pixeltable/functions/video.py +1515 -107
- pixeltable/functions/vision.py +8 -8
- pixeltable/functions/voyageai.py +289 -0
- pixeltable/functions/whisper.py +16 -8
- pixeltable/functions/whisperx.py +179 -0
- pixeltable/{ext/functions → functions}/yolox.py +2 -4
- pixeltable/globals.py +362 -115
- pixeltable/index/base.py +17 -21
- pixeltable/index/btree.py +28 -22
- pixeltable/index/embedding_index.py +100 -118
- pixeltable/io/__init__.py +4 -2
- pixeltable/io/datarows.py +8 -7
- pixeltable/io/external_store.py +56 -105
- pixeltable/io/fiftyone.py +13 -13
- pixeltable/io/globals.py +31 -30
- pixeltable/io/hf_datasets.py +61 -16
- pixeltable/io/label_studio.py +74 -70
- pixeltable/io/lancedb.py +3 -0
- pixeltable/io/pandas.py +21 -12
- pixeltable/io/parquet.py +25 -105
- pixeltable/io/table_data_conduit.py +250 -123
- pixeltable/io/utils.py +4 -4
- pixeltable/iterators/__init__.py +2 -1
- pixeltable/iterators/audio.py +26 -25
- pixeltable/iterators/base.py +9 -3
- pixeltable/iterators/document.py +112 -78
- pixeltable/iterators/image.py +12 -15
- pixeltable/iterators/string.py +11 -4
- pixeltable/iterators/video.py +523 -120
- pixeltable/metadata/__init__.py +14 -3
- pixeltable/metadata/converters/convert_13.py +2 -2
- pixeltable/metadata/converters/convert_18.py +2 -2
- pixeltable/metadata/converters/convert_19.py +2 -2
- pixeltable/metadata/converters/convert_20.py +2 -2
- pixeltable/metadata/converters/convert_21.py +2 -2
- pixeltable/metadata/converters/convert_22.py +2 -2
- pixeltable/metadata/converters/convert_24.py +2 -2
- pixeltable/metadata/converters/convert_25.py +2 -2
- pixeltable/metadata/converters/convert_26.py +2 -2
- pixeltable/metadata/converters/convert_29.py +4 -4
- pixeltable/metadata/converters/convert_30.py +34 -21
- pixeltable/metadata/converters/convert_34.py +2 -2
- pixeltable/metadata/converters/convert_35.py +9 -0
- pixeltable/metadata/converters/convert_36.py +38 -0
- pixeltable/metadata/converters/convert_37.py +15 -0
- pixeltable/metadata/converters/convert_38.py +39 -0
- pixeltable/metadata/converters/convert_39.py +124 -0
- pixeltable/metadata/converters/convert_40.py +73 -0
- pixeltable/metadata/converters/convert_41.py +12 -0
- pixeltable/metadata/converters/convert_42.py +9 -0
- pixeltable/metadata/converters/convert_43.py +44 -0
- pixeltable/metadata/converters/util.py +20 -31
- pixeltable/metadata/notes.py +9 -0
- pixeltable/metadata/schema.py +140 -53
- pixeltable/metadata/utils.py +74 -0
- pixeltable/mypy/__init__.py +3 -0
- pixeltable/mypy/mypy_plugin.py +123 -0
- pixeltable/plan.py +382 -115
- pixeltable/share/__init__.py +1 -1
- pixeltable/share/packager.py +547 -83
- pixeltable/share/protocol/__init__.py +33 -0
- pixeltable/share/protocol/common.py +165 -0
- pixeltable/share/protocol/operation_types.py +33 -0
- pixeltable/share/protocol/replica.py +119 -0
- pixeltable/share/publish.py +257 -59
- pixeltable/store.py +311 -194
- pixeltable/type_system.py +373 -211
- pixeltable/utils/__init__.py +2 -3
- pixeltable/utils/arrow.py +131 -17
- pixeltable/utils/av.py +298 -0
- pixeltable/utils/azure_store.py +346 -0
- pixeltable/utils/coco.py +6 -6
- pixeltable/utils/code.py +3 -3
- pixeltable/utils/console_output.py +4 -1
- pixeltable/utils/coroutine.py +6 -23
- pixeltable/utils/dbms.py +32 -6
- pixeltable/utils/description_helper.py +4 -5
- pixeltable/utils/documents.py +7 -18
- pixeltable/utils/exception_handler.py +7 -30
- pixeltable/utils/filecache.py +6 -6
- pixeltable/utils/formatter.py +86 -48
- pixeltable/utils/gcs_store.py +295 -0
- pixeltable/utils/http.py +133 -0
- pixeltable/utils/http_server.py +2 -3
- pixeltable/utils/iceberg.py +1 -2
- pixeltable/utils/image.py +17 -0
- pixeltable/utils/lancedb.py +90 -0
- pixeltable/utils/local_store.py +322 -0
- pixeltable/utils/misc.py +5 -0
- pixeltable/utils/object_stores.py +573 -0
- pixeltable/utils/pydantic.py +60 -0
- pixeltable/utils/pytorch.py +5 -6
- pixeltable/utils/s3_store.py +527 -0
- pixeltable/utils/sql.py +26 -0
- pixeltable/utils/system.py +30 -0
- pixeltable-0.5.7.dist-info/METADATA +579 -0
- pixeltable-0.5.7.dist-info/RECORD +227 -0
- {pixeltable-0.3.14.dist-info → pixeltable-0.5.7.dist-info}/WHEEL +1 -1
- pixeltable-0.5.7.dist-info/entry_points.txt +2 -0
- pixeltable/__version__.py +0 -3
- pixeltable/catalog/named_function.py +0 -40
- pixeltable/ext/__init__.py +0 -17
- pixeltable/ext/functions/__init__.py +0 -11
- pixeltable/ext/functions/whisperx.py +0 -77
- pixeltable/utils/media_store.py +0 -77
- pixeltable/utils/s3.py +0 -17
- pixeltable-0.3.14.dist-info/METADATA +0 -434
- pixeltable-0.3.14.dist-info/RECORD +0 -186
- pixeltable-0.3.14.dist-info/entry_points.txt +0 -3
- {pixeltable-0.3.14.dist-info → pixeltable-0.5.7.dist-info/licenses}/LICENSE +0 -0
|
@@ -4,17 +4,21 @@ import asyncio
|
|
|
4
4
|
import datetime
|
|
5
5
|
import inspect
|
|
6
6
|
import logging
|
|
7
|
+
import math
|
|
7
8
|
import sys
|
|
8
9
|
import time
|
|
9
|
-
from typing import Awaitable, Collection
|
|
10
|
+
from typing import Awaitable, Collection
|
|
10
11
|
|
|
11
12
|
from pixeltable import env, func
|
|
12
13
|
from pixeltable.config import Config
|
|
14
|
+
from pixeltable.utils.http import exponential_backoff, is_retriable_error
|
|
13
15
|
|
|
14
16
|
from .globals import Dispatcher, ExecCtx, FnCallArgs, Scheduler
|
|
15
17
|
|
|
16
18
|
_logger = logging.getLogger('pixeltable')
|
|
17
19
|
|
|
20
|
+
__all__ = ['RateLimitsScheduler', 'RequestRateScheduler']
|
|
21
|
+
|
|
18
22
|
|
|
19
23
|
class RateLimitsScheduler(Scheduler):
|
|
20
24
|
"""
|
|
@@ -34,7 +38,7 @@ class RateLimitsScheduler(Scheduler):
|
|
|
34
38
|
get_request_resources_param_names: list[str] # names of parameters of RateLimitsInfo.get_request_resources()
|
|
35
39
|
|
|
36
40
|
# scheduling-related state
|
|
37
|
-
pool_info:
|
|
41
|
+
pool_info: env.RateLimitsInfo | None
|
|
38
42
|
est_usage: dict[str, int] # value per resource; accumulated estimates since the last util. report
|
|
39
43
|
|
|
40
44
|
num_in_flight: int # unfinished tasks
|
|
@@ -76,14 +80,15 @@ class RateLimitsScheduler(Scheduler):
|
|
|
76
80
|
self.est_usage = dict.fromkeys(self._resources, 0)
|
|
77
81
|
|
|
78
82
|
async def _main_loop(self) -> None:
|
|
79
|
-
item:
|
|
83
|
+
item: RateLimitsScheduler.QueueItem | None = None
|
|
80
84
|
while True:
|
|
81
85
|
if item is None:
|
|
82
86
|
item = await self.queue.get()
|
|
87
|
+
assert isinstance(item.request.fn_call.fn, func.CallableFunction)
|
|
88
|
+
assert '_runtime_ctx' in item.request.fn_call.fn.signature.system_parameters
|
|
83
89
|
if item.num_retries > 0:
|
|
84
90
|
self.total_retried += 1
|
|
85
91
|
|
|
86
|
-
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
87
92
|
if self.pool_info is None or not self.pool_info.is_initialized():
|
|
88
93
|
# wait for a single request to get rate limits
|
|
89
94
|
_logger.debug(f'initializing rate limits for {self.resource_pool}')
|
|
@@ -96,14 +101,13 @@ class RateLimitsScheduler(Scheduler):
|
|
|
96
101
|
continue
|
|
97
102
|
|
|
98
103
|
# check rate limits
|
|
99
|
-
_logger.debug(f'checking rate limits for {self.resource_pool}')
|
|
100
104
|
request_resources = self._get_request_resources(item.request)
|
|
101
|
-
|
|
105
|
+
resource_delay = self._resource_delay(request_resources)
|
|
102
106
|
aws: list[Awaitable[None]] = []
|
|
103
|
-
completed_aw:
|
|
104
|
-
wait_for_reset:
|
|
105
|
-
if
|
|
106
|
-
#
|
|
107
|
+
completed_aw: asyncio.Task | None = None
|
|
108
|
+
wait_for_reset: asyncio.Task | None = None
|
|
109
|
+
if resource_delay > 0:
|
|
110
|
+
# Some resource or resources are nearing depletion
|
|
107
111
|
|
|
108
112
|
if self.num_in_flight > 0:
|
|
109
113
|
# a completed request can free up capacity
|
|
@@ -112,12 +116,10 @@ class RateLimitsScheduler(Scheduler):
|
|
|
112
116
|
aws.append(completed_aw)
|
|
113
117
|
_logger.debug(f'waiting for completed request for {self.resource_pool}')
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
aws.append(wait_for_reset)
|
|
120
|
-
_logger.debug(f'waiting for rate limit reset for {self.resource_pool}')
|
|
119
|
+
# Schedule a sleep until sufficient resources are available
|
|
120
|
+
wait_for_reset = asyncio.create_task(asyncio.sleep(resource_delay))
|
|
121
|
+
aws.append(wait_for_reset)
|
|
122
|
+
_logger.debug(f'waiting {resource_delay:.1f}s for resource availability')
|
|
121
123
|
|
|
122
124
|
if len(aws) > 0:
|
|
123
125
|
# we have something to wait for
|
|
@@ -126,16 +128,12 @@ class RateLimitsScheduler(Scheduler):
|
|
|
126
128
|
task.cancel()
|
|
127
129
|
if completed_aw in done:
|
|
128
130
|
_logger.debug(f'wait(): completed request for {self.resource_pool}')
|
|
129
|
-
if wait_for_reset in done:
|
|
130
|
-
_logger.debug(f'wait(): rate limit reset for {self.resource_pool}')
|
|
131
|
-
# force waiting for another rate limit report before making any scheduling decisions
|
|
132
|
-
self.pool_info.reset()
|
|
133
131
|
# re-evaluate current capacity for current item
|
|
134
132
|
continue
|
|
135
133
|
|
|
136
134
|
# we have a new in-flight request
|
|
137
135
|
for resource, val in request_resources.items():
|
|
138
|
-
self.est_usage[resource]
|
|
136
|
+
self.est_usage[resource] = self.est_usage.get(resource, 0) + val
|
|
139
137
|
_logger.debug(f'creating task for {self.resource_pool}')
|
|
140
138
|
self.num_in_flight += 1
|
|
141
139
|
task = asyncio.create_task(self._exec(item.request, item.exec_ctx, item.num_retries, is_task=True))
|
|
@@ -155,25 +153,30 @@ class RateLimitsScheduler(Scheduler):
|
|
|
155
153
|
constant_kwargs, batch_kwargs = request.pxt_fn.create_batch_kwargs(batch_kwargs)
|
|
156
154
|
return self.pool_info.get_request_resources(**constant_kwargs, **batch_kwargs)
|
|
157
155
|
|
|
158
|
-
def
|
|
159
|
-
"""
|
|
160
|
-
|
|
156
|
+
def _resource_delay(self, request_resources: dict[str, int]) -> float:
|
|
157
|
+
"""For the provided resources and usage, attempts to estimate the time to wait until sufficient resources are
|
|
158
|
+
available."""
|
|
159
|
+
highest_wait = 0.0
|
|
160
|
+
highest_wait_resource = None
|
|
161
161
|
for resource, usage in request_resources.items():
|
|
162
|
-
# 0.05: leave some headroom, we don't have perfect information
|
|
163
162
|
info = self.pool_info.resource_limits[resource]
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
163
|
+
# Note: usage and est_usage are estimated costs of requests, and it may be way off (for example, if max
|
|
164
|
+
# tokens is unspecified for an openAI request).
|
|
165
|
+
time_until = info.estimated_resource_refill_delay(
|
|
166
|
+
math.ceil(info.limit * env.TARGET_RATE_LIMIT_RESOURCE_FRACT + usage + self.est_usage.get(resource, 0))
|
|
167
|
+
)
|
|
168
|
+
if time_until is not None and highest_wait < time_until:
|
|
169
|
+
highest_wait = time_until
|
|
170
|
+
highest_wait_resource = resource
|
|
171
|
+
_logger.debug(f'Determined wait time of {highest_wait:.1f}s for resource {highest_wait_resource}')
|
|
172
|
+
return highest_wait
|
|
170
173
|
|
|
171
174
|
async def _exec(self, request: FnCallArgs, exec_ctx: ExecCtx, num_retries: int, is_task: bool) -> None:
|
|
172
175
|
assert all(not row.has_val[request.fn_call.slot_idx] for row in request.rows)
|
|
173
176
|
assert all(not row.has_exc(request.fn_call.slot_idx) for row in request.rows)
|
|
174
177
|
|
|
178
|
+
start_ts = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
175
179
|
try:
|
|
176
|
-
start_ts = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
177
180
|
pxt_fn = request.fn_call.fn
|
|
178
181
|
assert isinstance(pxt_fn, func.CallableFunction)
|
|
179
182
|
_logger.debug(
|
|
@@ -187,7 +190,8 @@ class RateLimitsScheduler(Scheduler):
|
|
|
187
190
|
for row, result in zip(request.rows, batch_result):
|
|
188
191
|
row[request.fn_call.slot_idx] = result
|
|
189
192
|
else:
|
|
190
|
-
|
|
193
|
+
request_kwargs = {**request.kwargs, '_runtime_ctx': env.RuntimeCtx(is_retry=num_retries > 0)}
|
|
194
|
+
result = await pxt_fn.aexec(*request.args, **request_kwargs)
|
|
191
195
|
request.row[request.fn_call.slot_idx] = result
|
|
192
196
|
end_ts = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
193
197
|
_logger.debug(
|
|
@@ -200,20 +204,32 @@ class RateLimitsScheduler(Scheduler):
|
|
|
200
204
|
|
|
201
205
|
self.dispatcher.dispatch(request.rows, exec_ctx)
|
|
202
206
|
except Exception as exc:
|
|
203
|
-
_logger.
|
|
204
|
-
if
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
207
|
+
_logger.exception(f'scheduler {self.resource_pool}: exception in slot {request.fn_call.slot_idx}: {exc}')
|
|
208
|
+
if hasattr(exc, 'response') and hasattr(exc.response, 'headers'):
|
|
209
|
+
_logger.debug(f'scheduler {self.resource_pool}: exception headers: {exc.response.headers}')
|
|
210
|
+
|
|
211
|
+
# If pool info is available, attempt to retry based on the resource information
|
|
212
|
+
# Pool info may not be available yet if the exception occurred before the UDF set it
|
|
213
|
+
if self.pool_info is not None:
|
|
214
|
+
self.pool_info.record_exc(start_ts, exc)
|
|
215
|
+
|
|
216
|
+
if num_retries < self.MAX_RETRIES:
|
|
217
|
+
retry_delay = self.pool_info.get_retry_delay(exc, num_retries)
|
|
218
|
+
if retry_delay is None:
|
|
219
|
+
# The resource pool did not recognize it as a retriable error. Try our generic best-effort logic
|
|
220
|
+
# before giving up.
|
|
221
|
+
is_retriable, retry_delay = is_retriable_error(exc)
|
|
222
|
+
if is_retriable:
|
|
223
|
+
retry_delay = retry_delay or exponential_backoff(num_retries)
|
|
224
|
+
if retry_delay is not None:
|
|
225
|
+
self.total_retried += 1
|
|
226
|
+
_logger.debug(
|
|
227
|
+
f'scheduler {self.resource_pool}: sleeping {retry_delay:.2f}s before retrying'
|
|
228
|
+
f' attempt {num_retries} based on the information in the error'
|
|
229
|
+
)
|
|
230
|
+
await asyncio.sleep(retry_delay)
|
|
231
|
+
self.queue.put_nowait(self.QueueItem(request, num_retries + 1, exec_ctx))
|
|
232
|
+
return
|
|
217
233
|
|
|
218
234
|
# record the exception
|
|
219
235
|
_, _, exc_tb = sys.exc_info()
|
|
@@ -248,11 +264,16 @@ class RequestRateScheduler(Scheduler):
|
|
|
248
264
|
num_in_flight: int
|
|
249
265
|
total_requests: int
|
|
250
266
|
total_retried: int
|
|
267
|
+
total_errors: int
|
|
251
268
|
|
|
252
269
|
TIME_FORMAT = '%H:%M.%S %f'
|
|
253
|
-
MAX_RETRIES =
|
|
270
|
+
MAX_RETRIES = 3
|
|
254
271
|
DEFAULT_RATE_LIMIT = 600 # requests per minute
|
|
255
272
|
|
|
273
|
+
# Exponential backoff defaults
|
|
274
|
+
BASE_RETRY_DELAY = 1.0 # in seconds
|
|
275
|
+
MAX_RETRY_DELAY = 60.0 # in seconds
|
|
276
|
+
|
|
256
277
|
def __init__(self, resource_pool: str, dispatcher: Dispatcher):
|
|
257
278
|
super().__init__(resource_pool, dispatcher)
|
|
258
279
|
loop_task = asyncio.create_task(self._main_loop())
|
|
@@ -260,6 +281,7 @@ class RequestRateScheduler(Scheduler):
|
|
|
260
281
|
self.num_in_flight = 0
|
|
261
282
|
self.total_requests = 0
|
|
262
283
|
self.total_retried = 0
|
|
284
|
+
self.total_errors = 0
|
|
263
285
|
|
|
264
286
|
# try to get the rate limit from the config
|
|
265
287
|
elems = resource_pool.split(':')
|
|
@@ -278,6 +300,7 @@ class RequestRateScheduler(Scheduler):
|
|
|
278
300
|
key = model
|
|
279
301
|
requests_per_min = Config.get().get_int_value(key, section=section)
|
|
280
302
|
requests_per_min = requests_per_min or self.DEFAULT_RATE_LIMIT
|
|
303
|
+
_logger.debug(f'rate limit for {self.resource_pool}: {requests_per_min} RPM')
|
|
281
304
|
self.secs_per_request = 1 / (requests_per_min / 60)
|
|
282
305
|
|
|
283
306
|
@classmethod
|
|
@@ -291,8 +314,12 @@ class RequestRateScheduler(Scheduler):
|
|
|
291
314
|
if item.num_retries > 0:
|
|
292
315
|
self.total_retried += 1
|
|
293
316
|
now = time.monotonic()
|
|
317
|
+
wait_duration = 0.0
|
|
318
|
+
if item.retry_after is not None:
|
|
319
|
+
wait_duration = item.retry_after - now
|
|
294
320
|
if now - last_request_ts < self.secs_per_request:
|
|
295
|
-
wait_duration = self.secs_per_request - (now - last_request_ts)
|
|
321
|
+
wait_duration = max(wait_duration, self.secs_per_request - (now - last_request_ts))
|
|
322
|
+
if wait_duration > 0:
|
|
296
323
|
_logger.debug(f'waiting for {wait_duration} for {self.resource_pool}')
|
|
297
324
|
await asyncio.sleep(wait_duration)
|
|
298
325
|
|
|
@@ -337,15 +364,21 @@ class RequestRateScheduler(Scheduler):
|
|
|
337
364
|
self.dispatcher.dispatch(request.rows, exec_ctx)
|
|
338
365
|
|
|
339
366
|
except Exception as exc:
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if num_retries < self.MAX_RETRIES:
|
|
345
|
-
self.
|
|
367
|
+
_logger.exception(f'exception for {self.resource_pool}: type={type(exc)}\n{exc}')
|
|
368
|
+
if hasattr(exc, 'response') and hasattr(exc.response, 'headers'):
|
|
369
|
+
_logger.debug(f'scheduler {self.resource_pool}: exception headers: {exc.response.headers}')
|
|
370
|
+
is_retriable, retry_after = is_retriable_error(exc)
|
|
371
|
+
if is_retriable and num_retries < self.MAX_RETRIES:
|
|
372
|
+
retry_delay = self._compute_retry_delay(num_retries, retry_after)
|
|
373
|
+
_logger.debug(f'scheduler {self.resource_pool}: retrying after {retry_delay}')
|
|
374
|
+
now = time.monotonic()
|
|
375
|
+
# put the request back in the queue right away, which prevents new requests from being generated until
|
|
376
|
+
# this one succeeds or exceeds its retry limit
|
|
377
|
+
self.queue.put_nowait(self.QueueItem(request, num_retries + 1, exec_ctx, retry_after=now + retry_delay))
|
|
346
378
|
return
|
|
347
379
|
|
|
348
380
|
# record the exception
|
|
381
|
+
self.total_errors += 1
|
|
349
382
|
_, _, exc_tb = sys.exc_info()
|
|
350
383
|
for row in request.rows:
|
|
351
384
|
row.set_exc(request.fn_call.slot_idx, exc)
|
|
@@ -353,11 +386,28 @@ class RequestRateScheduler(Scheduler):
|
|
|
353
386
|
finally:
|
|
354
387
|
_logger.debug(
|
|
355
388
|
f'Scheduler stats: #in-flight={self.num_in_flight} #requests={self.total_requests}, '
|
|
356
|
-
f'#retried={self.total_retried}'
|
|
389
|
+
f'#retried={self.total_retried} #errors={self.total_errors}'
|
|
357
390
|
)
|
|
358
391
|
if is_task:
|
|
359
392
|
self.num_in_flight -= 1
|
|
360
393
|
|
|
394
|
+
def _compute_retry_delay(self, num_retries: int, retry_after: float | None = None) -> float:
|
|
395
|
+
"""
|
|
396
|
+
Calculate exponential backoff delay for rate limit errors.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
retry_count: Number of retries attempted (0-based)
|
|
400
|
+
retry_after: Suggested delay from Retry-After header
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Delay in seconds
|
|
404
|
+
"""
|
|
405
|
+
if retry_after is not None and retry_after > 0:
|
|
406
|
+
# Use server-suggested delay, but cap it at max_delay
|
|
407
|
+
return max(min(retry_after, self.MAX_RETRY_DELAY), self.BASE_RETRY_DELAY)
|
|
408
|
+
else:
|
|
409
|
+
return exponential_backoff(num_retries, max_delay=self.MAX_RETRY_DELAY)
|
|
410
|
+
|
|
361
411
|
|
|
362
412
|
# all concrete Scheduler subclasses that implement matches()
|
|
363
413
|
SCHEDULERS = [RateLimitsScheduler, RequestRateScheduler]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
|
|
5
|
+
from pixeltable.exprs import ArrayMd, BinaryMd
|
|
6
|
+
from pixeltable.utils.misc import non_none_dict_factory
|
|
7
|
+
|
|
8
|
+
INLINED_OBJECT_MD_KEY = '__pxtinlinedobjmd__'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclasses.dataclass
|
|
12
|
+
class InlinedObjectMd:
|
|
13
|
+
type: str # corresponds to ts.ColumnType.Type
|
|
14
|
+
url_idx: int
|
|
15
|
+
img_start: int | None = None
|
|
16
|
+
img_end: int | None = None
|
|
17
|
+
array_md: ArrayMd | None = None
|
|
18
|
+
binary_md: BinaryMd | None = None
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_dict(cls, d: dict) -> InlinedObjectMd:
|
|
22
|
+
d = d.copy()
|
|
23
|
+
if 'array_md' in d:
|
|
24
|
+
d['array_md'] = ArrayMd(**d['array_md'])
|
|
25
|
+
if 'binary_md' in d:
|
|
26
|
+
d['binary_md'] = BinaryMd(**d['binary_md'])
|
|
27
|
+
return cls(**d)
|
|
28
|
+
|
|
29
|
+
def as_dict(self) -> dict:
|
|
30
|
+
result = dataclasses.asdict(self, dict_factory=non_none_dict_factory)
|
|
31
|
+
if self.array_md is not None:
|
|
32
|
+
result['array_md'] = self.array_md.as_dict()
|
|
33
|
+
if self.binary_md is not None:
|
|
34
|
+
result['binary_md'] = dataclasses.asdict(self.binary_md)
|
|
35
|
+
return result
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Any, AsyncIterator
|
|
2
|
+
from typing import Any, AsyncIterator
|
|
3
3
|
|
|
4
4
|
from pixeltable import catalog, exprs
|
|
5
|
-
from pixeltable.utils.
|
|
5
|
+
from pixeltable.utils.local_store import TempStore
|
|
6
6
|
|
|
7
7
|
from .data_row_batch import DataRowBatch
|
|
8
8
|
from .exec_node import ExecNode
|
|
@@ -23,7 +23,7 @@ class InMemoryDataNode(ExecNode):
|
|
|
23
23
|
|
|
24
24
|
input_rows: list[dict[str, Any]]
|
|
25
25
|
start_row_id: int
|
|
26
|
-
|
|
26
|
+
output_batch: DataRowBatch | None
|
|
27
27
|
|
|
28
28
|
# output_exprs is declared in the superclass, but we redeclare it here with a more specific type
|
|
29
29
|
output_exprs: list[exprs.ColumnRef]
|
|
@@ -38,11 +38,11 @@ class InMemoryDataNode(ExecNode):
|
|
|
38
38
|
# we materialize the input slots
|
|
39
39
|
output_exprs = list(row_builder.input_exprs)
|
|
40
40
|
super().__init__(row_builder, output_exprs, [], None)
|
|
41
|
-
assert tbl.get().is_insertable
|
|
41
|
+
assert tbl.get().is_insertable
|
|
42
42
|
self.tbl = tbl
|
|
43
43
|
self.input_rows = rows
|
|
44
44
|
self.start_row_id = start_row_id
|
|
45
|
-
self.
|
|
45
|
+
self.output_batch = None
|
|
46
46
|
|
|
47
47
|
def _open(self) -> None:
|
|
48
48
|
"""Create row batch and populate with self.input_rows"""
|
|
@@ -56,22 +56,21 @@ class InMemoryDataNode(ExecNode):
|
|
|
56
56
|
}
|
|
57
57
|
output_slot_idxs = {e.slot_idx for e in self.output_exprs}
|
|
58
58
|
|
|
59
|
-
self.
|
|
60
|
-
for
|
|
59
|
+
self.output_batch = DataRowBatch(self.row_builder)
|
|
60
|
+
for input_row in self.input_rows:
|
|
61
|
+
output_row = self.row_builder.make_row()
|
|
61
62
|
# populate the output row with the values provided in the input row
|
|
62
63
|
input_slot_idxs: set[int] = set()
|
|
63
64
|
for col_name, val in input_row.items():
|
|
64
65
|
col_info = user_cols_by_name.get(col_name)
|
|
65
66
|
assert col_info is not None
|
|
66
|
-
|
|
67
|
-
if
|
|
68
|
-
# this is a literal
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
fp.write(val)
|
|
72
|
-
self.output_rows[row_idx][col_info.slot_idx] = path
|
|
67
|
+
col = col_info.col
|
|
68
|
+
if col.col_type.is_image_type() and isinstance(val, bytes):
|
|
69
|
+
# this is a literal media file, ie, a sequence of bytes; save it as a binary file and store the path
|
|
70
|
+
filepath, _ = TempStore.save_media_object(val, col, format=None)
|
|
71
|
+
output_row[col_info.slot_idx] = str(filepath)
|
|
73
72
|
else:
|
|
74
|
-
|
|
73
|
+
output_row[col_info.slot_idx] = val
|
|
75
74
|
|
|
76
75
|
input_slot_idxs.add(col_info.slot_idx)
|
|
77
76
|
|
|
@@ -80,10 +79,11 @@ class InMemoryDataNode(ExecNode):
|
|
|
80
79
|
for slot_idx in missing_slot_idxs:
|
|
81
80
|
col_info = output_cols_by_idx.get(slot_idx)
|
|
82
81
|
assert col_info is not None
|
|
83
|
-
|
|
82
|
+
output_row[col_info.slot_idx] = None
|
|
83
|
+
self.output_batch.add_row(output_row)
|
|
84
84
|
|
|
85
|
-
self.ctx.num_rows = len(self.
|
|
85
|
+
self.ctx.num_rows = len(self.output_batch)
|
|
86
86
|
|
|
87
87
|
async def __aiter__(self) -> AsyncIterator[DataRowBatch]:
|
|
88
|
-
_logger.debug(f'InMemoryDataNode: created row batch with {len(self.
|
|
89
|
-
yield self.
|
|
88
|
+
_logger.debug(f'InMemoryDataNode: created row batch with {len(self.output_batch)} rows')
|
|
89
|
+
yield self.output_batch
|