modal 1.2.2.dev8__tar.gz → 1.2.2.dev19__tar.gz

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 (200) hide show
  1. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/PKG-INFO +1 -1
  2. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_clustered_functions.py +1 -3
  3. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_functions.py +33 -49
  4. modal-1.2.2.dev19/modal/_grpc_client.py +148 -0
  5. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_output.py +3 -4
  6. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_runtime/container_io_manager.py +21 -22
  7. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/async_utils.py +12 -3
  8. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/auth_token_manager.py +1 -4
  9. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/blob_utils.py +3 -4
  10. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/grpc_utils.py +80 -51
  11. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/task_command_router_client.py +3 -4
  12. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/app.py +3 -4
  13. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/config.py +3 -1
  14. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/container.py +1 -2
  15. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/network_file_system.py +1 -4
  16. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/queues.py +1 -2
  17. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/secret.py +1 -2
  18. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/client.py +5 -115
  19. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/client.pyi +2 -91
  20. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cls.py +1 -2
  21. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/config.py +1 -1
  22. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/container_process.py +2 -5
  23. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/dict.py +12 -12
  24. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/environments.py +1 -2
  25. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/experimental/__init__.py +2 -3
  26. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/experimental/flash.py +6 -10
  27. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/file_io.py +13 -27
  28. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/image.py +3 -3
  29. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/io_streams.py +3 -5
  30. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/mount.py +4 -4
  31. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/network_file_system.py +5 -6
  32. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/parallel_map.py +29 -31
  33. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/parallel_map.pyi +3 -9
  34. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/queue.py +17 -18
  35. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/runner.py +8 -8
  36. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/sandbox.py +19 -27
  37. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/secret.py +4 -5
  38. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/snapshot.py +1 -4
  39. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/token_flow.py +1 -1
  40. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/volume.py +19 -21
  41. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal.egg-info/PKG-INFO +1 -1
  42. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal.egg-info/SOURCES.txt +1 -0
  43. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/api.proto +2 -0
  44. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/api_pb2.py +838 -838
  45. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/api_pb2.pyi +8 -2
  46. modal-1.2.2.dev19/modal_proto/modal_api_grpc.py +194 -0
  47. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_version/__init__.py +1 -1
  48. modal-1.2.2.dev8/modal_proto/modal_api_grpc.py +0 -194
  49. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/LICENSE +0 -0
  50. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/README.md +0 -0
  51. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/__init__.py +0 -0
  52. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/__main__.py +0 -0
  53. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_billing.py +0 -0
  54. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_clustered_functions.pyi +0 -0
  55. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_container_entrypoint.py +0 -0
  56. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_ipython.py +0 -0
  57. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_location.py +0 -0
  58. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_object.py +0 -0
  59. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_partial_function.py +0 -0
  60. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_pty.py +0 -0
  61. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_resolver.py +0 -0
  62. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_resources.py +0 -0
  63. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_runtime/__init__.py +0 -0
  64. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_runtime/asgi.py +0 -0
  65. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_runtime/container_io_manager.pyi +0 -0
  66. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_runtime/execution_context.py +0 -0
  67. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_runtime/execution_context.pyi +0 -0
  68. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  69. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_runtime/telemetry.py +0 -0
  70. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_runtime/user_code_imports.py +0 -0
  71. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_serialization.py +0 -0
  72. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_traceback.py +0 -0
  73. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_tunnel.py +0 -0
  74. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_tunnel.pyi +0 -0
  75. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_type_manager.py +0 -0
  76. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/__init__.py +0 -0
  77. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/app_utils.py +0 -0
  78. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/bytes_io_segment_payload.py +0 -0
  79. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/deprecation.py +0 -0
  80. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/docker_utils.py +0 -0
  81. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/function_utils.py +0 -0
  82. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/git_utils.py +0 -0
  83. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/grpc_testing.py +0 -0
  84. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/hash_utils.py +0 -0
  85. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/http_utils.py +0 -0
  86. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/jwt_utils.py +0 -0
  87. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/logger.py +0 -0
  88. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/mount_utils.py +0 -0
  89. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/name_utils.py +0 -0
  90. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/package_utils.py +0 -0
  91. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/pattern_utils.py +0 -0
  92. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/rand_pb_testing.py +0 -0
  93. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/shell_utils.py +0 -0
  94. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_utils/time_utils.py +0 -0
  95. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_vendor/__init__.py +0 -0
  96. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  97. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_vendor/cloudpickle.py +0 -0
  98. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_vendor/tblib.py +0 -0
  99. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/_watcher.py +0 -0
  100. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/app.pyi +0 -0
  101. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/billing.py +0 -0
  102. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/builder/2023.12.312.txt +0 -0
  103. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/builder/2023.12.txt +0 -0
  104. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/builder/2024.04.txt +0 -0
  105. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/builder/2024.10.txt +0 -0
  106. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/builder/2025.06.txt +0 -0
  107. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/builder/PREVIEW.txt +0 -0
  108. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/builder/README.md +0 -0
  109. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/builder/base-images.json +0 -0
  110. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/call_graph.py +0 -0
  111. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/__init__.py +0 -0
  112. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/_download.py +0 -0
  113. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/_traceback.py +0 -0
  114. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/app.py +0 -0
  115. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/cluster.py +0 -0
  116. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/dict.py +0 -0
  117. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/entry_point.py +0 -0
  118. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/environment.py +0 -0
  119. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/import_refs.py +0 -0
  120. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/launch.py +0 -0
  121. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/profile.py +0 -0
  122. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/programs/__init__.py +0 -0
  123. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/programs/launch_instance_ssh.py +0 -0
  124. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/programs/run_jupyter.py +0 -0
  125. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/programs/run_marimo.py +0 -0
  126. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/programs/vscode.py +0 -0
  127. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/run.py +0 -0
  128. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/token.py +0 -0
  129. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/utils.py +0 -0
  130. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cli/volume.py +0 -0
  131. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cloud_bucket_mount.py +0 -0
  132. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cloud_bucket_mount.pyi +0 -0
  133. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/cls.pyi +0 -0
  134. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/container_process.pyi +0 -0
  135. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/dict.pyi +0 -0
  136. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/environments.pyi +0 -0
  137. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/exception.py +0 -0
  138. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/experimental/flash.pyi +0 -0
  139. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/experimental/ipython.py +0 -0
  140. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/file_io.pyi +0 -0
  141. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/file_pattern_matcher.py +0 -0
  142. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/functions.py +0 -0
  143. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/functions.pyi +0 -0
  144. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/gpu.py +0 -0
  145. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/image.pyi +0 -0
  146. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/io_streams.pyi +0 -0
  147. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/mount.pyi +0 -0
  148. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/network_file_system.pyi +0 -0
  149. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/object.py +0 -0
  150. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/object.pyi +0 -0
  151. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/output.py +0 -0
  152. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/partial_function.py +0 -0
  153. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/partial_function.pyi +0 -0
  154. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/proxy.py +0 -0
  155. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/proxy.pyi +0 -0
  156. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/py.typed +0 -0
  157. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/queue.pyi +0 -0
  158. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/retries.py +0 -0
  159. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/runner.pyi +0 -0
  160. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/running_app.py +0 -0
  161. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/sandbox.pyi +0 -0
  162. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/schedule.py +0 -0
  163. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/scheduler_placement.py +0 -0
  164. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/secret.pyi +0 -0
  165. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/serving.py +0 -0
  166. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/serving.pyi +0 -0
  167. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/snapshot.pyi +0 -0
  168. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/stream_type.py +0 -0
  169. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/token_flow.pyi +0 -0
  170. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal/volume.pyi +0 -0
  171. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal.egg-info/dependency_links.txt +0 -0
  172. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal.egg-info/entry_points.txt +0 -0
  173. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal.egg-info/requires.txt +0 -0
  174. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal.egg-info/top_level.txt +0 -0
  175. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_docs/__init__.py +0 -0
  176. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_docs/gen_cli_docs.py +0 -0
  177. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_docs/gen_reference_docs.py +0 -0
  178. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_docs/mdmd/__init__.py +0 -0
  179. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_docs/mdmd/mdmd.py +0 -0
  180. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_docs/mdmd/signatures.py +0 -0
  181. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/__init__.py +0 -0
  182. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/api_grpc.py +0 -0
  183. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/api_pb2_grpc.py +0 -0
  184. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/api_pb2_grpc.pyi +0 -0
  185. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/py.typed +0 -0
  186. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/sandbox_router.proto +0 -0
  187. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/sandbox_router_grpc.py +0 -0
  188. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/sandbox_router_pb2.py +0 -0
  189. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/sandbox_router_pb2.pyi +0 -0
  190. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/sandbox_router_pb2_grpc.py +0 -0
  191. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/sandbox_router_pb2_grpc.pyi +0 -0
  192. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/task_command_router.proto +0 -0
  193. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/task_command_router_grpc.py +0 -0
  194. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/task_command_router_pb2.py +0 -0
  195. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/task_command_router_pb2.pyi +0 -0
  196. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/task_command_router_pb2_grpc.py +0 -0
  197. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
  198. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/modal_version/__main__.py +0 -0
  199. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/pyproject.toml +0 -0
  200. {modal-1.2.2.dev8 → modal-1.2.2.dev19}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.2.dev8
3
+ Version: 1.2.2.dev19
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -5,7 +5,6 @@ from dataclasses import dataclass
5
5
  from typing import Optional
6
6
 
7
7
  from modal._utils.async_utils import synchronize_api
8
- from modal._utils.grpc_utils import retry_transient_errors
9
8
  from modal.client import _Client
10
9
  from modal.exception import InvalidError
11
10
  from modal_proto import api_pb2
@@ -61,8 +60,7 @@ async def _initialize_clustered_function(client: _Client, task_id: str, world_si
61
60
  os.environ["NCCL_NSOCKS_PERTHREAD"] = "1"
62
61
 
63
62
  if world_size > 1:
64
- resp: api_pb2.TaskClusterHelloResponse = await retry_transient_errors(
65
- client.stub.TaskClusterHello,
63
+ resp = await client.stub.TaskClusterHello(
66
64
  api_pb2.TaskClusterHelloRequest(
67
65
  task_id=task_id,
68
66
  container_ip=container_ip,
@@ -53,7 +53,7 @@ from ._utils.function_utils import (
53
53
  get_function_type,
54
54
  is_async,
55
55
  )
56
- from ._utils.grpc_utils import RetryWarningMessage, retry_transient_errors
56
+ from ._utils.grpc_utils import Retry, RetryWarningMessage
57
57
  from ._utils.mount_utils import validate_network_file_systems, validate_volumes
58
58
  from .call_graph import InputInfo, _reconstruct_call_graph
59
59
  from .client import _Client
@@ -164,21 +164,22 @@ class _Invocation:
164
164
 
165
165
  if from_spawn_map:
166
166
  request.from_spawn_map = True
167
- response = await retry_transient_errors(
168
- client.stub.FunctionMap,
167
+ response = await client.stub.FunctionMap(
169
168
  request,
170
- max_retries=None,
171
- max_delay=30.0,
172
- retry_warning_message=RetryWarningMessage(
173
- message="Warning: `.spawn_map(...)` for function `{self._function_name}` is waiting to create"
174
- "more function calls. This may be due to hitting rate limits or function backlog limits.",
175
- warning_interval=10,
176
- errors_to_warn_for=[Status.RESOURCE_EXHAUSTED],
169
+ retry=Retry(
170
+ max_retries=None,
171
+ max_delay=30.0,
172
+ warning_message=RetryWarningMessage(
173
+ message="Warning: `.spawn_map(...)` for function `{self._function_name}` is waiting to create"
174
+ "more function calls. This may be due to hitting rate limits or function backlog limits.",
175
+ warning_interval=10,
176
+ errors_to_warn_for=[Status.RESOURCE_EXHAUSTED],
177
+ ),
178
+ additional_status_codes=[Status.RESOURCE_EXHAUSTED],
177
179
  ),
178
- additional_status_codes=[Status.RESOURCE_EXHAUSTED],
179
180
  )
180
181
  else:
181
- response = await retry_transient_errors(client.stub.FunctionMap, request)
182
+ response = await client.stub.FunctionMap(request)
182
183
 
183
184
  function_call_id = response.function_call_id
184
185
  if response.pipelined_inputs:
@@ -198,10 +199,7 @@ class _Invocation:
198
199
  request_put = api_pb2.FunctionPutInputsRequest(
199
200
  function_id=function_id, inputs=[item], function_call_id=function_call_id
200
201
  )
201
- inputs_response: api_pb2.FunctionPutInputsResponse = await retry_transient_errors(
202
- client.stub.FunctionPutInputs,
203
- request_put,
204
- )
202
+ inputs_response: api_pb2.FunctionPutInputsResponse = await client.stub.FunctionPutInputs(request_put)
205
203
  processed_inputs = inputs_response.inputs
206
204
  if not processed_inputs:
207
205
  raise Exception("Could not create function call - the input queue seems to be full")
@@ -243,10 +241,9 @@ class _Invocation:
243
241
  start_idx=index,
244
242
  end_idx=index,
245
243
  )
246
- response: api_pb2.FunctionGetOutputsResponse = await retry_transient_errors(
247
- self.stub.FunctionGetOutputs,
244
+ response: api_pb2.FunctionGetOutputsResponse = await self.stub.FunctionGetOutputs(
248
245
  request,
249
- attempt_timeout=backend_timeout + ATTEMPT_TIMEOUT_GRACE_PERIOD,
246
+ retry=Retry(attempt_timeout=backend_timeout + ATTEMPT_TIMEOUT_GRACE_PERIOD),
250
247
  )
251
248
 
252
249
  if len(response.outputs) > 0:
@@ -266,10 +263,7 @@ class _Invocation:
266
263
 
267
264
  item = api_pb2.FunctionRetryInputsItem(input_jwt=ctx.input_jwt, input=ctx.item.input)
268
265
  request = api_pb2.FunctionRetryInputsRequest(function_call_jwt=ctx.function_call_jwt, inputs=[item])
269
- await retry_transient_errors(
270
- self.stub.FunctionRetryInputs,
271
- request,
272
- )
266
+ await self.stub.FunctionRetryInputs(request)
273
267
 
274
268
  async def _get_single_output(self, expected_jwt: Optional[str] = None) -> api_pb2.FunctionGetOutputsItem:
275
269
  # waits indefinitely for a single result for the function, and clear the outputs buffer after
@@ -373,10 +367,8 @@ class _Invocation:
373
367
  start_idx=current_index,
374
368
  end_idx=batch_end_index,
375
369
  )
376
- response: api_pb2.FunctionGetOutputsResponse = await retry_transient_errors(
377
- self.stub.FunctionGetOutputs,
378
- request,
379
- attempt_timeout=ATTEMPT_TIMEOUT_GRACE_PERIOD,
370
+ response: api_pb2.FunctionGetOutputsResponse = await self.stub.FunctionGetOutputs(
371
+ request, retry=Retry(attempt_timeout=ATTEMPT_TIMEOUT_GRACE_PERIOD)
380
372
  )
381
373
 
382
374
  outputs = list(response.outputs)
@@ -448,7 +440,7 @@ class _InputPlaneInvocation:
448
440
  )
449
441
 
450
442
  metadata = await client.get_input_plane_metadata(input_plane_region)
451
- response = await retry_transient_errors(stub.AttemptStart, request, metadata=metadata)
443
+ response = await stub.AttemptStart(request, metadata=metadata)
452
444
  attempt_token = response.attempt_token
453
445
 
454
446
  return _InputPlaneInvocation(
@@ -468,10 +460,9 @@ class _InputPlaneInvocation:
468
460
  requested_at=time.time(),
469
461
  )
470
462
  metadata = await self.client.get_input_plane_metadata(self.input_plane_region)
471
- await_response: api_pb2.AttemptAwaitResponse = await retry_transient_errors(
472
- self.stub.AttemptAwait,
463
+ await_response: api_pb2.AttemptAwaitResponse = await self.stub.AttemptAwait(
473
464
  await_request,
474
- attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD,
465
+ retry=Retry(attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD),
475
466
  metadata=metadata,
476
467
  )
477
468
 
@@ -511,11 +502,7 @@ class _InputPlaneInvocation:
511
502
  input=self.input_item,
512
503
  attempt_token=self.attempt_token,
513
504
  )
514
- retry_response = await retry_transient_errors(
515
- self.stub.AttemptRetry,
516
- retry_request,
517
- metadata=metadata,
518
- )
505
+ retry_response = await self.stub.AttemptRetry(retry_request, metadata=metadata)
519
506
  return retry_response.attempt_token
520
507
 
521
508
  async def run_generator(self):
@@ -916,7 +903,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
916
903
  elif webhook_config:
917
904
  req.webhook_config.CopyFrom(webhook_config)
918
905
 
919
- response = await retry_transient_errors(resolver.client.stub.FunctionPrecreate, req)
906
+ response = await resolver.client.stub.FunctionPrecreate(req)
920
907
  self._hydrate(response.function_id, resolver.client, response.handle_metadata)
921
908
 
922
909
  async def _load(self: _Function, resolver: Resolver, existing_object_id: Optional[str]):
@@ -1125,9 +1112,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1125
1112
  existing_function_id=existing_object_id or "",
1126
1113
  )
1127
1114
  try:
1128
- response: api_pb2.FunctionCreateResponse = await retry_transient_errors(
1129
- resolver.client.stub.FunctionCreate, request
1130
- )
1115
+ response: api_pb2.FunctionCreateResponse = await resolver.client.stub.FunctionCreate(request)
1131
1116
  except GRPCError as exc:
1132
1117
  if exc.status == Status.INVALID_ARGUMENT:
1133
1118
  raise InvalidError(exc.message)
@@ -1264,7 +1249,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1264
1249
  or "", # TODO: investigate shouldn't environment name always be specified here?
1265
1250
  )
1266
1251
 
1267
- response = await retry_transient_errors(parent._client.stub.FunctionBindParams, req)
1252
+ response = await parent._client.stub.FunctionBindParams(req)
1268
1253
  param_bound_func._hydrate(response.bound_function_id, parent._client, response.handle_metadata)
1269
1254
 
1270
1255
  def _deps():
@@ -1328,7 +1313,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1328
1313
  scaledown_window=scaledown_window,
1329
1314
  )
1330
1315
  request = api_pb2.FunctionUpdateSchedulingParamsRequest(function_id=self.object_id, settings=settings)
1331
- await retry_transient_errors(self.client.stub.FunctionUpdateSchedulingParams, request)
1316
+ await self.client.stub.FunctionUpdateSchedulingParams(request)
1332
1317
 
1333
1318
  # One idea would be for FunctionUpdateScheduleParams to return the current (coalesced) settings
1334
1319
  # and then we could return them here (would need some ad hoc dataclass, which I don't love)
@@ -1388,7 +1373,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1388
1373
  environment_name=_get_environment_name(environment_name, resolver) or "",
1389
1374
  )
1390
1375
  try:
1391
- response = await retry_transient_errors(resolver.client.stub.FunctionGet, request)
1376
+ response = await resolver.client.stub.FunctionGet(request)
1392
1377
  except NotFoundError as exc:
1393
1378
  # refine the error message
1394
1379
  env_context = f" (in the '{environment_name}' environment)" if environment_name else ""
@@ -1888,10 +1873,9 @@ Use the `Function.get_web_url()` method instead.
1888
1873
  @live_method
1889
1874
  async def get_current_stats(self) -> FunctionStats:
1890
1875
  """Return a `FunctionStats` object describing the current function's queue and runner counts."""
1891
- resp = await retry_transient_errors(
1892
- self.client.stub.FunctionGetCurrentStats,
1876
+ resp = await self.client.stub.FunctionGetCurrentStats(
1893
1877
  api_pb2.FunctionGetCurrentStatsRequest(function_id=self.object_id),
1894
- total_timeout=10.0,
1878
+ retry=Retry(total_timeout=10.0),
1895
1879
  )
1896
1880
  return FunctionStats(backlog=resp.backlog, num_total_runners=resp.num_total_tasks)
1897
1881
 
@@ -1994,7 +1978,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
1994
1978
  """
1995
1979
  assert self._client and self._client.stub
1996
1980
  request = api_pb2.FunctionGetCallGraphRequest(function_call_id=self.object_id)
1997
- response = await retry_transient_errors(self._client.stub.FunctionGetCallGraph, request)
1981
+ response = await self._client.stub.FunctionGetCallGraph(request)
1998
1982
  return _reconstruct_call_graph(response)
1999
1983
 
2000
1984
  async def cancel(
@@ -2012,7 +1996,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2012
1996
  function_call_id=self.object_id, terminate_containers=terminate_containers
2013
1997
  )
2014
1998
  assert self._client and self._client.stub
2015
- await retry_transient_errors(self._client.stub.FunctionCallCancel, request)
1999
+ await self._client.stub.FunctionCallCancel(request)
2016
2000
 
2017
2001
  @staticmethod
2018
2002
  async def from_id(function_call_id: str, client: Optional[_Client] = None) -> "_FunctionCall[Any]":
@@ -2039,7 +2023,7 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
2039
2023
 
2040
2024
  async def _load(self: _FunctionCall, resolver: Resolver, existing_object_id: Optional[str]):
2041
2025
  request = api_pb2.FunctionCallFromIdRequest(function_call_id=function_call_id)
2042
- resp = await retry_transient_errors(resolver.client.stub.FunctionCallFromId, request)
2026
+ resp = await resolver.client.stub.FunctionCallFromId(request)
2043
2027
  self._hydrate(function_call_id, resolver.client, resp)
2044
2028
 
2045
2029
  rep = f"FunctionCall.from_id({function_call_id!r})"
@@ -0,0 +1,148 @@
1
+ # Copyright Modal Labs 2025
2
+ from typing import TYPE_CHECKING, Any, Collection, Generic, Literal, Mapping, Optional, TypeVar, Union
3
+
4
+ import grpclib.client
5
+ from google.protobuf.message import Message
6
+ from grpclib import GRPCError, Status
7
+
8
+ from ._traceback import suppress_tb_frames
9
+ from ._utils.grpc_utils import Retry, _retry_transient_errors
10
+ from .config import config, logger
11
+ from .exception import InvalidError, NotFoundError
12
+
13
+ if TYPE_CHECKING:
14
+ from .client import _Client
15
+
16
+
17
+ _Value = Union[str, bytes]
18
+ _MetadataLike = Union[Mapping[str, _Value], Collection[tuple[str, _Value]]]
19
+ RequestType = TypeVar("RequestType", bound=Message)
20
+ ResponseType = TypeVar("ResponseType", bound=Message)
21
+
22
+
23
+ class grpc_error_converter:
24
+ def __enter__(self):
25
+ pass
26
+
27
+ def __exit__(self, exc_type, exc, traceback) -> Literal[False]:
28
+ # skip all internal frames from grpclib
29
+ use_full_traceback = config.get("traceback")
30
+ with suppress_tb_frames(1):
31
+ if isinstance(exc, GRPCError):
32
+ if exc.status == Status.NOT_FOUND:
33
+ if use_full_traceback:
34
+ raise NotFoundError(exc.message)
35
+ else:
36
+ raise NotFoundError(exc.message) from None # from None to skip the grpc-internal cause
37
+
38
+ if not use_full_traceback:
39
+ # just include the frame in grpclib that actually raises the GRPCError
40
+ tb = exc.__traceback__
41
+ while tb.tb_next:
42
+ tb = tb.tb_next
43
+ exc.with_traceback(tb)
44
+ raise exc from None # from None to skip the grpc-internal cause
45
+ raise exc
46
+
47
+ return False
48
+
49
+
50
+ class UnaryUnaryWrapper(Generic[RequestType, ResponseType]):
51
+ # Calls a grpclib.UnaryUnaryMethod using a specific Client instance, respecting
52
+ # if that client is closed etc. and possibly introducing Modal-specific retry logic
53
+ wrapped_method: grpclib.client.UnaryUnaryMethod[RequestType, ResponseType]
54
+ client: "_Client"
55
+
56
+ def __init__(
57
+ self,
58
+ wrapped_method: grpclib.client.UnaryUnaryMethod[RequestType, ResponseType],
59
+ client: "_Client",
60
+ server_url: str,
61
+ ):
62
+ self.wrapped_method = wrapped_method
63
+ self.client = client
64
+ self.server_url = server_url
65
+
66
+ @property
67
+ def name(self) -> str:
68
+ return self.wrapped_method.name
69
+
70
+ async def __call__(
71
+ self,
72
+ req: RequestType,
73
+ *,
74
+ retry: Optional[Retry] = Retry(),
75
+ timeout: Optional[float] = None,
76
+ metadata: Optional[list[tuple[str, str]]] = None,
77
+ ) -> ResponseType:
78
+ with suppress_tb_frames(1):
79
+ if timeout is not None and retry is not None:
80
+ raise InvalidError("Retry must be None when timeout is set")
81
+
82
+ if retry is None:
83
+ return await self.direct(req, timeout=timeout, metadata=metadata)
84
+
85
+ return await _retry_transient_errors(
86
+ self, # type: ignore
87
+ req,
88
+ retry=retry,
89
+ metadata=metadata,
90
+ )
91
+
92
+ async def direct(
93
+ self,
94
+ req: RequestType,
95
+ *,
96
+ timeout: Optional[float] = None,
97
+ metadata: Optional[_MetadataLike] = None,
98
+ ) -> ResponseType:
99
+ from .client import _Client
100
+
101
+ if self.client._snapshotted:
102
+ logger.debug(f"refreshing client after snapshot for {self.name.rsplit('/', 1)[1]}")
103
+ self.client = await _Client.from_env()
104
+
105
+ # Note: We override the grpclib method's channel (see grpclib's code [1]). I think this is fine
106
+ # since grpclib's code doesn't seem to change very much, but we could also recreate the
107
+ # grpclib stub if we aren't comfortable with this. The downside is then we need to cache
108
+ # the grpclib stub so the rest of our code becomes a bit more complicated.
109
+ #
110
+ # We need to override the channel because after the process is forked or the client is
111
+ # snapshotted, the existing channel may be stale / unusable.
112
+ #
113
+ # [1]: https://github.com/vmagamedov/grpclib/blob/62f968a4c84e3f64e6966097574ff0a59969ea9b/grpclib/client.py#L844
114
+ self.wrapped_method.channel = await self.client._get_channel(self.server_url)
115
+ with suppress_tb_frames(1), grpc_error_converter():
116
+ return await self.client._call_unary(self.wrapped_method, req, timeout=timeout, metadata=metadata)
117
+
118
+
119
+ class UnaryStreamWrapper(Generic[RequestType, ResponseType]):
120
+ wrapped_method: grpclib.client.UnaryStreamMethod[RequestType, ResponseType]
121
+
122
+ def __init__(
123
+ self,
124
+ wrapped_method: grpclib.client.UnaryStreamMethod[RequestType, ResponseType],
125
+ client: "_Client",
126
+ server_url: str,
127
+ ):
128
+ self.wrapped_method = wrapped_method
129
+ self.client = client
130
+ self.server_url = server_url
131
+
132
+ @property
133
+ def name(self) -> str:
134
+ return self.wrapped_method.name
135
+
136
+ async def unary_stream(
137
+ self,
138
+ request,
139
+ metadata: Optional[Any] = None,
140
+ ):
141
+ from .client import _Client
142
+
143
+ if self.client._snapshotted:
144
+ logger.debug(f"refreshing client after snapshot for {self.name.rsplit('/', 1)[1]}")
145
+ self.client = await _Client.from_env()
146
+ self.wrapped_method.channel = await self.client._get_channel(self.server_url)
147
+ async for response in self.client._call_stream(self.wrapped_method, request, metadata=metadata):
148
+ yield response
@@ -34,7 +34,7 @@ from rich.text import Text
34
34
  from modal._utils.time_utils import timestamp_to_localized_str
35
35
  from modal_proto import api_pb2
36
36
 
37
- from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES, retry_transient_errors
37
+ from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES, Retry
38
38
  from ._utils.shell_utils import stream_from_stdin, write_to_fd
39
39
  from .client import _Client
40
40
  from .config import logger
@@ -489,12 +489,11 @@ async def stream_pty_shell_input(client: _Client, exec_id: str, finish_event: as
489
489
  """
490
490
 
491
491
  async def _handle_input(data: bytes, message_index: int):
492
- await retry_transient_errors(
493
- client.stub.ContainerExecPutInput,
492
+ await client.stub.ContainerExecPutInput(
494
493
  api_pb2.ContainerExecPutInputRequest(
495
494
  exec_id=exec_id, input=api_pb2.RuntimeInputMessage(message=data, message_index=message_index)
496
495
  ),
497
- total_timeout=10,
496
+ retry=Retry(total_timeout=10),
498
497
  )
499
498
 
500
499
  async with stream_from_stdin(_handle_input, use_raw_terminal=True):
@@ -36,7 +36,7 @@ from modal._traceback import print_exception
36
36
  from modal._utils.async_utils import TaskContext, aclosing, asyncify, synchronize_api, synchronizer
37
37
  from modal._utils.blob_utils import MAX_OBJECT_SIZE_BYTES, blob_download, blob_upload, format_blob_data
38
38
  from modal._utils.function_utils import _stream_function_call_data
39
- from modal._utils.grpc_utils import retry_transient_errors
39
+ from modal._utils.grpc_utils import Retry
40
40
  from modal._utils.package_utils import parse_major_minor_version
41
41
  from modal.client import HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, _Client
42
42
  from modal.config import config, logger
@@ -623,8 +623,8 @@ class _ContainerIOManager:
623
623
  await self.heartbeat_condition.wait()
624
624
 
625
625
  request = api_pb2.ContainerHeartbeatRequest(canceled_inputs_return_outputs_v2=True)
626
- response = await retry_transient_errors(
627
- self._client.stub.ContainerHeartbeat, request, attempt_timeout=HEARTBEAT_TIMEOUT
626
+ response = await self._client.stub.ContainerHeartbeat(
627
+ request, retry=Retry(attempt_timeout=HEARTBEAT_TIMEOUT)
628
628
  )
629
629
 
630
630
  if response.HasField("cancel_input_event"):
@@ -671,10 +671,9 @@ class _ContainerIOManager:
671
671
  target_concurrency=self._target_concurrency,
672
672
  max_concurrency=self._max_concurrency,
673
673
  )
674
- resp = await retry_transient_errors(
675
- self._client.stub.FunctionGetDynamicConcurrency,
674
+ resp = await self._client.stub.FunctionGetDynamicConcurrency(
676
675
  request,
677
- attempt_timeout=DYNAMIC_CONCURRENCY_TIMEOUT_SECS,
676
+ retry=Retry(attempt_timeout=DYNAMIC_CONCURRENCY_TIMEOUT_SECS),
678
677
  )
679
678
  if resp.concurrency != self._input_slots.value and not self._stop_concurrency_loop:
680
679
  logger.debug(f"Dynamic concurrency set from {self._input_slots.value} to {resp.concurrency}")
@@ -725,9 +724,9 @@ class _ContainerIOManager:
725
724
 
726
725
  if self.input_plane_server_url:
727
726
  stub = await self._client.get_stub(self.input_plane_server_url)
728
- await retry_transient_errors(stub.FunctionCallPutDataOut, req)
727
+ await stub.FunctionCallPutDataOut(req)
729
728
  else:
730
- await retry_transient_errors(self._client.stub.FunctionCallPutDataOut, req)
729
+ await self._client.stub.FunctionCallPutDataOut(req)
731
730
 
732
731
  @asynccontextmanager
733
732
  async def generator_output_sender(
@@ -815,9 +814,7 @@ class _ContainerIOManager:
815
814
  try:
816
815
  # If number of active inputs is at max queue size, this will block.
817
816
  iteration += 1
818
- response: api_pb2.FunctionGetInputsResponse = await retry_transient_errors(
819
- self._client.stub.FunctionGetInputs, request
820
- )
817
+ response: api_pb2.FunctionGetInputsResponse = await self._client.stub.FunctionGetInputs(request)
821
818
 
822
819
  if response.rate_limit_sleep_duration:
823
820
  logger.info(
@@ -887,11 +884,12 @@ class _ContainerIOManager:
887
884
  # Limit the batch size to 20 to stay within message size limits and buffer size limits.
888
885
  output_batch_size = 20
889
886
  for i in range(0, len(outputs), output_batch_size):
890
- await retry_transient_errors(
891
- self._client.stub.FunctionPutOutputs,
887
+ await self._client.stub.FunctionPutOutputs(
892
888
  api_pb2.FunctionPutOutputsRequest(outputs=outputs[i : i + output_batch_size]),
893
- additional_status_codes=[Status.RESOURCE_EXHAUSTED],
894
- max_retries=None, # Retry indefinitely, trying every 1s.
889
+ retry=Retry(
890
+ additional_status_codes=[Status.RESOURCE_EXHAUSTED],
891
+ max_retries=None, # Retry indefinitely, trying every 1s.
892
+ ),
895
893
  )
896
894
  input_ids = [output.input_id for output in outputs]
897
895
  self.exit_context(started_at, input_ids)
@@ -932,7 +930,7 @@ class _ContainerIOManager:
932
930
  )
933
931
 
934
932
  req = api_pb2.TaskResultRequest(result=result)
935
- await retry_transient_errors(self._client.stub.TaskResult, req)
933
+ await self._client.stub.TaskResult(req)
936
934
 
937
935
  # Shut down the task gracefully
938
936
  raise UserException()
@@ -1082,13 +1080,14 @@ class _ContainerIOManager:
1082
1080
  await asyncify(os.sync)()
1083
1081
  results = await asyncio.gather(
1084
1082
  *[
1085
- retry_transient_errors(
1086
- self._client.stub.VolumeCommit,
1083
+ self._client.stub.VolumeCommit(
1087
1084
  api_pb2.VolumeCommitRequest(volume_id=v_id),
1088
- max_retries=9,
1089
- base_delay=0.25,
1090
- max_delay=256,
1091
- delay_factor=2,
1085
+ retry=Retry(
1086
+ max_retries=9,
1087
+ base_delay=0.25,
1088
+ max_delay=256,
1089
+ delay_factor=2,
1090
+ ),
1092
1091
  )
1093
1092
  for v_id in volume_ids
1094
1093
  ],
@@ -51,6 +51,10 @@ def synchronize_api(obj, target_module=None):
51
51
  return synchronizer.create_blocking(obj, blocking_name, target_module=target_module)
52
52
 
53
53
 
54
+ # Used for testing to configure the `n_attempts` that `retry` will use.
55
+ RETRY_N_ATTEMPTS_OVERRIDE: Optional[int] = None
56
+
57
+
54
58
  def retry(direct_fn=None, *, n_attempts=3, base_delay=0, delay_factor=2, timeout=90):
55
59
  """Decorator that calls an async function multiple times, with a given timeout.
56
60
 
@@ -75,8 +79,13 @@ def retry(direct_fn=None, *, n_attempts=3, base_delay=0, delay_factor=2, timeout
75
79
  def decorator(fn):
76
80
  @functools.wraps(fn)
77
81
  async def f_wrapped(*args, **kwargs):
82
+ if RETRY_N_ATTEMPTS_OVERRIDE is not None:
83
+ local_n_attempts = RETRY_N_ATTEMPTS_OVERRIDE
84
+ else:
85
+ local_n_attempts = n_attempts
86
+
78
87
  delay = base_delay
79
- for i in range(n_attempts):
88
+ for i in range(local_n_attempts):
80
89
  t0 = time.time()
81
90
  try:
82
91
  return await asyncio.wait_for(fn(*args, **kwargs), timeout=timeout)
@@ -84,12 +93,12 @@ def retry(direct_fn=None, *, n_attempts=3, base_delay=0, delay_factor=2, timeout
84
93
  logger.debug(f"Function {fn} was cancelled")
85
94
  raise
86
95
  except Exception as e:
87
- if i >= n_attempts - 1:
96
+ if i >= local_n_attempts - 1:
88
97
  raise
89
98
  logger.debug(
90
99
  f"Failed invoking function {fn}: {e}"
91
100
  f" (took {time.time() - t0}s, sleeping {delay}s"
92
- f" and trying {n_attempts - i - 1} more times)"
101
+ f" and trying {local_n_attempts - i - 1} more times)"
93
102
  )
94
103
  await asyncio.sleep(delay)
95
104
  delay *= delay_factor
@@ -9,7 +9,6 @@ from typing import Any
9
9
  from modal.exception import ExecutionError
10
10
  from modal_proto import api_pb2, modal_api_grpc
11
11
 
12
- from .grpc_utils import retry_transient_errors
13
12
  from .logger import logger
14
13
 
15
14
 
@@ -66,9 +65,7 @@ class _AuthTokenManager:
66
65
  # new token. Once we have a new token, the other coroutines will unblock and return from here.
67
66
  if self._token and not self._needs_refresh():
68
67
  return
69
- resp: api_pb2.AuthTokenGetResponse = await retry_transient_errors(
70
- self._stub.AuthTokenGet, api_pb2.AuthTokenGetRequest()
71
- )
68
+ resp: api_pb2.AuthTokenGetResponse = await self._stub.AuthTokenGet(api_pb2.AuthTokenGetRequest())
72
69
  if not resp.token:
73
70
  # Not expected
74
71
  raise ExecutionError(
@@ -27,7 +27,6 @@ from modal_proto.modal_api_grpc import ModalClientModal
27
27
 
28
28
  from ..exception import ExecutionError
29
29
  from .async_utils import TaskContext, retry
30
- from .grpc_utils import retry_transient_errors
31
30
  from .hash_utils import UploadHashes, get_upload_hashes
32
31
  from .http_utils import ClientSessionRegistry
33
32
  from .logger import logger
@@ -229,7 +228,7 @@ async def _blob_upload(
229
228
  content_sha256_base64=upload_hashes.sha256_base64,
230
229
  content_length=content_length,
231
230
  )
232
- resp = await retry_transient_errors(stub.BlobCreate, req)
231
+ resp = await stub.BlobCreate(req)
233
232
 
234
233
  if resp.WhichOneof("upload_types_oneof") == "multiparts":
235
234
 
@@ -335,7 +334,7 @@ async def blob_download(blob_id: str, stub: ModalClientModal) -> bytes:
335
334
  logger.debug(f"Downloading large blob {blob_id}")
336
335
  t0 = time.time()
337
336
  req = api_pb2.BlobGetRequest(blob_id=blob_id)
338
- resp = await retry_transient_errors(stub.BlobGet, req)
337
+ resp = await stub.BlobGet(req)
339
338
  data = await _download_from_url(resp.download_url)
340
339
  size_mib = len(data) / 1024 / 1024
341
340
  dur_s = max(time.time() - t0, 0.001) # avoid division by zero
@@ -348,7 +347,7 @@ async def blob_download(blob_id: str, stub: ModalClientModal) -> bytes:
348
347
 
349
348
  async def blob_iter(blob_id: str, stub: ModalClientModal) -> AsyncIterator[bytes]:
350
349
  req = api_pb2.BlobGetRequest(blob_id=blob_id)
351
- resp = await retry_transient_errors(stub.BlobGet, req)
350
+ resp = await stub.BlobGet(req)
352
351
  download_url = resp.download_url
353
352
  async with ClientSessionRegistry.get_session().get(download_url) as s3_resp:
354
353
  # S3 signal to slow down request rate.