modal 0.67.10__py3-none-any.whl → 0.67.13__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/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.10"
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.13"
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.10"
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.13"
85
85
  ): ...
86
86
  def is_closed(self) -> bool: ...
87
87
  @property
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__(self, stub: ModalClientModal, function_call_id: str, client: _Client):
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
- return _Invocation(client.stub, function_call_id, client)
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
- return _Invocation(client.stub, function_call_id, client)
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 run_function(self) -> Any:
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=api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC_LEGACY,
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(
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, stub: modal_proto.modal_api_grpc.ModalClientModal, function_call_id: str, client: modal.client._Client
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): ...
modal/partial_function.py CHANGED
@@ -155,7 +155,7 @@ def _find_partial_methods_for_user_cls(user_cls: type[Any], flags: int) -> dict[
155
155
  deprecation_error((2024, 2, 21), message)
156
156
 
157
157
  partial_functions: dict[str, PartialFunction] = {}
158
- for parent_cls in user_cls.mro():
158
+ for parent_cls in reversed(user_cls.mro()):
159
159
  if parent_cls is not object:
160
160
  for k, v in parent_cls.__dict__.items():
161
161
  if isinstance(v, PartialFunction):
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.67.10
3
+ Version: 0.67.13
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -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=0skCUbPqSA3HK_pQDcrHVTveRpJGE9b5ShO7lcOChlc,7354
22
+ modal/client.pyi,sha256=eg7nuSgtNxJkB8fQLRu6VDD6b8w8fSBcW9-5wO2Phzo,7354
23
23
  modal/cloud_bucket_mount.py,sha256=G7T7jWLD0QkmrfKR75mSTwdUZ2xNfj7pkVqb4ipmxmI,5735
24
24
  modal/cloud_bucket_mount.pyi,sha256=CEi7vrH3kDUF4LAy4qP6tfImy2UJuFRcRbsgRNM1wo8,1403
25
25
  modal/cls.py,sha256=F2jk5zFCAA8h-GfM0dbdBG3Mu5wiG9k9Z9JLYRYuT2Q,24758
26
26
  modal/cls.pyi,sha256=2_nbvSlkh2d0tfibTIxsThPiL0Xcrcosc5f_ET-i0sk,8147
27
- modal/config.py,sha256=86vrmCPiH7PhrbC7-EDuU9yuUnqTpDGy1OEvGTdt7Fs,10923
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=lXr32JDqWaGvzzquCmHXIMIuK61361ZeiQ0As7LobWc,66915
37
- modal/functions.pyi,sha256=yAb8-EE-Y7bbZXC0P5lEvCvmNVgxZCYlZxdagGdC3_M,24325
36
+ modal/functions.py,sha256=Pwebl3aeEkNrniXCzuDdjfxgExykTWQo7o0VjFD4To8,69853
37
+ modal/functions.pyi,sha256=fifvDS5GDEYmXjko1UGZrKqmhfnQn6GRwCblM9hrRWo,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
@@ -49,14 +49,14 @@ modal/object.pyi,sha256=MO78H9yFSE5i1gExPEwyyQzLdlshkcGHN1aQ0ylyvq0,8802
49
49
  modal/output.py,sha256=N0xf4qeudEaYrslzdAl35VKV8rapstgIM2e9wO8_iy0,1967
50
50
  modal/parallel_map.py,sha256=4aoMXIrlG3wl5Ifk2YDNOQkXsGRsm6Xbfm6WtJ2t3WY,16002
51
51
  modal/parallel_map.pyi,sha256=pOhT0P3DDYlwLx0fR3PTsecA7DI8uOdXC1N8i-ZkyOY,2328
52
- modal/partial_function.py,sha256=onHBLCbQLJJu1h9--L7hw_gmvEdbLm-pNSa4xxk7ydA,28205
52
+ modal/partial_function.py,sha256=938kcVJHcdGXKWsO7NE_FBxPldZ304a_GyhjxD79wHE,28215
53
53
  modal/partial_function.pyi,sha256=EafGOzZdEq-yE5bYRoMfnMqw-o8Hk_So8MRPDSB99_0,8982
54
54
  modal/proxy.py,sha256=ZrOsuQP7dSZFq1OrIxalNnt0Zvsnp1h86Th679sSL40,1417
55
55
  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=z4dYXdksUcjkefM3vGLkhCQ_m_TUPLJgC4uSYDzWSOU,3750
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
@@ -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=VjasJiIlfVPaP7-gpJdaquzgJzYHAGWOjcQUF7FZYng,149
163
- modal-0.67.10.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
164
- modal-0.67.10.dist-info/METADATA,sha256=H-AaVUFjPBcgdJVblJM0xr80oz0xS55-_rFd2_MCWPw,2329
165
- modal-0.67.10.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
166
- modal-0.67.10.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
167
- modal-0.67.10.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
168
- modal-0.67.10.dist-info/RECORD,,
162
+ modal_version/_version_generated.py,sha256=n6vTPP069df_S0Gqp_hCTlgHvSYzFTpcSw2VEbRd6Mo,149
163
+ modal-0.67.13.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
164
+ modal-0.67.13.dist-info/METADATA,sha256=9cCPrWPQbv5ADr9zsoeQ0TW32a2uNY12MQdaINSIuyY,2329
165
+ modal-0.67.13.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
166
+ modal-0.67.13.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
167
+ modal-0.67.13.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
168
+ modal-0.67.13.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2024
2
2
 
3
3
  # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client.
4
- build_number = 10 # git: 0d5a2b8
4
+ build_number = 13 # git: b71220f