modal 0.67.9__py3-none-any.whl → 0.67.11__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.
- modal/_runtime/user_code_imports.py +1 -1
- modal/client.pyi +2 -2
- modal/cls.py +14 -15
- modal/cls.pyi +12 -12
- modal/config.py +1 -0
- modal/functions.py +85 -8
- modal/functions.pyi +33 -7
- modal/retries.py +38 -0
- {modal-0.67.9.dist-info → modal-0.67.11.dist-info}/METADATA +1 -1
- {modal-0.67.9.dist-info → modal-0.67.11.dist-info}/RECORD +15 -15
- modal_version/_version_generated.py +1 -1
- {modal-0.67.9.dist-info → modal-0.67.11.dist-info}/LICENSE +0 -0
- {modal-0.67.9.dist-info → modal-0.67.11.dist-info}/WHEEL +0 -0
- {modal-0.67.9.dist-info → modal-0.67.11.dist-info}/entry_points.txt +0 -0
- {modal-0.67.9.dist-info → modal-0.67.11.dist-info}/top_level.txt +0 -0
@@ -193,7 +193,7 @@ def get_user_class_instance(cls: typing.Union[type, modal.cls.Cls], args: tuple,
|
|
193
193
|
if isinstance(cls, modal.cls.Cls):
|
194
194
|
# globally @app.cls-decorated class
|
195
195
|
modal_obj: modal.cls.Obj = cls(*args, **kwargs)
|
196
|
-
modal_obj.
|
196
|
+
modal_obj._entered = True # ugly but prevents .local() from triggering additional enter-logic
|
197
197
|
# TODO: unify lifecycle logic between .local() and container_entrypoint
|
198
198
|
user_cls_instance = modal_obj._cached_user_cls_instance()
|
199
199
|
else:
|
modal/client.pyi
CHANGED
@@ -26,7 +26,7 @@ class _Client:
|
|
26
26
|
_stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
|
27
27
|
|
28
28
|
def __init__(
|
29
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.
|
29
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.11"
|
30
30
|
): ...
|
31
31
|
def is_closed(self) -> bool: ...
|
32
32
|
@property
|
@@ -81,7 +81,7 @@ class Client:
|
|
81
81
|
_stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
|
82
82
|
|
83
83
|
def __init__(
|
84
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.
|
84
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.11"
|
85
85
|
): ...
|
86
86
|
def is_closed(self) -> bool: ...
|
87
87
|
@property
|
modal/cls.py
CHANGED
@@ -78,7 +78,7 @@ class _Obj:
|
|
78
78
|
All this class does is to return `Function` objects."""
|
79
79
|
|
80
80
|
_functions: dict[str, _Function]
|
81
|
-
|
81
|
+
_has_entered: bool
|
82
82
|
_user_cls_instance: Optional[Any] = None
|
83
83
|
_construction_args: tuple[tuple, dict[str, Any]]
|
84
84
|
|
@@ -121,8 +121,7 @@ class _Obj:
|
|
121
121
|
self._method_functions[method_name] = method
|
122
122
|
|
123
123
|
# Used for construction local object lazily
|
124
|
-
self.
|
125
|
-
self._local_user_cls_instance = None
|
124
|
+
self._has_entered = False
|
126
125
|
self._user_cls = user_cls
|
127
126
|
self._construction_args = (args, kwargs) # used for lazy construction in case of explicit constructors
|
128
127
|
|
@@ -176,8 +175,8 @@ class _Obj:
|
|
176
175
|
|
177
176
|
return self._user_cls_instance
|
178
177
|
|
179
|
-
def
|
180
|
-
if not self.
|
178
|
+
def _enter(self):
|
179
|
+
if not self._has_entered:
|
181
180
|
if hasattr(self._user_cls_instance, "__enter__"):
|
182
181
|
self._user_cls_instance.__enter__()
|
183
182
|
|
@@ -188,26 +187,26 @@ class _Obj:
|
|
188
187
|
for enter_method in _find_callables_for_obj(self._user_cls_instance, method_flag).values():
|
189
188
|
enter_method()
|
190
189
|
|
191
|
-
self.
|
190
|
+
self._has_entered = True
|
192
191
|
|
193
192
|
@property
|
194
|
-
def
|
195
|
-
# needed because
|
196
|
-
return self.
|
193
|
+
def _entered(self) -> bool:
|
194
|
+
# needed because _aenter is nowrap
|
195
|
+
return self._has_entered
|
197
196
|
|
198
|
-
@
|
199
|
-
def
|
200
|
-
self.
|
197
|
+
@_entered.setter
|
198
|
+
def _entered(self, val: bool):
|
199
|
+
self._has_entered = val
|
201
200
|
|
202
201
|
@synchronizer.nowrap
|
203
|
-
async def
|
204
|
-
if not self.
|
202
|
+
async def _aenter(self):
|
203
|
+
if not self._entered: # use the property to get at the impl class
|
205
204
|
user_cls_instance = self._cached_user_cls_instance()
|
206
205
|
if hasattr(user_cls_instance, "__aenter__"):
|
207
206
|
await user_cls_instance.__aenter__()
|
208
207
|
elif hasattr(user_cls_instance, "__enter__"):
|
209
208
|
user_cls_instance.__enter__()
|
210
|
-
self.
|
209
|
+
self._has_entered = True
|
211
210
|
|
212
211
|
def __getattr__(self, k):
|
213
212
|
if k in self._method_functions:
|
modal/cls.pyi
CHANGED
@@ -22,7 +22,7 @@ def _get_class_constructor_signature(user_cls: type) -> inspect.Signature: ...
|
|
22
22
|
|
23
23
|
class _Obj:
|
24
24
|
_functions: dict[str, modal.functions._Function]
|
25
|
-
|
25
|
+
_has_entered: bool
|
26
26
|
_user_cls_instance: typing.Optional[typing.Any]
|
27
27
|
_construction_args: tuple[tuple, dict[str, typing.Any]]
|
28
28
|
_instance_service_function: typing.Optional[modal.functions._Function]
|
@@ -41,17 +41,17 @@ class _Obj:
|
|
41
41
|
def _new_user_cls_instance(self): ...
|
42
42
|
async def keep_warm(self, warm_pool_size: int) -> None: ...
|
43
43
|
def _cached_user_cls_instance(self): ...
|
44
|
-
def
|
44
|
+
def _enter(self): ...
|
45
45
|
@property
|
46
|
-
def
|
47
|
-
@
|
48
|
-
def
|
49
|
-
async def
|
46
|
+
def _entered(self) -> bool: ...
|
47
|
+
@_entered.setter
|
48
|
+
def _entered(self, val: bool): ...
|
49
|
+
async def _aenter(self): ...
|
50
50
|
def __getattr__(self, k): ...
|
51
51
|
|
52
52
|
class Obj:
|
53
53
|
_functions: dict[str, modal.functions.Function]
|
54
|
-
|
54
|
+
_has_entered: bool
|
55
55
|
_user_cls_instance: typing.Optional[typing.Any]
|
56
56
|
_construction_args: tuple[tuple, dict[str, typing.Any]]
|
57
57
|
_instance_service_function: typing.Optional[modal.functions.Function]
|
@@ -76,12 +76,12 @@ class Obj:
|
|
76
76
|
keep_warm: __keep_warm_spec
|
77
77
|
|
78
78
|
def _cached_user_cls_instance(self): ...
|
79
|
-
def
|
79
|
+
def _enter(self): ...
|
80
80
|
@property
|
81
|
-
def
|
82
|
-
@
|
83
|
-
def
|
84
|
-
async def
|
81
|
+
def _entered(self) -> bool: ...
|
82
|
+
@_entered.setter
|
83
|
+
def _entered(self, val: bool): ...
|
84
|
+
async def _aenter(self): ...
|
85
85
|
def __getattr__(self, k): ...
|
86
86
|
|
87
87
|
class _Cls(modal.object._Object):
|
modal/config.py
CHANGED
@@ -221,6 +221,7 @@ _SETTINGS = {
|
|
221
221
|
"image_builder_version": _Setting(),
|
222
222
|
"strict_parameters": _Setting(False, transform=_to_boolean), # For internal/experimental use
|
223
223
|
"snapshot_debug": _Setting(False, transform=_to_boolean),
|
224
|
+
"client_retries": _Setting(False, transform=_to_boolean), # For internal testing.
|
224
225
|
}
|
225
226
|
|
226
227
|
|
modal/functions.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# Copyright Modal Labs 2023
|
2
|
+
import dataclasses
|
2
3
|
import inspect
|
3
4
|
import textwrap
|
4
5
|
import time
|
@@ -19,6 +20,7 @@ import typing_extensions
|
|
19
20
|
from google.protobuf.message import Message
|
20
21
|
from grpclib import GRPCError, Status
|
21
22
|
from synchronicity.combined_types import MethodWithAio
|
23
|
+
from synchronicity.exceptions import UserCodeException
|
22
24
|
|
23
25
|
from modal._utils.async_utils import aclosing
|
24
26
|
from modal_proto import api_pb2
|
@@ -57,6 +59,7 @@ from .cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
|
|
57
59
|
from .config import config
|
58
60
|
from .exception import (
|
59
61
|
ExecutionError,
|
62
|
+
FunctionTimeoutError,
|
60
63
|
InvalidError,
|
61
64
|
NotFoundError,
|
62
65
|
OutputExpiredError,
|
@@ -79,7 +82,7 @@ from .parallel_map import (
|
|
79
82
|
_SynchronizedQueue,
|
80
83
|
)
|
81
84
|
from .proxy import _Proxy
|
82
|
-
from .retries import Retries
|
85
|
+
from .retries import Retries, RetryManager
|
83
86
|
from .schedule import Schedule
|
84
87
|
from .scheduler_placement import SchedulerPlacement
|
85
88
|
from .secret import _Secret
|
@@ -91,15 +94,32 @@ if TYPE_CHECKING:
|
|
91
94
|
import modal.partial_function
|
92
95
|
|
93
96
|
|
97
|
+
@dataclasses.dataclass
|
98
|
+
class _RetryContext:
|
99
|
+
function_call_invocation_type: "api_pb2.FunctionCallInvocationType.ValueType"
|
100
|
+
retry_policy: api_pb2.FunctionRetryPolicy
|
101
|
+
function_call_jwt: str
|
102
|
+
input_jwt: str
|
103
|
+
input_id: str
|
104
|
+
item: api_pb2.FunctionPutInputsItem
|
105
|
+
|
106
|
+
|
94
107
|
class _Invocation:
|
95
108
|
"""Internal client representation of a single-input call to a Modal Function or Generator"""
|
96
109
|
|
97
110
|
stub: ModalClientModal
|
98
111
|
|
99
|
-
def __init__(
|
112
|
+
def __init__(
|
113
|
+
self,
|
114
|
+
stub: ModalClientModal,
|
115
|
+
function_call_id: str,
|
116
|
+
client: _Client,
|
117
|
+
retry_context: Optional[_RetryContext] = None,
|
118
|
+
):
|
100
119
|
self.stub = stub
|
101
120
|
self.client = client # Used by the deserializer.
|
102
121
|
self.function_call_id = function_call_id # TODO: remove and use only input_id
|
122
|
+
self._retry_context = retry_context
|
103
123
|
|
104
124
|
@staticmethod
|
105
125
|
async def create(
|
@@ -125,7 +145,17 @@ class _Invocation:
|
|
125
145
|
function_call_id = response.function_call_id
|
126
146
|
|
127
147
|
if response.pipelined_inputs:
|
128
|
-
|
148
|
+
assert len(response.pipelined_inputs) == 1
|
149
|
+
input = response.pipelined_inputs[0]
|
150
|
+
retry_context = _RetryContext(
|
151
|
+
function_call_invocation_type=function_call_invocation_type,
|
152
|
+
retry_policy=response.retry_policy,
|
153
|
+
function_call_jwt=response.function_call_jwt,
|
154
|
+
input_jwt=input.input_jwt,
|
155
|
+
input_id=input.input_id,
|
156
|
+
item=item,
|
157
|
+
)
|
158
|
+
return _Invocation(client.stub, function_call_id, client, retry_context)
|
129
159
|
|
130
160
|
request_put = api_pb2.FunctionPutInputsRequest(
|
131
161
|
function_id=function_id, inputs=[item], function_call_id=function_call_id
|
@@ -137,7 +167,16 @@ class _Invocation:
|
|
137
167
|
processed_inputs = inputs_response.inputs
|
138
168
|
if not processed_inputs:
|
139
169
|
raise Exception("Could not create function call - the input queue seems to be full")
|
140
|
-
|
170
|
+
input = inputs_response.inputs[0]
|
171
|
+
retry_context = _RetryContext(
|
172
|
+
function_call_invocation_type=function_call_invocation_type,
|
173
|
+
retry_policy=response.retry_policy,
|
174
|
+
function_call_jwt=response.function_call_jwt,
|
175
|
+
input_jwt=input.input_jwt,
|
176
|
+
input_id=input.input_id,
|
177
|
+
item=item,
|
178
|
+
)
|
179
|
+
return _Invocation(client.stub, function_call_id, client, retry_context)
|
141
180
|
|
142
181
|
async def pop_function_call_outputs(
|
143
182
|
self, timeout: Optional[float], clear_on_success: bool
|
@@ -173,13 +212,46 @@ class _Invocation:
|
|
173
212
|
# return the last response to check for state of num_unfinished_inputs
|
174
213
|
return response
|
175
214
|
|
176
|
-
async def
|
215
|
+
async def _retry_input(self) -> None:
|
216
|
+
ctx = self._retry_context
|
217
|
+
if not ctx:
|
218
|
+
raise ValueError("Cannot retry input when _retry_context is empty.")
|
219
|
+
|
220
|
+
item = api_pb2.FunctionRetryInputsItem(input_jwt=ctx.input_jwt, input=ctx.item.input)
|
221
|
+
request = api_pb2.FunctionRetryInputsRequest(function_call_jwt=ctx.function_call_jwt, inputs=[item])
|
222
|
+
await retry_transient_errors(
|
223
|
+
self.client.stub.FunctionRetryInputs,
|
224
|
+
request,
|
225
|
+
)
|
226
|
+
|
227
|
+
async def _get_single_output(self) -> Any:
|
177
228
|
# waits indefinitely for a single result for the function, and clear the outputs buffer after
|
178
229
|
item: api_pb2.FunctionGetOutputsItem = (
|
179
230
|
await self.pop_function_call_outputs(timeout=None, clear_on_success=True)
|
180
231
|
).outputs[0]
|
181
232
|
return await _process_result(item.result, item.data_format, self.stub, self.client)
|
182
233
|
|
234
|
+
async def run_function(self) -> Any:
|
235
|
+
# Use retry logic only if retry policy is specified and
|
236
|
+
ctx = self._retry_context
|
237
|
+
if (
|
238
|
+
not ctx
|
239
|
+
or not ctx.retry_policy
|
240
|
+
or ctx.retry_policy.retries == 0
|
241
|
+
or ctx.function_call_invocation_type != api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC
|
242
|
+
):
|
243
|
+
return await self._get_single_output()
|
244
|
+
|
245
|
+
# User errors including timeouts are managed by the user specified retry policy.
|
246
|
+
user_retry_manager = RetryManager(ctx.retry_policy)
|
247
|
+
|
248
|
+
while True:
|
249
|
+
try:
|
250
|
+
return await self._get_single_output()
|
251
|
+
except (UserCodeException, FunctionTimeoutError) as exc:
|
252
|
+
await user_retry_manager.raise_or_sleep(exc)
|
253
|
+
await self._retry_input()
|
254
|
+
|
183
255
|
async def poll_function(self, timeout: Optional[float] = None):
|
184
256
|
"""Waits up to timeout for a result from a function.
|
185
257
|
|
@@ -1225,13 +1297,18 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1225
1297
|
yield item
|
1226
1298
|
|
1227
1299
|
async def _call_function(self, args, kwargs) -> ReturnType:
|
1300
|
+
if config.get("client_retries"):
|
1301
|
+
function_call_invocation_type = api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC
|
1302
|
+
else:
|
1303
|
+
function_call_invocation_type = api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC_LEGACY
|
1228
1304
|
invocation = await _Invocation.create(
|
1229
1305
|
self,
|
1230
1306
|
args,
|
1231
1307
|
kwargs,
|
1232
1308
|
client=self._client,
|
1233
|
-
function_call_invocation_type=
|
1309
|
+
function_call_invocation_type=function_call_invocation_type,
|
1234
1310
|
)
|
1311
|
+
|
1235
1312
|
return await invocation.run_function()
|
1236
1313
|
|
1237
1314
|
async def _call_function_nowait(
|
@@ -1348,12 +1425,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1348
1425
|
if is_async(info.raw_f):
|
1349
1426
|
# We want to run __aenter__ and fun in the same coroutine
|
1350
1427
|
async def coro():
|
1351
|
-
await obj.
|
1428
|
+
await obj._aenter()
|
1352
1429
|
return await fun(*args, **kwargs)
|
1353
1430
|
|
1354
1431
|
return coro() # type: ignore
|
1355
1432
|
else:
|
1356
|
-
obj.
|
1433
|
+
obj._enter()
|
1357
1434
|
return fun(*args, **kwargs)
|
1358
1435
|
|
1359
1436
|
@synchronizer.no_input_translation
|
modal/functions.pyi
CHANGED
@@ -26,11 +26,35 @@ import pathlib
|
|
26
26
|
import typing
|
27
27
|
import typing_extensions
|
28
28
|
|
29
|
+
class _RetryContext:
|
30
|
+
function_call_invocation_type: int
|
31
|
+
retry_policy: modal_proto.api_pb2.FunctionRetryPolicy
|
32
|
+
function_call_jwt: str
|
33
|
+
input_jwt: str
|
34
|
+
input_id: str
|
35
|
+
item: modal_proto.api_pb2.FunctionPutInputsItem
|
36
|
+
|
37
|
+
def __init__(
|
38
|
+
self,
|
39
|
+
function_call_invocation_type: int,
|
40
|
+
retry_policy: modal_proto.api_pb2.FunctionRetryPolicy,
|
41
|
+
function_call_jwt: str,
|
42
|
+
input_jwt: str,
|
43
|
+
input_id: str,
|
44
|
+
item: modal_proto.api_pb2.FunctionPutInputsItem,
|
45
|
+
) -> None: ...
|
46
|
+
def __repr__(self): ...
|
47
|
+
def __eq__(self, other): ...
|
48
|
+
|
29
49
|
class _Invocation:
|
30
50
|
stub: modal_proto.modal_api_grpc.ModalClientModal
|
31
51
|
|
32
52
|
def __init__(
|
33
|
-
self,
|
53
|
+
self,
|
54
|
+
stub: modal_proto.modal_api_grpc.ModalClientModal,
|
55
|
+
function_call_id: str,
|
56
|
+
client: modal.client._Client,
|
57
|
+
retry_context: typing.Optional[_RetryContext] = None,
|
34
58
|
): ...
|
35
59
|
@staticmethod
|
36
60
|
async def create(
|
@@ -39,6 +63,8 @@ class _Invocation:
|
|
39
63
|
async def pop_function_call_outputs(
|
40
64
|
self, timeout: typing.Optional[float], clear_on_success: bool
|
41
65
|
) -> modal_proto.api_pb2.FunctionGetOutputsResponse: ...
|
66
|
+
async def _retry_input(self) -> None: ...
|
67
|
+
async def _get_single_output(self) -> typing.Any: ...
|
42
68
|
async def run_function(self) -> typing.Any: ...
|
43
69
|
async def poll_function(self, timeout: typing.Optional[float] = None): ...
|
44
70
|
def run_generator(self): ...
|
@@ -433,11 +459,11 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
433
459
|
|
434
460
|
_call_generator_nowait: ___call_generator_nowait_spec
|
435
461
|
|
436
|
-
class __remote_spec(typing_extensions.Protocol[
|
462
|
+
class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
|
437
463
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
|
438
464
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
|
439
465
|
|
440
|
-
remote: __remote_spec[
|
466
|
+
remote: __remote_spec[P, ReturnType]
|
441
467
|
|
442
468
|
class __remote_gen_spec(typing_extensions.Protocol):
|
443
469
|
def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
|
@@ -449,17 +475,17 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
449
475
|
def _get_obj(self) -> typing.Optional[modal.cls.Obj]: ...
|
450
476
|
def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
|
451
477
|
|
452
|
-
class ___experimental_spawn_spec(typing_extensions.Protocol[
|
478
|
+
class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
|
453
479
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
454
480
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
455
481
|
|
456
|
-
_experimental_spawn: ___experimental_spawn_spec[
|
482
|
+
_experimental_spawn: ___experimental_spawn_spec[P, ReturnType]
|
457
483
|
|
458
|
-
class __spawn_spec(typing_extensions.Protocol[
|
484
|
+
class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
|
459
485
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
460
486
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
461
487
|
|
462
|
-
spawn: __spawn_spec[
|
488
|
+
spawn: __spawn_spec[P, ReturnType]
|
463
489
|
|
464
490
|
def get_raw_f(self) -> typing.Callable[..., typing.Any]: ...
|
465
491
|
|
modal/retries.py
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
|
+
import asyncio
|
2
3
|
from datetime import timedelta
|
3
4
|
|
4
5
|
from modal_proto import api_pb2
|
5
6
|
|
6
7
|
from .exception import InvalidError
|
7
8
|
|
9
|
+
MIN_INPUT_RETRY_DELAY_MS = 1000
|
10
|
+
MAX_INPUT_RETRY_DELAY_MS = 24 * 60 * 60 * 1000
|
11
|
+
|
8
12
|
|
9
13
|
class Retries:
|
10
14
|
"""Adds a retry policy to a Modal function.
|
@@ -103,3 +107,37 @@ class Retries:
|
|
103
107
|
initial_delay_ms=self.initial_delay // timedelta(milliseconds=1),
|
104
108
|
max_delay_ms=self.max_delay // timedelta(milliseconds=1),
|
105
109
|
)
|
110
|
+
|
111
|
+
|
112
|
+
class RetryManager:
|
113
|
+
"""
|
114
|
+
Helper class to apply the specified retry policy.
|
115
|
+
"""
|
116
|
+
|
117
|
+
def __init__(self, retry_policy: api_pb2.FunctionRetryPolicy):
|
118
|
+
self.retry_policy = retry_policy
|
119
|
+
self.attempt_count = 0
|
120
|
+
|
121
|
+
async def raise_or_sleep(self, exc: Exception):
|
122
|
+
"""
|
123
|
+
Raises an exception if the maximum retry count has been reached, otherwise sleeps for calculated delay.
|
124
|
+
"""
|
125
|
+
self.attempt_count += 1
|
126
|
+
if self.attempt_count > self.retry_policy.retries:
|
127
|
+
raise exc
|
128
|
+
delay_ms = self._retry_delay_ms(self.attempt_count, self.retry_policy)
|
129
|
+
await asyncio.sleep(delay_ms / 1000)
|
130
|
+
|
131
|
+
@staticmethod
|
132
|
+
def _retry_delay_ms(attempt_count: int, retry_policy: api_pb2.FunctionRetryPolicy) -> float:
|
133
|
+
"""
|
134
|
+
Computes the amount of time to sleep before retrying based on the backend_coefficient and initial_delay_ms args.
|
135
|
+
"""
|
136
|
+
if attempt_count < 1:
|
137
|
+
raise ValueError(f"Cannot compute retry delay. attempt_count must be at least 1, but was {attempt_count}")
|
138
|
+
delay_ms = retry_policy.initial_delay_ms * (retry_policy.backoff_coefficient ** (attempt_count - 1))
|
139
|
+
if delay_ms < MIN_INPUT_RETRY_DELAY_MS:
|
140
|
+
return MIN_INPUT_RETRY_DELAY_MS
|
141
|
+
if delay_ms > MAX_INPUT_RETRY_DELAY_MS:
|
142
|
+
return MAX_INPUT_RETRY_DELAY_MS
|
143
|
+
return delay_ms
|
@@ -19,12 +19,12 @@ modal/app.py,sha256=EJ7FUN6rWnSwLJoYJh8nmKg_t-8hdN8_rt0OrkP7JvQ,46084
|
|
19
19
|
modal/app.pyi,sha256=BE5SlR5tRECuc6-e2lUuOknDdov3zxgZ4N0AsLb5ZVQ,25270
|
20
20
|
modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
|
21
21
|
modal/client.py,sha256=VMg_aIuo_LOEe2ttxBHEND3PLhTp5lo-onH4wELhIyY,16375
|
22
|
-
modal/client.pyi,sha256=
|
22
|
+
modal/client.pyi,sha256=B7hnEwCh6sm4OeZ4kEeSBWemVRup_MP8GsTAuAXGZ5g,7354
|
23
23
|
modal/cloud_bucket_mount.py,sha256=G7T7jWLD0QkmrfKR75mSTwdUZ2xNfj7pkVqb4ipmxmI,5735
|
24
24
|
modal/cloud_bucket_mount.pyi,sha256=CEi7vrH3kDUF4LAy4qP6tfImy2UJuFRcRbsgRNM1wo8,1403
|
25
|
-
modal/cls.py,sha256=
|
26
|
-
modal/cls.pyi,sha256=
|
27
|
-
modal/config.py,sha256=
|
25
|
+
modal/cls.py,sha256=F2jk5zFCAA8h-GfM0dbdBG3Mu5wiG9k9Z9JLYRYuT2Q,24758
|
26
|
+
modal/cls.pyi,sha256=2_nbvSlkh2d0tfibTIxsThPiL0Xcrcosc5f_ET-i0sk,8147
|
27
|
+
modal/config.py,sha256=1KhNJkjYsJkX1V8RPPdRYPlM2HE-ZZs0JVSxbiXjmrw,11010
|
28
28
|
modal/container_process.py,sha256=c_jBPtyPeSxbIcbLfs_FzTrt-1eErtRSnsfxkDozFoY,5589
|
29
29
|
modal/container_process.pyi,sha256=k2kClwaSzz11eci1pzFZgCm-ptXapHAyHTOENorlazA,2594
|
30
30
|
modal/dict.py,sha256=RmJlEwFJOdSfAYcVa50hbbFccV8e7BvC5tc5g1HXF-c,12622
|
@@ -33,8 +33,8 @@ modal/environments.py,sha256=5cgA-zbm6ngKLsRA19zSOgtgo9-BarJK3FJK0BiF2Lo,6505
|
|
33
33
|
modal/environments.pyi,sha256=XalNpiPkAtHWAvOU2Cotq0ozmtl-Jv0FDsR8h9mr27Q,3521
|
34
34
|
modal/exception.py,sha256=EBkdWVved2XEPsXaoPRu56xfxFFHL9iuqvUsdj42WDA,6392
|
35
35
|
modal/experimental.py,sha256=jFuNbwrNHos47viMB9q-cHJSvf2RDxDdoEcss9plaZE,2302
|
36
|
-
modal/functions.py,sha256=
|
37
|
-
modal/functions.pyi,sha256=
|
36
|
+
modal/functions.py,sha256=Pwebl3aeEkNrniXCzuDdjfxgExykTWQo7o0VjFD4To8,69853
|
37
|
+
modal/functions.pyi,sha256=qVcpwr9I48PGGDCshtkYxsesQrMdRgmleRvIXUtyAVk,25107
|
38
38
|
modal/gpu.py,sha256=r4rL6uH3UJIQthzYvfWauXNyh01WqCPtKZCmmSX1fd4,6881
|
39
39
|
modal/image.py,sha256=ZIC8tgjJnqWamN4sZ0Gch3x2VmcM671MWfRLR5SMmoc,79423
|
40
40
|
modal/image.pyi,sha256=JjicLNuaBsfuPZ_xo_eN0zKZkDrEm2alYg-szENhJjM,24591
|
@@ -56,7 +56,7 @@ modal/proxy.pyi,sha256=UvygdOYneLTuoDY6hVaMNCyZ947Tmx93IdLjErUqkvM,368
|
|
56
56
|
modal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
57
57
|
modal/queue.py,sha256=q0SDkrN2e2CNxsHjhs6xMwf03LZdvuUFYL7SZPZpKps,18390
|
58
58
|
modal/queue.pyi,sha256=di3ownBw4jc6d4X7ygXtbpjlUMOK69qyaD3lVsJbpoM,9900
|
59
|
-
modal/retries.py,sha256=
|
59
|
+
modal/retries.py,sha256=HKR2Q9aNPWkMjQ5nwobqYTuZaSuw0a8lI2zrtY5IW98,5230
|
60
60
|
modal/runner.py,sha256=7obU-Gq1ocpBGCuR6pvn1T-D6ggg1T48qFo2TNUGWkU,24089
|
61
61
|
modal/runner.pyi,sha256=RAtCvx_lXWjyFjIaZ3t9-X1c7rqpgAQlhl4Hww53OY8,5038
|
62
62
|
modal/running_app.py,sha256=CshNvGDJtagOdKW54uYjY8HY73j2TpnsL9jkPFZAsfA,560
|
@@ -78,7 +78,7 @@ modal/_runtime/asgi.py,sha256=GvuxZqWnIHMIR-Bx5f7toCQlkERaJO8CHjTPNM9IFIw,21537
|
|
78
78
|
modal/_runtime/container_io_manager.py,sha256=yVKSBBybfciDaady7NxOPVU5cm9qL690OgdZ4dZN1bs,44134
|
79
79
|
modal/_runtime/execution_context.py,sha256=E6ofm6j1POXGPxS841X3V7JU6NheVb8OkQc7JpLq4Kg,2712
|
80
80
|
modal/_runtime/telemetry.py,sha256=T1RoAGyjBDr1swiM6pPsGRSITm7LI5FDK18oNXxY08U,5163
|
81
|
-
modal/_runtime/user_code_imports.py,sha256=
|
81
|
+
modal/_runtime/user_code_imports.py,sha256=q_3JOYqCPDcVFZWCHEjyEqj8yzdFsQ49HzeqYmFDLbk,14521
|
82
82
|
modal/_utils/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
|
83
83
|
modal/_utils/app_utils.py,sha256=88BT4TPLWfYAQwKTHcyzNQRHg8n9B-QE2UyJs96iV-0,108
|
84
84
|
modal/_utils/async_utils.py,sha256=CYXogDVqqUtSe-DVP2A3F-6KztjPZaW6ez2lrYBCW_Y,24917
|
@@ -159,10 +159,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
|
|
159
159
|
modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
160
160
|
modal_version/__init__.py,sha256=3IY-AWLH55r35_mQXIaut0jrJvoPuf1NZJBQQfSbPuo,470
|
161
161
|
modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
|
162
|
-
modal_version/_version_generated.py,sha256=
|
163
|
-
modal-0.67.
|
164
|
-
modal-0.67.
|
165
|
-
modal-0.67.
|
166
|
-
modal-0.67.
|
167
|
-
modal-0.67.
|
168
|
-
modal-0.67.
|
162
|
+
modal_version/_version_generated.py,sha256=YsypBM6HmXq8DPKueY9FgGnZ2xdO5KF3g6dZfzBASeg,149
|
163
|
+
modal-0.67.11.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
|
164
|
+
modal-0.67.11.dist-info/METADATA,sha256=StjByc8ZnmcoGsEIpjloa7D5qiraqKpSoc5zvrmvT4o,2329
|
165
|
+
modal-0.67.11.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
166
|
+
modal-0.67.11.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
|
167
|
+
modal-0.67.11.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
|
168
|
+
modal-0.67.11.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|