modal 1.1.1.dev44__tar.gz → 1.1.2__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.
- {modal-1.1.1.dev44 → modal-1.1.2}/PKG-INFO +2 -2
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/__main__.py +1 -2
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_container_entrypoint.py +18 -7
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_functions.py +135 -13
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_object.py +13 -2
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_partial_function.py +8 -8
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_runtime/asgi.py +3 -2
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_runtime/container_io_manager.py +20 -14
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_runtime/container_io_manager.pyi +38 -13
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_runtime/execution_context.py +18 -2
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_runtime/execution_context.pyi +4 -1
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/blob_utils.py +83 -24
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/function_utils.py +4 -3
- modal-1.1.2/modal/_utils/time_utils.py +43 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/app.py +8 -4
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/app.pyi +8 -8
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/dict.py +14 -11
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/entry_point.py +9 -3
- modal-1.1.2/modal/cli/launch.py +195 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/profile.py +1 -0
- modal-1.1.2/modal/cli/programs/launch_instance_ssh.py +94 -0
- modal-1.1.2/modal/cli/programs/run_marimo.py +95 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/queues.py +49 -19
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/secret.py +45 -18
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/volume.py +14 -16
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/client.pyi +2 -10
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cls.py +12 -2
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cls.pyi +9 -1
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/config.py +7 -7
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/dict.py +206 -12
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/dict.pyi +358 -4
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/experimental/__init__.py +130 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/file_io.py +1 -1
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/file_io.pyi +2 -2
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/file_pattern_matcher.py +25 -16
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/functions.pyi +105 -5
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/image.py +1 -1
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/image.pyi +7 -7
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/mount.py +20 -13
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/mount.pyi +16 -3
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/network_file_system.py +8 -2
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/object.pyi +3 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/parallel_map.py +346 -101
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/parallel_map.pyi +108 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/proxy.py +2 -1
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/queue.py +199 -9
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/queue.pyi +357 -3
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/sandbox.py +6 -5
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/sandbox.pyi +17 -14
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/secret.py +196 -3
- modal-1.1.2/modal/secret.pyi +669 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/volume.py +239 -23
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/volume.pyi +405 -10
- {modal-1.1.1.dev44 → modal-1.1.2}/modal.egg-info/PKG-INFO +2 -2
- {modal-1.1.1.dev44 → modal-1.1.2}/modal.egg-info/SOURCES.txt +2 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal.egg-info/requires.txt +1 -1
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_docs/mdmd/mdmd.py +11 -1
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_proto/api.proto +36 -10
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_proto/api_grpc.py +32 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_proto/api_pb2.py +627 -597
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_proto/api_pb2.pyi +101 -17
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_proto/api_pb2_grpc.py +67 -2
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_proto/api_pb2_grpc.pyi +24 -8
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_proto/modal_api_grpc.py +2 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_version/__init__.py +1 -1
- {modal-1.1.1.dev44 → modal-1.1.2}/pyproject.toml +1 -1
- modal-1.1.1.dev44/modal/_utils/time_utils.py +0 -19
- modal-1.1.1.dev44/modal/cli/launch.py +0 -97
- modal-1.1.1.dev44/modal/secret.pyi +0 -297
- {modal-1.1.1.dev44 → modal-1.1.2}/LICENSE +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/README.md +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/__init__.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_clustered_functions.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_clustered_functions.pyi +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_ipython.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_location.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_output.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_pty.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_resolver.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_resources.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_runtime/__init__.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_runtime/telemetry.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_serialization.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_traceback.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_tunnel.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_tunnel.pyi +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_type_manager.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/__init__.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/app_utils.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/async_utils.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/deprecation.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/docker_utils.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/git_utils.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/hash_utils.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/http_utils.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/logger.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/mount_utils.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/name_utils.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/package_utils.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_utils/shell_utils.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_vendor/__init__.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_vendor/tblib.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/_watcher.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/builder/2023.12.txt +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/builder/2024.04.txt +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/builder/2024.10.txt +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/builder/2025.06.txt +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/builder/README.md +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/builder/base-images.json +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/call_graph.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/__init__.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/_download.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/_traceback.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/app.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/cluster.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/config.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/container.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/environment.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/import_refs.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/network_file_system.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/programs/__init__.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/programs/vscode.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/run.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/token.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cli/utils.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/client.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/container_process.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/container_process.pyi +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/environments.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/environments.pyi +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/exception.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/experimental/flash.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/experimental/flash.pyi +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/experimental/ipython.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/functions.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/gpu.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/io_streams.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/io_streams.pyi +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/network_file_system.pyi +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/object.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/output.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/partial_function.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/partial_function.pyi +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/proxy.pyi +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/py.typed +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/retries.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/runner.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/runner.pyi +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/running_app.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/schedule.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/scheduler_placement.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/serving.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/serving.pyi +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/snapshot.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/snapshot.pyi +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/stream_type.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/token_flow.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal/token_flow.pyi +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal.egg-info/top_level.txt +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_docs/__init__.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_proto/__init__.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_proto/modal_options_grpc.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_proto/options.proto +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_proto/options_grpc.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_proto/options_pb2.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_proto/options_pb2.pyi +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_proto/options_pb2_grpc.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_proto/options_pb2_grpc.pyi +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_proto/py.typed +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/modal_version/__main__.py +0 -0
- {modal-1.1.1.dev44 → modal-1.1.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modal
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.2
|
|
4
4
|
Summary: Python client library for Modal
|
|
5
5
|
Author-email: Modal Labs <support@modal.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -22,7 +22,7 @@ Requires-Dist: click~=8.1
|
|
|
22
22
|
Requires-Dist: grpclib<0.4.9,>=0.4.7
|
|
23
23
|
Requires-Dist: protobuf!=4.24.0,<7.0,>=3.19
|
|
24
24
|
Requires-Dist: rich>=12.0.0
|
|
25
|
-
Requires-Dist: synchronicity~=0.10.
|
|
25
|
+
Requires-Dist: synchronicity~=0.10.2
|
|
26
26
|
Requires-Dist: toml
|
|
27
27
|
Requires-Dist: typer>=0.9
|
|
28
28
|
Requires-Dist: types-certifi
|
|
@@ -37,7 +37,6 @@ def main():
|
|
|
37
37
|
|
|
38
38
|
from grpclib import GRPCError, Status
|
|
39
39
|
from rich.panel import Panel
|
|
40
|
-
from rich.text import Text
|
|
41
40
|
|
|
42
41
|
if isinstance(exc, GRPCError):
|
|
43
42
|
status_map = {
|
|
@@ -69,7 +68,7 @@ def main():
|
|
|
69
68
|
content = f"{content}\n\nNote: {' '.join(notes)}"
|
|
70
69
|
|
|
71
70
|
console = make_console(stderr=True)
|
|
72
|
-
panel = Panel(
|
|
71
|
+
panel = Panel(content, title=title, title_align="left", border_style="red")
|
|
73
72
|
console.print(panel, highlight=False)
|
|
74
73
|
sys.exit(1)
|
|
75
74
|
|
|
@@ -185,8 +185,9 @@ def call_function(
|
|
|
185
185
|
):
|
|
186
186
|
async def run_input_async(io_context: IOContext) -> None:
|
|
187
187
|
started_at = time.time()
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
reset_context = execution_context._set_current_context_ids(
|
|
189
|
+
io_context.input_ids, io_context.function_call_ids, io_context.attempt_tokens
|
|
190
|
+
)
|
|
190
191
|
async with container_io_manager.handle_input_exception.aio(io_context, started_at):
|
|
191
192
|
res = io_context.call_finalized_function()
|
|
192
193
|
# TODO(erikbern): any exception below shouldn't be considered a user exception
|
|
@@ -195,9 +196,14 @@ def call_function(
|
|
|
195
196
|
raise InvalidError(f"Async generator function returned value of type {type(res)}")
|
|
196
197
|
|
|
197
198
|
# Send up to this many outputs at a time.
|
|
199
|
+
current_function_call_id = execution_context.current_function_call_id()
|
|
200
|
+
assert current_function_call_id is not None # Set above.
|
|
201
|
+
current_attempt_token = execution_context.current_attempt_token()
|
|
202
|
+
assert current_attempt_token is not None # Set above, but can be empty string.
|
|
198
203
|
generator_queue: asyncio.Queue[Any] = await container_io_manager._queue_create.aio(1024)
|
|
199
204
|
async with container_io_manager.generator_output_sender(
|
|
200
|
-
|
|
205
|
+
current_function_call_id,
|
|
206
|
+
current_attempt_token,
|
|
201
207
|
io_context.finalized_function.data_format,
|
|
202
208
|
generator_queue,
|
|
203
209
|
):
|
|
@@ -230,8 +236,9 @@ def call_function(
|
|
|
230
236
|
|
|
231
237
|
def run_input_sync(io_context: IOContext) -> None:
|
|
232
238
|
started_at = time.time()
|
|
233
|
-
|
|
234
|
-
|
|
239
|
+
reset_context = execution_context._set_current_context_ids(
|
|
240
|
+
io_context.input_ids, io_context.function_call_ids, io_context.attempt_tokens
|
|
241
|
+
)
|
|
235
242
|
with container_io_manager.handle_input_exception(io_context, started_at):
|
|
236
243
|
res = io_context.call_finalized_function()
|
|
237
244
|
|
|
@@ -241,10 +248,14 @@ def call_function(
|
|
|
241
248
|
raise InvalidError(f"Generator function returned value of type {type(res)}")
|
|
242
249
|
|
|
243
250
|
# Send up to this many outputs at a time.
|
|
251
|
+
current_function_call_id = execution_context.current_function_call_id()
|
|
252
|
+
assert current_function_call_id is not None # Set above.
|
|
253
|
+
current_attempt_token = execution_context.current_attempt_token()
|
|
254
|
+
assert current_attempt_token is not None # Set above, but can be empty string.
|
|
244
255
|
generator_queue: asyncio.Queue[Any] = container_io_manager._queue_create(1024)
|
|
245
|
-
|
|
246
256
|
with container_io_manager.generator_output_sender(
|
|
247
|
-
|
|
257
|
+
current_function_call_id,
|
|
258
|
+
current_attempt_token,
|
|
248
259
|
io_context.finalized_function.data_format,
|
|
249
260
|
generator_queue,
|
|
250
261
|
):
|
|
@@ -9,7 +9,7 @@ import warnings
|
|
|
9
9
|
from collections.abc import AsyncGenerator, Sequence, Sized
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from pathlib import PurePosixPath
|
|
12
|
-
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
|
12
|
+
from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Optional, Union
|
|
13
13
|
|
|
14
14
|
import typing_extensions
|
|
15
15
|
from google.protobuf.message import Message
|
|
@@ -71,6 +71,8 @@ from .mount import _get_client_mount, _Mount
|
|
|
71
71
|
from .network_file_system import _NetworkFileSystem, network_file_system_mount_protos
|
|
72
72
|
from .output import _get_output_manager
|
|
73
73
|
from .parallel_map import (
|
|
74
|
+
_experimental_spawn_map_async,
|
|
75
|
+
_experimental_spawn_map_sync,
|
|
74
76
|
_for_each_async,
|
|
75
77
|
_for_each_sync,
|
|
76
78
|
_map_async,
|
|
@@ -78,6 +80,7 @@ from .parallel_map import (
|
|
|
78
80
|
_map_invocation_inputplane,
|
|
79
81
|
_map_sync,
|
|
80
82
|
_spawn_map_async,
|
|
83
|
+
_spawn_map_invocation,
|
|
81
84
|
_spawn_map_sync,
|
|
82
85
|
_starmap_async,
|
|
83
86
|
_starmap_sync,
|
|
@@ -214,7 +217,11 @@ class _Invocation:
|
|
|
214
217
|
return _Invocation(stub, function_call_id, client, retry_context)
|
|
215
218
|
|
|
216
219
|
async def pop_function_call_outputs(
|
|
217
|
-
self,
|
|
220
|
+
self,
|
|
221
|
+
index: int = 0,
|
|
222
|
+
timeout: Optional[float] = None,
|
|
223
|
+
clear_on_success: bool = False,
|
|
224
|
+
input_jwts: Optional[list[str]] = None,
|
|
218
225
|
) -> api_pb2.FunctionGetOutputsResponse:
|
|
219
226
|
t0 = time.time()
|
|
220
227
|
if timeout is None:
|
|
@@ -232,6 +239,8 @@ class _Invocation:
|
|
|
232
239
|
clear_on_success=clear_on_success,
|
|
233
240
|
requested_at=time.time(),
|
|
234
241
|
input_jwts=input_jwts,
|
|
242
|
+
start_idx=index,
|
|
243
|
+
end_idx=index,
|
|
235
244
|
)
|
|
236
245
|
response: api_pb2.FunctionGetOutputsResponse = await retry_transient_errors(
|
|
237
246
|
self.stub.FunctionGetOutputs,
|
|
@@ -265,6 +274,7 @@ class _Invocation:
|
|
|
265
274
|
# waits indefinitely for a single result for the function, and clear the outputs buffer after
|
|
266
275
|
item: api_pb2.FunctionGetOutputsItem = (
|
|
267
276
|
await self.pop_function_call_outputs(
|
|
277
|
+
index=0,
|
|
268
278
|
timeout=None,
|
|
269
279
|
clear_on_success=True,
|
|
270
280
|
input_jwts=[expected_jwt] if expected_jwt else None,
|
|
@@ -308,14 +318,16 @@ class _Invocation:
|
|
|
308
318
|
|
|
309
319
|
await self._retry_input()
|
|
310
320
|
|
|
311
|
-
async def poll_function(self, timeout: Optional[float] = None):
|
|
321
|
+
async def poll_function(self, timeout: Optional[float] = None, *, index: int = 0):
|
|
312
322
|
"""Waits up to timeout for a result from a function.
|
|
313
323
|
|
|
314
324
|
If timeout is `None`, waits indefinitely. This function is not
|
|
315
325
|
cancellation-safe.
|
|
316
326
|
"""
|
|
317
327
|
response: api_pb2.FunctionGetOutputsResponse = await self.pop_function_call_outputs(
|
|
318
|
-
|
|
328
|
+
index=index,
|
|
329
|
+
timeout=timeout,
|
|
330
|
+
clear_on_success=False,
|
|
319
331
|
)
|
|
320
332
|
if len(response.outputs) == 0 and response.num_unfinished_inputs == 0:
|
|
321
333
|
# if no unfinished inputs and no outputs, then function expired
|
|
@@ -348,11 +360,47 @@ class _Invocation:
|
|
|
348
360
|
if items_total is not None and items_received >= items_total:
|
|
349
361
|
break
|
|
350
362
|
|
|
363
|
+
async def enumerate(self, start_index: int, end_index: int):
|
|
364
|
+
"""Iterate over the results of the function call in the range [start_index, end_index)."""
|
|
365
|
+
limit = 49
|
|
366
|
+
current_index = start_index
|
|
367
|
+
while current_index < end_index:
|
|
368
|
+
# batch_end_indx is inclusive, so we subtract 1 to get the last index in the batch.
|
|
369
|
+
batch_end_index = min(current_index + limit, end_index) - 1
|
|
370
|
+
request = api_pb2.FunctionGetOutputsRequest(
|
|
371
|
+
function_call_id=self.function_call_id,
|
|
372
|
+
timeout=0,
|
|
373
|
+
last_entry_id="0-0",
|
|
374
|
+
clear_on_success=False,
|
|
375
|
+
requested_at=time.time(),
|
|
376
|
+
start_idx=current_index,
|
|
377
|
+
end_idx=batch_end_index,
|
|
378
|
+
)
|
|
379
|
+
response: api_pb2.FunctionGetOutputsResponse = await retry_transient_errors(
|
|
380
|
+
self.stub.FunctionGetOutputs,
|
|
381
|
+
request,
|
|
382
|
+
attempt_timeout=ATTEMPT_TIMEOUT_GRACE_PERIOD,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
outputs = list(response.outputs)
|
|
386
|
+
outputs.sort(key=lambda x: x.idx)
|
|
387
|
+
for output in outputs:
|
|
388
|
+
if output.idx != current_index:
|
|
389
|
+
break
|
|
390
|
+
result = await _process_result(output.result, output.data_format, self.stub, self.client)
|
|
391
|
+
yield output.idx, result
|
|
392
|
+
current_index += 1
|
|
393
|
+
|
|
394
|
+
# We're missing current_index, so we need to poll the function for the next result
|
|
395
|
+
if len(outputs) < (batch_end_index - current_index + 1):
|
|
396
|
+
result = await self.poll_function(index=current_index)
|
|
397
|
+
yield current_index, result
|
|
398
|
+
current_index += 1
|
|
399
|
+
|
|
351
400
|
|
|
352
401
|
class _InputPlaneInvocation:
|
|
353
402
|
"""Internal client representation of a single-input call to a Modal Function using the input
|
|
354
|
-
plane server API.
|
|
355
|
-
It is OK to make breaking changes to this class."""
|
|
403
|
+
plane server API."""
|
|
356
404
|
|
|
357
405
|
stub: ModalClientModal
|
|
358
406
|
|
|
@@ -462,7 +510,7 @@ class _InputPlaneInvocation:
|
|
|
462
510
|
_stream_function_call_data(
|
|
463
511
|
self.client,
|
|
464
512
|
self.stub,
|
|
465
|
-
|
|
513
|
+
function_call_id=None,
|
|
466
514
|
variant="data_out",
|
|
467
515
|
attempt_token=self.attempt_token,
|
|
468
516
|
),
|
|
@@ -603,7 +651,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
603
651
|
memory: Optional[Union[int, tuple[int, int]]] = None,
|
|
604
652
|
proxy: Optional[_Proxy] = None,
|
|
605
653
|
retries: Optional[Union[int, Retries]] = None,
|
|
606
|
-
timeout:
|
|
654
|
+
timeout: int = 300,
|
|
607
655
|
min_containers: Optional[int] = None,
|
|
608
656
|
max_containers: Optional[int] = None,
|
|
609
657
|
buffer_containers: Optional[int] = None,
|
|
@@ -1130,6 +1178,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1130
1178
|
target_concurrent_inputs=options.target_concurrent_inputs,
|
|
1131
1179
|
batch_max_size=options.batch_max_size,
|
|
1132
1180
|
batch_linger_ms=options.batch_wait_ms,
|
|
1181
|
+
scheduler_placement=options.scheduler_placement,
|
|
1182
|
+
cloud_provider_str=options.cloud,
|
|
1133
1183
|
)
|
|
1134
1184
|
else:
|
|
1135
1185
|
options_pb = None
|
|
@@ -1274,7 +1324,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1274
1324
|
|
|
1275
1325
|
self._hydrate(response.function_id, resolver.client, response.handle_metadata)
|
|
1276
1326
|
|
|
1277
|
-
|
|
1327
|
+
environment_rep = f", environment_name={environment_name!r}" if environment_name else ""
|
|
1328
|
+
rep = f"modal.Function.from_name('{app_name}', '{name}'{environment_rep})"
|
|
1278
1329
|
return cls._from_loader(_load_remote, rep, is_another_app=True, hydrate_lazily=True)
|
|
1279
1330
|
|
|
1280
1331
|
@classmethod
|
|
@@ -1543,6 +1594,22 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1543
1594
|
async for item in stream:
|
|
1544
1595
|
yield item
|
|
1545
1596
|
|
|
1597
|
+
@live_method
|
|
1598
|
+
async def _spawn_map(self, input_queue: _SynchronizedQueue) -> "_FunctionCall[ReturnType]":
|
|
1599
|
+
self._check_no_web_url("spawn_map")
|
|
1600
|
+
if self._is_generator:
|
|
1601
|
+
raise InvalidError("A generator function cannot be called with `.spawn_map(...)`.")
|
|
1602
|
+
|
|
1603
|
+
assert self._function_name
|
|
1604
|
+
function_call_id, num_inputs = await _spawn_map_invocation(
|
|
1605
|
+
self,
|
|
1606
|
+
input_queue,
|
|
1607
|
+
self.client,
|
|
1608
|
+
)
|
|
1609
|
+
metadata = api_pb2.FunctionCallFromIdResponse(function_call_id=function_call_id, num_inputs=num_inputs)
|
|
1610
|
+
fc: _FunctionCall[ReturnType] = _FunctionCall._new_hydrated(function_call_id, self.client, metadata)
|
|
1611
|
+
return fc
|
|
1612
|
+
|
|
1546
1613
|
async def _call_function(self, args, kwargs) -> ReturnType:
|
|
1547
1614
|
invocation: Union[_Invocation, _InputPlaneInvocation]
|
|
1548
1615
|
if self._input_plane_url:
|
|
@@ -1789,6 +1856,7 @@ Use the `Function.get_web_url()` method instead.
|
|
|
1789
1856
|
starmap = MethodWithAio(_starmap_sync, _starmap_async, synchronizer)
|
|
1790
1857
|
for_each = MethodWithAio(_for_each_sync, _for_each_async, synchronizer)
|
|
1791
1858
|
spawn_map = MethodWithAio(_spawn_map_sync, _spawn_map_async, synchronizer)
|
|
1859
|
+
experimental_spawn_map = MethodWithAio(_experimental_spawn_map_sync, _experimental_spawn_map_async, synchronizer)
|
|
1792
1860
|
|
|
1793
1861
|
|
|
1794
1862
|
class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
@@ -1803,12 +1871,28 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
1803
1871
|
"""
|
|
1804
1872
|
|
|
1805
1873
|
_is_generator: bool = False
|
|
1874
|
+
_num_inputs: Optional[int] = None
|
|
1806
1875
|
|
|
1807
1876
|
def _invocation(self):
|
|
1808
1877
|
return _Invocation(self.client.stub, self.object_id, self.client)
|
|
1809
1878
|
|
|
1810
|
-
|
|
1811
|
-
|
|
1879
|
+
def _hydrate_metadata(self, metadata: Optional[Message]):
|
|
1880
|
+
if not metadata:
|
|
1881
|
+
return
|
|
1882
|
+
assert isinstance(metadata, api_pb2.FunctionCallFromIdResponse)
|
|
1883
|
+
self._num_inputs = metadata.num_inputs
|
|
1884
|
+
|
|
1885
|
+
@live_method
|
|
1886
|
+
async def num_inputs(self) -> int:
|
|
1887
|
+
"""Get the number of inputs in the function call."""
|
|
1888
|
+
# Should have been hydrated.
|
|
1889
|
+
assert self._num_inputs is not None
|
|
1890
|
+
return self._num_inputs
|
|
1891
|
+
|
|
1892
|
+
async def get(self, timeout: Optional[float] = None, *, index: int = 0) -> ReturnType:
|
|
1893
|
+
"""Get the result of the index-th input of the function call.
|
|
1894
|
+
`.spawn()` calls have a single output, so only specifying `index=0` is valid.
|
|
1895
|
+
A non-zero index is useful when your function has multiple outputs, like via `.spawn_map()`.
|
|
1812
1896
|
|
|
1813
1897
|
This function waits indefinitely by default. It takes an optional
|
|
1814
1898
|
`timeout` argument that specifies the maximum number of seconds to wait,
|
|
@@ -1816,7 +1900,37 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
1816
1900
|
|
|
1817
1901
|
The returned coroutine is not cancellation-safe.
|
|
1818
1902
|
"""
|
|
1819
|
-
return await self._invocation().poll_function(timeout=timeout)
|
|
1903
|
+
return await self._invocation().poll_function(timeout=timeout, index=index)
|
|
1904
|
+
|
|
1905
|
+
@live_method_gen
|
|
1906
|
+
async def iter(self, *, start: int = 0, end: Optional[int] = None) -> AsyncIterator[ReturnType]:
|
|
1907
|
+
"""Iterate in-order over the results of the function call.
|
|
1908
|
+
|
|
1909
|
+
Optionally, specify a range [start, end) to iterate over.
|
|
1910
|
+
|
|
1911
|
+
Example:
|
|
1912
|
+
```python
|
|
1913
|
+
@app.function()
|
|
1914
|
+
def my_func(a):
|
|
1915
|
+
return a ** 2
|
|
1916
|
+
|
|
1917
|
+
|
|
1918
|
+
@app.local_entrypoint()
|
|
1919
|
+
def main():
|
|
1920
|
+
fc = my_func.spawn_map([1, 2, 3, 4])
|
|
1921
|
+
assert list(fc.iter()) == [1, 4, 9, 16]
|
|
1922
|
+
assert list(fc.iter(start=1, end=3)) == [4, 9]
|
|
1923
|
+
```
|
|
1924
|
+
|
|
1925
|
+
If `end` is not provided, it will iterate over all results.
|
|
1926
|
+
"""
|
|
1927
|
+
num_inputs = await self.num_inputs()
|
|
1928
|
+
if end is None:
|
|
1929
|
+
end = num_inputs
|
|
1930
|
+
if start < 0 or end > num_inputs:
|
|
1931
|
+
raise ValueError(f"Invalid index range: {start} to {end} for {num_inputs} inputs")
|
|
1932
|
+
async for _, item in self._invocation().enumerate(start_index=start, end_index=end):
|
|
1933
|
+
yield item
|
|
1820
1934
|
|
|
1821
1935
|
async def get_call_graph(self) -> list[InputInfo]:
|
|
1822
1936
|
"""Returns a structure representing the call graph from a given root
|
|
@@ -1870,7 +1984,15 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
1870
1984
|
if client is None:
|
|
1871
1985
|
client = await _Client.from_env()
|
|
1872
1986
|
|
|
1873
|
-
|
|
1987
|
+
async def _load(self: _FunctionCall, resolver: Resolver, existing_object_id: Optional[str]):
|
|
1988
|
+
request = api_pb2.FunctionCallFromIdRequest(function_call_id=function_call_id)
|
|
1989
|
+
resp = await retry_transient_errors(resolver.client.stub.FunctionCallFromId, request)
|
|
1990
|
+
self._hydrate(function_call_id, resolver.client, resp)
|
|
1991
|
+
|
|
1992
|
+
rep = f"FunctionCall.from_id({function_call_id!r})"
|
|
1993
|
+
fc: _FunctionCall[Any] = _FunctionCall._from_loader(_load, rep, hydrate_lazily=True)
|
|
1994
|
+
# We already know the object ID, so we can set it directly
|
|
1995
|
+
fc._object_id = function_call_id
|
|
1874
1996
|
return fc
|
|
1875
1997
|
|
|
1876
1998
|
@staticmethod
|
|
@@ -191,9 +191,20 @@ class _Object:
|
|
|
191
191
|
def _is_id_type(cls, object_id) -> bool:
|
|
192
192
|
return cls._get_type_from_id(object_id) == cls
|
|
193
193
|
|
|
194
|
+
@classmethod
|
|
195
|
+
def _repr(cls, name: str, environment_name: Optional[str] = None) -> str:
|
|
196
|
+
public_cls = cls.__name__.strip("_")
|
|
197
|
+
environment_repr = f", environment_name={environment_name!r}" if environment_name else ""
|
|
198
|
+
return f"modal.{public_cls}.from_name({name!r}{environment_repr})"
|
|
199
|
+
|
|
194
200
|
@classmethod
|
|
195
201
|
def _new_hydrated(
|
|
196
|
-
cls,
|
|
202
|
+
cls,
|
|
203
|
+
object_id: str,
|
|
204
|
+
client: _Client,
|
|
205
|
+
handle_metadata: Optional[Message],
|
|
206
|
+
is_another_app: bool = False,
|
|
207
|
+
rep: Optional[str] = None,
|
|
197
208
|
) -> Self:
|
|
198
209
|
obj_cls: type[Self]
|
|
199
210
|
if cls._type_prefix is not None:
|
|
@@ -210,7 +221,7 @@ class _Object:
|
|
|
210
221
|
|
|
211
222
|
# Instantiate provider
|
|
212
223
|
obj = _Object.__new__(obj_cls)
|
|
213
|
-
rep = f"
|
|
224
|
+
rep = rep or f"modal.{obj_cls.__name__.strip('_')}.from_id({object_id!r})"
|
|
214
225
|
obj._init(rep, is_another_app=is_another_app)
|
|
215
226
|
obj._hydrate(object_id, client, handle_metadata)
|
|
216
227
|
|
|
@@ -282,7 +282,7 @@ class _MethodDecoratorType:
|
|
|
282
282
|
|
|
283
283
|
# TODO(elias): fix support for coroutine type unwrapping for methods (static typing)
|
|
284
284
|
def _method(
|
|
285
|
-
_warn_parentheses_missing=None,
|
|
285
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
286
286
|
*,
|
|
287
287
|
# Set this to True if it's a non-generator function returning
|
|
288
288
|
# a [sync/async] generator object
|
|
@@ -337,7 +337,7 @@ def _parse_custom_domains(custom_domains: Optional[Iterable[str]] = None) -> lis
|
|
|
337
337
|
|
|
338
338
|
|
|
339
339
|
def _fastapi_endpoint(
|
|
340
|
-
_warn_parentheses_missing=None,
|
|
340
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
341
341
|
*,
|
|
342
342
|
method: str = "GET", # REST method for the created endpoint.
|
|
343
343
|
label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
|
|
@@ -400,7 +400,7 @@ def _fastapi_endpoint(
|
|
|
400
400
|
|
|
401
401
|
|
|
402
402
|
def _web_endpoint(
|
|
403
|
-
_warn_parentheses_missing=None,
|
|
403
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
404
404
|
*,
|
|
405
405
|
method: str = "GET", # REST method for the created endpoint.
|
|
406
406
|
label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
|
|
@@ -468,7 +468,7 @@ def _web_endpoint(
|
|
|
468
468
|
|
|
469
469
|
|
|
470
470
|
def _asgi_app(
|
|
471
|
-
_warn_parentheses_missing=None,
|
|
471
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
472
472
|
*,
|
|
473
473
|
label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
|
|
474
474
|
custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
|
|
@@ -525,7 +525,7 @@ def _asgi_app(
|
|
|
525
525
|
|
|
526
526
|
|
|
527
527
|
def _wsgi_app(
|
|
528
|
-
_warn_parentheses_missing=None,
|
|
528
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
529
529
|
*,
|
|
530
530
|
label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
|
|
531
531
|
custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
|
|
@@ -645,7 +645,7 @@ def _web_server(
|
|
|
645
645
|
|
|
646
646
|
|
|
647
647
|
def _enter(
|
|
648
|
-
_warn_parentheses_missing=None,
|
|
648
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
649
649
|
*,
|
|
650
650
|
snap: bool = False,
|
|
651
651
|
) -> Callable[[Union[_PartialFunction, NullaryMethod]], _PartialFunction]:
|
|
@@ -696,7 +696,7 @@ def _exit(_warn_parentheses_missing=None) -> Callable[[NullaryMethod], _PartialF
|
|
|
696
696
|
|
|
697
697
|
|
|
698
698
|
def _batched(
|
|
699
|
-
_warn_parentheses_missing=None,
|
|
699
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
700
700
|
*,
|
|
701
701
|
max_batch_size: int,
|
|
702
702
|
wait_ms: int,
|
|
@@ -758,7 +758,7 @@ def _batched(
|
|
|
758
758
|
|
|
759
759
|
|
|
760
760
|
def _concurrent(
|
|
761
|
-
_warn_parentheses_missing=None,
|
|
761
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
762
762
|
*,
|
|
763
763
|
max_inputs: int, # Hard limit on each container's input concurrency
|
|
764
764
|
target_inputs: Optional[int] = None, # Input concurrency that Modal's autoscaler should target
|
|
@@ -16,7 +16,7 @@ from modal.config import logger
|
|
|
16
16
|
from modal.exception import ExecutionError, InvalidError
|
|
17
17
|
from modal.experimental import stop_fetching_inputs
|
|
18
18
|
|
|
19
|
-
from .execution_context import current_function_call_id
|
|
19
|
+
from .execution_context import current_attempt_token, current_function_call_id
|
|
20
20
|
|
|
21
21
|
FIRST_MESSAGE_TIMEOUT_SECONDS = 5.0
|
|
22
22
|
|
|
@@ -106,6 +106,7 @@ def asgi_app_wrapper(asgi_app, container_io_manager) -> tuple[Callable[..., Asyn
|
|
|
106
106
|
raise ExecutionError("Unpexected state in ASGI scope")
|
|
107
107
|
scope["state"] = state
|
|
108
108
|
function_call_id = current_function_call_id()
|
|
109
|
+
attempt_token = current_attempt_token()
|
|
109
110
|
assert function_call_id, "internal error: function_call_id not set in asgi_app() scope"
|
|
110
111
|
|
|
111
112
|
messages_from_app: asyncio.Queue[dict[str, Any]] = asyncio.Queue(1)
|
|
@@ -142,7 +143,7 @@ def asgi_app_wrapper(asgi_app, container_io_manager) -> tuple[Callable[..., Asyn
|
|
|
142
143
|
# This initial message, "http.request" or "websocket.connect", should be sent
|
|
143
144
|
# immediately after starting the ASGI app's function call. If it is not received, that
|
|
144
145
|
# indicates a request cancellation or other abnormal circumstance.
|
|
145
|
-
message_gen = container_io_manager.get_data_in.aio(function_call_id)
|
|
146
|
+
message_gen = container_io_manager.get_data_in.aio(function_call_id, attempt_token)
|
|
146
147
|
first_message_task = asyncio.create_task(message_gen.__anext__())
|
|
147
148
|
|
|
148
149
|
try:
|
|
@@ -39,7 +39,6 @@ from modal.exception import ClientClosed, InputCancellation, InvalidError, Seria
|
|
|
39
39
|
from modal_proto import api_pb2
|
|
40
40
|
|
|
41
41
|
if TYPE_CHECKING:
|
|
42
|
-
import modal._runtime.asgi
|
|
43
42
|
import modal._runtime.user_code_imports
|
|
44
43
|
|
|
45
44
|
|
|
@@ -66,6 +65,7 @@ class IOContext:
|
|
|
66
65
|
input_ids: list[str]
|
|
67
66
|
retry_counts: list[int]
|
|
68
67
|
function_call_ids: list[str]
|
|
68
|
+
attempt_tokens: list[str]
|
|
69
69
|
function_inputs: list[api_pb2.FunctionInput]
|
|
70
70
|
finalized_function: "modal._runtime.user_code_imports.FinalizedFunction"
|
|
71
71
|
|
|
@@ -77,6 +77,7 @@ class IOContext:
|
|
|
77
77
|
input_ids: list[str],
|
|
78
78
|
retry_counts: list[int],
|
|
79
79
|
function_call_ids: list[str],
|
|
80
|
+
attempt_tokens: list[str],
|
|
80
81
|
finalized_function: "modal._runtime.user_code_imports.FinalizedFunction",
|
|
81
82
|
function_inputs: list[api_pb2.FunctionInput],
|
|
82
83
|
is_batched: bool,
|
|
@@ -85,6 +86,7 @@ class IOContext:
|
|
|
85
86
|
self.input_ids = input_ids
|
|
86
87
|
self.retry_counts = retry_counts
|
|
87
88
|
self.function_call_ids = function_call_ids
|
|
89
|
+
self.attempt_tokens = attempt_tokens
|
|
88
90
|
self.finalized_function = finalized_function
|
|
89
91
|
self.function_inputs = function_inputs
|
|
90
92
|
self._is_batched = is_batched
|
|
@@ -95,11 +97,11 @@ class IOContext:
|
|
|
95
97
|
cls,
|
|
96
98
|
client: _Client,
|
|
97
99
|
finalized_functions: dict[str, "modal._runtime.user_code_imports.FinalizedFunction"],
|
|
98
|
-
inputs: list[tuple[str, int, str, api_pb2.FunctionInput]],
|
|
100
|
+
inputs: list[tuple[str, int, str, str, api_pb2.FunctionInput]],
|
|
99
101
|
is_batched: bool,
|
|
100
102
|
) -> "IOContext":
|
|
101
103
|
assert len(inputs) >= 1 if is_batched else len(inputs) == 1
|
|
102
|
-
input_ids, retry_counts, function_call_ids, function_inputs = zip(*inputs)
|
|
104
|
+
input_ids, retry_counts, function_call_ids, attempt_tokens, function_inputs = zip(*inputs)
|
|
103
105
|
|
|
104
106
|
async def _populate_input_blobs(client: _Client, input: api_pb2.FunctionInput) -> api_pb2.FunctionInput:
|
|
105
107
|
# If we got a pointer to a blob, download it from S3.
|
|
@@ -121,6 +123,7 @@ class IOContext:
|
|
|
121
123
|
cast(list[str], input_ids),
|
|
122
124
|
cast(list[int], retry_counts),
|
|
123
125
|
cast(list[str], function_call_ids),
|
|
126
|
+
cast(list[str], attempt_tokens),
|
|
124
127
|
finalized_function,
|
|
125
128
|
cast(list[api_pb2.FunctionInput], function_inputs),
|
|
126
129
|
is_batched,
|
|
@@ -300,11 +303,7 @@ class _ContainerIOManager:
|
|
|
300
303
|
self.function_def = container_args.function_def
|
|
301
304
|
self.checkpoint_id = container_args.checkpoint_id or None
|
|
302
305
|
|
|
303
|
-
|
|
304
|
-
self.input_plane_server_url = None
|
|
305
|
-
for obj in container_args.app_layout.objects:
|
|
306
|
-
if obj.object_id == self.function_id:
|
|
307
|
-
self.input_plane_server_url = obj.function_handle_metadata.input_plane_url
|
|
306
|
+
self.input_plane_server_url = container_args.input_plane_server_url
|
|
308
307
|
|
|
309
308
|
self.calls_completed = 0
|
|
310
309
|
self.total_user_time = 0.0
|
|
@@ -484,18 +483,21 @@ class _ContainerIOManager:
|
|
|
484
483
|
else {"data": data}
|
|
485
484
|
)
|
|
486
485
|
|
|
487
|
-
async def get_data_in(self, function_call_id: str) -> AsyncIterator[Any]:
|
|
486
|
+
async def get_data_in(self, function_call_id: str, attempt_token: Optional[str]) -> AsyncIterator[Any]:
|
|
488
487
|
"""Read from the `data_in` stream of a function call."""
|
|
489
488
|
stub = self._client.stub
|
|
490
489
|
if self.input_plane_server_url:
|
|
491
490
|
stub = await self._client.get_stub(self.input_plane_server_url)
|
|
492
491
|
|
|
493
|
-
async for data in _stream_function_call_data(
|
|
492
|
+
async for data in _stream_function_call_data(
|
|
493
|
+
self._client, stub, function_call_id, variant="data_in", attempt_token=attempt_token
|
|
494
|
+
):
|
|
494
495
|
yield data
|
|
495
496
|
|
|
496
497
|
async def put_data_out(
|
|
497
498
|
self,
|
|
498
499
|
function_call_id: str,
|
|
500
|
+
attempt_token: str,
|
|
499
501
|
start_index: int,
|
|
500
502
|
data_format: int,
|
|
501
503
|
serialized_messages: list[Any],
|
|
@@ -516,6 +518,8 @@ class _ContainerIOManager:
|
|
|
516
518
|
data_chunks.append(chunk)
|
|
517
519
|
|
|
518
520
|
req = api_pb2.FunctionCallPutDataRequest(function_call_id=function_call_id, data_chunks=data_chunks)
|
|
521
|
+
if attempt_token:
|
|
522
|
+
req.attempt_token = attempt_token # oneof clears function_call_id.
|
|
519
523
|
|
|
520
524
|
if self.input_plane_server_url:
|
|
521
525
|
stub = await self._client.get_stub(self.input_plane_server_url)
|
|
@@ -525,7 +529,7 @@ class _ContainerIOManager:
|
|
|
525
529
|
|
|
526
530
|
@asynccontextmanager
|
|
527
531
|
async def generator_output_sender(
|
|
528
|
-
self, function_call_id: str, data_format: int, message_rx: asyncio.Queue
|
|
532
|
+
self, function_call_id: str, attempt_token: str, data_format: int, message_rx: asyncio.Queue
|
|
529
533
|
) -> AsyncGenerator[None, None]:
|
|
530
534
|
"""Runs background task that feeds generator outputs into a function call's `data_out` stream."""
|
|
531
535
|
GENERATOR_STOP_SENTINEL = Sentinel()
|
|
@@ -554,7 +558,7 @@ class _ContainerIOManager:
|
|
|
554
558
|
else:
|
|
555
559
|
serialized_messages.append(serialize_data_format(message, data_format))
|
|
556
560
|
total_size += len(serialized_messages[-1]) + 512 # 512 bytes for estimated framing overhead
|
|
557
|
-
await self.put_data_out(function_call_id, index, data_format, serialized_messages)
|
|
561
|
+
await self.put_data_out(function_call_id, attempt_token, index, data_format, serialized_messages)
|
|
558
562
|
index += len(serialized_messages)
|
|
559
563
|
|
|
560
564
|
task = asyncio.create_task(generator_output_task())
|
|
@@ -590,7 +594,7 @@ class _ContainerIOManager:
|
|
|
590
594
|
self,
|
|
591
595
|
batch_max_size: int,
|
|
592
596
|
batch_wait_ms: int,
|
|
593
|
-
) -> AsyncIterator[list[tuple[str, int, str, api_pb2.FunctionInput]]]:
|
|
597
|
+
) -> AsyncIterator[list[tuple[str, int, str, str, api_pb2.FunctionInput]]]:
|
|
594
598
|
request = api_pb2.FunctionGetInputsRequest(function_id=self.function_id)
|
|
595
599
|
iteration = 0
|
|
596
600
|
while self._fetching_inputs:
|
|
@@ -625,7 +629,9 @@ class _ContainerIOManager:
|
|
|
625
629
|
if item.kill_switch:
|
|
626
630
|
logger.debug(f"Task {self.task_id} input kill signal input.")
|
|
627
631
|
return
|
|
628
|
-
inputs.append(
|
|
632
|
+
inputs.append(
|
|
633
|
+
(item.input_id, item.retry_count, item.function_call_id, item.attempt_token, item.input)
|
|
634
|
+
)
|
|
629
635
|
if item.input.final_input:
|
|
630
636
|
if request.batch_max_size > 0:
|
|
631
637
|
logger.debug(f"Task {self.task_id} Final input not expected in batch input stream")
|