modal 1.2.1.dev19__py3-none-any.whl → 1.2.2.dev21__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 +1 -3
- modal/_container_entrypoint.py +4 -1
- modal/_functions.py +33 -49
- modal/_grpc_client.py +148 -0
- modal/_output.py +3 -4
- modal/_runtime/container_io_manager.py +21 -22
- modal/_utils/async_utils.py +12 -3
- modal/_utils/auth_token_manager.py +1 -4
- modal/_utils/blob_utils.py +3 -4
- modal/_utils/grpc_utils.py +80 -51
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/task_command_router_client.py +3 -4
- modal/app.py +3 -4
- modal/cli/config.py +3 -1
- modal/cli/container.py +1 -2
- modal/cli/entry_point.py +1 -0
- modal/cli/launch.py +1 -2
- modal/cli/network_file_system.py +1 -4
- modal/cli/queues.py +1 -2
- modal/cli/secret.py +1 -2
- modal/client.py +5 -115
- modal/client.pyi +2 -91
- modal/cls.py +1 -2
- modal/config.py +1 -1
- modal/container_process.py +4 -8
- modal/dict.py +12 -12
- modal/environments.py +1 -2
- modal/experimental/__init__.py +2 -3
- modal/experimental/flash.py +6 -10
- modal/file_io.py +13 -27
- modal/functions.pyi +6 -6
- modal/image.py +24 -3
- modal/image.pyi +4 -0
- modal/io_streams.py +61 -91
- modal/io_streams.pyi +33 -95
- modal/mount.py +4 -4
- modal/network_file_system.py +5 -6
- modal/parallel_map.py +29 -31
- modal/parallel_map.pyi +3 -9
- modal/queue.py +17 -18
- modal/runner.py +8 -8
- modal/sandbox.py +23 -36
- modal/secret.py +4 -5
- modal/snapshot.py +1 -4
- modal/token_flow.py +1 -1
- modal/volume.py +20 -22
- {modal-1.2.1.dev19.dist-info → modal-1.2.2.dev21.dist-info}/METADATA +1 -1
- {modal-1.2.1.dev19.dist-info → modal-1.2.2.dev21.dist-info}/RECORD +57 -56
- modal_proto/api.proto +3 -0
- modal_proto/api_pb2.py +1028 -1015
- modal_proto/api_pb2.pyi +29 -3
- modal_proto/modal_api_grpc.py +175 -175
- modal_version/__init__.py +1 -1
- {modal-1.2.1.dev19.dist-info → modal-1.2.2.dev21.dist-info}/WHEEL +0 -0
- {modal-1.2.1.dev19.dist-info → modal-1.2.2.dev21.dist-info}/entry_points.txt +0 -0
- {modal-1.2.1.dev19.dist-info → modal-1.2.2.dev21.dist-info}/licenses/LICENSE +0 -0
- {modal-1.2.1.dev19.dist-info → modal-1.2.2.dev21.dist-info}/top_level.txt +0 -0
modal/network_file_system.py
CHANGED
|
@@ -22,7 +22,6 @@ from ._resolver import Resolver
|
|
|
22
22
|
from ._utils.async_utils import TaskContext, aclosing, async_map, sync_or_async_iter, synchronize_api
|
|
23
23
|
from ._utils.blob_utils import LARGE_FILE_LIMIT, blob_iter, blob_upload_file
|
|
24
24
|
from ._utils.deprecation import warn_if_passing_namespace
|
|
25
|
-
from ._utils.grpc_utils import retry_transient_errors
|
|
26
25
|
from ._utils.hash_utils import get_sha256_hex
|
|
27
26
|
from ._utils.name_utils import check_object_name
|
|
28
27
|
from .client import _Client
|
|
@@ -188,14 +187,14 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
|
|
|
188
187
|
environment_name=_get_environment_name(environment_name),
|
|
189
188
|
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_CREATE_FAIL_IF_EXISTS,
|
|
190
189
|
)
|
|
191
|
-
resp = await
|
|
190
|
+
resp = await client.stub.SharedVolumeGetOrCreate(request)
|
|
192
191
|
return resp.shared_volume_id
|
|
193
192
|
|
|
194
193
|
@staticmethod
|
|
195
194
|
async def delete(name: str, client: Optional[_Client] = None, environment_name: Optional[str] = None):
|
|
196
195
|
obj = await _NetworkFileSystem.from_name(name, environment_name=environment_name).hydrate(client)
|
|
197
196
|
req = api_pb2.SharedVolumeDeleteRequest(shared_volume_id=obj.object_id)
|
|
198
|
-
await
|
|
197
|
+
await obj._client.stub.SharedVolumeDelete(req)
|
|
199
198
|
|
|
200
199
|
@live_method
|
|
201
200
|
async def write_file(self, remote_path: str, fp: BinaryIO, progress_cb: Optional[Callable[..., Any]] = None) -> int:
|
|
@@ -235,7 +234,7 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
|
|
|
235
234
|
|
|
236
235
|
t0 = time.monotonic()
|
|
237
236
|
while time.monotonic() - t0 < NETWORK_FILE_SYSTEM_PUT_FILE_CLIENT_TIMEOUT:
|
|
238
|
-
response = await
|
|
237
|
+
response = await self._client.stub.SharedVolumePutFile(req)
|
|
239
238
|
if response.exists:
|
|
240
239
|
break
|
|
241
240
|
else:
|
|
@@ -248,7 +247,7 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
|
|
|
248
247
|
"""Read a file from the network file system"""
|
|
249
248
|
req = api_pb2.SharedVolumeGetFileRequest(shared_volume_id=self.object_id, path=path)
|
|
250
249
|
try:
|
|
251
|
-
response = await
|
|
250
|
+
response = await self._client.stub.SharedVolumeGetFile(req)
|
|
252
251
|
except modal.exception.NotFoundError as exc:
|
|
253
252
|
raise FileNotFoundError(exc.args[0])
|
|
254
253
|
|
|
@@ -333,7 +332,7 @@ class _NetworkFileSystem(_Object, type_prefix="sv"):
|
|
|
333
332
|
"""Remove a file in a network file system."""
|
|
334
333
|
req = api_pb2.SharedVolumeRemoveFileRequest(shared_volume_id=self.object_id, path=path, recursive=recursive)
|
|
335
334
|
try:
|
|
336
|
-
await
|
|
335
|
+
await self._client.stub.SharedVolumeRemoveFile(req)
|
|
337
336
|
except modal.exception.NotFoundError as exc:
|
|
338
337
|
raise FileNotFoundError(exc.args[0])
|
|
339
338
|
|
modal/parallel_map.py
CHANGED
|
@@ -35,7 +35,7 @@ from modal._utils.function_utils import (
|
|
|
35
35
|
_create_input,
|
|
36
36
|
_process_result,
|
|
37
37
|
)
|
|
38
|
-
from modal._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES,
|
|
38
|
+
from modal._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES, Retry, RetryWarningMessage
|
|
39
39
|
from modal._utils.jwt_utils import DecodedJwt
|
|
40
40
|
from modal.config import logger
|
|
41
41
|
from modal.retries import RetryManager
|
|
@@ -187,7 +187,7 @@ class InputPumper:
|
|
|
187
187
|
f" push is {self.input_queue.qsize()}. "
|
|
188
188
|
)
|
|
189
189
|
|
|
190
|
-
resp = await self.
|
|
190
|
+
resp = await self.client.stub.FunctionPutInputs(request, retry=self._function_inputs_retry)
|
|
191
191
|
self.inputs_sent += len(items)
|
|
192
192
|
# Change item state to WAITING_FOR_OUTPUT, and set the input_id and input_jwt which are in the response.
|
|
193
193
|
if self.map_items_manager is not None:
|
|
@@ -198,11 +198,8 @@ class InputPumper:
|
|
|
198
198
|
)
|
|
199
199
|
yield
|
|
200
200
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
fn: "modal.client.UnaryUnaryWrapper",
|
|
204
|
-
request: typing.Union[api_pb2.FunctionPutInputsRequest, api_pb2.FunctionRetryInputsRequest],
|
|
205
|
-
) -> typing.Union[api_pb2.FunctionPutInputsResponse, api_pb2.FunctionRetryInputsResponse]:
|
|
201
|
+
@property
|
|
202
|
+
def _function_inputs_retry(self) -> Retry:
|
|
206
203
|
# with 8 retries we log the warning below about every 30 seconds which isn't too spammy.
|
|
207
204
|
retry_warning_message = RetryWarningMessage(
|
|
208
205
|
message=f"Warning: map progress for function {self.function._function_name} is limited."
|
|
@@ -210,13 +207,11 @@ class InputPumper:
|
|
|
210
207
|
warning_interval=8,
|
|
211
208
|
errors_to_warn_for=[Status.RESOURCE_EXHAUSTED],
|
|
212
209
|
)
|
|
213
|
-
return
|
|
214
|
-
fn,
|
|
215
|
-
request,
|
|
210
|
+
return Retry(
|
|
216
211
|
max_retries=None,
|
|
217
212
|
max_delay=PUMP_INPUTS_MAX_RETRY_DELAY,
|
|
218
213
|
additional_status_codes=[Status.RESOURCE_EXHAUSTED],
|
|
219
|
-
|
|
214
|
+
warning_message=retry_warning_message,
|
|
220
215
|
)
|
|
221
216
|
|
|
222
217
|
|
|
@@ -255,7 +250,7 @@ class SyncInputPumper(InputPumper):
|
|
|
255
250
|
function_call_jwt=self.function_call_jwt,
|
|
256
251
|
inputs=inputs,
|
|
257
252
|
)
|
|
258
|
-
resp = await self.
|
|
253
|
+
resp = await self.client.stub.FunctionRetryInputs(request, retry=self._function_inputs_retry)
|
|
259
254
|
# Update the state to WAITING_FOR_OUTPUT, and update the input_jwt in the context
|
|
260
255
|
# to the new value in the response.
|
|
261
256
|
self.map_items_manager.handle_retry_response(resp.input_jwts)
|
|
@@ -289,7 +284,7 @@ class AsyncInputPumper(InputPumper):
|
|
|
289
284
|
function_call_id=self.function_call_id,
|
|
290
285
|
num_inputs=self.inputs_sent,
|
|
291
286
|
)
|
|
292
|
-
await
|
|
287
|
+
await self.client.stub.FunctionFinishInputs(request, retry=Retry(max_retries=None))
|
|
293
288
|
yield
|
|
294
289
|
|
|
295
290
|
|
|
@@ -303,7 +298,7 @@ async def _spawn_map_invocation(
|
|
|
303
298
|
function_call_type=api_pb2.FUNCTION_CALL_TYPE_MAP,
|
|
304
299
|
function_call_invocation_type=api_pb2.FUNCTION_CALL_INVOCATION_TYPE_ASYNC,
|
|
305
300
|
)
|
|
306
|
-
response: api_pb2.FunctionMapResponse = await
|
|
301
|
+
response: api_pb2.FunctionMapResponse = await client.stub.FunctionMap(request)
|
|
307
302
|
function_call_id = response.function_call_id
|
|
308
303
|
|
|
309
304
|
have_all_inputs = False
|
|
@@ -382,7 +377,7 @@ async def _map_invocation(
|
|
|
382
377
|
return_exceptions=return_exceptions,
|
|
383
378
|
function_call_invocation_type=function_call_invocation_type,
|
|
384
379
|
)
|
|
385
|
-
response: api_pb2.FunctionMapResponse = await
|
|
380
|
+
response: api_pb2.FunctionMapResponse = await client.stub.FunctionMap(request)
|
|
386
381
|
|
|
387
382
|
function_call_id = response.function_call_id
|
|
388
383
|
function_call_jwt = response.function_call_jwt
|
|
@@ -478,11 +473,12 @@ async def _map_invocation(
|
|
|
478
473
|
input_jwts=input_jwts,
|
|
479
474
|
)
|
|
480
475
|
get_response_task = asyncio.create_task(
|
|
481
|
-
|
|
482
|
-
client.stub.FunctionGetOutputs,
|
|
476
|
+
client.stub.FunctionGetOutputs(
|
|
483
477
|
request,
|
|
484
|
-
|
|
485
|
-
|
|
478
|
+
retry=Retry(
|
|
479
|
+
max_retries=20,
|
|
480
|
+
attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD,
|
|
481
|
+
),
|
|
486
482
|
)
|
|
487
483
|
)
|
|
488
484
|
map_done_task = asyncio.create_task(map_done_event.wait())
|
|
@@ -541,7 +537,7 @@ async def _map_invocation(
|
|
|
541
537
|
clear_on_success=True,
|
|
542
538
|
requested_at=time.time(),
|
|
543
539
|
)
|
|
544
|
-
await
|
|
540
|
+
await client.stub.FunctionGetOutputs(request)
|
|
545
541
|
await retry_queue.close()
|
|
546
542
|
|
|
547
543
|
async def fetch_output(item: api_pb2.FunctionGetOutputsItem) -> tuple[int, Any]:
|
|
@@ -770,13 +766,14 @@ async def _map_invocation_inputplane(
|
|
|
770
766
|
|
|
771
767
|
metadata = await client.get_input_plane_metadata(function._input_plane_region)
|
|
772
768
|
|
|
773
|
-
response: api_pb2.MapStartOrContinueResponse = await
|
|
774
|
-
input_plane_stub.MapStartOrContinue,
|
|
769
|
+
response: api_pb2.MapStartOrContinueResponse = await input_plane_stub.MapStartOrContinue(
|
|
775
770
|
request,
|
|
771
|
+
retry=Retry(
|
|
772
|
+
additional_status_codes=[Status.RESOURCE_EXHAUSTED],
|
|
773
|
+
max_delay=PUMP_INPUTS_MAX_RETRY_DELAY,
|
|
774
|
+
max_retries=None,
|
|
775
|
+
),
|
|
776
776
|
metadata=metadata,
|
|
777
|
-
additional_status_codes=[Status.RESOURCE_EXHAUSTED],
|
|
778
|
-
max_delay=PUMP_INPUTS_MAX_RETRY_DELAY,
|
|
779
|
-
max_retries=None,
|
|
780
777
|
)
|
|
781
778
|
|
|
782
779
|
# match response items to the corresponding request item index
|
|
@@ -824,8 +821,8 @@ async def _map_invocation_inputplane(
|
|
|
824
821
|
)
|
|
825
822
|
|
|
826
823
|
metadata = await client.get_input_plane_metadata(function._input_plane_region)
|
|
827
|
-
response: api_pb2.MapCheckInputsResponse = await
|
|
828
|
-
|
|
824
|
+
response: api_pb2.MapCheckInputsResponse = await input_plane_stub.MapCheckInputs(
|
|
825
|
+
request, metadata=metadata
|
|
829
826
|
)
|
|
830
827
|
check_inputs_response = [
|
|
831
828
|
(check_inputs[resp_idx][0], response.lost[resp_idx]) for resp_idx, _ in enumerate(response.lost)
|
|
@@ -859,11 +856,12 @@ async def _map_invocation_inputplane(
|
|
|
859
856
|
)
|
|
860
857
|
metadata = await client.get_input_plane_metadata(function._input_plane_region)
|
|
861
858
|
get_response_task = asyncio.create_task(
|
|
862
|
-
|
|
863
|
-
input_plane_stub.MapAwait,
|
|
859
|
+
input_plane_stub.MapAwait(
|
|
864
860
|
request,
|
|
865
|
-
|
|
866
|
-
|
|
861
|
+
retry=Retry(
|
|
862
|
+
max_retries=20,
|
|
863
|
+
attempt_timeout=OUTPUTS_TIMEOUT + ATTEMPT_TIMEOUT_GRACE_PERIOD,
|
|
864
|
+
),
|
|
867
865
|
metadata=metadata,
|
|
868
866
|
)
|
|
869
867
|
)
|
modal/parallel_map.pyi
CHANGED
|
@@ -5,6 +5,7 @@ import collections.abc
|
|
|
5
5
|
import enum
|
|
6
6
|
import modal._functions
|
|
7
7
|
import modal._utils.async_utils
|
|
8
|
+
import modal._utils.grpc_utils
|
|
8
9
|
import modal.client
|
|
9
10
|
import modal.functions
|
|
10
11
|
import modal.retries
|
|
@@ -96,15 +97,8 @@ class InputPumper:
|
|
|
96
97
|
...
|
|
97
98
|
|
|
98
99
|
def pump_inputs(self): ...
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
fn: modal.client.UnaryUnaryWrapper,
|
|
102
|
-
request: typing.Union[
|
|
103
|
-
modal_proto.api_pb2.FunctionPutInputsRequest, modal_proto.api_pb2.FunctionRetryInputsRequest
|
|
104
|
-
],
|
|
105
|
-
) -> typing.Union[
|
|
106
|
-
modal_proto.api_pb2.FunctionPutInputsResponse, modal_proto.api_pb2.FunctionRetryInputsResponse
|
|
107
|
-
]: ...
|
|
100
|
+
@property
|
|
101
|
+
def _function_inputs_retry(self) -> modal._utils.grpc_utils.Retry: ...
|
|
108
102
|
|
|
109
103
|
class SyncInputPumper(InputPumper):
|
|
110
104
|
"""Reads inputs from a queue of FunctionPutInputsItems, and sends them to the server."""
|
modal/queue.py
CHANGED
|
@@ -25,7 +25,7 @@ from ._resolver import Resolver
|
|
|
25
25
|
from ._serialization import deserialize, serialize
|
|
26
26
|
from ._utils.async_utils import TaskContext, synchronize_api, warn_if_generator_is_not_consumed
|
|
27
27
|
from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
|
|
28
|
-
from ._utils.grpc_utils import
|
|
28
|
+
from ._utils.grpc_utils import Retry
|
|
29
29
|
from ._utils.name_utils import check_object_name
|
|
30
30
|
from ._utils.time_utils import as_timestamp, timestamp_to_localized_dt
|
|
31
31
|
from .client import _Client
|
|
@@ -95,7 +95,7 @@ class _QueueManager:
|
|
|
95
95
|
object_creation_type=object_creation_type,
|
|
96
96
|
)
|
|
97
97
|
try:
|
|
98
|
-
await
|
|
98
|
+
await client.stub.QueueGetOrCreate(req)
|
|
99
99
|
except GRPCError as exc:
|
|
100
100
|
if exc.status == Status.ALREADY_EXISTS and not allow_existing:
|
|
101
101
|
raise AlreadyExistsError(exc.message)
|
|
@@ -147,7 +147,7 @@ class _QueueManager:
|
|
|
147
147
|
req = api_pb2.QueueListRequest(
|
|
148
148
|
environment_name=_get_environment_name(environment_name), pagination=pagination
|
|
149
149
|
)
|
|
150
|
-
resp = await
|
|
150
|
+
resp = await client.stub.QueueList(req)
|
|
151
151
|
items.extend(resp.queues)
|
|
152
152
|
finished = (len(resp.queues) < max_page_size) or (max_objects is not None and len(items) >= max_objects)
|
|
153
153
|
return finished
|
|
@@ -205,7 +205,7 @@ class _QueueManager:
|
|
|
205
205
|
raise
|
|
206
206
|
else:
|
|
207
207
|
req = api_pb2.QueueDeleteRequest(queue_id=obj.object_id)
|
|
208
|
-
await
|
|
208
|
+
await obj._client.stub.QueueDelete(req)
|
|
209
209
|
|
|
210
210
|
|
|
211
211
|
QueueManager = synchronize_api(_QueueManager)
|
|
@@ -424,7 +424,7 @@ class _Queue(_Object, type_prefix="qu"):
|
|
|
424
424
|
n_values=n_values,
|
|
425
425
|
)
|
|
426
426
|
|
|
427
|
-
response = await
|
|
427
|
+
response = await self._client.stub.QueueGet(request)
|
|
428
428
|
if response.values:
|
|
429
429
|
return [deserialize(value, self._client) for value in response.values]
|
|
430
430
|
else:
|
|
@@ -449,7 +449,7 @@ class _Queue(_Object, type_prefix="qu"):
|
|
|
449
449
|
n_values=n_values,
|
|
450
450
|
)
|
|
451
451
|
|
|
452
|
-
response = await
|
|
452
|
+
response = await self._client.stub.QueueGet(request)
|
|
453
453
|
|
|
454
454
|
if response.values:
|
|
455
455
|
return [deserialize(value, self._client) for value in response.values]
|
|
@@ -469,7 +469,7 @@ class _Queue(_Object, type_prefix="qu"):
|
|
|
469
469
|
partition_key=self.validate_partition_key(partition),
|
|
470
470
|
all_partitions=all,
|
|
471
471
|
)
|
|
472
|
-
await
|
|
472
|
+
await self._client.stub.QueueClear(request)
|
|
473
473
|
|
|
474
474
|
@live_method
|
|
475
475
|
async def get(
|
|
@@ -578,14 +578,15 @@ class _Queue(_Object, type_prefix="qu"):
|
|
|
578
578
|
partition_ttl_seconds=partition_ttl,
|
|
579
579
|
)
|
|
580
580
|
try:
|
|
581
|
-
await
|
|
582
|
-
self._client.stub.QueuePut,
|
|
581
|
+
await self._client.stub.QueuePut(
|
|
583
582
|
request,
|
|
584
583
|
# A full queue will return this status.
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
584
|
+
retry=Retry(
|
|
585
|
+
additional_status_codes=[Status.RESOURCE_EXHAUSTED],
|
|
586
|
+
max_delay=30.0,
|
|
587
|
+
max_retries=None,
|
|
588
|
+
total_timeout=timeout,
|
|
589
|
+
),
|
|
589
590
|
)
|
|
590
591
|
except GRPCError as exc:
|
|
591
592
|
if exc.status == Status.RESOURCE_EXHAUSTED:
|
|
@@ -605,7 +606,7 @@ class _Queue(_Object, type_prefix="qu"):
|
|
|
605
606
|
partition_ttl_seconds=partition_ttl,
|
|
606
607
|
)
|
|
607
608
|
try:
|
|
608
|
-
await
|
|
609
|
+
await self._client.stub.QueuePut(request)
|
|
609
610
|
except GRPCError as exc:
|
|
610
611
|
if exc.status == Status.RESOURCE_EXHAUSTED:
|
|
611
612
|
raise queue.Full(exc.message)
|
|
@@ -625,7 +626,7 @@ class _Queue(_Object, type_prefix="qu"):
|
|
|
625
626
|
partition_key=self.validate_partition_key(partition),
|
|
626
627
|
total=total,
|
|
627
628
|
)
|
|
628
|
-
response = await
|
|
629
|
+
response = await self._client.stub.QueueLen(request)
|
|
629
630
|
return response.len
|
|
630
631
|
|
|
631
632
|
@warn_if_generator_is_not_consumed()
|
|
@@ -651,9 +652,7 @@ class _Queue(_Object, type_prefix="qu"):
|
|
|
651
652
|
item_poll_timeout=poll_duration,
|
|
652
653
|
)
|
|
653
654
|
|
|
654
|
-
response: api_pb2.QueueNextItemsResponse = await
|
|
655
|
-
self._client.stub.QueueNextItems, request
|
|
656
|
-
)
|
|
655
|
+
response: api_pb2.QueueNextItemsResponse = await self._client.stub.QueueNextItems(request)
|
|
657
656
|
if response.items:
|
|
658
657
|
for item in response.items:
|
|
659
658
|
yield deserialize(item.value, self._client)
|
modal/runner.py
CHANGED
|
@@ -19,6 +19,7 @@ from synchronicity.async_wrap import asynccontextmanager
|
|
|
19
19
|
|
|
20
20
|
import modal._runtime.execution_context
|
|
21
21
|
import modal_proto.api_pb2
|
|
22
|
+
from modal._utils.grpc_utils import Retry
|
|
22
23
|
from modal_proto import api_pb2
|
|
23
24
|
|
|
24
25
|
from ._functions import _Function
|
|
@@ -29,7 +30,6 @@ from ._traceback import print_server_warnings, traceback_contains_remote_call
|
|
|
29
30
|
from ._utils.async_utils import TaskContext, gather_cancel_on_exc, synchronize_api
|
|
30
31
|
from ._utils.deprecation import warn_if_passing_namespace
|
|
31
32
|
from ._utils.git_utils import get_git_commit_info
|
|
32
|
-
from ._utils.grpc_utils import retry_transient_errors
|
|
33
33
|
from ._utils.name_utils import check_object_name, is_valid_tag
|
|
34
34
|
from .client import HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, _Client
|
|
35
35
|
from .cls import _Cls
|
|
@@ -54,14 +54,14 @@ async def _heartbeat(client: _Client, app_id: str) -> None:
|
|
|
54
54
|
# TODO(erikbern): we should capture exceptions here
|
|
55
55
|
# * if request fails: destroy the client
|
|
56
56
|
# * if server says the app is gone: print a helpful warning about detaching
|
|
57
|
-
await
|
|
57
|
+
await client.stub.AppHeartbeat(request, retry=Retry(attempt_timeout=HEARTBEAT_TIMEOUT))
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
async def _init_local_app_existing(client: _Client, existing_app_id: str, environment_name: str) -> RunningApp:
|
|
61
61
|
# Get all the objects first
|
|
62
62
|
obj_req = api_pb2.AppGetLayoutRequest(app_id=existing_app_id)
|
|
63
63
|
obj_resp, _ = await gather_cancel_on_exc(
|
|
64
|
-
|
|
64
|
+
client.stub.AppGetLayout(obj_req),
|
|
65
65
|
# Cache the environment associated with the app now as we will use it later
|
|
66
66
|
_get_environment_cached(environment_name, client),
|
|
67
67
|
)
|
|
@@ -86,7 +86,7 @@ async def _init_local_app_new(
|
|
|
86
86
|
app_state=app_state, # type: ignore
|
|
87
87
|
)
|
|
88
88
|
app_resp, _ = await gather_cancel_on_exc( # TODO: use TaskGroup?
|
|
89
|
-
|
|
89
|
+
client.stub.AppCreate(app_req),
|
|
90
90
|
# Cache the environment associated with the app now as we will use it later
|
|
91
91
|
_get_environment_cached(environment_name, client),
|
|
92
92
|
)
|
|
@@ -109,7 +109,7 @@ async def _init_local_app_from_name(
|
|
|
109
109
|
name=name,
|
|
110
110
|
environment_name=environment_name,
|
|
111
111
|
)
|
|
112
|
-
app_resp = await
|
|
112
|
+
app_resp = await client.stub.AppGetByDeploymentName(app_req)
|
|
113
113
|
existing_app_id = app_resp.app_id or None
|
|
114
114
|
|
|
115
115
|
# Grab the app
|
|
@@ -201,7 +201,7 @@ async def _publish_app(
|
|
|
201
201
|
)
|
|
202
202
|
|
|
203
203
|
try:
|
|
204
|
-
response = await
|
|
204
|
+
response = await client.stub.AppPublish(request)
|
|
205
205
|
except GRPCError as exc:
|
|
206
206
|
if exc.status == Status.INVALID_ARGUMENT or exc.status == Status.FAILED_PRECONDITION:
|
|
207
207
|
raise InvalidError(exc.message)
|
|
@@ -225,7 +225,7 @@ async def _disconnect(
|
|
|
225
225
|
|
|
226
226
|
logger.debug("Sending app disconnect/stop request")
|
|
227
227
|
req_disconnect = api_pb2.AppClientDisconnectRequest(app_id=app_id, reason=reason, exception=exc_str)
|
|
228
|
-
await
|
|
228
|
+
await client.stub.AppClientDisconnect(req_disconnect)
|
|
229
229
|
logger.debug("App disconnected")
|
|
230
230
|
|
|
231
231
|
|
|
@@ -635,7 +635,7 @@ async def _interactive_shell(
|
|
|
635
635
|
except InteractiveTimeoutError:
|
|
636
636
|
# Check on status of Sandbox. It may have crashed, causing connection failure.
|
|
637
637
|
req = api_pb2.SandboxWaitRequest(sandbox_id=sandbox._object_id, timeout=0)
|
|
638
|
-
resp = await
|
|
638
|
+
resp = await sandbox._client.stub.SandboxWait(req)
|
|
639
639
|
if resp.result.exception:
|
|
640
640
|
raise RemoteError(resp.result.exception)
|
|
641
641
|
else:
|
modal/sandbox.py
CHANGED
|
@@ -28,7 +28,6 @@ from ._resolver import Resolver
|
|
|
28
28
|
from ._resources import convert_fn_config_to_resources_config
|
|
29
29
|
from ._utils.async_utils import TaskContext, synchronize_api
|
|
30
30
|
from ._utils.deprecation import deprecation_warning
|
|
31
|
-
from ._utils.grpc_utils import retry_transient_errors
|
|
32
31
|
from ._utils.mount_utils import validate_network_file_systems, validate_volumes
|
|
33
32
|
from ._utils.name_utils import is_valid_object_name
|
|
34
33
|
from ._utils.task_command_router_client import TaskCommandRouterClient
|
|
@@ -273,7 +272,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
273
272
|
|
|
274
273
|
create_req = api_pb2.SandboxCreateRequest(app_id=resolver.app_id, definition=definition)
|
|
275
274
|
try:
|
|
276
|
-
create_resp = await
|
|
275
|
+
create_resp = await resolver.client.stub.SandboxCreate(create_req)
|
|
277
276
|
except GRPCError as exc:
|
|
278
277
|
if exc.status == Status.ALREADY_EXISTS:
|
|
279
278
|
raise AlreadyExistsError(exc.message)
|
|
@@ -516,10 +515,10 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
516
515
|
return obj
|
|
517
516
|
|
|
518
517
|
def _hydrate_metadata(self, handle_metadata: Optional[Message]):
|
|
519
|
-
self._stdout
|
|
518
|
+
self._stdout = StreamReader(
|
|
520
519
|
api_pb2.FILE_DESCRIPTOR_STDOUT, self.object_id, "sandbox", self._client, by_line=True
|
|
521
520
|
)
|
|
522
|
-
self._stderr
|
|
521
|
+
self._stderr = StreamReader(
|
|
523
522
|
api_pb2.FILE_DESCRIPTOR_STDERR, self.object_id, "sandbox", self._client, by_line=True
|
|
524
523
|
)
|
|
525
524
|
self._stdin = StreamWriter(self.object_id, "sandbox", self._client)
|
|
@@ -547,7 +546,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
547
546
|
env_name = _get_environment_name(environment_name)
|
|
548
547
|
|
|
549
548
|
req = api_pb2.SandboxGetFromNameRequest(sandbox_name=name, app_name=app_name, environment_name=env_name)
|
|
550
|
-
resp = await
|
|
549
|
+
resp = await client.stub.SandboxGetFromName(req)
|
|
551
550
|
return _Sandbox._new_hydrated(resp.sandbox_id, client, None)
|
|
552
551
|
|
|
553
552
|
@staticmethod
|
|
@@ -560,7 +559,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
560
559
|
client = await _Client.from_env()
|
|
561
560
|
|
|
562
561
|
req = api_pb2.SandboxWaitRequest(sandbox_id=sandbox_id, timeout=0)
|
|
563
|
-
resp = await
|
|
562
|
+
resp = await client.stub.SandboxWait(req)
|
|
564
563
|
|
|
565
564
|
obj = _Sandbox._new_hydrated(sandbox_id, client, None)
|
|
566
565
|
|
|
@@ -573,7 +572,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
573
572
|
"""Fetches any tags (key-value pairs) currently attached to this Sandbox from the server."""
|
|
574
573
|
req = api_pb2.SandboxTagsGetRequest(sandbox_id=self.object_id)
|
|
575
574
|
try:
|
|
576
|
-
resp = await
|
|
575
|
+
resp = await self._client.stub.SandboxTagsGet(req)
|
|
577
576
|
except GRPCError as exc:
|
|
578
577
|
raise InvalidError(exc.message) if exc.status == Status.INVALID_ARGUMENT else exc
|
|
579
578
|
|
|
@@ -597,7 +596,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
597
596
|
tags=tags_list,
|
|
598
597
|
)
|
|
599
598
|
try:
|
|
600
|
-
await
|
|
599
|
+
await self._client.stub.SandboxTagsSet(req)
|
|
601
600
|
except GRPCError as exc:
|
|
602
601
|
raise InvalidError(exc.message) if exc.status == Status.INVALID_ARGUMENT else exc
|
|
603
602
|
|
|
@@ -609,7 +608,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
609
608
|
"""
|
|
610
609
|
await self._get_task_id() # Ensure the sandbox has started
|
|
611
610
|
req = api_pb2.SandboxSnapshotFsRequest(sandbox_id=self.object_id, timeout=timeout)
|
|
612
|
-
resp = await
|
|
611
|
+
resp = await self._client.stub.SandboxSnapshotFs(req)
|
|
613
612
|
|
|
614
613
|
if resp.result.status != api_pb2.GenericResult.GENERIC_STATUS_SUCCESS:
|
|
615
614
|
raise ExecutionError(resp.result.exception)
|
|
@@ -634,7 +633,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
634
633
|
|
|
635
634
|
while True:
|
|
636
635
|
req = api_pb2.SandboxWaitRequest(sandbox_id=self.object_id, timeout=10)
|
|
637
|
-
resp = await
|
|
636
|
+
resp = await self._client.stub.SandboxWait(req)
|
|
638
637
|
if resp.result.status:
|
|
639
638
|
logger.debug(f"Sandbox {self.object_id} wait completed with status {resp.result.status}")
|
|
640
639
|
self._result = resp.result
|
|
@@ -660,7 +659,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
660
659
|
return self._tunnels
|
|
661
660
|
|
|
662
661
|
req = api_pb2.SandboxGetTunnelsRequest(sandbox_id=self.object_id, timeout=timeout)
|
|
663
|
-
resp = await
|
|
662
|
+
resp = await self._client.stub.SandboxGetTunnels(req)
|
|
664
663
|
|
|
665
664
|
# If we couldn't get the tunnels in time, report the timeout.
|
|
666
665
|
if resp.result.status == api_pb2.GenericResult.GENERIC_STATUS_TIMEOUT:
|
|
@@ -688,7 +687,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
688
687
|
raise InvalidError(f"Failed to serialize user_metadata: {e}")
|
|
689
688
|
|
|
690
689
|
req = api_pb2.SandboxCreateConnectTokenRequest(sandbox_id=self.object_id, user_metadata=user_metadata)
|
|
691
|
-
resp = await
|
|
690
|
+
resp = await self._client.stub.SandboxCreateConnectToken(req)
|
|
692
691
|
return SandboxConnectCredentials(resp.url, resp.token)
|
|
693
692
|
|
|
694
693
|
async def reload_volumes(self) -> None:
|
|
@@ -697,8 +696,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
697
696
|
Added in v1.1.0.
|
|
698
697
|
"""
|
|
699
698
|
task_id = await self._get_task_id()
|
|
700
|
-
await
|
|
701
|
-
self._client.stub.ContainerReloadVolumes,
|
|
699
|
+
await self._client.stub.ContainerReloadVolumes(
|
|
702
700
|
api_pb2.ContainerReloadVolumesRequest(
|
|
703
701
|
task_id=task_id,
|
|
704
702
|
),
|
|
@@ -709,9 +707,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
709
707
|
|
|
710
708
|
This is a no-op if the Sandbox has already finished running."""
|
|
711
709
|
|
|
712
|
-
await
|
|
713
|
-
self._client.stub.SandboxTerminate, api_pb2.SandboxTerminateRequest(sandbox_id=self.object_id)
|
|
714
|
-
)
|
|
710
|
+
await self._client.stub.SandboxTerminate(api_pb2.SandboxTerminateRequest(sandbox_id=self.object_id))
|
|
715
711
|
|
|
716
712
|
async def poll(self) -> Optional[int]:
|
|
717
713
|
"""Check if the Sandbox has finished running.
|
|
@@ -720,7 +716,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
720
716
|
"""
|
|
721
717
|
|
|
722
718
|
req = api_pb2.SandboxWaitRequest(sandbox_id=self.object_id, timeout=0)
|
|
723
|
-
resp = await
|
|
719
|
+
resp = await self._client.stub.SandboxWait(req)
|
|
724
720
|
|
|
725
721
|
if resp.result.status:
|
|
726
722
|
self._result = resp.result
|
|
@@ -729,9 +725,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
729
725
|
|
|
730
726
|
async def _get_task_id(self) -> str:
|
|
731
727
|
while not self._task_id:
|
|
732
|
-
resp = await
|
|
733
|
-
self._client.stub.SandboxGetTaskId, api_pb2.SandboxGetTaskIdRequest(sandbox_id=self.object_id)
|
|
734
|
-
)
|
|
728
|
+
resp = await self._client.stub.SandboxGetTaskId(api_pb2.SandboxGetTaskIdRequest(sandbox_id=self.object_id))
|
|
735
729
|
self._task_id = resp.task_id
|
|
736
730
|
if not self._task_id:
|
|
737
731
|
await asyncio.sleep(0.5)
|
|
@@ -805,13 +799,8 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
805
799
|
|
|
806
800
|
**Usage**
|
|
807
801
|
|
|
808
|
-
```python
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
sandbox = modal.Sandbox.create("sleep", "infinity", app=app)
|
|
812
|
-
|
|
813
|
-
process = sandbox.exec("bash", "-c", "for i in $(seq 1 10); do echo foo $i; sleep 0.5; done")
|
|
814
|
-
|
|
802
|
+
```python fixture:sandbox
|
|
803
|
+
process = sandbox.exec("bash", "-c", "for i in $(seq 1 3); do echo foo $i; sleep 0.1; done")
|
|
815
804
|
for line in process.stdout:
|
|
816
805
|
print(line)
|
|
817
806
|
```
|
|
@@ -913,7 +902,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
913
902
|
workdir=workdir,
|
|
914
903
|
secret_ids=secret_ids,
|
|
915
904
|
)
|
|
916
|
-
resp = await
|
|
905
|
+
resp = await self._client.stub.ContainerExec(req)
|
|
917
906
|
by_line = bufsize == 1
|
|
918
907
|
exec_deadline = time.monotonic() + int(timeout) + CONTAINER_EXEC_TIMEOUT_BUFFER if timeout else None
|
|
919
908
|
logger.debug(f"Created ContainerProcess for exec_id {resp.exec_id} on Sandbox {self.object_id}")
|
|
@@ -1000,14 +989,14 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1000
989
|
async def _experimental_snapshot(self) -> _SandboxSnapshot:
|
|
1001
990
|
await self._get_task_id()
|
|
1002
991
|
snap_req = api_pb2.SandboxSnapshotRequest(sandbox_id=self.object_id)
|
|
1003
|
-
snap_resp = await
|
|
992
|
+
snap_resp = await self._client.stub.SandboxSnapshot(snap_req)
|
|
1004
993
|
|
|
1005
994
|
snapshot_id = snap_resp.snapshot_id
|
|
1006
995
|
|
|
1007
996
|
# wait for the snapshot to succeed. this is implemented as a second idempotent rpc
|
|
1008
997
|
# because the snapshot itself may take a while to complete.
|
|
1009
998
|
wait_req = api_pb2.SandboxSnapshotWaitRequest(snapshot_id=snapshot_id, timeout=55.0)
|
|
1010
|
-
wait_resp = await
|
|
999
|
+
wait_resp = await self._client.stub.SandboxSnapshotWait(wait_req)
|
|
1011
1000
|
if wait_resp.result.status != api_pb2.GenericResult.GENERIC_STATUS_SUCCESS:
|
|
1012
1001
|
raise ExecutionError(wait_resp.result.exception)
|
|
1013
1002
|
|
|
@@ -1050,9 +1039,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1050
1039
|
sandbox_name_override_type=api_pb2.SandboxRestoreRequest.SANDBOX_NAME_OVERRIDE_TYPE_STRING,
|
|
1051
1040
|
)
|
|
1052
1041
|
try:
|
|
1053
|
-
restore_resp: api_pb2.SandboxRestoreResponse = await
|
|
1054
|
-
client.stub.SandboxRestore, restore_req
|
|
1055
|
-
)
|
|
1042
|
+
restore_resp: api_pb2.SandboxRestoreResponse = await client.stub.SandboxRestore(restore_req)
|
|
1056
1043
|
except GRPCError as exc:
|
|
1057
1044
|
if exc.status == Status.ALREADY_EXISTS:
|
|
1058
1045
|
raise AlreadyExistsError(exc.message)
|
|
@@ -1063,7 +1050,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1063
1050
|
task_id_req = api_pb2.SandboxGetTaskIdRequest(
|
|
1064
1051
|
sandbox_id=restore_resp.sandbox_id, wait_until_ready=True, timeout=55.0
|
|
1065
1052
|
)
|
|
1066
|
-
resp = await
|
|
1053
|
+
resp = await client.stub.SandboxGetTaskId(task_id_req)
|
|
1067
1054
|
if resp.task_result.status not in [
|
|
1068
1055
|
api_pb2.GenericResult.GENERIC_STATUS_UNSPECIFIED,
|
|
1069
1056
|
api_pb2.GenericResult.GENERIC_STATUS_SUCCESS,
|
|
@@ -1198,7 +1185,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1198
1185
|
|
|
1199
1186
|
# Fetches a batch of sandboxes.
|
|
1200
1187
|
try:
|
|
1201
|
-
resp = await
|
|
1188
|
+
resp = await client.stub.SandboxList(req)
|
|
1202
1189
|
except GRPCError as exc:
|
|
1203
1190
|
raise InvalidError(exc.message) if exc.status == Status.INVALID_ARGUMENT else exc
|
|
1204
1191
|
|