modAL 1.2.3.dev13__py3-none-any.whl → 1.2.3.dev14__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.
@@ -29,6 +29,7 @@ from modal_proto import api_pb2
29
29
  from modal_version import __version__
30
30
 
31
31
  from .._traceback import suppress_tb_frames
32
+ from ..config import config
32
33
  from .async_utils import retry
33
34
  from .logger import logger
34
35
 
@@ -72,6 +73,7 @@ RETRYABLE_GRPC_STATUS_CODES = [
72
73
  Status.INTERNAL,
73
74
  Status.UNKNOWN,
74
75
  ]
76
+ SERVER_RETRY_WARNING_TIME_INTERVAL = 30.0
75
77
 
76
78
 
77
79
  @dataclass
@@ -251,6 +253,50 @@ async def retry_transient_errors(
251
253
  return await _retry_transient_errors(fn, req, retry=Retry(max_retries=max_retries))
252
254
 
253
255
 
256
+ def get_server_retry_policy(exc: Exception) -> Optional[api_pb2.RPCRetryPolicy]:
257
+ """Get server retry policy."""
258
+ if not isinstance(exc, GRPCError) or not exc.details:
259
+ return None
260
+
261
+ # Server should not set multiple retry instructions, but if there is more than one, pick the first one
262
+ for entry in exc.details:
263
+ if isinstance(entry, api_pb2.RPCRetryPolicy):
264
+ return entry
265
+ return None
266
+
267
+
268
+ def process_exception_before_retry(
269
+ exc: Exception,
270
+ final_attempt: bool,
271
+ fn_name: str,
272
+ n_retries: int,
273
+ delay: float,
274
+ total_deadline: Optional[float],
275
+ idempotency_key: str,
276
+ ):
277
+ """Process exception before retry, used by `_retry_transient_errors`."""
278
+ with suppress_tb_frames(1):
279
+ if final_attempt:
280
+ logger.debug(
281
+ f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} "
282
+ f"{total_deadline=} for {fn_name} ({idempotency_key[:8]})"
283
+ )
284
+ if isinstance(exc, OSError):
285
+ raise ConnectionError(str(exc))
286
+ elif isinstance(exc, asyncio.TimeoutError):
287
+ raise ConnectionError(str(exc))
288
+ else:
289
+ raise exc
290
+
291
+ if isinstance(exc, AttributeError) and "_write_appdata" not in str(exc):
292
+ # StreamTerminatedError are not properly raised in grpclib<=0.4.7
293
+ # fixed in https://github.com/vmagamedov/grpclib/issues/185
294
+ # TODO: update to newer version (>=0.4.8) once stable
295
+ raise exc
296
+
297
+ logger.debug(f"Retryable failure {repr(exc)} {n_retries=} {delay=} for {fn_name} ({idempotency_key[:8]})")
298
+
299
+
254
300
  async def _retry_transient_errors(
255
301
  fn: typing.Union[
256
302
  "modal._grpc_client.UnaryUnaryWrapper[RequestType, ResponseType]",
@@ -273,12 +319,15 @@ async def _retry_transient_errors(
273
319
 
274
320
  delay = retry.base_delay
275
321
  n_retries = 0
322
+ n_throttled_retries = 0
276
323
 
277
324
  status_codes = [*RETRYABLE_GRPC_STATUS_CODES, *retry.additional_status_codes]
278
325
 
279
326
  idempotency_key = str(uuid.uuid4())
280
327
 
281
328
  t0 = time.time()
329
+ last_server_retry_warning_time = None
330
+
282
331
  if retry.total_timeout is not None:
283
332
  total_deadline = t0 + retry.total_timeout
284
333
  else:
@@ -290,14 +339,18 @@ async def _retry_transient_errors(
290
339
  attempt_metadata = [
291
340
  ("x-idempotency-key", idempotency_key),
292
341
  ("x-retry-attempt", str(n_retries)),
342
+ ("x-throttle-retry-attempt", str(n_throttled_retries)),
293
343
  *metadata,
294
344
  ]
295
345
  if n_retries > 0:
296
346
  attempt_metadata.append(("x-retry-delay", str(time.time() - t0)))
347
+ if n_throttled_retries > 0:
348
+ attempt_metadata.append(("x-throttle-retry-delay", str(time.time() - t0)))
349
+
297
350
  timeouts = []
298
351
  if retry.attempt_timeout is not None:
299
352
  timeouts.append(retry.attempt_timeout)
300
- if retry.total_timeout is not None and total_deadline is not None:
353
+ if total_deadline is not None:
301
354
  timeouts.append(max(total_deadline - time.time(), retry.attempt_timeout_floor))
302
355
  if timeouts:
303
356
  timeout = min(timeouts) # In case the function provided both types of timeouts
@@ -307,6 +360,39 @@ async def _retry_transient_errors(
307
360
  with suppress_tb_frames(1):
308
361
  return await fn_callable(req, metadata=attempt_metadata, timeout=timeout)
309
362
  except (StreamTerminatedError, GRPCError, OSError, asyncio.TimeoutError, AttributeError) as exc:
363
+ # Server side instruction for retries
364
+ if isinstance(exc, GRPCError) and (server_retry_policy := get_server_retry_policy(exc)):
365
+ server_delay = server_retry_policy.retry_after_secs
366
+
367
+ # When `total_deadline` is not defined and server gives instruction for retries, then use
368
+ # `max_throttle_wait`.
369
+ if total_deadline is None and (max_throttle_wait := config.get("max_throttle_wait")):
370
+ total_deadline = t0 + max_throttle_wait
371
+
372
+ final_attempt = (
373
+ total_deadline is not None
374
+ and time.time() + server_delay + retry.attempt_timeout_floor >= total_deadline
375
+ )
376
+ with suppress_tb_frames(1):
377
+ process_exception_before_retry(
378
+ exc, final_attempt, fn.name, n_retries, server_delay, total_deadline, idempotency_key
379
+ )
380
+
381
+ now = time.time()
382
+ if last_server_retry_warning_time is None or (
383
+ now - last_server_retry_warning_time >= SERVER_RETRY_WARNING_TIME_INTERVAL
384
+ ):
385
+ last_server_retry_warning_time = now
386
+ logger.warning(
387
+ f"Warning: Received failure {exc.status}: {exc.message}. "
388
+ f"Will retry in {server_delay:0.2f} seconds."
389
+ )
390
+
391
+ n_throttled_retries += 1
392
+ await asyncio.sleep(server_delay)
393
+ continue
394
+
395
+ # Client handles retry
310
396
  if isinstance(exc, GRPCError) and exc.status not in status_codes:
311
397
  if exc.status == Status.UNAUTHENTICATED:
312
398
  raise AuthError(exc.message)
@@ -320,25 +406,9 @@ async def _retry_transient_errors(
320
406
  final_attempt = False
321
407
 
322
408
  with suppress_tb_frames(1):
323
- if final_attempt:
324
- logger.debug(
325
- f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} "
326
- f"{total_deadline=} for {fn.name} ({idempotency_key[:8]})"
327
- )
328
- if isinstance(exc, OSError):
329
- raise ConnectionError(str(exc))
330
- elif isinstance(exc, asyncio.TimeoutError):
331
- raise ConnectionError(str(exc))
332
- else:
333
- raise exc
334
-
335
- if isinstance(exc, AttributeError) and "_write_appdata" not in str(exc):
336
- # StreamTerminatedError are not properly raised in grpclib<=0.4.7
337
- # fixed in https://github.com/vmagamedov/grpclib/issues/185
338
- # TODO: update to newer version (>=0.4.8) once stable
339
- raise exc
340
-
341
- logger.debug(f"Retryable failure {repr(exc)} {n_retries=} {delay=} for {fn.name} ({idempotency_key[:8]})")
409
+ process_exception_before_retry(
410
+ exc, final_attempt, fn.name, n_retries, delay, total_deadline, idempotency_key
411
+ )
342
412
 
343
413
  n_retries += 1
344
414
 
modal/client.pyi CHANGED
@@ -32,7 +32,7 @@ class _Client:
32
32
  server_url: str,
33
33
  client_type: int,
34
34
  credentials: typing.Optional[tuple[str, str]],
35
- version: str = "1.2.3.dev13",
35
+ version: str = "1.2.3.dev14",
36
36
  ):
37
37
  """mdmd:hidden
38
38
  The Modal client object is not intended to be instantiated directly by users.
@@ -163,7 +163,7 @@ class Client:
163
163
  server_url: str,
164
164
  client_type: int,
165
165
  credentials: typing.Optional[tuple[str, str]],
166
- version: str = "1.2.3.dev13",
166
+ version: str = "1.2.3.dev14",
167
167
  ):
168
168
  """mdmd:hidden
169
169
  The Modal client object is not intended to be instantiated directly by users.
modal/config.py CHANGED
@@ -217,6 +217,13 @@ def _enforce_suffix_rules(x: str) -> str:
217
217
  return x
218
218
 
219
219
 
220
+ def _int_or_none(x: str) -> Optional[int]:
221
+ if not x:
222
+ return None
223
+ x_int = int(x)
224
+ return x_int if x_int > 0 else None
225
+
226
+
220
227
  class _Setting(typing.NamedTuple):
221
228
  default: typing.Any = None
222
229
  transform: typing.Callable[[str], typing.Any] = lambda x: x # noqa: E731
@@ -258,6 +265,7 @@ _SETTINGS = {
258
265
  transform=lambda s: _check_value(["pickle", "cbor"])(s.lower()),
259
266
  ),
260
267
  "dev_suffix": _Setting("", transform=_enforce_suffix_rules),
268
+ "max_throttle_wait": _Setting(None, transform=_int_or_none),
261
269
  }
262
270
 
263
271
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.3.dev13
3
+ Version: 1.2.3.dev14
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -26,12 +26,12 @@ modal/app.pyi,sha256=nI_Tibj-rI5I7dJFJnczbHvcmVzCvvCih0tMzoQlr_Y,50835
26
26
  modal/billing.py,sha256=zmQ3bcCJlwa4KD1IA_QgdWpm1pn13c-7qfy79iEauYI,195
27
27
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
28
28
  modal/client.py,sha256=k1CqQ_0t4-vHylkqYZgYCnClg-FT0QrTPAKivL01A_k,14354
29
- modal/client.pyi,sha256=JRrhM5jL-pRUDESJdtpjOEHBO7VpbH0oUGv3MCqXhn8,13148
29
+ modal/client.pyi,sha256=x5RdbYVkp8jG9PmbnHJ_Lu8kTm9g2UmtGZLCKWipErM,13148
30
30
  modal/cloud_bucket_mount.py,sha256=cvVj5Rt4nDHoQbTneoJPTaAcnl_Ab_mrsjnnzd0v9OI,6013
31
31
  modal/cloud_bucket_mount.pyi,sha256=vXusGMOUk9HjCs4lhxteuCK4xLQclPfugGWKo7LECEA,7539
32
32
  modal/cls.py,sha256=4T__mtYlQUnkatif1Wnr8Ld9DN_ySydHj2UwhRg89ok,41984
33
33
  modal/cls.pyi,sha256=p8Unbfr0yUdAzYYnD7erSRO7dH-87HkAg4oAn6srBzA,27424
34
- modal/config.py,sha256=xUbw_ETdR7S3guj4dyzqdd9EYwRRht3aGYQRogZbi1o,13050
34
+ modal/config.py,sha256=xF2OQztc67ZcpppkL1pQyzIVrvmyEqhouRaRp-iwZJY,13253
35
35
  modal/container_process.py,sha256=KG2ZkFOWe2KN1b8dFSBLBoW5pWoRxBzJ20MLhDTicJs,16749
36
36
  modal/container_process.pyi,sha256=xMKr-VbQsydS8AbhAys9UTpHHnH2QRyINpPtPG7NwmI,8373
37
37
  modal/dict.py,sha256=Omfmj3fUyg7ijTI33Z97geyK7MW8T31LRm-N7pZqSwU,21582
@@ -104,7 +104,7 @@ modal/_utils/docker_utils.py,sha256=h1uETghR40mp_y3fSWuZAfbIASH1HMzuphJHghAL6DU,
104
104
  modal/_utils/function_utils.py,sha256=wL2AA8aEftxCBATPStHgKDRPxrFjPAv-rHt0Wb7y9Ts,28629
105
105
  modal/_utils/git_utils.py,sha256=qtUU6JAttF55ZxYq51y55OR58B0tDPZsZWK5dJe6W5g,3182
106
106
  modal/_utils/grpc_testing.py,sha256=H1zHqthv19eGPJz2HKXDyWXWGSqO4BRsxah3L5Xaa8A,8619
107
- modal/_utils/grpc_utils.py,sha256=xAdatGkwJq6c_49wFYZ2tg3xGc1fIEDi9ItgwMuypOs,13280
107
+ modal/_utils/grpc_utils.py,sha256=z4JlF7_uTaDUwD14YEtK6ClvlMGa8oQwx1PGRbvSRtk,15936
108
108
  modal/_utils/hash_utils.py,sha256=zg3J6OGxTFGSFri1qQ12giDz90lWk8bzaxCTUCRtiX4,3034
109
109
  modal/_utils/http_utils.py,sha256=yeTFsXYr0rYMEhB7vBP7audG9Uc7OLhzKBANFDZWVt0,2451
110
110
  modal/_utils/jwt_utils.py,sha256=fxH9plyrbAemTbjSsQtzIdDXE9QXxvMC4DiUZ16G0aA,1360
@@ -158,7 +158,7 @@ modal/experimental/__init__.py,sha256=sCwNbBLcR2t-jhrpwtMAPGKt2WNqXBg0xkNZdyB-6C
158
158
  modal/experimental/flash.py,sha256=RKS3X3h4tY_mOLcMzbRs5tNCLPFL01dBAhCZSqCeBgA,26675
159
159
  modal/experimental/flash.pyi,sha256=uwinKAYxpunNNfBj58FP88DXb535Qik4F6tnJKPAIwQ,14696
160
160
  modal/experimental/ipython.py,sha256=TrCfmol9LGsRZMeDoeMPx3Hv3BFqQhYnmD_iH0pqdhk,2904
161
- modal-1.2.3.dev13.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
161
+ modal-1.2.3.dev14.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
162
162
  modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
163
163
  modal_docs/gen_cli_docs.py,sha256=c1yfBS_x--gL5bs0N4ihMwqwX8l3IBWSkBAKNNIi6bQ,3801
164
164
  modal_docs/gen_reference_docs.py,sha256=d_CQUGQ0rfw28u75I2mov9AlS773z9rG40-yq5o7g2U,6359
@@ -166,10 +166,10 @@ modal_docs/mdmd/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,2
166
166
  modal_docs/mdmd/mdmd.py,sha256=tUTImNd4UMFk1opkaw8J672gX8AkBO5gbY2S_NMxsxs,7140
167
167
  modal_docs/mdmd/signatures.py,sha256=XJaZrK7Mdepk5fdX51A8uENiLFNil85Ud0d4MH8H5f0,3218
168
168
  modal_proto/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
169
- modal_proto/api.proto,sha256=lJ7RboW0F29Ke4c8SkDUV0MZ81NMbfZjdk-ALJ_ffac,109898
169
+ modal_proto/api.proto,sha256=iZoGou5hhEQjUHHCCWZxrkBtF_Bpqc_f4rDyWqsO3_k,110064
170
170
  modal_proto/api_grpc.py,sha256=-8SHvPtalTAjQtRv7zzU7Z1VoDupoJs2ifG4gl_mF0s,134442
171
- modal_proto/api_pb2.py,sha256=66GekGGTUMSPYvGGP-K2AJq7biNQOu6pMkdr0ID7b44,383248
172
- modal_proto/api_pb2.pyi,sha256=bFFZymkxUh679BZlYhLdVEinvqZ6Y6wX8VeG0nM9mn4,540395
171
+ modal_proto/api_pb2.py,sha256=N34H92QmBNhW_5P_dmhKwQgqdnYjvFSXqSsMMDYyi6o,383758
172
+ modal_proto/api_pb2.pyi,sha256=1ioycGxYnlNt_WKxVK8uyjzczdMWtzndwZPpMly-Gss,540983
173
173
  modal_proto/api_pb2_grpc.py,sha256=Hqw9jcbhpr-W6jsfog_tGU55ouZjITxGvA-DGNBqOLA,289714
174
174
  modal_proto/api_pb2_grpc.pyi,sha256=QLJ58ANCx147HeGJva58h0MTCLIDs9JmVjrx8bDdwlg,67776
175
175
  modal_proto/modal_api_grpc.py,sha256=MqaBZB2ZqYj6XTIgI_p5dOPjt9gKT4pAJNd_WXR3W84,21295
@@ -186,10 +186,10 @@ modal_proto/task_command_router_pb2.py,sha256=_pD2ZpU0bNzhwBdzmLoLyLtAtftI_Agxwn
186
186
  modal_proto/task_command_router_pb2.pyi,sha256=EyDgXPLr7alqjXYERV8w_MPuO404x0uCppmSkrfE9IE,14589
187
187
  modal_proto/task_command_router_pb2_grpc.py,sha256=uEQ0HdrCp8v-9bB5yIic9muA8spCShLHY6Bz9cCgOUE,10114
188
188
  modal_proto/task_command_router_pb2_grpc.pyi,sha256=s3Yxsrawdj4nr8vqQqsAxyX6ilWaGbdECy425KKbLIA,3301
189
- modal_version/__init__.py,sha256=HeIZKLKt1lG5JikR44WzNsyYbellHhVvSzuBFd0J1Qg,121
189
+ modal_version/__init__.py,sha256=ya-4fgDQ6rszSVtsSMXCAD0_zM8EcLYkOuQqxnt0wXg,121
190
190
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
191
- modal-1.2.3.dev13.dist-info/METADATA,sha256=XSnUFwjlqFCvBAZe_5_77lJdQcK0gOMHqd6cgTbzvDk,2490
192
- modal-1.2.3.dev13.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
193
- modal-1.2.3.dev13.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
194
- modal-1.2.3.dev13.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
195
- modal-1.2.3.dev13.dist-info/RECORD,,
191
+ modal-1.2.3.dev14.dist-info/METADATA,sha256=5EXf_2CjGPFKquGh4Au4xW8ihsebd1VO4XzNvuVcBi4,2490
192
+ modal-1.2.3.dev14.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
193
+ modal-1.2.3.dev14.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
194
+ modal-1.2.3.dev14.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
195
+ modal-1.2.3.dev14.dist-info/RECORD,,
modal_proto/api.proto CHANGED
@@ -2642,6 +2642,12 @@ message QueuePutRequest {
2642
2642
  int32 partition_ttl_seconds = 6;
2643
2643
  }
2644
2644
 
2645
+ // Retry repolicy used by GRPCError.details for the server to give instructions
2646
+ // for the client to retry.
2647
+ message RPCRetryPolicy {
2648
+ float retry_after_secs = 1;
2649
+ }
2650
+
2645
2651
  // A copy google.rpc.Status for GRPCError.details:
2646
2652
  // https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto
2647
2653
  // RPCStatus is compatible with google.rpc.Status, so one can encode messages using