modal 1.2.1.dev8__py3-none-any.whl → 1.2.2.dev19__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.
Files changed (70) hide show
  1. modal/_clustered_functions.py +1 -3
  2. modal/_container_entrypoint.py +4 -1
  3. modal/_functions.py +33 -49
  4. modal/_grpc_client.py +148 -0
  5. modal/_output.py +3 -4
  6. modal/_partial_function.py +22 -2
  7. modal/_runtime/container_io_manager.py +21 -22
  8. modal/_utils/async_utils.py +12 -3
  9. modal/_utils/auth_token_manager.py +1 -4
  10. modal/_utils/blob_utils.py +3 -4
  11. modal/_utils/function_utils.py +4 -0
  12. modal/_utils/grpc_utils.py +80 -51
  13. modal/_utils/mount_utils.py +26 -1
  14. modal/_utils/task_command_router_client.py +536 -0
  15. modal/app.py +7 -5
  16. modal/cli/cluster.py +4 -2
  17. modal/cli/config.py +3 -1
  18. modal/cli/container.py +5 -4
  19. modal/cli/entry_point.py +1 -0
  20. modal/cli/launch.py +1 -2
  21. modal/cli/network_file_system.py +1 -4
  22. modal/cli/queues.py +1 -2
  23. modal/cli/secret.py +1 -2
  24. modal/client.py +5 -115
  25. modal/client.pyi +2 -91
  26. modal/cls.py +1 -2
  27. modal/config.py +3 -1
  28. modal/container_process.py +287 -11
  29. modal/container_process.pyi +95 -32
  30. modal/dict.py +12 -12
  31. modal/environments.py +1 -2
  32. modal/exception.py +4 -0
  33. modal/experimental/__init__.py +2 -3
  34. modal/experimental/flash.py +27 -57
  35. modal/experimental/flash.pyi +6 -20
  36. modal/file_io.py +13 -27
  37. modal/functions.pyi +6 -6
  38. modal/image.py +24 -3
  39. modal/image.pyi +4 -0
  40. modal/io_streams.py +433 -127
  41. modal/io_streams.pyi +236 -171
  42. modal/mount.py +4 -4
  43. modal/network_file_system.py +5 -6
  44. modal/parallel_map.py +29 -31
  45. modal/parallel_map.pyi +3 -9
  46. modal/partial_function.pyi +4 -1
  47. modal/queue.py +17 -18
  48. modal/runner.py +12 -11
  49. modal/sandbox.py +148 -42
  50. modal/sandbox.pyi +139 -0
  51. modal/secret.py +4 -5
  52. modal/snapshot.py +1 -4
  53. modal/token_flow.py +1 -1
  54. modal/volume.py +22 -22
  55. {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/METADATA +1 -1
  56. {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/RECORD +70 -68
  57. modal_proto/api.proto +2 -24
  58. modal_proto/api_grpc.py +0 -32
  59. modal_proto/api_pb2.py +838 -878
  60. modal_proto/api_pb2.pyi +8 -70
  61. modal_proto/api_pb2_grpc.py +0 -67
  62. modal_proto/api_pb2_grpc.pyi +0 -22
  63. modal_proto/modal_api_grpc.py +175 -177
  64. modal_proto/sandbox_router.proto +0 -4
  65. modal_proto/sandbox_router_pb2.pyi +0 -4
  66. modal_version/__init__.py +1 -1
  67. {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/WHEEL +0 -0
  68. {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/entry_points.txt +0 -0
  69. {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/licenses/LICENSE +0 -0
  70. {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/top_level.txt +0 -0
modal/sandbox.py CHANGED
@@ -3,6 +3,7 @@ import asyncio
3
3
  import json
4
4
  import os
5
5
  import time
6
+ import uuid
6
7
  from collections.abc import AsyncGenerator, Collection, Sequence
7
8
  from dataclasses import dataclass
8
9
  from typing import TYPE_CHECKING, Any, AsyncIterator, Literal, Optional, Union, overload
@@ -20,16 +21,16 @@ from modal._tunnel import Tunnel
20
21
  from modal.cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
21
22
  from modal.mount import _Mount
22
23
  from modal.volume import _Volume
23
- from modal_proto import api_pb2
24
+ from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
24
25
 
25
26
  from ._object import _get_environment_name, _Object
26
27
  from ._resolver import Resolver
27
28
  from ._resources import convert_fn_config_to_resources_config
28
29
  from ._utils.async_utils import TaskContext, synchronize_api
29
30
  from ._utils.deprecation import deprecation_warning
30
- from ._utils.grpc_utils import retry_transient_errors
31
31
  from ._utils.mount_utils import validate_network_file_systems, validate_volumes
32
32
  from ._utils.name_utils import is_valid_object_name
33
+ from ._utils.task_command_router_client import TaskCommandRouterClient
33
34
  from .client import _Client
34
35
  from .container_process import _ContainerProcess
35
36
  from .exception import AlreadyExistsError, ExecutionError, InvalidError, SandboxTerminatedError, SandboxTimeoutError
@@ -121,9 +122,10 @@ class _Sandbox(_Object, type_prefix="sb"):
121
122
  _stdout: _StreamReader[str]
122
123
  _stderr: _StreamReader[str]
123
124
  _stdin: _StreamWriter
124
- _task_id: Optional[str] = None
125
- _tunnels: Optional[dict[int, Tunnel]] = None
126
- _enable_snapshot: bool = False
125
+ _task_id: Optional[str]
126
+ _tunnels: Optional[dict[int, Tunnel]]
127
+ _enable_snapshot: bool
128
+ _command_router_client: Optional[TaskCommandRouterClient]
127
129
 
128
130
  @staticmethod
129
131
  def _default_pty_info() -> api_pb2.PTYInfo:
@@ -270,7 +272,7 @@ class _Sandbox(_Object, type_prefix="sb"):
270
272
 
271
273
  create_req = api_pb2.SandboxCreateRequest(app_id=resolver.app_id, definition=definition)
272
274
  try:
273
- create_resp = await retry_transient_errors(resolver.client.stub.SandboxCreate, create_req)
275
+ create_resp = await resolver.client.stub.SandboxCreate(create_req)
274
276
  except GRPCError as exc:
275
277
  if exc.status == Status.ALREADY_EXISTS:
276
278
  raise AlreadyExistsError(exc.message)
@@ -513,14 +515,18 @@ class _Sandbox(_Object, type_prefix="sb"):
513
515
  return obj
514
516
 
515
517
  def _hydrate_metadata(self, handle_metadata: Optional[Message]):
516
- self._stdout: _StreamReader[str] = StreamReader[str](
518
+ self._stdout = StreamReader(
517
519
  api_pb2.FILE_DESCRIPTOR_STDOUT, self.object_id, "sandbox", self._client, by_line=True
518
520
  )
519
- self._stderr: _StreamReader[str] = StreamReader[str](
521
+ self._stderr = StreamReader(
520
522
  api_pb2.FILE_DESCRIPTOR_STDERR, self.object_id, "sandbox", self._client, by_line=True
521
523
  )
522
524
  self._stdin = StreamWriter(self.object_id, "sandbox", self._client)
523
525
  self._result = None
526
+ self._task_id = None
527
+ self._tunnels = None
528
+ self._enable_snapshot = False
529
+ self._command_router_client = None
524
530
 
525
531
  @staticmethod
526
532
  async def from_name(
@@ -540,7 +546,7 @@ class _Sandbox(_Object, type_prefix="sb"):
540
546
  env_name = _get_environment_name(environment_name)
541
547
 
542
548
  req = api_pb2.SandboxGetFromNameRequest(sandbox_name=name, app_name=app_name, environment_name=env_name)
543
- resp = await retry_transient_errors(client.stub.SandboxGetFromName, req)
549
+ resp = await client.stub.SandboxGetFromName(req)
544
550
  return _Sandbox._new_hydrated(resp.sandbox_id, client, None)
545
551
 
546
552
  @staticmethod
@@ -553,7 +559,7 @@ class _Sandbox(_Object, type_prefix="sb"):
553
559
  client = await _Client.from_env()
554
560
 
555
561
  req = api_pb2.SandboxWaitRequest(sandbox_id=sandbox_id, timeout=0)
556
- resp = await retry_transient_errors(client.stub.SandboxWait, req)
562
+ resp = await client.stub.SandboxWait(req)
557
563
 
558
564
  obj = _Sandbox._new_hydrated(sandbox_id, client, None)
559
565
 
@@ -566,7 +572,7 @@ class _Sandbox(_Object, type_prefix="sb"):
566
572
  """Fetches any tags (key-value pairs) currently attached to this Sandbox from the server."""
567
573
  req = api_pb2.SandboxTagsGetRequest(sandbox_id=self.object_id)
568
574
  try:
569
- resp = await retry_transient_errors(self._client.stub.SandboxTagsGet, req)
575
+ resp = await self._client.stub.SandboxTagsGet(req)
570
576
  except GRPCError as exc:
571
577
  raise InvalidError(exc.message) if exc.status == Status.INVALID_ARGUMENT else exc
572
578
 
@@ -590,7 +596,7 @@ class _Sandbox(_Object, type_prefix="sb"):
590
596
  tags=tags_list,
591
597
  )
592
598
  try:
593
- await retry_transient_errors(self._client.stub.SandboxTagsSet, req)
599
+ await self._client.stub.SandboxTagsSet(req)
594
600
  except GRPCError as exc:
595
601
  raise InvalidError(exc.message) if exc.status == Status.INVALID_ARGUMENT else exc
596
602
 
@@ -602,7 +608,7 @@ class _Sandbox(_Object, type_prefix="sb"):
602
608
  """
603
609
  await self._get_task_id() # Ensure the sandbox has started
604
610
  req = api_pb2.SandboxSnapshotFsRequest(sandbox_id=self.object_id, timeout=timeout)
605
- resp = await retry_transient_errors(self._client.stub.SandboxSnapshotFs, req)
611
+ resp = await self._client.stub.SandboxSnapshotFs(req)
606
612
 
607
613
  if resp.result.status != api_pb2.GenericResult.GENERIC_STATUS_SUCCESS:
608
614
  raise ExecutionError(resp.result.exception)
@@ -627,7 +633,7 @@ class _Sandbox(_Object, type_prefix="sb"):
627
633
 
628
634
  while True:
629
635
  req = api_pb2.SandboxWaitRequest(sandbox_id=self.object_id, timeout=10)
630
- resp = await retry_transient_errors(self._client.stub.SandboxWait, req)
636
+ resp = await self._client.stub.SandboxWait(req)
631
637
  if resp.result.status:
632
638
  logger.debug(f"Sandbox {self.object_id} wait completed with status {resp.result.status}")
633
639
  self._result = resp.result
@@ -653,7 +659,7 @@ class _Sandbox(_Object, type_prefix="sb"):
653
659
  return self._tunnels
654
660
 
655
661
  req = api_pb2.SandboxGetTunnelsRequest(sandbox_id=self.object_id, timeout=timeout)
656
- resp = await retry_transient_errors(self._client.stub.SandboxGetTunnels, req)
662
+ resp = await self._client.stub.SandboxGetTunnels(req)
657
663
 
658
664
  # If we couldn't get the tunnels in time, report the timeout.
659
665
  if resp.result.status == api_pb2.GenericResult.GENERIC_STATUS_TIMEOUT:
@@ -681,7 +687,7 @@ class _Sandbox(_Object, type_prefix="sb"):
681
687
  raise InvalidError(f"Failed to serialize user_metadata: {e}")
682
688
 
683
689
  req = api_pb2.SandboxCreateConnectTokenRequest(sandbox_id=self.object_id, user_metadata=user_metadata)
684
- resp = await retry_transient_errors(self._client.stub.SandboxCreateConnectToken, req)
690
+ resp = await self._client.stub.SandboxCreateConnectToken(req)
685
691
  return SandboxConnectCredentials(resp.url, resp.token)
686
692
 
687
693
  async def reload_volumes(self) -> None:
@@ -690,8 +696,7 @@ class _Sandbox(_Object, type_prefix="sb"):
690
696
  Added in v1.1.0.
691
697
  """
692
698
  task_id = await self._get_task_id()
693
- await retry_transient_errors(
694
- self._client.stub.ContainerReloadVolumes,
699
+ await self._client.stub.ContainerReloadVolumes(
695
700
  api_pb2.ContainerReloadVolumesRequest(
696
701
  task_id=task_id,
697
702
  ),
@@ -702,9 +707,7 @@ class _Sandbox(_Object, type_prefix="sb"):
702
707
 
703
708
  This is a no-op if the Sandbox has already finished running."""
704
709
 
705
- await retry_transient_errors(
706
- self._client.stub.SandboxTerminate, api_pb2.SandboxTerminateRequest(sandbox_id=self.object_id)
707
- )
710
+ await self._client.stub.SandboxTerminate(api_pb2.SandboxTerminateRequest(sandbox_id=self.object_id))
708
711
 
709
712
  async def poll(self) -> Optional[int]:
710
713
  """Check if the Sandbox has finished running.
@@ -713,7 +716,7 @@ class _Sandbox(_Object, type_prefix="sb"):
713
716
  """
714
717
 
715
718
  req = api_pb2.SandboxWaitRequest(sandbox_id=self.object_id, timeout=0)
716
- resp = await retry_transient_errors(self._client.stub.SandboxWait, req)
719
+ resp = await self._client.stub.SandboxWait(req)
717
720
 
718
721
  if resp.result.status:
719
722
  self._result = resp.result
@@ -722,14 +725,19 @@ class _Sandbox(_Object, type_prefix="sb"):
722
725
 
723
726
  async def _get_task_id(self) -> str:
724
727
  while not self._task_id:
725
- resp = await retry_transient_errors(
726
- self._client.stub.SandboxGetTaskId, api_pb2.SandboxGetTaskIdRequest(sandbox_id=self.object_id)
727
- )
728
+ resp = await self._client.stub.SandboxGetTaskId(api_pb2.SandboxGetTaskIdRequest(sandbox_id=self.object_id))
728
729
  self._task_id = resp.task_id
729
730
  if not self._task_id:
730
731
  await asyncio.sleep(0.5)
731
732
  return self._task_id
732
733
 
734
+ async def _get_command_router_client(self, task_id: str) -> Optional[TaskCommandRouterClient]:
735
+ if self._command_router_client is None:
736
+ # Attempt to initialize a router client. Returns None if the new exec path not enabled
737
+ # for this sandbox.
738
+ self._command_router_client = await TaskCommandRouterClient.try_init(self._client, task_id)
739
+ return self._command_router_client
740
+
733
741
  @overload
734
742
  async def exec(
735
743
  self,
@@ -791,13 +799,8 @@ class _Sandbox(_Object, type_prefix="sb"):
791
799
 
792
800
  **Usage**
793
801
 
794
- ```python
795
- app = modal.App.lookup("my-app", create_if_missing=True)
796
-
797
- sandbox = modal.Sandbox.create("sleep", "infinity", app=app)
798
-
799
- process = sandbox.exec("bash", "-c", "for i in $(seq 1 10); do echo foo $i; sleep 0.5; done")
800
-
802
+ ```python fixture:sandbox
803
+ process = sandbox.exec("bash", "-c", "for i in $(seq 1 3); do echo foo $i; sleep 0.1; done")
801
804
  for line in process.stdout:
802
805
  print(line)
803
806
  ```
@@ -855,21 +858,57 @@ class _Sandbox(_Object, type_prefix="sb"):
855
858
  await TaskContext.gather(*secret_coros)
856
859
 
857
860
  task_id = await self._get_task_id()
861
+ kwargs = {
862
+ "task_id": task_id,
863
+ "pty_info": pty_info,
864
+ "stdout": stdout,
865
+ "stderr": stderr,
866
+ "timeout": timeout,
867
+ "workdir": workdir,
868
+ "secret_ids": [secret.object_id for secret in secrets],
869
+ "text": text,
870
+ "bufsize": bufsize,
871
+ "runtime_debug": config.get("function_runtime_debug"),
872
+ }
873
+ # NB: This must come after the task ID is set, since the sandbox must be
874
+ # scheduled before we can create a router client.
875
+ if (command_router_client := await self._get_command_router_client(task_id)) is not None:
876
+ kwargs["command_router_client"] = command_router_client
877
+ return await self._exec_through_command_router(*args, **kwargs)
878
+ else:
879
+ return await self._exec_through_server(*args, **kwargs)
880
+
881
+ async def _exec_through_server(
882
+ self,
883
+ *args: str,
884
+ task_id: str,
885
+ pty_info: Optional[api_pb2.PTYInfo] = None,
886
+ stdout: StreamType = StreamType.PIPE,
887
+ stderr: StreamType = StreamType.PIPE,
888
+ timeout: Optional[int] = None,
889
+ workdir: Optional[str] = None,
890
+ secret_ids: Optional[Collection[str]] = None,
891
+ text: bool = True,
892
+ bufsize: Literal[-1, 1] = -1,
893
+ runtime_debug: bool = False,
894
+ ) -> Union[_ContainerProcess[bytes], _ContainerProcess[str]]:
895
+ """Execute a command through the Modal server."""
858
896
  req = api_pb2.ContainerExecRequest(
859
897
  task_id=task_id,
860
898
  command=args,
861
899
  pty_info=pty_info,
862
- runtime_debug=config.get("function_runtime_debug"),
900
+ runtime_debug=runtime_debug,
863
901
  timeout_secs=timeout or 0,
864
902
  workdir=workdir,
865
- secret_ids=[secret.object_id for secret in secrets],
903
+ secret_ids=secret_ids,
866
904
  )
867
- resp = await retry_transient_errors(self._client.stub.ContainerExec, req)
905
+ resp = await self._client.stub.ContainerExec(req)
868
906
  by_line = bufsize == 1
869
907
  exec_deadline = time.monotonic() + int(timeout) + CONTAINER_EXEC_TIMEOUT_BUFFER if timeout else None
870
908
  logger.debug(f"Created ContainerProcess for exec_id {resp.exec_id} on Sandbox {self.object_id}")
871
909
  return _ContainerProcess(
872
910
  resp.exec_id,
911
+ task_id,
873
912
  self._client,
874
913
  stdout=stdout,
875
914
  stderr=stderr,
@@ -878,17 +917,86 @@ class _Sandbox(_Object, type_prefix="sb"):
878
917
  by_line=by_line,
879
918
  )
880
919
 
920
+ async def _exec_through_command_router(
921
+ self,
922
+ *args: str,
923
+ task_id: str,
924
+ command_router_client: TaskCommandRouterClient,
925
+ pty_info: Optional[api_pb2.PTYInfo] = None,
926
+ stdout: StreamType = StreamType.PIPE,
927
+ stderr: StreamType = StreamType.PIPE,
928
+ timeout: Optional[int] = None,
929
+ workdir: Optional[str] = None,
930
+ secret_ids: Optional[Collection[str]] = None,
931
+ text: bool = True,
932
+ bufsize: Literal[-1, 1] = -1,
933
+ runtime_debug: bool = False,
934
+ ) -> Union[_ContainerProcess[bytes], _ContainerProcess[str]]:
935
+ """Execute a command through a task command router running on the Modal worker."""
936
+
937
+ # Generate a random process ID to use as a combination of idempotency key/process identifier.
938
+ process_id = str(uuid.uuid4())
939
+ if stdout == StreamType.PIPE:
940
+ stdout_config = sr_pb2.TaskExecStdoutConfig.TASK_EXEC_STDOUT_CONFIG_PIPE
941
+ elif stdout == StreamType.DEVNULL:
942
+ stdout_config = sr_pb2.TaskExecStdoutConfig.TASK_EXEC_STDOUT_CONFIG_DEVNULL
943
+ elif stdout == StreamType.STDOUT:
944
+ # TODO(saltzm): This is a behavior change from the old implementation. We should
945
+ # probably implement the old behavior of printing to stdout before moving out of beta.
946
+ raise NotImplementedError(
947
+ "Currently the STDOUT stream type is not supported when using exec "
948
+ "through a task command router, which is currently in beta."
949
+ )
950
+ else:
951
+ raise ValueError("Unsupported StreamType for stdout")
952
+
953
+ if stderr == StreamType.PIPE:
954
+ stderr_config = sr_pb2.TaskExecStderrConfig.TASK_EXEC_STDERR_CONFIG_PIPE
955
+ elif stderr == StreamType.DEVNULL:
956
+ stderr_config = sr_pb2.TaskExecStderrConfig.TASK_EXEC_STDERR_CONFIG_DEVNULL
957
+ elif stderr == StreamType.STDOUT:
958
+ stderr_config = sr_pb2.TaskExecStderrConfig.TASK_EXEC_STDERR_CONFIG_STDOUT
959
+ else:
960
+ raise ValueError("Unsupported StreamType for stderr")
961
+
962
+ # Start the process.
963
+ start_req = sr_pb2.TaskExecStartRequest(
964
+ task_id=task_id,
965
+ exec_id=process_id,
966
+ command_args=args,
967
+ stdout_config=stdout_config,
968
+ stderr_config=stderr_config,
969
+ timeout_secs=timeout,
970
+ workdir=workdir,
971
+ secret_ids=secret_ids,
972
+ pty_info=pty_info,
973
+ runtime_debug=runtime_debug,
974
+ )
975
+ _ = await command_router_client.exec_start(start_req)
976
+
977
+ return _ContainerProcess(
978
+ process_id,
979
+ task_id,
980
+ self._client,
981
+ command_router_client=command_router_client,
982
+ stdout=stdout,
983
+ stderr=stderr,
984
+ text=text,
985
+ by_line=bufsize == 1,
986
+ exec_deadline=time.monotonic() + int(timeout) if timeout else None,
987
+ )
988
+
881
989
  async def _experimental_snapshot(self) -> _SandboxSnapshot:
882
990
  await self._get_task_id()
883
991
  snap_req = api_pb2.SandboxSnapshotRequest(sandbox_id=self.object_id)
884
- snap_resp = await retry_transient_errors(self._client.stub.SandboxSnapshot, snap_req)
992
+ snap_resp = await self._client.stub.SandboxSnapshot(snap_req)
885
993
 
886
994
  snapshot_id = snap_resp.snapshot_id
887
995
 
888
996
  # wait for the snapshot to succeed. this is implemented as a second idempotent rpc
889
997
  # because the snapshot itself may take a while to complete.
890
998
  wait_req = api_pb2.SandboxSnapshotWaitRequest(snapshot_id=snapshot_id, timeout=55.0)
891
- wait_resp = await retry_transient_errors(self._client.stub.SandboxSnapshotWait, wait_req)
999
+ wait_resp = await self._client.stub.SandboxSnapshotWait(wait_req)
892
1000
  if wait_resp.result.status != api_pb2.GenericResult.GENERIC_STATUS_SUCCESS:
893
1001
  raise ExecutionError(wait_resp.result.exception)
894
1002
 
@@ -931,9 +1039,7 @@ class _Sandbox(_Object, type_prefix="sb"):
931
1039
  sandbox_name_override_type=api_pb2.SandboxRestoreRequest.SANDBOX_NAME_OVERRIDE_TYPE_STRING,
932
1040
  )
933
1041
  try:
934
- restore_resp: api_pb2.SandboxRestoreResponse = await retry_transient_errors(
935
- client.stub.SandboxRestore, restore_req
936
- )
1042
+ restore_resp: api_pb2.SandboxRestoreResponse = await client.stub.SandboxRestore(restore_req)
937
1043
  except GRPCError as exc:
938
1044
  if exc.status == Status.ALREADY_EXISTS:
939
1045
  raise AlreadyExistsError(exc.message)
@@ -944,7 +1050,7 @@ class _Sandbox(_Object, type_prefix="sb"):
944
1050
  task_id_req = api_pb2.SandboxGetTaskIdRequest(
945
1051
  sandbox_id=restore_resp.sandbox_id, wait_until_ready=True, timeout=55.0
946
1052
  )
947
- resp = await retry_transient_errors(client.stub.SandboxGetTaskId, task_id_req)
1053
+ resp = await client.stub.SandboxGetTaskId(task_id_req)
948
1054
  if resp.task_result.status not in [
949
1055
  api_pb2.GenericResult.GENERIC_STATUS_UNSPECIFIED,
950
1056
  api_pb2.GenericResult.GENERIC_STATUS_SUCCESS,
@@ -1079,7 +1185,7 @@ class _Sandbox(_Object, type_prefix="sb"):
1079
1185
 
1080
1186
  # Fetches a batch of sandboxes.
1081
1187
  try:
1082
- resp = await retry_transient_errors(client.stub.SandboxList, req)
1188
+ resp = await client.stub.SandboxList(req)
1083
1189
  except GRPCError as exc:
1084
1190
  raise InvalidError(exc.message) if exc.status == Status.INVALID_ARGUMENT else exc
1085
1191
 
modal/sandbox.pyi CHANGED
@@ -3,6 +3,7 @@ import collections.abc
3
3
  import google.protobuf.message
4
4
  import modal._object
5
5
  import modal._tunnel
6
+ import modal._utils.task_command_router_client
6
7
  import modal.app
7
8
  import modal.client
8
9
  import modal.cloud_bucket_mount
@@ -83,6 +84,7 @@ class _Sandbox(modal._object._Object):
83
84
  _task_id: typing.Optional[str]
84
85
  _tunnels: typing.Optional[dict[int, modal._tunnel.Tunnel]]
85
86
  _enable_snapshot: bool
87
+ _command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient]
86
88
 
87
89
  @staticmethod
88
90
  def _default_pty_info() -> modal_proto.api_pb2.PTYInfo: ...
@@ -305,6 +307,9 @@ class _Sandbox(modal._object._Object):
305
307
  ...
306
308
 
307
309
  async def _get_task_id(self) -> str: ...
310
+ async def _get_command_router_client(
311
+ self, task_id: str
312
+ ) -> typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient]: ...
308
313
  @typing.overload
309
314
  async def exec(
310
315
  self,
@@ -356,6 +361,41 @@ class _Sandbox(modal._object._Object):
356
361
  """
357
362
  ...
358
363
 
364
+ async def _exec_through_server(
365
+ self,
366
+ *args: str,
367
+ task_id: str,
368
+ pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
369
+ stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
370
+ stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
371
+ timeout: typing.Optional[int] = None,
372
+ workdir: typing.Optional[str] = None,
373
+ secret_ids: typing.Optional[collections.abc.Collection[str]] = None,
374
+ text: bool = True,
375
+ bufsize: typing.Literal[-1, 1] = -1,
376
+ runtime_debug: bool = False,
377
+ ) -> typing.Union[modal.container_process._ContainerProcess[bytes], modal.container_process._ContainerProcess[str]]:
378
+ """Execute a command through the Modal server."""
379
+ ...
380
+
381
+ async def _exec_through_command_router(
382
+ self,
383
+ *args: str,
384
+ task_id: str,
385
+ command_router_client: modal._utils.task_command_router_client.TaskCommandRouterClient,
386
+ pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
387
+ stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
388
+ stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
389
+ timeout: typing.Optional[int] = None,
390
+ workdir: typing.Optional[str] = None,
391
+ secret_ids: typing.Optional[collections.abc.Collection[str]] = None,
392
+ text: bool = True,
393
+ bufsize: typing.Literal[-1, 1] = -1,
394
+ runtime_debug: bool = False,
395
+ ) -> typing.Union[modal.container_process._ContainerProcess[bytes], modal.container_process._ContainerProcess[str]]:
396
+ """Execute a command through a task command router running on the Modal worker."""
397
+ ...
398
+
359
399
  async def _experimental_snapshot(self) -> modal.snapshot._SandboxSnapshot: ...
360
400
  @staticmethod
361
401
  async def _experimental_from_snapshot(
@@ -444,6 +484,7 @@ class Sandbox(modal.object.Object):
444
484
  _task_id: typing.Optional[str]
445
485
  _tunnels: typing.Optional[dict[int, modal._tunnel.Tunnel]]
446
486
  _enable_snapshot: bool
487
+ _command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient]
447
488
 
448
489
  def __init__(self, *args, **kwargs):
449
490
  """mdmd:hidden"""
@@ -907,6 +948,16 @@ class Sandbox(modal.object.Object):
907
948
 
908
949
  _get_task_id: ___get_task_id_spec[typing_extensions.Self]
909
950
 
951
+ class ___get_command_router_client_spec(typing_extensions.Protocol[SUPERSELF]):
952
+ def __call__(
953
+ self, /, task_id: str
954
+ ) -> typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient]: ...
955
+ async def aio(
956
+ self, /, task_id: str
957
+ ) -> typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient]: ...
958
+
959
+ _get_command_router_client: ___get_command_router_client_spec[typing_extensions.Self]
960
+
910
961
  class __exec_spec(typing_extensions.Protocol[SUPERSELF]):
911
962
  @typing.overload
912
963
  def __call__(
@@ -1026,6 +1077,94 @@ class Sandbox(modal.object.Object):
1026
1077
 
1027
1078
  _exec: ___exec_spec[typing_extensions.Self]
1028
1079
 
1080
+ class ___exec_through_server_spec(typing_extensions.Protocol[SUPERSELF]):
1081
+ def __call__(
1082
+ self,
1083
+ /,
1084
+ *args: str,
1085
+ task_id: str,
1086
+ pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
1087
+ stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
1088
+ stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
1089
+ timeout: typing.Optional[int] = None,
1090
+ workdir: typing.Optional[str] = None,
1091
+ secret_ids: typing.Optional[collections.abc.Collection[str]] = None,
1092
+ text: bool = True,
1093
+ bufsize: typing.Literal[-1, 1] = -1,
1094
+ runtime_debug: bool = False,
1095
+ ) -> typing.Union[
1096
+ modal.container_process.ContainerProcess[bytes], modal.container_process.ContainerProcess[str]
1097
+ ]:
1098
+ """Execute a command through the Modal server."""
1099
+ ...
1100
+
1101
+ async def aio(
1102
+ self,
1103
+ /,
1104
+ *args: str,
1105
+ task_id: str,
1106
+ pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
1107
+ stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
1108
+ stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
1109
+ timeout: typing.Optional[int] = None,
1110
+ workdir: typing.Optional[str] = None,
1111
+ secret_ids: typing.Optional[collections.abc.Collection[str]] = None,
1112
+ text: bool = True,
1113
+ bufsize: typing.Literal[-1, 1] = -1,
1114
+ runtime_debug: bool = False,
1115
+ ) -> typing.Union[
1116
+ modal.container_process.ContainerProcess[bytes], modal.container_process.ContainerProcess[str]
1117
+ ]:
1118
+ """Execute a command through the Modal server."""
1119
+ ...
1120
+
1121
+ _exec_through_server: ___exec_through_server_spec[typing_extensions.Self]
1122
+
1123
+ class ___exec_through_command_router_spec(typing_extensions.Protocol[SUPERSELF]):
1124
+ def __call__(
1125
+ self,
1126
+ /,
1127
+ *args: str,
1128
+ task_id: str,
1129
+ command_router_client: modal._utils.task_command_router_client.TaskCommandRouterClient,
1130
+ pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
1131
+ stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
1132
+ stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
1133
+ timeout: typing.Optional[int] = None,
1134
+ workdir: typing.Optional[str] = None,
1135
+ secret_ids: typing.Optional[collections.abc.Collection[str]] = None,
1136
+ text: bool = True,
1137
+ bufsize: typing.Literal[-1, 1] = -1,
1138
+ runtime_debug: bool = False,
1139
+ ) -> typing.Union[
1140
+ modal.container_process.ContainerProcess[bytes], modal.container_process.ContainerProcess[str]
1141
+ ]:
1142
+ """Execute a command through a task command router running on the Modal worker."""
1143
+ ...
1144
+
1145
+ async def aio(
1146
+ self,
1147
+ /,
1148
+ *args: str,
1149
+ task_id: str,
1150
+ command_router_client: modal._utils.task_command_router_client.TaskCommandRouterClient,
1151
+ pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
1152
+ stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
1153
+ stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
1154
+ timeout: typing.Optional[int] = None,
1155
+ workdir: typing.Optional[str] = None,
1156
+ secret_ids: typing.Optional[collections.abc.Collection[str]] = None,
1157
+ text: bool = True,
1158
+ bufsize: typing.Literal[-1, 1] = -1,
1159
+ runtime_debug: bool = False,
1160
+ ) -> typing.Union[
1161
+ modal.container_process.ContainerProcess[bytes], modal.container_process.ContainerProcess[str]
1162
+ ]:
1163
+ """Execute a command through a task command router running on the Modal worker."""
1164
+ ...
1165
+
1166
+ _exec_through_command_router: ___exec_through_command_router_spec[typing_extensions.Self]
1167
+
1029
1168
  class ___experimental_snapshot_spec(typing_extensions.Protocol[SUPERSELF]):
1030
1169
  def __call__(self, /) -> modal.snapshot.SandboxSnapshot: ...
1031
1170
  async def aio(self, /) -> modal.snapshot.SandboxSnapshot: ...
modal/secret.py CHANGED
@@ -15,7 +15,6 @@ from ._resolver import Resolver
15
15
  from ._runtime.execution_context import is_local
16
16
  from ._utils.async_utils import synchronize_api
17
17
  from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
18
- from ._utils.grpc_utils import retry_transient_errors
19
18
  from ._utils.name_utils import check_object_name
20
19
  from ._utils.time_utils import as_timestamp, timestamp_to_localized_dt
21
20
  from .client import _Client
@@ -91,7 +90,7 @@ class _SecretManager:
91
90
  env_dict=env_dict,
92
91
  )
93
92
  try:
94
- await retry_transient_errors(client.stub.SecretGetOrCreate, req)
93
+ await client.stub.SecretGetOrCreate(req)
95
94
  except GRPCError as exc:
96
95
  if exc.status == Status.ALREADY_EXISTS and not allow_existing:
97
96
  raise AlreadyExistsError(exc.message)
@@ -143,7 +142,7 @@ class _SecretManager:
143
142
  req = api_pb2.SecretListRequest(
144
143
  environment_name=_get_environment_name(environment_name), pagination=pagination
145
144
  )
146
- resp = await retry_transient_errors(client.stub.SecretList, req)
145
+ resp = await client.stub.SecretList(req)
147
146
  items.extend(resp.items)
148
147
  finished = (len(resp.items) < max_page_size) or (max_objects is not None and len(items) >= max_objects)
149
148
  return finished
@@ -200,7 +199,7 @@ class _SecretManager:
200
199
  raise
201
200
  else:
202
201
  req = api_pb2.SecretDeleteRequest(secret_id=obj.object_id)
203
- await retry_transient_errors(obj._client.stub.SecretDelete, req)
202
+ await obj._client.stub.SecretDelete(req)
204
203
 
205
204
 
206
205
  SecretManager = synchronize_api(_SecretManager)
@@ -454,7 +453,7 @@ class _Secret(_Object, type_prefix="st"):
454
453
  object_creation_type=object_creation_type,
455
454
  env_dict=env_dict,
456
455
  )
457
- resp = await retry_transient_errors(client.stub.SecretGetOrCreate, request)
456
+ resp = await client.stub.SecretGetOrCreate(request)
458
457
  return resp.secret_id
459
458
 
460
459
  @live_method
modal/snapshot.py CHANGED
@@ -6,7 +6,6 @@ from modal_proto import api_pb2
6
6
  from ._object import _Object
7
7
  from ._resolver import Resolver
8
8
  from ._utils.async_utils import synchronize_api
9
- from ._utils.grpc_utils import retry_transient_errors
10
9
  from .client import _Client
11
10
 
12
11
 
@@ -28,9 +27,7 @@ class _SandboxSnapshot(_Object, type_prefix="sn"):
28
27
  client = await _Client.from_env()
29
28
 
30
29
  async def _load(self: _SandboxSnapshot, resolver: Resolver, existing_object_id: Optional[str]):
31
- await retry_transient_errors(
32
- client.stub.SandboxSnapshotGet, api_pb2.SandboxSnapshotGetRequest(snapshot_id=sandbox_snapshot_id)
33
- )
30
+ await client.stub.SandboxSnapshotGet(api_pb2.SandboxSnapshotGetRequest(snapshot_id=sandbox_snapshot_id))
34
31
 
35
32
  rep = "SandboxSnapshot()"
36
33
  obj = _SandboxSnapshot._from_loader(_load, rep)
modal/token_flow.py CHANGED
@@ -56,7 +56,7 @@ class _TokenFlow:
56
56
  req = api_pb2.TokenFlowWaitRequest(
57
57
  token_flow_id=self.token_flow_id, timeout=timeout, wait_secret=self.wait_secret
58
58
  )
59
- resp = await self.stub.TokenFlowWait(req, timeout=(timeout + grpc_extra_timeout))
59
+ resp = await self.stub.TokenFlowWait(req, retry=None, timeout=timeout + grpc_extra_timeout)
60
60
  if not resp.timeout:
61
61
  return resp
62
62
  else: