modal 1.1.1.dev39__tar.gz → 1.1.1.dev40__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.

Potentially problematic release.


This version of modal might be problematic. Click here for more details.

Files changed (188) hide show
  1. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/PKG-INFO +1 -1
  2. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_functions.py +1 -2
  3. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/async_utils.py +6 -4
  4. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/client.pyi +2 -2
  5. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/functions.pyi +6 -6
  6. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/parallel_map.py +224 -85
  7. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/parallel_map.pyi +12 -5
  8. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal.egg-info/PKG-INFO +1 -1
  9. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_version/__init__.py +1 -1
  10. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/LICENSE +0 -0
  11. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/README.md +0 -0
  12. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/__init__.py +0 -0
  13. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/__main__.py +0 -0
  14. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_clustered_functions.py +0 -0
  15. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_clustered_functions.pyi +0 -0
  16. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_container_entrypoint.py +0 -0
  17. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_ipython.py +0 -0
  18. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_location.py +0 -0
  19. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_object.py +0 -0
  20. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_output.py +0 -0
  21. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_partial_function.py +0 -0
  22. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_pty.py +0 -0
  23. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_resolver.py +0 -0
  24. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_resources.py +0 -0
  25. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_runtime/__init__.py +0 -0
  26. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_runtime/asgi.py +0 -0
  27. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_runtime/container_io_manager.py +0 -0
  28. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_runtime/container_io_manager.pyi +0 -0
  29. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_runtime/execution_context.py +0 -0
  30. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_runtime/execution_context.pyi +0 -0
  31. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_runtime/gpu_memory_snapshot.py +0 -0
  32. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_runtime/telemetry.py +0 -0
  33. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_runtime/user_code_imports.py +0 -0
  34. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_serialization.py +0 -0
  35. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_traceback.py +0 -0
  36. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_tunnel.py +0 -0
  37. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_tunnel.pyi +0 -0
  38. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_type_manager.py +0 -0
  39. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/__init__.py +0 -0
  40. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/app_utils.py +0 -0
  41. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/auth_token_manager.py +0 -0
  42. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/blob_utils.py +0 -0
  43. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/bytes_io_segment_payload.py +0 -0
  44. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/deprecation.py +0 -0
  45. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/docker_utils.py +0 -0
  46. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/function_utils.py +0 -0
  47. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/git_utils.py +0 -0
  48. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/grpc_testing.py +0 -0
  49. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/grpc_utils.py +0 -0
  50. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/hash_utils.py +0 -0
  51. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/http_utils.py +0 -0
  52. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/jwt_utils.py +0 -0
  53. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/logger.py +0 -0
  54. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/mount_utils.py +0 -0
  55. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/name_utils.py +0 -0
  56. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/package_utils.py +0 -0
  57. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/pattern_utils.py +0 -0
  58. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/rand_pb_testing.py +0 -0
  59. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/shell_utils.py +0 -0
  60. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_utils/time_utils.py +0 -0
  61. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_vendor/__init__.py +0 -0
  62. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_vendor/a2wsgi_wsgi.py +0 -0
  63. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_vendor/cloudpickle.py +0 -0
  64. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_vendor/tblib.py +0 -0
  65. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/_watcher.py +0 -0
  66. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/app.py +0 -0
  67. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/app.pyi +0 -0
  68. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/builder/2023.12.312.txt +0 -0
  69. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/builder/2023.12.txt +0 -0
  70. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/builder/2024.04.txt +0 -0
  71. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/builder/2024.10.txt +0 -0
  72. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/builder/2025.06.txt +0 -0
  73. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/builder/PREVIEW.txt +0 -0
  74. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/builder/README.md +0 -0
  75. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/builder/base-images.json +0 -0
  76. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/call_graph.py +0 -0
  77. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/__init__.py +0 -0
  78. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/_download.py +0 -0
  79. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/_traceback.py +0 -0
  80. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/app.py +0 -0
  81. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/cluster.py +0 -0
  82. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/config.py +0 -0
  83. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/container.py +0 -0
  84. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/dict.py +0 -0
  85. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/entry_point.py +0 -0
  86. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/environment.py +0 -0
  87. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/import_refs.py +0 -0
  88. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/launch.py +0 -0
  89. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/network_file_system.py +0 -0
  90. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/profile.py +0 -0
  91. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/programs/__init__.py +0 -0
  92. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/programs/run_jupyter.py +0 -0
  93. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/programs/vscode.py +0 -0
  94. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/queues.py +0 -0
  95. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/run.py +0 -0
  96. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/secret.py +0 -0
  97. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/token.py +0 -0
  98. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/utils.py +0 -0
  99. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cli/volume.py +0 -0
  100. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/client.py +0 -0
  101. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cloud_bucket_mount.py +0 -0
  102. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cloud_bucket_mount.pyi +0 -0
  103. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cls.py +0 -0
  104. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/cls.pyi +0 -0
  105. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/config.py +0 -0
  106. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/container_process.py +0 -0
  107. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/container_process.pyi +0 -0
  108. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/dict.py +0 -0
  109. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/dict.pyi +0 -0
  110. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/environments.py +0 -0
  111. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/environments.pyi +0 -0
  112. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/exception.py +0 -0
  113. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/experimental/__init__.py +0 -0
  114. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/experimental/flash.py +0 -0
  115. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/experimental/flash.pyi +0 -0
  116. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/experimental/ipython.py +0 -0
  117. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/file_io.py +0 -0
  118. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/file_io.pyi +0 -0
  119. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/file_pattern_matcher.py +0 -0
  120. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/functions.py +0 -0
  121. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/gpu.py +0 -0
  122. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/image.py +0 -0
  123. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/image.pyi +0 -0
  124. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/io_streams.py +0 -0
  125. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/io_streams.pyi +0 -0
  126. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/mount.py +0 -0
  127. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/mount.pyi +0 -0
  128. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/network_file_system.py +0 -0
  129. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/network_file_system.pyi +0 -0
  130. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/object.py +0 -0
  131. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/object.pyi +0 -0
  132. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/output.py +0 -0
  133. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/partial_function.py +0 -0
  134. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/partial_function.pyi +0 -0
  135. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/proxy.py +0 -0
  136. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/proxy.pyi +0 -0
  137. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/py.typed +0 -0
  138. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/queue.py +0 -0
  139. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/queue.pyi +0 -0
  140. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/retries.py +0 -0
  141. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/runner.py +0 -0
  142. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/runner.pyi +0 -0
  143. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/running_app.py +0 -0
  144. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/sandbox.py +0 -0
  145. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/sandbox.pyi +0 -0
  146. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/schedule.py +0 -0
  147. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/scheduler_placement.py +0 -0
  148. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/secret.py +0 -0
  149. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/secret.pyi +0 -0
  150. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/serving.py +0 -0
  151. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/serving.pyi +0 -0
  152. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/snapshot.py +0 -0
  153. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/snapshot.pyi +0 -0
  154. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/stream_type.py +0 -0
  155. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/token_flow.py +0 -0
  156. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/token_flow.pyi +0 -0
  157. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/volume.py +0 -0
  158. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal/volume.pyi +0 -0
  159. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal.egg-info/SOURCES.txt +0 -0
  160. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal.egg-info/dependency_links.txt +0 -0
  161. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal.egg-info/entry_points.txt +0 -0
  162. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal.egg-info/requires.txt +0 -0
  163. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal.egg-info/top_level.txt +0 -0
  164. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_docs/__init__.py +0 -0
  165. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_docs/gen_cli_docs.py +0 -0
  166. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_docs/gen_reference_docs.py +0 -0
  167. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_docs/mdmd/__init__.py +0 -0
  168. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_docs/mdmd/mdmd.py +0 -0
  169. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_docs/mdmd/signatures.py +0 -0
  170. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_proto/__init__.py +0 -0
  171. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_proto/api.proto +0 -0
  172. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_proto/api_grpc.py +0 -0
  173. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_proto/api_pb2.py +0 -0
  174. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_proto/api_pb2.pyi +0 -0
  175. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_proto/api_pb2_grpc.py +0 -0
  176. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_proto/api_pb2_grpc.pyi +0 -0
  177. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_proto/modal_api_grpc.py +0 -0
  178. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_proto/modal_options_grpc.py +0 -0
  179. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_proto/options.proto +0 -0
  180. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_proto/options_grpc.py +0 -0
  181. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_proto/options_pb2.py +0 -0
  182. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_proto/options_pb2.pyi +0 -0
  183. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_proto/options_pb2_grpc.py +0 -0
  184. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_proto/options_pb2_grpc.pyi +0 -0
  185. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_proto/py.typed +0 -0
  186. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/modal_version/__main__.py +0 -0
  187. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/pyproject.toml +0 -0
  188. {modal-1.1.1.dev39 → modal-1.1.1.dev40}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.1.1.dev39
