modAL 1.2.2.dev38__tar.gz → 1.2.3__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.
- {modal-1.2.2.dev38 → modal-1.2.3}/PKG-INFO +1 -1
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_functions.py +10 -4
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_load_context.py +2 -1
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_object.py +12 -6
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_partial_function.py +1 -1
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/async_utils.py +15 -5
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/grpc_utils.py +157 -24
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/app.py +47 -21
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/app.pyi +8 -4
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/run.py +24 -1
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/client.py +9 -9
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/client.pyi +8 -17
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cloud_bucket_mount.py +1 -1
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cls.py +6 -4
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/config.py +5 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/functions.pyi +8 -8
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/image.py +1 -4
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/io_streams.py +101 -32
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/io_streams.pyi +30 -5
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/sandbox.py +8 -21
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/sandbox.pyi +0 -9
- {modal-1.2.2.dev38 → modal-1.2.3}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/api.proto +31 -1
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/api_grpc.py +1 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/api_pb2.py +1052 -1021
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/api_pb2.pyi +103 -10
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_version/__init__.py +1 -1
- {modal-1.2.2.dev38 → modal-1.2.3}/LICENSE +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/README.md +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/__init__.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/__main__.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_billing.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_clustered_functions.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_clustered_functions.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_container_entrypoint.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_grpc_client.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_ipython.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_location.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_output.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_pty.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_resolver.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_resources.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_runtime/__init__.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_runtime/asgi.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_runtime/execution_context.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_runtime/telemetry.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_serialization.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_traceback.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_tunnel.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_tunnel.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_type_manager.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/__init__.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/app_utils.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/blob_utils.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/deprecation.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/docker_utils.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/function_utils.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/git_utils.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/hash_utils.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/http_utils.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/logger.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/mount_utils.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/name_utils.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/package_utils.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/shell_utils.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/task_command_router_client.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_utils/time_utils.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_vendor/__init__.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_vendor/tblib.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/_watcher.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/billing.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/builder/2023.12.txt +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/builder/2024.04.txt +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/builder/2024.10.txt +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/builder/2025.06.txt +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/builder/README.md +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/builder/base-images.json +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/call_graph.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/__init__.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/_download.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/_traceback.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/app.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/cluster.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/config.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/container.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/dict.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/entry_point.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/environment.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/import_refs.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/launch.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/network_file_system.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/profile.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/programs/__init__.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/programs/launch_instance_ssh.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/programs/run_marimo.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/programs/vscode.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/queues.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/secret.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/token.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/utils.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cli/volume.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/cls.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/container_process.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/container_process.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/dict.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/dict.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/environments.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/environments.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/exception.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/experimental/__init__.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/experimental/flash.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/experimental/flash.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/experimental/ipython.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/file_io.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/file_io.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/file_pattern_matcher.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/functions.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/gpu.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/image.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/mount.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/mount.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/network_file_system.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/network_file_system.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/object.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/object.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/output.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/parallel_map.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/parallel_map.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/partial_function.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/partial_function.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/proxy.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/proxy.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/py.typed +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/queue.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/queue.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/retries.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/runner.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/runner.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/running_app.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/schedule.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/scheduler_placement.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/secret.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/secret.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/serving.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/serving.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/snapshot.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/snapshot.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/stream_type.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/token_flow.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/token_flow.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/volume.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal/volume.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal.egg-info/requires.txt +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal.egg-info/top_level.txt +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_docs/__init__.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/__init__.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/py.typed +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/sandbox_router.proto +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/sandbox_router_grpc.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/sandbox_router_pb2.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/sandbox_router_pb2.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/sandbox_router_pb2_grpc.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/sandbox_router_pb2_grpc.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/task_command_router.proto +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/modal_version/__main__.py +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/pyproject.toml +0 -0
- {modal-1.2.2.dev38 → modal-1.2.3}/setup.cfg +0 -0
|
@@ -90,7 +90,6 @@ from .parallel_map import (
|
|
|
90
90
|
from .proxy import _Proxy
|
|
91
91
|
from .retries import Retries, RetryManager
|
|
92
92
|
from .schedule import Schedule
|
|
93
|
-
from .scheduler_placement import SchedulerPlacement
|
|
94
93
|
from .secret import _Secret
|
|
95
94
|
from .volume import _Volume
|
|
96
95
|
|
|
@@ -537,6 +536,7 @@ class _InputPlaneInvocation:
|
|
|
537
536
|
async def _get_metadata(input_plane_region: str, client: _Client) -> list[tuple[str, str]]:
|
|
538
537
|
if not input_plane_region:
|
|
539
538
|
return []
|
|
539
|
+
assert client._auth_token_manager, "Client is not open"
|
|
540
540
|
token = await client._auth_token_manager.get_token()
|
|
541
541
|
return [("x-modal-input-plane-region", input_plane_region), ("x-modal-auth-token", token)]
|
|
542
542
|
|
|
@@ -589,7 +589,7 @@ class _FunctionSpec:
|
|
|
589
589
|
cpu: Optional[Union[float, tuple[float, float]]]
|
|
590
590
|
memory: Optional[Union[int, tuple[int, int]]]
|
|
591
591
|
ephemeral_disk: Optional[int]
|
|
592
|
-
scheduler_placement: Optional[SchedulerPlacement]
|
|
592
|
+
scheduler_placement: Optional[api_pb2.SchedulerPlacement]
|
|
593
593
|
proxy: Optional[_Proxy]
|
|
594
594
|
|
|
595
595
|
|
|
@@ -683,7 +683,8 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
683
683
|
batch_max_size: Optional[int] = None,
|
|
684
684
|
batch_wait_ms: Optional[int] = None,
|
|
685
685
|
cloud: Optional[str] = None,
|
|
686
|
-
|
|
686
|
+
region: Optional[Union[str, Sequence[str]]] = None,
|
|
687
|
+
nonpreemptible: bool = False,
|
|
687
688
|
is_builder_function: bool = False,
|
|
688
689
|
is_auto_snapshot: bool = False,
|
|
689
690
|
enable_memory_snapshot: bool = False,
|
|
@@ -746,6 +747,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
746
747
|
if env:
|
|
747
748
|
secrets = [*secrets, _Secret.from_dict(env)]
|
|
748
749
|
|
|
750
|
+
scheduler_placement: Optional[api_pb2.SchedulerPlacement] = None
|
|
751
|
+
if region or nonpreemptible:
|
|
752
|
+
regions = [region] if isinstance(region, str) else (list(region) if region else None)
|
|
753
|
+
scheduler_placement = api_pb2.SchedulerPlacement(regions=regions, nonpreemptible=nonpreemptible)
|
|
754
|
+
|
|
749
755
|
function_spec = _FunctionSpec(
|
|
750
756
|
mounts=all_mounts,
|
|
751
757
|
secrets=secrets,
|
|
@@ -1018,7 +1024,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1018
1024
|
untrusted=restrict_modal_access,
|
|
1019
1025
|
max_inputs=max_inputs or 0,
|
|
1020
1026
|
cloud_bucket_mounts=cloud_bucket_mounts_to_proto(cloud_bucket_mounts),
|
|
1021
|
-
scheduler_placement=scheduler_placement
|
|
1027
|
+
scheduler_placement=scheduler_placement,
|
|
1022
1028
|
is_class=info.is_service_class(),
|
|
1023
1029
|
class_parameter_info=info.class_parameter_info(),
|
|
1024
1030
|
i6pn_enabled=i6pn_enabled,
|
|
@@ -66,8 +66,9 @@ class LoadContext:
|
|
|
66
66
|
|
|
67
67
|
Returns a new instance (no in place mutation)"""
|
|
68
68
|
|
|
69
|
+
is_valid_client = self._client is not None and not self._client._snapshotted
|
|
69
70
|
return LoadContext(
|
|
70
|
-
client=
|
|
71
|
+
client=self.client if is_valid_client else await _Client.from_env(),
|
|
71
72
|
environment_name=self._environment_name or config.get("environment") or "",
|
|
72
73
|
app_id=self._app_id,
|
|
73
74
|
)
|
|
@@ -43,7 +43,7 @@ class _Object:
|
|
|
43
43
|
_prefix_to_type: ClassVar[dict[str, type]] = {}
|
|
44
44
|
|
|
45
45
|
# For constructors
|
|
46
|
-
_load: Optional[Callable[[Self, Resolver, LoadContext, Optional[str]], Awaitable[None]]]
|
|
46
|
+
_load: Optional[Callable[[Self, Resolver, LoadContext, Optional[str]], Awaitable[None]]] = None
|
|
47
47
|
_preload: Optional[Callable[[Self, Resolver, LoadContext, Optional[str]], Awaitable[None]]]
|
|
48
48
|
_rep: str
|
|
49
49
|
_is_another_app: bool
|
|
@@ -308,11 +308,17 @@ class _Object:
|
|
|
308
308
|
# memory snapshots capture references which must be rehydrated
|
|
309
309
|
# on restore to handle staleness.
|
|
310
310
|
logger.debug(f"rehydrating {self} after snapshot")
|
|
311
|
-
self.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
311
|
+
if self._hydrate_lazily:
|
|
312
|
+
logger.debug(f"reloading lazy {self} from server")
|
|
313
|
+
self._is_hydrated = False # un-hydrate and re-resolve
|
|
314
|
+
# we don't set an explicit Client here, relying on the default
|
|
315
|
+
# env client to be applied by LoadContext.apply_default
|
|
316
|
+
root_load_context = LoadContext.empty()
|
|
317
|
+
resolver = Resolver()
|
|
318
|
+
await resolver.load(typing.cast(_Object, self), root_load_context)
|
|
319
|
+
else:
|
|
320
|
+
logger.debug(f"reloading non-lazy {self} by replacing client")
|
|
321
|
+
self._client = client or await _Client.from_env()
|
|
316
322
|
self._is_rehydrated = True
|
|
317
323
|
logger.debug(f"rehydrated {self} with client {id(self.client)}")
|
|
318
324
|
elif not self._hydrate_lazily:
|
|
@@ -219,7 +219,7 @@ class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
|
|
219
219
|
# of the type PartialFunction and this descriptor would be triggered when accessing it,
|
|
220
220
|
#
|
|
221
221
|
# However, modal classes are *actually* Cls instances (which isn't reflected in type checkers
|
|
222
|
-
# due to Python's lack of type
|
|
222
|
+
# due to Python's lack of type checking intersection types), so at runtime the Cls instance would
|
|
223
223
|
# use its __getattr__ rather than this descriptor.
|
|
224
224
|
assert self.raw_f is not None # Should only be relevant in a method context
|
|
225
225
|
k = self.raw_f.__name__
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
2
|
import asyncio
|
|
3
3
|
import concurrent.futures
|
|
4
|
+
import contextlib
|
|
4
5
|
import functools
|
|
5
6
|
import inspect
|
|
6
7
|
import itertools
|
|
7
8
|
import sys
|
|
8
9
|
import time
|
|
9
10
|
import typing
|
|
11
|
+
import warnings
|
|
10
12
|
from collections.abc import AsyncGenerator, AsyncIterable, Awaitable, Iterable, Iterator
|
|
11
13
|
from contextlib import asynccontextmanager
|
|
12
14
|
from dataclasses import dataclass
|
|
@@ -134,7 +136,8 @@ class TaskContext:
|
|
|
134
136
|
_loops: set[asyncio.Task]
|
|
135
137
|
|
|
136
138
|
def __init__(self, grace: Optional[float] = None):
|
|
137
|
-
self._grace = grace
|
|
139
|
+
self._grace = grace # grace is the time we want for tasks to finish before cancelling them
|
|
140
|
+
self._cancellation_grace: float = 1.0 # extra graceperiod for the cancellation itself to "bubble up"
|
|
138
141
|
self._loops = set()
|
|
139
142
|
|
|
140
143
|
async def start(self):
|
|
@@ -166,22 +169,29 @@ class TaskContext:
|
|
|
166
169
|
# still needs to be handled
|
|
167
170
|
# (https://stackoverflow.com/a/63356323/2475114)
|
|
168
171
|
if gather_future:
|
|
169
|
-
|
|
172
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
170
173
|
await gather_future
|
|
171
|
-
except asyncio.CancelledError:
|
|
172
|
-
pass
|
|
173
174
|
|
|
175
|
+
cancelled_tasks: list[asyncio.Task] = []
|
|
174
176
|
for task in self._tasks:
|
|
175
177
|
if task.done() and not task.cancelled():
|
|
176
178
|
# Raise any exceptions if they happened.
|
|
177
179
|
# Only tasks without a done_callback will still be present in self._tasks
|
|
178
180
|
task.result()
|
|
179
181
|
|
|
180
|
-
if task.done()
|
|
182
|
+
if task.done():
|
|
181
183
|
continue
|
|
182
184
|
|
|
183
185
|
# Cancel any remaining unfinished tasks.
|
|
184
186
|
task.cancel()
|
|
187
|
+
cancelled_tasks.append(task)
|
|
188
|
+
|
|
189
|
+
cancellation_gather = asyncio.gather(*cancelled_tasks, return_exceptions=True)
|
|
190
|
+
try:
|
|
191
|
+
await asyncio.wait_for(cancellation_gather, timeout=self._cancellation_grace)
|
|
192
|
+
except asyncio.TimeoutError:
|
|
193
|
+
warnings.warn(f"Internal warning: Tasks did not cancel in a timely manner: {cancelled_tasks}")
|
|
194
|
+
|
|
185
195
|
await asyncio.sleep(0) # wake up coroutines waiting for cancellations
|
|
186
196
|
|
|
187
197
|
async def __aexit__(self, exc_type, value, tb):
|
|
@@ -9,7 +9,8 @@ import urllib.parse
|
|
|
9
9
|
import uuid
|
|
10
10
|
from collections.abc import AsyncIterator
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
|
-
from
|
|
12
|
+
from functools import cache
|
|
13
|
+
from typing import Any, Optional, Sequence, TypeVar
|
|
13
14
|
|
|
14
15
|
import grpclib.client
|
|
15
16
|
import grpclib.config
|
|
@@ -17,14 +18,18 @@ import grpclib.events
|
|
|
17
18
|
import grpclib.protocol
|
|
18
19
|
import grpclib.stream
|
|
19
20
|
from google.protobuf.message import Message
|
|
21
|
+
from google.protobuf.symbol_database import SymbolDatabase
|
|
20
22
|
from grpclib import GRPCError, Status
|
|
23
|
+
from grpclib.encoding.base import StatusDetailsCodecBase
|
|
21
24
|
from grpclib.exceptions import StreamTerminatedError
|
|
22
25
|
from grpclib.protocol import H2Protocol
|
|
23
26
|
|
|
24
27
|
from modal.exception import AuthError, ConnectionError
|
|
28
|
+
from modal_proto import api_pb2
|
|
25
29
|
from modal_version import __version__
|
|
26
30
|
|
|
27
31
|
from .._traceback import suppress_tb_frames
|
|
32
|
+
from ..config import config
|
|
28
33
|
from .async_utils import retry
|
|
29
34
|
from .logger import logger
|
|
30
35
|
|
|
@@ -68,6 +73,7 @@ RETRYABLE_GRPC_STATUS_CODES = [
|
|
|
68
73
|
Status.INTERNAL,
|
|
69
74
|
Status.UNKNOWN,
|
|
70
75
|
]
|
|
76
|
+
SERVER_RETRY_WARNING_TIME_INTERVAL = 30.0
|
|
71
77
|
|
|
72
78
|
|
|
73
79
|
@dataclass
|
|
@@ -107,6 +113,56 @@ class ConnectionManager:
|
|
|
107
113
|
self._channels.clear()
|
|
108
114
|
|
|
109
115
|
|
|
116
|
+
@cache
|
|
117
|
+
def _sym_db() -> SymbolDatabase:
|
|
118
|
+
from google.protobuf.symbol_database import Default
|
|
119
|
+
|
|
120
|
+
return Default()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class CustomProtoStatusDetailsCodec(StatusDetailsCodecBase):
|
|
124
|
+
"""grpclib compatible details codec.
|
|
125
|
+
|
|
126
|
+
The server can encode the details using `google.rpc.Status` using grpclib's default codec and this custom codec
|
|
127
|
+
can decode it into a `api_pb2.RPCStatus`.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def encode(
|
|
131
|
+
self,
|
|
132
|
+
status: Status,
|
|
133
|
+
message: Optional[str],
|
|
134
|
+
details: Optional[Sequence[Message]],
|
|
135
|
+
) -> bytes:
|
|
136
|
+
details_proto = api_pb2.RPCStatus(code=status.value, message=message or "")
|
|
137
|
+
if details is not None:
|
|
138
|
+
for detail in details:
|
|
139
|
+
detail_container = details_proto.details.add()
|
|
140
|
+
detail_container.Pack(detail)
|
|
141
|
+
return details_proto.SerializeToString()
|
|
142
|
+
|
|
143
|
+
def decode(
|
|
144
|
+
self,
|
|
145
|
+
status: Status,
|
|
146
|
+
message: Optional[str],
|
|
147
|
+
data: bytes,
|
|
148
|
+
) -> Any:
|
|
149
|
+
sym_db = _sym_db()
|
|
150
|
+
details_proto = api_pb2.RPCStatus.FromString(data)
|
|
151
|
+
|
|
152
|
+
details = []
|
|
153
|
+
for detail_container in details_proto.details:
|
|
154
|
+
# If we do not know how to decode an emssage, we'll ignore it.
|
|
155
|
+
with contextlib.suppress(Exception):
|
|
156
|
+
msg_type = sym_db.GetSymbol(detail_container.TypeName())
|
|
157
|
+
detail = msg_type()
|
|
158
|
+
detail_container.Unpack(detail)
|
|
159
|
+
details.append(detail)
|
|
160
|
+
return details
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
custom_detail_codec = CustomProtoStatusDetailsCodec()
|
|
164
|
+
|
|
165
|
+
|
|
110
166
|
def create_channel(
|
|
111
167
|
server_url: str,
|
|
112
168
|
metadata: dict[str, str] = {},
|
|
@@ -125,7 +181,7 @@ def create_channel(
|
|
|
125
181
|
)
|
|
126
182
|
|
|
127
183
|
if o.scheme == "unix":
|
|
128
|
-
channel = grpclib.client.Channel(path=o.path, config=config)
|
|
184
|
+
channel = grpclib.client.Channel(path=o.path, config=config, status_details_codec=custom_detail_codec)
|
|
129
185
|
elif o.scheme in ("http", "https"):
|
|
130
186
|
target = o.netloc
|
|
131
187
|
parts = target.split(":")
|
|
@@ -133,7 +189,7 @@ def create_channel(
|
|
|
133
189
|
ssl = o.scheme.endswith("s")
|
|
134
190
|
host = parts[0]
|
|
135
191
|
port = int(parts[1]) if len(parts) == 2 else 443 if ssl else 80
|
|
136
|
-
channel = grpclib.client.Channel(host, port, ssl=ssl, config=config)
|
|
192
|
+
channel = grpclib.client.Channel(host, port, ssl=ssl, config=config, status_details_codec=custom_detail_codec)
|
|
137
193
|
else:
|
|
138
194
|
raise Exception(f"Unknown scheme: {o.scheme}")
|
|
139
195
|
|
|
@@ -197,6 +253,50 @@ async def retry_transient_errors(
|
|
|
197
253
|
return await _retry_transient_errors(fn, req, retry=Retry(max_retries=max_retries))
|
|
198
254
|
|
|
199
255
|
|
|
256
|
+
def get_server_retry_policy(exc: Exception) -> Optional[api_pb2.RPCRetryPolicy]:
|
|
257
|
+
"""Get server retry policy."""
|
|
258
|
+
if not isinstance(exc, GRPCError) or not exc.details:
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
# Server should not set multiple retry instructions, but if there is more than one, pick the first one
|
|
262
|
+
for entry in exc.details:
|
|
263
|
+
if isinstance(entry, api_pb2.RPCRetryPolicy):
|
|
264
|
+
return entry
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def process_exception_before_retry(
|
|
269
|
+
exc: Exception,
|
|
270
|
+
final_attempt: bool,
|
|
271
|
+
fn_name: str,
|
|
272
|
+
n_retries: int,
|
|
273
|
+
delay: float,
|
|
274
|
+
idempotency_key: str,
|
|
275
|
+
):
|
|
276
|
+
"""Process exception before retry, used by `_retry_transient_errors`."""
|
|
277
|
+
with suppress_tb_frames(1):
|
|
278
|
+
if final_attempt:
|
|
279
|
+
logger.debug(
|
|
280
|
+
f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} for {fn_name} ({idempotency_key[:8]})"
|
|
281
|
+
)
|
|
282
|
+
if isinstance(exc, OSError):
|
|
283
|
+
raise ConnectionError(str(exc))
|
|
284
|
+
elif isinstance(exc, asyncio.TimeoutError):
|
|
285
|
+
raise ConnectionError(str(exc))
|
|
286
|
+
else:
|
|
287
|
+
raise exc
|
|
288
|
+
|
|
289
|
+
if isinstance(exc, AttributeError) and "_write_appdata" not in str(exc):
|
|
290
|
+
# StreamTerminatedError are not properly raised in grpclib<=0.4.7
|
|
291
|
+
# fixed in https://github.com/vmagamedov/grpclib/issues/185
|
|
292
|
+
# TODO: update to newer version (>=0.4.8) once stable
|
|
293
|
+
# Also be sure to remove the AttributeError from the set of exceptions
|
|
294
|
+
# we handle in the retry logic once we drop this check!
|
|
295
|
+
raise exc
|
|
296
|
+
|
|
297
|
+
logger.debug(f"Retryable failure {repr(exc)} {n_retries=} {delay=} for {fn_name} ({idempotency_key[:8]})")
|
|
298
|
+
|
|
299
|
+
|
|
200
300
|
async def _retry_transient_errors(
|
|
201
301
|
fn: typing.Union[
|
|
202
302
|
"modal._grpc_client.UnaryUnaryWrapper[RequestType, ResponseType]",
|
|
@@ -219,12 +319,15 @@ async def _retry_transient_errors(
|
|
|
219
319
|
|
|
220
320
|
delay = retry.base_delay
|
|
221
321
|
n_retries = 0
|
|
322
|
+
n_throttled_retries = 0
|
|
222
323
|
|
|
223
324
|
status_codes = [*RETRYABLE_GRPC_STATUS_CODES, *retry.additional_status_codes]
|
|
224
325
|
|
|
225
326
|
idempotency_key = str(uuid.uuid4())
|
|
226
327
|
|
|
227
328
|
t0 = time.time()
|
|
329
|
+
last_server_retry_warning_time = None
|
|
330
|
+
|
|
228
331
|
if retry.total_timeout is not None:
|
|
229
332
|
total_deadline = t0 + retry.total_timeout
|
|
230
333
|
else:
|
|
@@ -236,29 +339,77 @@ async def _retry_transient_errors(
|
|
|
236
339
|
attempt_metadata = [
|
|
237
340
|
("x-idempotency-key", idempotency_key),
|
|
238
341
|
("x-retry-attempt", str(n_retries)),
|
|
342
|
+
("x-throttle-retry-attempt", str(n_throttled_retries)),
|
|
239
343
|
*metadata,
|
|
240
344
|
]
|
|
241
345
|
if n_retries > 0:
|
|
242
346
|
attempt_metadata.append(("x-retry-delay", str(time.time() - t0)))
|
|
347
|
+
if n_throttled_retries > 0:
|
|
348
|
+
attempt_metadata.append(("x-throttle-retry-delay", str(time.time() - t0)))
|
|
349
|
+
|
|
243
350
|
timeouts = []
|
|
244
351
|
if retry.attempt_timeout is not None:
|
|
245
352
|
timeouts.append(retry.attempt_timeout)
|
|
246
|
-
if
|
|
353
|
+
if total_deadline is not None:
|
|
247
354
|
timeouts.append(max(total_deadline - time.time(), retry.attempt_timeout_floor))
|
|
248
355
|
if timeouts:
|
|
249
356
|
timeout = min(timeouts) # In case the function provided both types of timeouts
|
|
250
357
|
else:
|
|
251
358
|
timeout = None
|
|
359
|
+
|
|
252
360
|
try:
|
|
253
361
|
with suppress_tb_frames(1):
|
|
254
362
|
return await fn_callable(req, metadata=attempt_metadata, timeout=timeout)
|
|
255
363
|
except (StreamTerminatedError, GRPCError, OSError, asyncio.TimeoutError, AttributeError) as exc:
|
|
364
|
+
# Note that we only catch AttributeError to handle a specific case that works around a bug
|
|
365
|
+
# in grpclib<=0.4.7. See above (search for `write_appdata`).
|
|
366
|
+
|
|
367
|
+
# Server side instruction for retries
|
|
368
|
+
max_throttle_wait: Optional[int] = config.get("max_throttle_wait")
|
|
369
|
+
if (
|
|
370
|
+
max_throttle_wait != 0
|
|
371
|
+
and isinstance(exc, GRPCError)
|
|
372
|
+
and (server_retry_policy := get_server_retry_policy(exc))
|
|
373
|
+
):
|
|
374
|
+
server_delay = server_retry_policy.retry_after_secs
|
|
375
|
+
|
|
376
|
+
now = time.time()
|
|
377
|
+
|
|
378
|
+
# We check if the timeout will be reached **after** the sleep, so we can raise an error early
|
|
379
|
+
# without needing to actually sleep.
|
|
380
|
+
total_timeout_will_be_reached = (
|
|
381
|
+
retry.total_timeout is not None and (now + server_delay - t0) >= retry.total_timeout
|
|
382
|
+
)
|
|
383
|
+
max_throttle_will_be_reached = (
|
|
384
|
+
max_throttle_wait is not None and (now + server_delay - t0) >= max_throttle_wait
|
|
385
|
+
)
|
|
386
|
+
final_attempt = total_timeout_will_be_reached or max_throttle_will_be_reached
|
|
387
|
+
|
|
388
|
+
with suppress_tb_frames(1):
|
|
389
|
+
process_exception_before_retry(
|
|
390
|
+
exc, final_attempt, fn.name, n_retries, server_delay, idempotency_key
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
now = time.time()
|
|
394
|
+
if last_server_retry_warning_time is None or (
|
|
395
|
+
now - last_server_retry_warning_time >= SERVER_RETRY_WARNING_TIME_INTERVAL
|
|
396
|
+
):
|
|
397
|
+
last_server_retry_warning_time = now
|
|
398
|
+
logger.warning(
|
|
399
|
+
f"Warning: Received {exc.status} status: {exc.message}. "
|
|
400
|
+
f"Will retry in {server_delay:0.2f} seconds."
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
n_throttled_retries += 1
|
|
404
|
+
await asyncio.sleep(server_delay)
|
|
405
|
+
continue
|
|
406
|
+
|
|
407
|
+
# Client handles retry
|
|
256
408
|
if isinstance(exc, GRPCError) and exc.status not in status_codes:
|
|
257
409
|
if exc.status == Status.UNAUTHENTICATED:
|
|
258
410
|
raise AuthError(exc.message)
|
|
259
411
|
else:
|
|
260
412
|
raise exc
|
|
261
|
-
|
|
262
413
|
if retry.max_retries is not None and n_retries >= retry.max_retries:
|
|
263
414
|
final_attempt = True
|
|
264
415
|
elif total_deadline is not None and time.time() + delay + retry.attempt_timeout_floor >= total_deadline:
|
|
@@ -267,25 +418,7 @@ async def _retry_transient_errors(
|
|
|
267
418
|
final_attempt = False
|
|
268
419
|
|
|
269
420
|
with suppress_tb_frames(1):
|
|
270
|
-
|
|
271
|
-
logger.debug(
|
|
272
|
-
f"Final attempt failed with {repr(exc)} {n_retries=} {delay=} "
|
|
273
|
-
f"{total_deadline=} for {fn.name} ({idempotency_key[:8]})"
|
|
274
|
-
)
|
|
275
|
-
if isinstance(exc, OSError):
|
|
276
|
-
raise ConnectionError(str(exc))
|
|
277
|
-
elif isinstance(exc, asyncio.TimeoutError):
|
|
278
|
-
raise ConnectionError(str(exc))
|
|
279
|
-
else:
|
|
280
|
-
raise exc
|
|
281
|
-
|
|
282
|
-
if isinstance(exc, AttributeError) and "_write_appdata" not in str(exc):
|
|
283
|
-
# StreamTerminatedError are not properly raised in grpclib<=0.4.7
|
|
284
|
-
# fixed in https://github.com/vmagamedov/grpclib/issues/185
|
|
285
|
-
# TODO: update to newer version (>=0.4.8) once stable
|
|
286
|
-
raise exc
|
|
287
|
-
|
|
288
|
-
logger.debug(f"Retryable failure {repr(exc)} {n_retries=} {delay=} for {fn.name} ({idempotency_key[:8]})")
|
|
421
|
+
process_exception_before_retry(exc, final_attempt, fn.name, n_retries, delay, idempotency_key)
|
|
289
422
|
|
|
290
423
|
n_retries += 1
|
|
291
424
|
|
|
@@ -719,6 +719,7 @@ class _App:
|
|
|
719
719
|
] = None, # Set this to True if it's a non-generator function returning a [sync/async] generator object
|
|
720
720
|
cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
|
|
721
721
|
region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
|
|
722
|
+
nonpreemptible: bool = False, # Whether to run the function on a nonpreemptible instance.
|
|
722
723
|
enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
|
|
723
724
|
block_network: bool = False, # Whether to block network access
|
|
724
725
|
restrict_modal_access: bool = False, # Whether to allow this function access to other Modal resources
|
|
@@ -731,9 +732,6 @@ class _App:
|
|
|
731
732
|
include_source: Optional[bool] = None,
|
|
732
733
|
experimental_options: Optional[dict[str, Any]] = None,
|
|
733
734
|
# Parameters below here are experimental. Use with caution!
|
|
734
|
-
_experimental_scheduler_placement: Optional[
|
|
735
|
-
SchedulerPlacement
|
|
736
|
-
] = None, # Experimental controls over fine-grained scheduling (alpha).
|
|
737
735
|
_experimental_proxy_ip: Optional[str] = None, # IP address of proxy
|
|
738
736
|
_experimental_custom_scaling_factor: Optional[float] = None, # Custom scaling factor
|
|
739
737
|
_experimental_restrict_output: bool = False, # Don't use pickle for return values
|
|
@@ -743,6 +741,8 @@ class _App:
|
|
|
743
741
|
container_idle_timeout: Optional[int] = None, # Replaced with `scaledown_window`
|
|
744
742
|
allow_concurrent_inputs: Optional[int] = None, # Replaced with the `@modal.concurrent` decorator
|
|
745
743
|
_experimental_buffer_containers: Optional[int] = None, # Now stable API with `buffer_containers`
|
|
744
|
+
_experimental_scheduler_placement: Optional[SchedulerPlacement] = None, # Replaced in favor of
|
|
745
|
+
# using `region` and `nonpreemptible`
|
|
746
746
|
) -> _FunctionDecoratorType:
|
|
747
747
|
"""Decorator to register a new Modal Function with this App."""
|
|
748
748
|
if isinstance(_warn_parentheses_missing, _Image):
|
|
@@ -762,6 +762,24 @@ class _App:
|
|
|
762
762
|
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
|
763
763
|
)
|
|
764
764
|
|
|
765
|
+
if _experimental_scheduler_placement is not None:
|
|
766
|
+
deprecation_warning(
|
|
767
|
+
(2025, 11, 17),
|
|
768
|
+
"The `_experimental_scheduler_placement` parameter is deprecated."
|
|
769
|
+
" Please use the `region` and `nonpreemptible` parameters instead.",
|
|
770
|
+
)
|
|
771
|
+
if region is not None or nonpreemptible:
|
|
772
|
+
raise InvalidError(
|
|
773
|
+
"Cannot use `_experimental_scheduler_placement` together with "
|
|
774
|
+
"`region` or `nonpreemptible` parameters."
|
|
775
|
+
)
|
|
776
|
+
# Extract regions and lifecycle from scheduler placement
|
|
777
|
+
if _experimental_scheduler_placement.proto.regions:
|
|
778
|
+
region = list(_experimental_scheduler_placement.proto.regions)
|
|
779
|
+
if _experimental_scheduler_placement.proto._lifecycle:
|
|
780
|
+
# Convert lifecycle to nonpreemptible: "on-demand" -> True, "spot" -> False
|
|
781
|
+
nonpreemptible = _experimental_scheduler_placement.proto._lifecycle == "on-demand"
|
|
782
|
+
|
|
765
783
|
secrets = secrets or []
|
|
766
784
|
if env:
|
|
767
785
|
secrets = [*secrets, _Secret.from_dict(env)]
|
|
@@ -771,7 +789,7 @@ class _App:
|
|
|
771
789
|
def wrapped(
|
|
772
790
|
f: Union[_PartialFunction, Callable[..., Any], None],
|
|
773
791
|
) -> _Function:
|
|
774
|
-
nonlocal is_generator, cloud, serialized
|
|
792
|
+
nonlocal is_generator, cloud, serialized, region, nonpreemptible
|
|
775
793
|
|
|
776
794
|
# Check if the decorated object is a class
|
|
777
795
|
if inspect.isclass(f):
|
|
@@ -860,12 +878,6 @@ class _App:
|
|
|
860
878
|
if is_generator is None:
|
|
861
879
|
is_generator = inspect.isgeneratorfunction(raw_f) or inspect.isasyncgenfunction(raw_f)
|
|
862
880
|
|
|
863
|
-
scheduler_placement: Optional[SchedulerPlacement] = _experimental_scheduler_placement
|
|
864
|
-
if region:
|
|
865
|
-
if scheduler_placement:
|
|
866
|
-
raise InvalidError("`region` and `_experimental_scheduler_placement` cannot be used together")
|
|
867
|
-
scheduler_placement = SchedulerPlacement(region=region)
|
|
868
|
-
|
|
869
881
|
function = _Function.from_local(
|
|
870
882
|
info,
|
|
871
883
|
app=self,
|
|
@@ -892,12 +904,13 @@ class _App:
|
|
|
892
904
|
timeout=timeout,
|
|
893
905
|
startup_timeout=startup_timeout or timeout,
|
|
894
906
|
cloud=cloud,
|
|
907
|
+
region=region,
|
|
908
|
+
nonpreemptible=nonpreemptible,
|
|
895
909
|
webhook_config=webhook_config,
|
|
896
910
|
enable_memory_snapshot=enable_memory_snapshot,
|
|
897
911
|
block_network=block_network,
|
|
898
912
|
restrict_modal_access=restrict_modal_access,
|
|
899
913
|
max_inputs=max_inputs,
|
|
900
|
-
scheduler_placement=scheduler_placement,
|
|
901
914
|
i6pn_enabled=i6pn_enabled,
|
|
902
915
|
cluster_size=cluster_size, # Experimental: Clustered functions
|
|
903
916
|
rdma=rdma,
|
|
@@ -950,6 +963,7 @@ class _App:
|
|
|
950
963
|
startup_timeout: Optional[int] = None, # Maximum startup time in seconds with higher precedence than `timeout`.
|
|
951
964
|
cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
|
|
952
965
|
region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
|
|
966
|
+
nonpreemptible: bool = False, # Whether to run the function on a non-preemptible instance.
|
|
953
967
|
enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
|
|
954
968
|
block_network: bool = False, # Whether to block network access
|
|
955
969
|
restrict_modal_access: bool = False, # Whether to allow this class access to other Modal resources
|
|
@@ -960,9 +974,6 @@ class _App:
|
|
|
960
974
|
include_source: Optional[bool] = None, # When `False`, don't automatically add the App source to the container.
|
|
961
975
|
experimental_options: Optional[dict[str, Any]] = None,
|
|
962
976
|
# Parameters below here are experimental. Use with caution!
|
|
963
|
-
_experimental_scheduler_placement: Optional[
|
|
964
|
-
SchedulerPlacement
|
|
965
|
-
] = None, # Experimental controls over fine-grained scheduling (alpha).
|
|
966
977
|
_experimental_proxy_ip: Optional[str] = None, # IP address of proxy
|
|
967
978
|
_experimental_custom_scaling_factor: Optional[float] = None, # Custom scaling factor
|
|
968
979
|
_experimental_restrict_output: bool = False, # Don't use pickle for return values
|
|
@@ -972,6 +983,8 @@ class _App:
|
|
|
972
983
|
container_idle_timeout: Optional[int] = None, # Replaced with `scaledown_window`
|
|
973
984
|
allow_concurrent_inputs: Optional[int] = None, # Replaced with the `@modal.concurrent` decorator
|
|
974
985
|
_experimental_buffer_containers: Optional[int] = None, # Now stable API with `buffer_containers`
|
|
986
|
+
_experimental_scheduler_placement: Optional[SchedulerPlacement] = None, # Replaced in favor of
|
|
987
|
+
# using `region` and `nonpreemptible`
|
|
975
988
|
) -> Callable[[Union[CLS_T, _PartialFunction]], CLS_T]:
|
|
976
989
|
"""
|
|
977
990
|
Decorator to register a new Modal [Cls](https://modal.com/docs/reference/modal.Cls) with this App.
|
|
@@ -979,12 +992,6 @@ class _App:
|
|
|
979
992
|
if _warn_parentheses_missing:
|
|
980
993
|
raise InvalidError("Did you forget parentheses? Suggestion: `@app.cls()`.")
|
|
981
994
|
|
|
982
|
-
scheduler_placement = _experimental_scheduler_placement
|
|
983
|
-
if region:
|
|
984
|
-
if scheduler_placement:
|
|
985
|
-
raise InvalidError("`region` and `_experimental_scheduler_placement` cannot be used together")
|
|
986
|
-
scheduler_placement = SchedulerPlacement(region=region)
|
|
987
|
-
|
|
988
995
|
if allow_concurrent_inputs is not None:
|
|
989
996
|
deprecation_warning(
|
|
990
997
|
(2025, 4, 9),
|
|
@@ -993,6 +1000,24 @@ class _App:
|
|
|
993
1000
|
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
|
994
1001
|
)
|
|
995
1002
|
|
|
1003
|
+
if _experimental_scheduler_placement is not None:
|
|
1004
|
+
deprecation_warning(
|
|
1005
|
+
(2025, 11, 17),
|
|
1006
|
+
"The `_experimental_scheduler_placement` parameter is deprecated."
|
|
1007
|
+
" Please use the `region` and `nonpreemptible` parameters instead.",
|
|
1008
|
+
)
|
|
1009
|
+
if region is not None or nonpreemptible:
|
|
1010
|
+
raise InvalidError(
|
|
1011
|
+
"Cannot use `_experimental_scheduler_placement` together with "
|
|
1012
|
+
"`region` or `nonpreemptible` parameters."
|
|
1013
|
+
)
|
|
1014
|
+
# Extract regions and lifecycle from scheduler placement
|
|
1015
|
+
if _experimental_scheduler_placement.proto.regions:
|
|
1016
|
+
region = list(_experimental_scheduler_placement.proto.regions)
|
|
1017
|
+
if _experimental_scheduler_placement.proto._lifecycle:
|
|
1018
|
+
# Convert lifecycle to nonpreemptible: "on-demand" -> True, "spot" -> False
|
|
1019
|
+
nonpreemptible = _experimental_scheduler_placement.proto._lifecycle == "on-demand"
|
|
1020
|
+
|
|
996
1021
|
secrets = secrets or []
|
|
997
1022
|
if env:
|
|
998
1023
|
secrets = [*secrets, _Secret.from_dict(env)]
|
|
@@ -1086,11 +1111,12 @@ class _App:
|
|
|
1086
1111
|
timeout=timeout,
|
|
1087
1112
|
startup_timeout=startup_timeout or timeout,
|
|
1088
1113
|
cloud=cloud,
|
|
1114
|
+
region=region,
|
|
1115
|
+
nonpreemptible=nonpreemptible,
|
|
1089
1116
|
enable_memory_snapshot=enable_memory_snapshot,
|
|
1090
1117
|
block_network=block_network,
|
|
1091
1118
|
restrict_modal_access=restrict_modal_access,
|
|
1092
1119
|
max_inputs=max_inputs,
|
|
1093
|
-
scheduler_placement=scheduler_placement,
|
|
1094
1120
|
i6pn_enabled=i6pn_enabled,
|
|
1095
1121
|
cluster_size=cluster_size,
|
|
1096
1122
|
rdma=rdma,
|