modal 1.1.3.dev6__py3-none-any.whl → 1.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modal/_clustered_functions.py +3 -0
- modal/_clustered_functions.pyi +3 -2
- modal/_functions.py +12 -0
- modal/_runtime/asgi.py +1 -1
- modal/_utils/grpc_utils.py +1 -0
- modal/app.py +6 -2
- modal/app.pyi +4 -0
- modal/builder/2025.06.txt +1 -0
- modal/builder/PREVIEW.txt +1 -0
- modal/client.pyi +2 -10
- modal/cls.py +6 -1
- modal/cls.pyi +16 -0
- modal/experimental/__init__.py +2 -1
- modal/experimental/flash.py +183 -23
- modal/experimental/flash.pyi +83 -9
- modal/functions.pyi +12 -0
- modal/image.py +8 -2
- modal/image.pyi +16 -4
- modal/mount.py +17 -11
- modal/mount.pyi +4 -0
- modal/parallel_map.py +26 -6
- modal/parallel_map.pyi +1 -0
- modal/sandbox.py +31 -4
- modal/sandbox.pyi +12 -3
- {modal-1.1.3.dev6.dist-info → modal-1.1.4.dist-info}/METADATA +1 -1
- {modal-1.1.3.dev6.dist-info → modal-1.1.4.dist-info}/RECORD +38 -38
- modal_proto/api.proto +30 -0
- modal_proto/api_grpc.py +32 -0
- modal_proto/api_pb2.py +893 -853
- modal_proto/api_pb2.pyi +94 -5
- modal_proto/api_pb2_grpc.py +68 -1
- modal_proto/api_pb2_grpc.pyi +25 -3
- modal_proto/modal_api_grpc.py +2 -0
- modal_version/__init__.py +1 -1
- {modal-1.1.3.dev6.dist-info → modal-1.1.4.dist-info}/WHEEL +0 -0
- {modal-1.1.3.dev6.dist-info → modal-1.1.4.dist-info}/entry_points.txt +0 -0
- {modal-1.1.3.dev6.dist-info → modal-1.1.4.dist-info}/licenses/LICENSE +0 -0
- {modal-1.1.3.dev6.dist-info → modal-1.1.4.dist-info}/top_level.txt +0 -0
modal/experimental/flash.pyi
CHANGED
@@ -1,13 +1,26 @@
|
|
1
1
|
import modal.client
|
2
|
+
import modal_proto.api_pb2
|
3
|
+
import subprocess
|
2
4
|
import typing
|
3
5
|
import typing_extensions
|
4
6
|
|
5
7
|
class _FlashManager:
|
6
|
-
def __init__(
|
8
|
+
def __init__(
|
9
|
+
self,
|
10
|
+
client: modal.client._Client,
|
11
|
+
port: int,
|
12
|
+
process: typing.Optional[subprocess.Popen] = None,
|
13
|
+
health_check_url: typing.Optional[str] = None,
|
14
|
+
):
|
7
15
|
"""Initialize self. See help(type(self)) for accurate signature."""
|
8
16
|
...
|
9
17
|
|
18
|
+
async def check_port_connection(self, process: typing.Optional[subprocess.Popen], timeout: int = 10): ...
|
10
19
|
async def _start(self): ...
|
20
|
+
async def _drain_container(self):
|
21
|
+
"""Background task that checks if we've encountered too many failures and drains the container if so."""
|
22
|
+
...
|
23
|
+
|
11
24
|
async def _run_heartbeat(self, host: str, port: int): ...
|
12
25
|
def get_container_url(self): ...
|
13
26
|
async def stop(self): ...
|
@@ -16,7 +29,19 @@ class _FlashManager:
|
|
16
29
|
SUPERSELF = typing.TypeVar("SUPERSELF", covariant=True)
|
17
30
|
|
18
31
|
class FlashManager:
|
19
|
-
def __init__(
|
32
|
+
def __init__(
|
33
|
+
self,
|
34
|
+
client: modal.client.Client,
|
35
|
+
port: int,
|
36
|
+
process: typing.Optional[subprocess.Popen] = None,
|
37
|
+
health_check_url: typing.Optional[str] = None,
|
38
|
+
): ...
|
39
|
+
|
40
|
+
class __check_port_connection_spec(typing_extensions.Protocol[SUPERSELF]):
|
41
|
+
def __call__(self, /, process: typing.Optional[subprocess.Popen], timeout: int = 10): ...
|
42
|
+
async def aio(self, /, process: typing.Optional[subprocess.Popen], timeout: int = 10): ...
|
43
|
+
|
44
|
+
check_port_connection: __check_port_connection_spec[typing_extensions.Self]
|
20
45
|
|
21
46
|
class ___start_spec(typing_extensions.Protocol[SUPERSELF]):
|
22
47
|
def __call__(self, /): ...
|
@@ -24,6 +49,17 @@ class FlashManager:
|
|
24
49
|
|
25
50
|
_start: ___start_spec[typing_extensions.Self]
|
26
51
|
|
52
|
+
class ___drain_container_spec(typing_extensions.Protocol[SUPERSELF]):
|
53
|
+
def __call__(self, /):
|
54
|
+
"""Background task that checks if we've encountered too many failures and drains the container if so."""
|
55
|
+
...
|
56
|
+
|
57
|
+
async def aio(self, /):
|
58
|
+
"""Background task that checks if we've encountered too many failures and drains the container if so."""
|
59
|
+
...
|
60
|
+
|
61
|
+
_drain_container: ___drain_container_spec[typing_extensions.Self]
|
62
|
+
|
27
63
|
class ___run_heartbeat_spec(typing_extensions.Protocol[SUPERSELF]):
|
28
64
|
def __call__(self, /, host: str, port: int): ...
|
29
65
|
async def aio(self, /, host: str, port: int): ...
|
@@ -45,17 +81,27 @@ class FlashManager:
|
|
45
81
|
close: __close_spec[typing_extensions.Self]
|
46
82
|
|
47
83
|
class __flash_forward_spec(typing_extensions.Protocol):
|
48
|
-
def __call__(
|
84
|
+
def __call__(
|
85
|
+
self,
|
86
|
+
/,
|
87
|
+
port: int,
|
88
|
+
process: typing.Optional[subprocess.Popen] = None,
|
89
|
+
health_check_url: typing.Optional[str] = None,
|
90
|
+
) -> FlashManager:
|
49
91
|
"""Forward a port to the Modal Flash service, exposing that port as a stable web endpoint.
|
50
|
-
|
51
92
|
This is a highly experimental method that can break or be removed at any time without warning.
|
52
93
|
Do not use this method unless explicitly instructed to do so by Modal support.
|
53
94
|
"""
|
54
95
|
...
|
55
96
|
|
56
|
-
async def aio(
|
97
|
+
async def aio(
|
98
|
+
self,
|
99
|
+
/,
|
100
|
+
port: int,
|
101
|
+
process: typing.Optional[subprocess.Popen] = None,
|
102
|
+
health_check_url: typing.Optional[str] = None,
|
103
|
+
) -> FlashManager:
|
57
104
|
"""Forward a port to the Modal Flash service, exposing that port as a stable web endpoint.
|
58
|
-
|
59
105
|
This is a highly experimental method that can break or be removed at any time without warning.
|
60
106
|
Do not use this method unless explicitly instructed to do so by Modal support.
|
61
107
|
"""
|
@@ -85,8 +131,15 @@ class _FlashPrometheusAutoscaler:
|
|
85
131
|
|
86
132
|
async def start(self): ...
|
87
133
|
async def _run_autoscaler_loop(self): ...
|
88
|
-
async def
|
134
|
+
async def _compute_target_containers_internal(self, current_replicas: int) -> int:
|
135
|
+
"""Gets internal metrics from container to autoscale up or down."""
|
136
|
+
...
|
137
|
+
|
138
|
+
async def _compute_target_containers_prometheus(self, current_replicas: int) -> int: ...
|
89
139
|
async def _get_metrics(self, url: str) -> typing.Optional[dict[str, list[typing.Any]]]: ...
|
140
|
+
async def _get_container_metrics(
|
141
|
+
self, container_id: str
|
142
|
+
) -> typing.Optional[modal_proto.api_pb2.TaskGetAutoscalingMetricsResponse]: ...
|
90
143
|
async def _get_all_containers(self): ...
|
91
144
|
def _make_scaling_decision(
|
92
145
|
self,
|
@@ -147,11 +200,22 @@ class FlashPrometheusAutoscaler:
|
|
147
200
|
|
148
201
|
_run_autoscaler_loop: ___run_autoscaler_loop_spec[typing_extensions.Self]
|
149
202
|
|
150
|
-
class
|
203
|
+
class ___compute_target_containers_internal_spec(typing_extensions.Protocol[SUPERSELF]):
|
204
|
+
def __call__(self, /, current_replicas: int) -> int:
|
205
|
+
"""Gets internal metrics from container to autoscale up or down."""
|
206
|
+
...
|
207
|
+
|
208
|
+
async def aio(self, /, current_replicas: int) -> int:
|
209
|
+
"""Gets internal metrics from container to autoscale up or down."""
|
210
|
+
...
|
211
|
+
|
212
|
+
_compute_target_containers_internal: ___compute_target_containers_internal_spec[typing_extensions.Self]
|
213
|
+
|
214
|
+
class ___compute_target_containers_prometheus_spec(typing_extensions.Protocol[SUPERSELF]):
|
151
215
|
def __call__(self, /, current_replicas: int) -> int: ...
|
152
216
|
async def aio(self, /, current_replicas: int) -> int: ...
|
153
217
|
|
154
|
-
|
218
|
+
_compute_target_containers_prometheus: ___compute_target_containers_prometheus_spec[typing_extensions.Self]
|
155
219
|
|
156
220
|
class ___get_metrics_spec(typing_extensions.Protocol[SUPERSELF]):
|
157
221
|
def __call__(self, /, url: str) -> typing.Optional[dict[str, list[typing.Any]]]: ...
|
@@ -159,6 +223,16 @@ class FlashPrometheusAutoscaler:
|
|
159
223
|
|
160
224
|
_get_metrics: ___get_metrics_spec[typing_extensions.Self]
|
161
225
|
|
226
|
+
class ___get_container_metrics_spec(typing_extensions.Protocol[SUPERSELF]):
|
227
|
+
def __call__(
|
228
|
+
self, /, container_id: str
|
229
|
+
) -> typing.Optional[modal_proto.api_pb2.TaskGetAutoscalingMetricsResponse]: ...
|
230
|
+
async def aio(
|
231
|
+
self, /, container_id: str
|
232
|
+
) -> typing.Optional[modal_proto.api_pb2.TaskGetAutoscalingMetricsResponse]: ...
|
233
|
+
|
234
|
+
_get_container_metrics: ___get_container_metrics_spec[typing_extensions.Self]
|
235
|
+
|
162
236
|
class ___get_all_containers_spec(typing_extensions.Protocol[SUPERSELF]):
|
163
237
|
def __call__(self, /): ...
|
164
238
|
async def aio(self, /): ...
|
modal/functions.pyi
CHANGED
@@ -85,6 +85,7 @@ class Function(
|
|
85
85
|
proxy: typing.Optional[modal.proxy.Proxy] = None,
|
86
86
|
retries: typing.Union[int, modal.retries.Retries, None] = None,
|
87
87
|
timeout: int = 300,
|
88
|
+
startup_timeout: typing.Optional[int] = None,
|
88
89
|
min_containers: typing.Optional[int] = None,
|
89
90
|
max_containers: typing.Optional[int] = None,
|
90
91
|
buffer_containers: typing.Optional[int] = None,
|
@@ -359,6 +360,17 @@ class Function(
|
|
359
360
|
|
360
361
|
get_web_url: __get_web_url_spec[typing_extensions.Self]
|
361
362
|
|
363
|
+
class ___experimental_get_flash_urls_spec(typing_extensions.Protocol[SUPERSELF]):
|
364
|
+
def __call__(self, /) -> typing.Optional[list[str]]:
|
365
|
+
"""URL of the flash service for the function."""
|
366
|
+
...
|
367
|
+
|
368
|
+
async def aio(self, /) -> typing.Optional[list[str]]:
|
369
|
+
"""URL of the flash service for the function."""
|
370
|
+
...
|
371
|
+
|
372
|
+
_experimental_get_flash_urls: ___experimental_get_flash_urls_spec[typing_extensions.Self]
|
373
|
+
|
362
374
|
@property
|
363
375
|
def is_generator(self) -> bool:
|
364
376
|
"""mdmd:hidden"""
|
modal/image.py
CHANGED
@@ -1863,12 +1863,18 @@ class _Image(_Object, type_prefix="im"):
|
|
1863
1863
|
) -> "_Image":
|
1864
1864
|
"""Build a Modal image from a private image in AWS Elastic Container Registry (ECR).
|
1865
1865
|
|
1866
|
-
You will need to pass a `modal.Secret` containing
|
1867
|
-
|
1866
|
+
You will need to pass a `modal.Secret` containing either IAM user credentials or OIDC
|
1867
|
+
configuration to access the target ECR registry.
|
1868
|
+
|
1869
|
+
For IAM user authentication, set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_REGION`.
|
1870
|
+
|
1871
|
+
For OIDC authentication, set `AWS_ROLE_ARN` and `AWS_REGION`.
|
1868
1872
|
|
1869
1873
|
IAM configuration details can be found in the AWS documentation for
|
1870
1874
|
["Private repository policies"](https://docs.aws.amazon.com/AmazonECR/latest/userguide/repository-policies.html).
|
1871
1875
|
|
1876
|
+
For more details on using an AWS role to access ECR, see the [OIDC integration guide](https://modal.com/docs/guide/oidc-integration).
|
1877
|
+
|
1872
1878
|
See `Image.from_registry()` for information about the other parameters.
|
1873
1879
|
|
1874
1880
|
**Example**
|
modal/image.pyi
CHANGED
@@ -720,12 +720,18 @@ class _Image(modal._object._Object):
|
|
720
720
|
) -> _Image:
|
721
721
|
"""Build a Modal image from a private image in AWS Elastic Container Registry (ECR).
|
722
722
|
|
723
|
-
You will need to pass a `modal.Secret` containing
|
724
|
-
|
723
|
+
You will need to pass a `modal.Secret` containing either IAM user credentials or OIDC
|
724
|
+
configuration to access the target ECR registry.
|
725
|
+
|
726
|
+
For IAM user authentication, set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_REGION`.
|
727
|
+
|
728
|
+
For OIDC authentication, set `AWS_ROLE_ARN` and `AWS_REGION`.
|
725
729
|
|
726
730
|
IAM configuration details can be found in the AWS documentation for
|
727
731
|
["Private repository policies"](https://docs.aws.amazon.com/AmazonECR/latest/userguide/repository-policies.html).
|
728
732
|
|
733
|
+
For more details on using an AWS role to access ECR, see the [OIDC integration guide](https://modal.com/docs/guide/oidc-integration).
|
734
|
+
|
729
735
|
See `Image.from_registry()` for information about the other parameters.
|
730
736
|
|
731
737
|
**Example**
|
@@ -1565,12 +1571,18 @@ class Image(modal.object.Object):
|
|
1565
1571
|
) -> Image:
|
1566
1572
|
"""Build a Modal image from a private image in AWS Elastic Container Registry (ECR).
|
1567
1573
|
|
1568
|
-
You will need to pass a `modal.Secret` containing
|
1569
|
-
|
1574
|
+
You will need to pass a `modal.Secret` containing either IAM user credentials or OIDC
|
1575
|
+
configuration to access the target ECR registry.
|
1576
|
+
|
1577
|
+
For IAM user authentication, set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_REGION`.
|
1578
|
+
|
1579
|
+
For OIDC authentication, set `AWS_ROLE_ARN` and `AWS_REGION`.
|
1570
1580
|
|
1571
1581
|
IAM configuration details can be found in the AWS documentation for
|
1572
1582
|
["Private repository policies"](https://docs.aws.amazon.com/AmazonECR/latest/userguide/repository-policies.html).
|
1573
1583
|
|
1584
|
+
For more details on using an AWS role to access ECR, see the [OIDC integration guide](https://modal.com/docs/guide/oidc-integration).
|
1585
|
+
|
1574
1586
|
See `Image.from_registry()` for information about the other parameters.
|
1575
1587
|
|
1576
1588
|
**Example**
|
modal/mount.py
CHANGED
@@ -866,6 +866,7 @@ async def _create_single_client_dependency_mount(
|
|
866
866
|
uv_python_platform: str,
|
867
867
|
check_if_exists: bool = True,
|
868
868
|
allow_overwrite: bool = False,
|
869
|
+
dry_run: bool = False,
|
869
870
|
):
|
870
871
|
import tempfile
|
871
872
|
|
@@ -930,17 +931,20 @@ async def _create_single_client_dependency_mount(
|
|
930
931
|
remote_path=REMOTE_SITECUSTOMIZE_PATH,
|
931
932
|
)
|
932
933
|
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
934
|
+
if not dry_run:
|
935
|
+
try:
|
936
|
+
await python_mount._deploy.aio(
|
937
|
+
mount_name,
|
938
|
+
api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
|
939
|
+
environment_name=profile_environment,
|
940
|
+
allow_overwrite=allow_overwrite,
|
941
|
+
client=client,
|
942
|
+
)
|
943
|
+
print(f"✅ Deployed mount {mount_name} to global namespace.")
|
944
|
+
except GRPCError as e:
|
945
|
+
print(f"⚠️ Mount creation failed with {e.status}: {e.message}")
|
946
|
+
else:
|
947
|
+
print(f"Dry run - skipping deployment of mount {mount_name}")
|
944
948
|
|
945
949
|
|
946
950
|
async def _create_client_dependency_mounts(
|
@@ -948,6 +952,7 @@ async def _create_client_dependency_mounts(
|
|
948
952
|
python_versions: list[str] = list(PYTHON_STANDALONE_VERSIONS),
|
949
953
|
builder_versions: list[str] = ["2025.06"], # Reenable "PREVIEW" during testing
|
950
954
|
check_if_exists=True,
|
955
|
+
dry_run=False,
|
951
956
|
):
|
952
957
|
arch = "x86_64"
|
953
958
|
platform_tags = [
|
@@ -971,6 +976,7 @@ async def _create_client_dependency_mounts(
|
|
971
976
|
# in theory we may need to do at some point (hopefully not, but...)
|
972
977
|
check_if_exists=check_if_exists and builder_version != "PREVIEW",
|
973
978
|
allow_overwrite=builder_version == "PREVIEW",
|
979
|
+
dry_run=dry_run,
|
974
980
|
)
|
975
981
|
)
|
976
982
|
await TaskContext.gather(*coros)
|
modal/mount.pyi
CHANGED
@@ -568,12 +568,14 @@ async def _create_single_client_dependency_mount(
|
|
568
568
|
uv_python_platform: str,
|
569
569
|
check_if_exists: bool = True,
|
570
570
|
allow_overwrite: bool = False,
|
571
|
+
dry_run: bool = False,
|
571
572
|
): ...
|
572
573
|
async def _create_client_dependency_mounts(
|
573
574
|
client=None,
|
574
575
|
python_versions: list[str] = ["3.9", "3.10", "3.11", "3.12", "3.13"],
|
575
576
|
builder_versions: list[str] = ["2025.06"],
|
576
577
|
check_if_exists=True,
|
578
|
+
dry_run=False,
|
577
579
|
): ...
|
578
580
|
|
579
581
|
class __create_client_dependency_mounts_spec(typing_extensions.Protocol):
|
@@ -584,6 +586,7 @@ class __create_client_dependency_mounts_spec(typing_extensions.Protocol):
|
|
584
586
|
python_versions: list[str] = ["3.9", "3.10", "3.11", "3.12", "3.13"],
|
585
587
|
builder_versions: list[str] = ["2025.06"],
|
586
588
|
check_if_exists=True,
|
589
|
+
dry_run=False,
|
587
590
|
): ...
|
588
591
|
async def aio(
|
589
592
|
self,
|
@@ -592,6 +595,7 @@ class __create_client_dependency_mounts_spec(typing_extensions.Protocol):
|
|
592
595
|
python_versions: list[str] = ["3.9", "3.10", "3.11", "3.12", "3.13"],
|
593
596
|
builder_versions: list[str] = ["2025.06"],
|
594
597
|
check_if_exists=True,
|
598
|
+
dry_run=False,
|
595
599
|
): ...
|
596
600
|
|
597
601
|
create_client_dependency_mounts: __create_client_dependency_mounts_spec
|
modal/parallel_map.py
CHANGED
@@ -79,8 +79,10 @@ class _OutputValue:
|
|
79
79
|
|
80
80
|
MAX_INPUTS_OUTSTANDING_DEFAULT = 1000
|
81
81
|
|
82
|
-
#
|
82
|
+
# Maximum number of inputs to send to the server per FunctionPutInputs request
|
83
83
|
MAP_INVOCATION_CHUNK_SIZE = 49
|
84
|
+
SPAWN_MAP_INVOCATION_CHUNK_SIZE = 512
|
85
|
+
|
84
86
|
|
85
87
|
if typing.TYPE_CHECKING:
|
86
88
|
import modal.functions
|
@@ -159,6 +161,7 @@ class InputPumper:
|
|
159
161
|
input_queue: asyncio.Queue,
|
160
162
|
function: "modal.functions._Function",
|
161
163
|
function_call_id: str,
|
164
|
+
max_batch_size: int,
|
162
165
|
map_items_manager: Optional["_MapItemsManager"] = None,
|
163
166
|
):
|
164
167
|
self.client = client
|
@@ -167,10 +170,11 @@ class InputPumper:
|
|
167
170
|
self.input_queue = input_queue
|
168
171
|
self.inputs_sent = 0
|
169
172
|
self.function_call_id = function_call_id
|
173
|
+
self.max_batch_size = max_batch_size
|
170
174
|
|
171
175
|
async def pump_inputs(self):
|
172
176
|
assert self.client.stub
|
173
|
-
async for items in queue_batch_iterator(self.input_queue, max_batch_size=
|
177
|
+
async for items in queue_batch_iterator(self.input_queue, max_batch_size=self.max_batch_size):
|
174
178
|
# Add items to the manager. Their state will be SENDING.
|
175
179
|
if self.map_items_manager is not None:
|
176
180
|
await self.map_items_manager.add_items(items)
|
@@ -234,6 +238,7 @@ class SyncInputPumper(InputPumper):
|
|
234
238
|
input_queue=input_queue,
|
235
239
|
function=function,
|
236
240
|
function_call_id=function_call_id,
|
241
|
+
max_batch_size=MAP_INVOCATION_CHUNK_SIZE,
|
237
242
|
map_items_manager=map_items_manager,
|
238
243
|
)
|
239
244
|
self.retry_queue = retry_queue
|
@@ -241,7 +246,7 @@ class SyncInputPumper(InputPumper):
|
|
241
246
|
self.function_call_jwt = function_call_jwt
|
242
247
|
|
243
248
|
async def retry_inputs(self):
|
244
|
-
async for retriable_idxs in queue_batch_iterator(self.retry_queue, max_batch_size=
|
249
|
+
async for retriable_idxs in queue_batch_iterator(self.retry_queue, max_batch_size=self.max_batch_size):
|
245
250
|
# For each index, use the context in the manager to create a FunctionRetryInputsItem.
|
246
251
|
# This will also update the context state to RETRYING.
|
247
252
|
inputs: list[api_pb2.FunctionRetryInputsItem] = await self.map_items_manager.prepare_items_for_retry(
|
@@ -269,7 +274,13 @@ class AsyncInputPumper(InputPumper):
|
|
269
274
|
function: "modal.functions._Function",
|
270
275
|
function_call_id: str,
|
271
276
|
):
|
272
|
-
super().__init__(
|
277
|
+
super().__init__(
|
278
|
+
client,
|
279
|
+
input_queue=input_queue,
|
280
|
+
function=function,
|
281
|
+
function_call_id=function_call_id,
|
282
|
+
max_batch_size=SPAWN_MAP_INVOCATION_CHUNK_SIZE,
|
283
|
+
)
|
273
284
|
|
274
285
|
async def pump_inputs(self):
|
275
286
|
async for _ in super().pump_inputs():
|
@@ -762,7 +773,12 @@ async def _map_invocation_inputplane(
|
|
762
773
|
metadata = await client.get_input_plane_metadata(function._input_plane_region)
|
763
774
|
|
764
775
|
response: api_pb2.MapStartOrContinueResponse = await retry_transient_errors(
|
765
|
-
input_plane_stub.MapStartOrContinue,
|
776
|
+
input_plane_stub.MapStartOrContinue,
|
777
|
+
request,
|
778
|
+
metadata=metadata,
|
779
|
+
additional_status_codes=[Status.RESOURCE_EXHAUSTED],
|
780
|
+
max_delay=PUMP_INPUTS_MAX_RETRY_DELAY,
|
781
|
+
max_retries=None,
|
766
782
|
)
|
767
783
|
|
768
784
|
# match response items to the corresponding request item index
|
@@ -794,7 +810,11 @@ async def _map_invocation_inputplane(
|
|
794
810
|
await function_call_id_received.wait()
|
795
811
|
continue
|
796
812
|
|
797
|
-
|
813
|
+
sleep_task = asyncio.create_task(asyncio.sleep(1))
|
814
|
+
map_done_task = asyncio.create_task(map_done_event.wait())
|
815
|
+
done, _ = await asyncio.wait([sleep_task, map_done_task], return_when=FIRST_COMPLETED)
|
816
|
+
if map_done_task in done:
|
817
|
+
break
|
798
818
|
|
799
819
|
# check_inputs = [(idx, attempt_token), ...]
|
800
820
|
check_inputs = map_items_manager.get_input_idxs_waiting_for_output()
|
modal/parallel_map.pyi
CHANGED
@@ -89,6 +89,7 @@ class InputPumper:
|
|
89
89
|
input_queue: asyncio.queues.Queue,
|
90
90
|
function: modal._functions._Function,
|
91
91
|
function_call_id: str,
|
92
|
+
max_batch_size: int,
|
92
93
|
map_items_manager: typing.Optional[_MapItemsManager] = None,
|
93
94
|
):
|
94
95
|
"""Initialize self. See help(type(self)) for accurate signature."""
|
modal/sandbox.py
CHANGED
@@ -24,6 +24,7 @@ from ._utils.async_utils import TaskContext, synchronize_api
|
|
24
24
|
from ._utils.deprecation import deprecation_warning
|
25
25
|
from ._utils.grpc_utils import retry_transient_errors
|
26
26
|
from ._utils.mount_utils import validate_network_file_systems, validate_volumes
|
27
|
+
from ._utils.name_utils import is_valid_object_name
|
27
28
|
from .client import _Client
|
28
29
|
from .config import config
|
29
30
|
from .container_process import _ContainerProcess
|
@@ -73,6 +74,16 @@ def _validate_exec_args(args: Sequence[str]) -> None:
|
|
73
74
|
)
|
74
75
|
|
75
76
|
|
77
|
+
def _warn_if_invalid_name(name: str) -> None:
|
78
|
+
if not is_valid_object_name(name):
|
79
|
+
deprecation_warning(
|
80
|
+
(2025, 9, 3),
|
81
|
+
f"Sandbox name '{name}' will be considered invalid in a future release."
|
82
|
+
"\n\nNames may contain only alphanumeric characters, dashes, periods, and underscores,"
|
83
|
+
" must be shorter than 64 characters, and cannot conflict with App ID strings.",
|
84
|
+
)
|
85
|
+
|
86
|
+
|
76
87
|
class DefaultSandboxNameOverride(str):
|
77
88
|
"""A singleton class that represents the default sandbox name override.
|
78
89
|
|
@@ -109,6 +120,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
109
120
|
secrets: Sequence[_Secret],
|
110
121
|
name: Optional[str] = None,
|
111
122
|
timeout: int = 300,
|
123
|
+
idle_timeout: Optional[int] = None,
|
112
124
|
workdir: Optional[str] = None,
|
113
125
|
gpu: GPU_T = None,
|
114
126
|
cloud: Optional[str] = None,
|
@@ -213,6 +225,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
213
225
|
mount_ids=[mount.object_id for mount in mounts] + [mount.object_id for mount in image._mount_layers],
|
214
226
|
secret_ids=[secret.object_id for secret in secrets],
|
215
227
|
timeout_secs=timeout,
|
228
|
+
idle_timeout_secs=idle_timeout,
|
216
229
|
workdir=workdir,
|
217
230
|
resources=convert_fn_config_to_resources_config(
|
218
231
|
cpu=cpu, memory=memory, gpu=gpu, ephemeral_disk=ephemeral_disk
|
@@ -257,7 +270,9 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
257
270
|
image: Optional[_Image] = None, # The image to run as the container for the sandbox.
|
258
271
|
secrets: Sequence[_Secret] = (), # Environment variables to inject into the sandbox.
|
259
272
|
network_file_systems: dict[Union[str, os.PathLike], _NetworkFileSystem] = {},
|
260
|
-
timeout: int = 300, # Maximum
|
273
|
+
timeout: int = 300, # Maximum lifetime of the sandbox in seconds.
|
274
|
+
# The amount of time in seconds that a sandbox can be idle before being terminated.
|
275
|
+
idle_timeout: Optional[int] = None,
|
261
276
|
workdir: Optional[str] = None, # Working directory of the sandbox.
|
262
277
|
gpu: GPU_T = None,
|
263
278
|
cloud: Optional[str] = None,
|
@@ -312,7 +327,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
312
327
|
if environment_name is not None:
|
313
328
|
deprecation_warning(
|
314
329
|
(2025, 7, 16),
|
315
|
-
"Passing `environment_name` to `Sandbox.create` is deprecated and will be removed in a future release."
|
330
|
+
"Passing `environment_name` to `Sandbox.create` is deprecated and will be removed in a future release. "
|
316
331
|
"A sandbox's environment is determined by the app it is associated with.",
|
317
332
|
)
|
318
333
|
|
@@ -324,6 +339,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
324
339
|
secrets=secrets,
|
325
340
|
network_file_systems=network_file_systems,
|
326
341
|
timeout=timeout,
|
342
|
+
idle_timeout=idle_timeout,
|
327
343
|
workdir=workdir,
|
328
344
|
gpu=gpu,
|
329
345
|
cloud=cloud,
|
@@ -355,7 +371,9 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
355
371
|
secrets: Sequence[_Secret] = (), # Environment variables to inject into the sandbox.
|
356
372
|
mounts: Sequence[_Mount] = (),
|
357
373
|
network_file_systems: dict[Union[str, os.PathLike], _NetworkFileSystem] = {},
|
358
|
-
timeout: int = 300, # Maximum
|
374
|
+
timeout: int = 300, # Maximum lifetime of the sandbox in seconds.
|
375
|
+
# The amount of time in seconds that a sandbox can be idle before being terminated.
|
376
|
+
idle_timeout: Optional[int] = None,
|
359
377
|
workdir: Optional[str] = None, # Working directory of the sandbox.
|
360
378
|
gpu: GPU_T = None,
|
361
379
|
cloud: Optional[str] = None,
|
@@ -397,6 +415,11 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
397
415
|
from .app import _App
|
398
416
|
|
399
417
|
_validate_exec_args(args)
|
418
|
+
if name is not None:
|
419
|
+
_warn_if_invalid_name(name)
|
420
|
+
|
421
|
+
if block_network and (encrypted_ports or h2_ports or unencrypted_ports):
|
422
|
+
raise InvalidError("Cannot specify open ports when `block_network` is enabled")
|
400
423
|
|
401
424
|
# TODO(erikbern): Get rid of the `_new` method and create an already-hydrated object
|
402
425
|
obj = _Sandbox._new(
|
@@ -405,6 +428,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
405
428
|
secrets=secrets,
|
406
429
|
name=name,
|
407
430
|
timeout=timeout,
|
431
|
+
idle_timeout=idle_timeout,
|
408
432
|
workdir=workdir,
|
409
433
|
gpu=gpu,
|
410
434
|
cloud=cloud,
|
@@ -479,7 +503,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
479
503
|
environment_name: Optional[str] = None,
|
480
504
|
client: Optional[_Client] = None,
|
481
505
|
) -> "_Sandbox":
|
482
|
-
"""Get a running Sandbox by name from
|
506
|
+
"""Get a running Sandbox by name from a deployed App.
|
483
507
|
|
484
508
|
Raises a modal.exception.NotFoundError if no running sandbox is found with the given name.
|
485
509
|
A Sandbox's name is the `name` argument passed to `Sandbox.create`.
|
@@ -776,6 +800,9 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
776
800
|
):
|
777
801
|
client = client or await _Client.from_env()
|
778
802
|
|
803
|
+
if name is not None and name != _DEFAULT_SANDBOX_NAME_OVERRIDE:
|
804
|
+
_warn_if_invalid_name(name)
|
805
|
+
|
779
806
|
if name is _DEFAULT_SANDBOX_NAME_OVERRIDE:
|
780
807
|
restore_req = api_pb2.SandboxRestoreRequest(
|
781
808
|
snapshot_id=snapshot.object_id,
|
modal/sandbox.pyi
CHANGED
@@ -26,6 +26,7 @@ import typing
|
|
26
26
|
import typing_extensions
|
27
27
|
|
28
28
|
def _validate_exec_args(args: collections.abc.Sequence[str]) -> None: ...
|
29
|
+
def _warn_if_invalid_name(name: str) -> None: ...
|
29
30
|
|
30
31
|
class DefaultSandboxNameOverride(str):
|
31
32
|
"""A singleton class that represents the default sandbox name override.
|
@@ -60,6 +61,7 @@ class _Sandbox(modal._object._Object):
|
|
60
61
|
secrets: collections.abc.Sequence[modal.secret._Secret],
|
61
62
|
name: typing.Optional[str] = None,
|
62
63
|
timeout: int = 300,
|
64
|
+
idle_timeout: typing.Optional[int] = None,
|
63
65
|
workdir: typing.Optional[str] = None,
|
64
66
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
65
67
|
cloud: typing.Optional[str] = None,
|
@@ -96,6 +98,7 @@ class _Sandbox(modal._object._Object):
|
|
96
98
|
secrets: collections.abc.Sequence[modal.secret._Secret] = (),
|
97
99
|
network_file_systems: dict[typing.Union[str, os.PathLike], modal.network_file_system._NetworkFileSystem] = {},
|
98
100
|
timeout: int = 300,
|
101
|
+
idle_timeout: typing.Optional[int] = None,
|
99
102
|
workdir: typing.Optional[str] = None,
|
100
103
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
101
104
|
cloud: typing.Optional[str] = None,
|
@@ -145,6 +148,7 @@ class _Sandbox(modal._object._Object):
|
|
145
148
|
mounts: collections.abc.Sequence[modal.mount._Mount] = (),
|
146
149
|
network_file_systems: dict[typing.Union[str, os.PathLike], modal.network_file_system._NetworkFileSystem] = {},
|
147
150
|
timeout: int = 300,
|
151
|
+
idle_timeout: typing.Optional[int] = None,
|
148
152
|
workdir: typing.Optional[str] = None,
|
149
153
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
150
154
|
cloud: typing.Optional[str] = None,
|
@@ -177,7 +181,7 @@ class _Sandbox(modal._object._Object):
|
|
177
181
|
environment_name: typing.Optional[str] = None,
|
178
182
|
client: typing.Optional[modal.client._Client] = None,
|
179
183
|
) -> _Sandbox:
|
180
|
-
"""Get a running Sandbox by name from
|
184
|
+
"""Get a running Sandbox by name from a deployed App.
|
181
185
|
|
182
186
|
Raises a modal.exception.NotFoundError if no running sandbox is found with the given name.
|
183
187
|
A Sandbox's name is the `name` argument passed to `Sandbox.create`.
|
@@ -370,6 +374,7 @@ class Sandbox(modal.object.Object):
|
|
370
374
|
secrets: collections.abc.Sequence[modal.secret.Secret],
|
371
375
|
name: typing.Optional[str] = None,
|
372
376
|
timeout: int = 300,
|
377
|
+
idle_timeout: typing.Optional[int] = None,
|
373
378
|
workdir: typing.Optional[str] = None,
|
374
379
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
375
380
|
cloud: typing.Optional[str] = None,
|
@@ -409,6 +414,7 @@ class Sandbox(modal.object.Object):
|
|
409
414
|
typing.Union[str, os.PathLike], modal.network_file_system.NetworkFileSystem
|
410
415
|
] = {},
|
411
416
|
timeout: int = 300,
|
417
|
+
idle_timeout: typing.Optional[int] = None,
|
412
418
|
workdir: typing.Optional[str] = None,
|
413
419
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
414
420
|
cloud: typing.Optional[str] = None,
|
@@ -460,6 +466,7 @@ class Sandbox(modal.object.Object):
|
|
460
466
|
typing.Union[str, os.PathLike], modal.network_file_system.NetworkFileSystem
|
461
467
|
] = {},
|
462
468
|
timeout: int = 300,
|
469
|
+
idle_timeout: typing.Optional[int] = None,
|
463
470
|
workdir: typing.Optional[str] = None,
|
464
471
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
465
472
|
cloud: typing.Optional[str] = None,
|
@@ -515,6 +522,7 @@ class Sandbox(modal.object.Object):
|
|
515
522
|
typing.Union[str, os.PathLike], modal.network_file_system.NetworkFileSystem
|
516
523
|
] = {},
|
517
524
|
timeout: int = 300,
|
525
|
+
idle_timeout: typing.Optional[int] = None,
|
518
526
|
workdir: typing.Optional[str] = None,
|
519
527
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
520
528
|
cloud: typing.Optional[str] = None,
|
@@ -551,6 +559,7 @@ class Sandbox(modal.object.Object):
|
|
551
559
|
typing.Union[str, os.PathLike], modal.network_file_system.NetworkFileSystem
|
552
560
|
] = {},
|
553
561
|
timeout: int = 300,
|
562
|
+
idle_timeout: typing.Optional[int] = None,
|
554
563
|
workdir: typing.Optional[str] = None,
|
555
564
|
gpu: typing.Union[None, str, modal.gpu._GPUConfig] = None,
|
556
565
|
cloud: typing.Optional[str] = None,
|
@@ -589,7 +598,7 @@ class Sandbox(modal.object.Object):
|
|
589
598
|
environment_name: typing.Optional[str] = None,
|
590
599
|
client: typing.Optional[modal.client.Client] = None,
|
591
600
|
) -> Sandbox:
|
592
|
-
"""Get a running Sandbox by name from
|
601
|
+
"""Get a running Sandbox by name from a deployed App.
|
593
602
|
|
594
603
|
Raises a modal.exception.NotFoundError if no running sandbox is found with the given name.
|
595
604
|
A Sandbox's name is the `name` argument passed to `Sandbox.create`.
|
@@ -605,7 +614,7 @@ class Sandbox(modal.object.Object):
|
|
605
614
|
environment_name: typing.Optional[str] = None,
|
606
615
|
client: typing.Optional[modal.client.Client] = None,
|
607
616
|
) -> Sandbox:
|
608
|
-
"""Get a running Sandbox by name from
|
617
|
+
"""Get a running Sandbox by name from a deployed App.
|
609
618
|
|
610
619
|
Raises a modal.exception.NotFoundError if no running sandbox is found with the given name.
|
611
620
|
A Sandbox's name is the `name` argument passed to `Sandbox.create`.
|