3
+ Version: 1.1.1.dev40
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -1513,8 +1513,7 @@ Use the `Function.get_web_url()` method instead.
1513
1513
  else:
1514
1514
  count_update_callback = None
1515
1515
 
1516
- # TODO(ben-okeefe): Feature gating for input plane map until feature is enabled.
1517
- if self._input_plane_url and False:
1516
+ if self._input_plane_url:
1518
1517
  async with aclosing(
1519
1518
  _map_invocation_inputplane(
1520
1519
  self,
@@ -279,7 +279,9 @@ class TimestampPriorityQueue(Generic[T]):
279
279
 
280
280
  def __init__(self, maxsize: int = 0):
281
281
  self.condition = asyncio.Condition()
282
- self._queue: asyncio.PriorityQueue[tuple[float, Union[T, None]]] = asyncio.PriorityQueue(maxsize=maxsize)
282
+ self._queue: asyncio.PriorityQueue[tuple[float, int, Union[T, None]]] = asyncio.PriorityQueue(maxsize=maxsize)
283
+ # Used to tiebreak items with the same timestamp that are not comparable. (eg. protos)
284
+ self._counter = itertools.count()
283
285
 
284
286
  async def close(self):
285
287
  await self.put(self._MAX_PRIORITY, None)
@@ -288,7 +290,7 @@ class TimestampPriorityQueue(Generic[T]):
288
290
  """
289
291
  Add an item to the queue to be processed at a specific timestamp.
290
292
  """
291
- await self._queue.put((timestamp, item))
293
+ await self._queue.put((timestamp, next(self._counter), item))
292
294
  async with self.condition:
293
295
  self.condition.notify_all() # notify any waiting coroutines
294
296
 
@@ -301,7 +303,7 @@ class TimestampPriorityQueue(Generic[T]):
301
303
  while self.empty():
302
304
  await self.condition.wait()
303
305
  # peek at the next item
304
- timestamp, item = await self._queue.get()
306
+ timestamp, counter, item = await self._queue.get()
305
307
  now = time.time()
306
308
  if timestamp < now:
307
309
  return item
@@ -309,7 +311,7 @@ class TimestampPriorityQueue(Generic[T]):
309
311
  return None
310
312
  # not ready yet, calculate sleep time
311
313
  sleep_time = timestamp - now
312
- self._queue.put_nowait((timestamp, item)) # put it back
314
+ self._queue.put_nowait((timestamp, counter, item)) # put it back
313
315
  # wait until either the timeout or a new item is added
314
316
  try:
315
317
  await asyncio.wait_for(self.condition.wait(), timeout=sleep_time)
@@ -33,7 +33,7 @@ class _Client:
33
33
  server_url: str,
34
34
  client_type: int,
35
35
  credentials: typing.Optional[tuple[str, str]],
36
- version: str = "1.1.1.dev39",
36
+ version: str = "1.1.1.dev40",
37
37
  ):
38
38
  """mdmd:hidden
39
39
  The Modal client object is not intended to be instantiated directly by users.
@@ -164,7 +164,7 @@ class Client:
164
164
  server_url: str,
165
165
  client_type: int,
166
166
  credentials: typing.Optional[tuple[str, str]],
167
- version: str = "1.1.1.dev39",
167
+ version: str = "1.1.1.dev40",
168
168
  ):
169
169
  """mdmd:hidden
170
170
  The Modal client object is not intended to be instantiated directly by users.
@@ -427,7 +427,7 @@ class Function(
427
427
 
428
428
  _call_generator: ___call_generator_spec[typing_extensions.Self]
429
429
 
430
- class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
430
+ class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
431
431
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER:
432
432
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
433
433
  ...
@@ -436,7 +436,7 @@ class Function(
436
436
  """Calls the function remotely, executing it with the given arguments and returning the execution's result."""
437
437
  ...
438
438
 
439
- remote: __remote_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
439
+ remote: __remote_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
440
440
 
441
441
  class __remote_gen_spec(typing_extensions.Protocol[SUPERSELF]):
442
442
  def __call__(self, /, *args, **kwargs) -> typing.Generator[typing.Any, None, None]:
@@ -463,7 +463,7 @@ class Function(
463
463
  """
464
464
  ...
465
465
 
466
- class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
466
+ class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
467
467
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
468
468
  """[Experimental] Calls the function with the given arguments, without waiting for the results.
469
469
 
@@ -487,7 +487,7 @@ class Function(
487
487
  ...
488
488
 
489
489
  _experimental_spawn: ___experimental_spawn_spec[
490
- modal._functions.ReturnType, modal._functions.P, typing_extensions.Self
490
+ modal._functions.P, modal._functions.ReturnType, typing_extensions.Self
491
491
  ]
492
492
 
493
493
  class ___spawn_map_inner_spec(typing_extensions.Protocol[P_INNER, SUPERSELF]):
@@ -496,7 +496,7 @@ class Function(
496
496
 
497
497
  _spawn_map_inner: ___spawn_map_inner_spec[modal._functions.P, typing_extensions.Self]
498
498
 
499
- class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER, SUPERSELF]):
499
+ class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER, SUPERSELF]):
500
500
  def __call__(self, /, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]:
501
501
  """Calls the function with the given arguments, without waiting for the results.
502
502
 
@@ -517,7 +517,7 @@ class Function(
517
517
  """
518
518
  ...
519
519
 
520
- spawn: __spawn_spec[modal._functions.ReturnType, modal._functions.P, typing_extensions.Self]
520
+ spawn: __spawn_spec[modal._functions.P, modal._functions.ReturnType, typing_extensions.Self]
521
521
 
522
522
  def get_raw_f(self) -> collections.abc.Callable[..., typing.Any]:
523
523
  """Return the inner Python object wrapped by this Modal Function."""
@@ -6,7 +6,7 @@ import time
6
6
  import typing
7
7
  from asyncio import FIRST_COMPLETED
8
8
  from dataclasses import dataclass
9
- from typing import Any, Callable, Optional
9
+ from typing import Any, Callable, Optional, Union
10
10
 
11
11
  from grpclib import Status
12
12
 
@@ -437,17 +437,14 @@ async def _map_invocation_inputplane(
437
437
 
438
438
  This is analogous to `_map_invocation`, but instead of the control-plane
439
439
  `FunctionMap` / `FunctionPutInputs` / `FunctionGetOutputs` RPCs it speaks
440
- the input-plane protocol consisting of `MapStartOrContinue` and `MapAwait`.
441
-
442
- The implementation purposefully ignores retry handling for now - a stub is
443
- left in place so that a future change can add support for the retry path
444
- without re-structuring the surrounding code.
440
+ the input-plane protocol consisting of `MapStartOrContinue`, `MapAwait`, and `MapCheckInputs`.
445
441
  """
446
442
 
447
443
  assert function._input_plane_url, "_map_invocation_inputplane should only be used for input-plane backed functions"
448
444
 
449
445
  input_plane_stub = await client.get_stub(function._input_plane_url)
450
446
 
447
+ # Required for _create_input.
451
448
  assert client.stub, "Client must be hydrated with a stub for _map_invocation_inputplane"
452
449
 
453
450
  # ------------------------------------------------------------
@@ -459,15 +456,19 @@ async def _map_invocation_inputplane(
459
456
 
460
457
  inputs_created = 0
461
458
  outputs_completed = 0
459
+ successful_completions = 0
460
+ failed_completions = 0
461
+ no_context_duplicates = 0
462
+ stale_retry_duplicates = 0
463
+ already_complete_duplicates = 0
464
+ retried_outputs = 0
465
+ input_queue_size = 0
466
+ last_entry_id = ""
462
467
 
463
468
  # The input-plane server returns this after the first request.
464
- function_call_id: str | None = None
469
+ function_call_id = None
465
470
  function_call_id_received = asyncio.Event()
466
471
 
467
- # Map of idx -> attempt_token returned by the server. This will be needed
468
- # for a future client-side retry implementation.
469
- attempt_tokens: dict[int, str] = {}
470
-
471
472
  # Single priority queue that holds *both* fresh inputs (timestamp == now)
472
473
  # and future retries (timestamp > now).
473
474
  queue: TimestampPriorityQueue[api_pb2.MapStartOrContinueItem] = TimestampPriorityQueue()
@@ -477,11 +478,25 @@ async def _map_invocation_inputplane(
477
478
  # any reason).
478
479
  max_inputs_outstanding = MAX_INPUTS_OUTSTANDING_DEFAULT
479
480
 
480
- # ------------------------------------------------------------
481
- # Helper functions
482
- # ------------------------------------------------------------
481
+ # Input plane does not yet return a retry policy. So we currently disable retries.
482
+ retry_policy = api_pb2.FunctionRetryPolicy(
483
+ retries=0, # Input plane does not yet return a retry policy. So only retry server failures for now.
484
+ initial_delay_ms=1000,
485
+ max_delay_ms=1000,
486
+ backoff_coefficient=1.0,
487
+ )
488
+ map_items_manager = _MapItemsManager(
489
+ retry_policy=retry_policy,
490
+ function_call_invocation_type=api_pb2.FUNCTION_CALL_INVOCATION_TYPE_SYNC,
491
+ retry_queue=queue,
492
+ sync_client_retries_enabled=True,
493
+ max_inputs_outstanding=MAX_INPUTS_OUTSTANDING_DEFAULT,
494
+ is_input_plane_instance=True,
495
+ )
483
496
 
484
- def update_counters(created_delta: int = 0, completed_delta: int = 0, set_have_all_inputs: bool | None = None):
497
+ def update_counters(
498
+ created_delta: int = 0, completed_delta: int = 0, set_have_all_inputs: Union[bool, None] = None
499
+ ):
485
500
  nonlocal inputs_created, outputs_completed, have_all_inputs
486
501
 
487
502
  if created_delta:
@@ -511,10 +526,6 @@ async def _map_invocation_inputplane(
511
526
  )
512
527
  return api_pb2.MapStartOrContinueItem(input=put_item)
513
528
 
514
- # ------------------------------------------------------------
515
- # Coroutine: drain user input iterator, upload blobs, enqueue for sending
516
- # ------------------------------------------------------------
517
-
518
529
  async def input_iter():
519
530
  while True:
520
531
  raw_input = await raw_input_queue.get()
@@ -530,22 +541,19 @@ async def _map_invocation_inputplane(
530
541
  await queue.put(time.time(), q_item)
531
542
 
532
543
  # All inputs have been read.
533
- await queue.close()
534
544
  update_counters(set_have_all_inputs=True)
535
545
  yield
536
546
 
537
- # ------------------------------------------------------------
538
- # Coroutine: send queued items to the input-plane server
539
- # ------------------------------------------------------------
540
-
541
547
  async def pump_inputs():
542
548
  nonlocal function_call_id, max_inputs_outstanding
543
-
544
549
  async for batch in queue_batch_iterator(queue, max_batch_size=MAP_INVOCATION_CHUNK_SIZE):
545
550
  # Convert the queued items into the proto format expected by the RPC.
546
551
  request_items: list[api_pb2.MapStartOrContinueItem] = [
547
552
  api_pb2.MapStartOrContinueItem(input=qi.input, attempt_token=qi.attempt_token) for qi in batch
548
553
  ]
554
+
555
+ await map_items_manager.add_items_inputplane(request_items)
556
+
549
557
  # Build request
550
558
  request = api_pb2.MapStartOrContinueRequest(
551
559
  function_id=function.object_id,
@@ -560,43 +568,62 @@ async def _map_invocation_inputplane(
560
568
  input_plane_stub.MapStartOrContinue, request, metadata=metadata
561
569
  )
562
570
 
563
- # TODO(ben-okeefe): Understand if an input could be lost at this step and not registered
571
+ # match response items to the corresponding request item index
572
+ response_items_idx_tuple = [
573
+ (request_items[idx].input.idx, attempt_token)
574
+ for idx, attempt_token in enumerate(response.attempt_tokens)
575
+ ]
576
+
577
+ map_items_manager.handle_put_continue_response(response_items_idx_tuple)
564
578
 
565
579
  if function_call_id is None:
566
580
  function_call_id = response.function_call_id
567
581
  function_call_id_received.set()
568
582
  max_inputs_outstanding = response.max_inputs_outstanding or MAX_INPUTS_OUTSTANDING_DEFAULT
569
-
570
- # Record attempt tokens for future retries; also release semaphore slots now that the
571
- # inputs are officially registered on the server.
572
- for idx, attempt_token in enumerate(response.attempt_tokens):
573
- # Client expects the server to return the attempt tokens in the same order as the inputs we sent.
574
- attempt_tokens[request_items[idx].input.idx] = attempt_token
575
-
576
583
  yield
577
584
 
578
- # ------------------------------------------------------------
579
- # Coroutine: **stub** – retry handling will be added in the future
580
- # ------------------------------------------------------------
581
-
582
- async def retry_inputs():
583
- """Temporary stub for retrying inputs. Retry handling will be added in the future."""
584
-
585
+ async def check_lost_inputs():
586
+ nonlocal last_entry_id # shared with get_all_outputs
585
587
  try:
586
588
  while not map_done_event.is_set():
589
+ if function_call_id is None:
590
+ await function_call_id_received.wait()
591
+ continue
592
+
587
593
  await asyncio.sleep(1)
588
- if False:
589
- yield
594
+
595
+ # check_inputs = [(idx, attempt_token), ...]
596
+ check_inputs = map_items_manager.get_input_idxs_waiting_for_output()
597
+ attempt_tokens = [attempt_token for _, attempt_token in check_inputs]
598
+ request = api_pb2.MapCheckInputsRequest(
599
+ last_entry_id=last_entry_id,
600
+ timeout=0, # Non-blocking read
601
+ attempt_tokens=attempt_tokens,
602
+ )
603
+
604
+ metadata = await client.get_input_plane_metadata(function._input_plane_region)
605
+ response: api_pb2.MapCheckInputsResponse = await retry_transient_errors(
606
+ input_plane_stub.MapCheckInputs, request, metadata=metadata
607
+ )
608
+ check_inputs_response = [
609
+ (check_inputs[resp_idx][0], response.lost[resp_idx]) for resp_idx, _ in enumerate(response.lost)
610
+ ]
611
+ # check_inputs_response = [(idx, lost: bool), ...]
612
+ await map_items_manager.handle_check_inputs_response(check_inputs_response)
613
+ yield
590
614
  except asyncio.CancelledError:
591
615
  pass
592
616
 
593
- # ------------------------------------------------------------
594
- # Coroutine: stream outputs via MapAwait
595
- # ------------------------------------------------------------
596
-
597
617
  async def get_all_outputs():
598
- """Continuously fetch outputs until the map is complete."""
599
- last_entry_id = ""
618
+ nonlocal \
619
+ successful_completions, \
620
+ failed_completions, \
621
+ no_context_duplicates, \
622
+ stale_retry_duplicates, \
623
+ already_complete_duplicates, \
624
+ retried_outputs, \
625
+ last_entry_id
626
+
600
627
  while not map_done_event.is_set():
601
628
  if function_call_id is None:
602
629
  await function_call_id_received.wait()
@@ -609,21 +636,51 @@ async def _map_invocation_inputplane(
609
636
  timeout=OUTPUTS_TIMEOUT,
610
637
  )
611
638
  metadata = await client.get_input_plane_metadata(function._input_plane_region)
612
- response: api_pb2.MapAwaitResponse = await retry_transient_errors(
613
- input_plane_stub.MapAwait,
614
- request,
615
- max_retries=20,
616
- attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD,
617
- metadata=metadata,
639
+ get_response_task = asyncio.create_task(
640
+ retry_transient_errors(
641
+ input_plane_stub.MapAwait,
642
+ request,
643
+ max_retries=20,
644
+ attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD,
645
+ metadata=metadata,
646
+ )
618
647
  )
648
+ map_done_task = asyncio.create_task(map_done_event.wait())
649
+ try:
650
+ done, pending = await asyncio.wait([get_response_task, map_done_task], return_when=FIRST_COMPLETED)
651
+ if get_response_task in done:
652
+ map_done_task.cancel()
653
+ response = get_response_task.result()
654
+ else:
655
+ assert map_done_event.is_set()
656
+ # map is done - no more outputs, so return early
657
+ return
658
+ finally:
659
+ # clean up tasks, in case of cancellations etc.
660
+ get_response_task.cancel()
661
+ map_done_task.cancel()
619
662
  last_entry_id = response.last_entry_id
620
663
 
621
664
  for output_item in response.outputs:
622
- yield output_item
623
-
624
- update_counters(completed_delta=1)
665
+ output_type = await map_items_manager.handle_get_outputs_response(output_item, int(time.time()))
666
+ if output_type == _OutputType.SUCCESSFUL_COMPLETION:
667
+ successful_completions += 1
668
+ elif output_type == _OutputType.FAILED_COMPLETION:
669
+ failed_completions += 1
670
+ elif output_type == _OutputType.RETRYING:
671
+ retried_outputs += 1
672
+ elif output_type == _OutputType.NO_CONTEXT_DUPLICATE:
673
+ no_context_duplicates += 1
674
+ elif output_type == _OutputType.STALE_RETRY_DUPLICATE:
675
+ stale_retry_duplicates += 1
676
+ elif output_type == _OutputType.ALREADY_COMPLETE_DUPLICATE:
677
+ already_complete_duplicates += 1
678
+ else:
679
+ raise Exception(f"Unknown output type: {output_type}")
625
680
 
626
- # The loop condition will exit when map_done_event is set from update_counters.
681
+ if output_type == _OutputType.SUCCESSFUL_COMPLETION or output_type == _OutputType.FAILED_COMPLETION:
682
+ update_counters(completed_delta=1)
683
+ yield output_item
627
684
 
628
685
  async def get_all_outputs_and_clean_up():
629
686
  try:
@@ -631,23 +688,24 @@ async def _map_invocation_inputplane(
631
688
  async for item in stream:
632
689
  yield item
633
690
  finally:
634
- # We could signal server we are done with outputs so it can clean up.
691
+ await queue.close()
635
692
  pass
636
693
 
637
- # ------------------------------------------------------------
638
- # Coroutine: convert FunctionGetOutputsItem → actual result value
639
- # ------------------------------------------------------------
640
-
641
694
  async def fetch_output(item: api_pb2.FunctionGetOutputsItem) -> tuple[int, Any]:
642
695
  try:
643
- output_val = await _process_result(item.result, item.data_format, input_plane_stub, client)
644
- except Exception as exc:
696
+ output = await _process_result(item.result, item.data_format, input_plane_stub, client)
697
+ except Exception as e:
645
698
  if return_exceptions:
646
- output_val = exc
699
+ if wrap_returned_exceptions:
700
+ # Prior to client 1.0.4 there was a bug where return_exceptions would wrap
701
+ # any returned exceptions in a synchronicity.UserCodeException. This adds
702
+ # deprecated non-breaking compatibility bandaid for migrating away from that:
703
+ output = modal.exception.UserCodeException(e)
704
+ else:
705
+ output = e
647
706
  else:
648
- raise exc
649
-
650
- return (item.idx, output_val)
707
+ raise e
708
+ return (item.idx, output)
651
709
 
652
710
  async def poll_outputs():
653
711
  # map to store out-of-order outputs received
@@ -677,17 +735,14 @@ async def _map_invocation_inputplane(
677
735
 
678
736
  assert len(received_outputs) == 0
679
737
 
680
- # ------------------------------------------------------------
681
- # Debug-logging helper
682
- # ------------------------------------------------------------
683
738
  async def log_debug_stats():
684
739
  def log_stats():
685
740
  logger.debug(
686
- "Map-IP stats: have_all_inputs=%s inputs_created=%d outputs_completed=%d queue_size=%d",
687
- have_all_inputs,
688
- inputs_created,
689
- outputs_completed,
690
- queue.qsize(),
741
+ f"Map stats:\nsuccessful_completions={successful_completions} failed_completions={failed_completions} "
742
+ f"no_context_duplicates={no_context_duplicates} stale_retry_duplicates={stale_retry_duplicates} "
743
+ f"already_complete_duplicates={already_complete_duplicates} retried_outputs={retried_outputs} "
744
+ f"function_call_id={function_call_id} max_inputs_outstanding={max_inputs_outstanding} "
745
+ f"map_items_manager_size={len(map_items_manager)} input_queue_size={input_queue_size}"
691
746
  )
692
747
 
693
748
  while True:
@@ -699,13 +754,11 @@ async def _map_invocation_inputplane(
699
754
  log_stats()
700
755
  break
701
756
 
702
- # ------------------------------------------------------------
703
- # Run the four coroutines concurrently and yield results as they arrive
704
- # ------------------------------------------------------------
705
-
706
757
  log_task = asyncio.create_task(log_debug_stats())
707
758
 
708
- async with aclosing(async_merge(drain_input_generator(), pump_inputs(), poll_outputs(), retry_inputs())) as merged:
759
+ async with aclosing(
760
+ async_merge(drain_input_generator(), pump_inputs(), poll_outputs(), check_lost_inputs())
761
+ ) as merged:
709
762
  async for maybe_output in merged:
710
763
  if maybe_output is not None: # ignore None sentinels
711
764
  yield maybe_output.value
@@ -1045,12 +1098,19 @@ class _MapItemContext:
1045
1098
  sync_client_retries_enabled: bool
1046
1099
  # Both these futures are strings. Omitting generic type because
1047
1100
  # it causes an error when running `inv protoc type-stubs`.
1101
+ # Unused. But important, input_id is not set for inputplane invocations.
1048
1102
  input_id: asyncio.Future
1049
1103
  input_jwt: asyncio.Future
1050
1104
  previous_input_jwt: Optional[str]
1051
1105
  _event_loop: asyncio.AbstractEventLoop
1052
1106
 
1053
- def __init__(self, input: api_pb2.FunctionInput, retry_manager: RetryManager, sync_client_retries_enabled: bool):
1107
+ def __init__(
1108
+ self,
1109
+ input: api_pb2.FunctionInput,
1110
+ retry_manager: RetryManager,
1111
+ sync_client_retries_enabled: bool,
1112
+ is_input_plane_instance: bool = False,
1113
+ ):
1054
1114
  self.state = _MapItemState.SENDING
1055
1115
  self.input = input
1056
1116
  self.retry_manager = retry_manager
@@ -1061,7 +1121,22 @@ class _MapItemContext:
1061
1121
  # a race condition where we could receive outputs before we have
1062
1122
  # recorded the input ID and JWT in `pending_outputs`.
1063
1123
  self.input_jwt = self._event_loop.create_future()
1124
+ # Unused. But important, this is not set for inputplane invocations.
1064
1125
  self.input_id = self._event_loop.create_future()
1126
+ self._is_input_plane_instance = is_input_plane_instance
1127
+
1128
+ def handle_map_start_or_continue_response(self, attempt_token: str):
1129
+ if not self.input_jwt.done():
1130
+ self.input_jwt.set_result(attempt_token)
1131
+ else:
1132
+ # Create a new future for the next value
1133
+ self.input_jwt = asyncio.Future()
1134
+ self.input_jwt.set_result(attempt_token)
1135
+
1136
+ # Set state to WAITING_FOR_OUTPUT only if current state is SENDING. If state is
1137
+ # RETRYING, WAITING_TO_RETRY, or COMPLETE, then we already got the output.
1138
+ if self.state == _MapItemState.SENDING:
1139
+ self.state = _MapItemState.WAITING_FOR_OUTPUT
1065
1140
 
1066
1141
  def handle_put_inputs_response(self, item: api_pb2.FunctionPutInputsResponseItem):
1067
1142
  self.input_jwt.set_result(item.input_jwt)
@@ -1088,7 +1163,7 @@ class _MapItemContext:
1088
1163
  if self.state == _MapItemState.COMPLETE:
1089
1164
  logger.debug(
1090
1165
  f"Received output for input marked as complete. Must be duplicate, so ignoring. "
1091
- f"idx={item.idx} input_id={item.input_id}, retry_count={item.retry_count}"
1166
+ f"idx={item.idx} input_id={item.input_id} retry_count={item.retry_count}"
1092
1167
  )
1093
1168
  return _OutputType.ALREADY_COMPLETE_DUPLICATE
1094
1169
  # If the item's retry count doesn't match our retry count, this is probably a duplicate of an old output.
@@ -1136,7 +1211,11 @@ class _MapItemContext:
1136
1211
 
1137
1212
  self.state = _MapItemState.WAITING_TO_RETRY
1138
1213
 
1139
- await retry_queue.put(now_seconds + (delay_ms / 1000), item.idx)
1214
+ if self._is_input_plane_instance:
1215
+ retry_item = await self.create_map_start_or_continue_item(item.idx)
1216
+ await retry_queue.put(now_seconds + delay_ms / 1_000, retry_item)
1217
+ else:
1218
+ await retry_queue.put(now_seconds + delay_ms / 1_000, item.idx)
1140
1219
 
1141
1220
  return _OutputType.RETRYING
1142
1221
 
@@ -1155,6 +1234,16 @@ class _MapItemContext:
1155
1234
  self.input_jwt.set_result(input_jwt)
1156
1235
  self.state = _MapItemState.WAITING_FOR_OUTPUT
1157
1236
 
1237
+ async def create_map_start_or_continue_item(self, idx: int) -> api_pb2.MapStartOrContinueItem:
1238
+ attempt_token = await self.input_jwt
1239
+ return api_pb2.MapStartOrContinueItem(
1240
+ input=api_pb2.FunctionPutInputsItem(
1241
+ input=self.input,
1242
+ idx=idx,
1243
+ ),
1244
+ attempt_token=attempt_token,
1245
+ )
1246
+
1158
1247
 
1159
1248
  class _MapItemsManager:
1160
1249
  def __init__(
@@ -1164,6 +1253,7 @@ class _MapItemsManager:
1164
1253
  retry_queue: TimestampPriorityQueue,
1165
1254
  sync_client_retries_enabled: bool,
1166
1255
  max_inputs_outstanding: int,
1256
+ is_input_plane_instance: bool = False,
1167
1257
  ):
1168
1258
  self._retry_policy = retry_policy
1169
1259
  self.function_call_invocation_type = function_call_invocation_type
@@ -1174,6 +1264,7 @@ class _MapItemsManager:
1174
1264
  self._inputs_outstanding = asyncio.BoundedSemaphore(max_inputs_outstanding)
1175
1265
  self._item_context: dict[int, _MapItemContext] = {}
1176
1266
  self._sync_client_retries_enabled = sync_client_retries_enabled
1267
+ self._is_input_plane_instance = is_input_plane_instance
1177
1268
 
1178
1269
  async def add_items(self, items: list[api_pb2.FunctionPutInputsItem]):
1179
1270
  for item in items:
@@ -1186,6 +1277,21 @@ class _MapItemsManager:
1186
1277
  sync_client_retries_enabled=self._sync_client_retries_enabled,
1187
1278
  )
1188
1279
 
1280
+ async def add_items_inputplane(self, items: list[api_pb2.MapStartOrContinueItem]):
1281
+ for item in items:
1282
+ # acquire semaphore to limit the number of inputs in progress
1283
+ # (either queued to be sent, waiting for completion, or retrying)
1284
+ if item.attempt_token != "": # if it is a retry item
1285
+ self._item_context[item.input.idx].state = _MapItemState.SENDING
1286
+ continue
1287
+ await self._inputs_outstanding.acquire()
1288
+ self._item_context[item.input.idx] = _MapItemContext(
1289
+ input=item.input.input,
1290
+ retry_manager=RetryManager(self._retry_policy),
1291
+ sync_client_retries_enabled=self._sync_client_retries_enabled,
1292
+ is_input_plane_instance=self._is_input_plane_instance,
1293
+ )
1294
+
1189
1295
  async def prepare_items_for_retry(self, retriable_idxs: list[int]) -> list[api_pb2.FunctionRetryInputsItem]:
1190
1296
  return [await self._item_context[idx].prepare_item_for_retry() for idx in retriable_idxs]
1191
1297
 
@@ -1200,6 +1306,17 @@ class _MapItemsManager:
1200
1306
  if ctx.state == _MapItemState.WAITING_FOR_OUTPUT and ctx.input_jwt.done()
1201
1307
  ]
1202
1308
 
1309
+ def get_input_idxs_waiting_for_output(self) -> list[tuple[int, str]]:
1310
+ """
1311
+ Returns a list of input_idxs for inputs that are waiting for output.
1312
+ """
1313
+ # Idx doesn't need a future because it is set by client and not server.
1314
+ return [
1315
+ (idx, ctx.input_jwt.result())
1316
+ for idx, ctx in self._item_context.items()
1317
+ if ctx.state == _MapItemState.WAITING_FOR_OUTPUT and ctx.input_jwt.done()
1318
+ ]
1319
+
1203
1320
  def _remove_item(self, item_idx: int):
1204
1321
  del self._item_context[item_idx]
1205
1322
  self._inputs_outstanding.release()
@@ -1207,6 +1324,18 @@ class _MapItemsManager:
1207
1324
  def get_item_context(self, item_idx: int) -> _MapItemContext:
1208
1325
  return self._item_context.get(item_idx)
1209
1326
 
1327
+ def handle_put_continue_response(
1328
+ self,
1329
+ items: list[tuple[int, str]], # idx, input_jwt
1330
+ ):
1331
+ for index, item in items:
1332
+ ctx = self._item_context.get(index, None)
1333
+ # If the context is None, then get_all_outputs() has already received a successful
1334
+ # output, and deleted the context. This happens if FunctionGetOutputs completes
1335
+ # before MapStartOrContinueResponse is received.
1336
+ if ctx is not None:
1337
+ ctx.handle_map_start_or_continue_response(item)
1338
+
1210
1339
  def handle_put_inputs_response(self, items: list[api_pb2.FunctionPutInputsResponseItem]):
1211
1340
  for item in items:
1212
1341
  ctx = self._item_context.get(item.idx, None)
@@ -1226,6 +1355,16 @@ class _MapItemsManager:
1226
1355
  if ctx is not None:
1227
1356
  ctx.handle_retry_response(input_jwt)
1228
1357
 
1358
+ async def handle_check_inputs_response(self, response: list[tuple[int, bool]]):
1359
+ for idx, lost in response:
1360
+ ctx = self._item_context.get(idx, None)
1361
+ if ctx is not None:
1362
+ if lost:
1363
+ ctx.state = _MapItemState.WAITING_TO_RETRY
1364
+ retry_item = await ctx.create_map_start_or_continue_item(idx)
1365
+ _ = ctx.retry_manager.get_delay_ms() # increment retry count but instant retry for lost inputs
1366
+ await self._retry_queue.put(time.time(), retry_item)
1367
+
1229
1368
  async def handle_get_outputs_response(self, item: api_pb2.FunctionGetOutputsItem, now_seconds: int) -> _OutputType:
1230
1369
  ctx = self._item_context.get(item.idx, None)
1231
1370
  if ctx is None: