modal 1.1.5.dev66__py3-none-any.whl → 1.3.1.dev8__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.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- modal/__init__.py +4 -4
- modal/__main__.py +4 -29
- modal/_billing.py +84 -0
- modal/_clustered_functions.py +1 -3
- modal/_container_entrypoint.py +33 -208
- modal/_functions.py +171 -138
- modal/_grpc_client.py +191 -0
- modal/_ipython.py +16 -6
- modal/_load_context.py +106 -0
- modal/_object.py +72 -21
- modal/_output.py +12 -14
- modal/_partial_function.py +31 -4
- modal/_resolver.py +44 -57
- modal/_runtime/container_io_manager.py +30 -28
- modal/_runtime/container_io_manager.pyi +42 -44
- modal/_runtime/gpu_memory_snapshot.py +9 -7
- modal/_runtime/user_code_event_loop.py +80 -0
- modal/_runtime/user_code_imports.py +236 -10
- modal/_serialization.py +2 -1
- modal/_traceback.py +4 -13
- modal/_tunnel.py +16 -11
- modal/_tunnel.pyi +25 -3
- modal/_utils/async_utils.py +337 -10
- modal/_utils/auth_token_manager.py +1 -4
- modal/_utils/blob_utils.py +29 -22
- modal/_utils/function_utils.py +20 -21
- modal/_utils/grpc_testing.py +6 -3
- modal/_utils/grpc_utils.py +223 -64
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +2 -3
- modal/_utils/package_utils.py +0 -1
- modal/_utils/rand_pb_testing.py +8 -1
- modal/_utils/task_command_router_client.py +524 -0
- modal/_vendor/cloudpickle.py +144 -48
- modal/app.py +285 -105
- modal/app.pyi +216 -53
- modal/billing.py +5 -0
- modal/builder/2025.06.txt +6 -3
- modal/builder/PREVIEW.txt +2 -1
- modal/builder/base-images.json +4 -2
- modal/cli/_download.py +19 -3
- modal/cli/cluster.py +4 -2
- modal/cli/config.py +3 -1
- modal/cli/container.py +5 -4
- modal/cli/dict.py +5 -2
- modal/cli/entry_point.py +26 -2
- modal/cli/environment.py +2 -16
- modal/cli/launch.py +1 -76
- modal/cli/network_file_system.py +5 -20
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +5 -4
- modal/cli/run.py +24 -204
- modal/cli/secret.py +1 -2
- modal/cli/shell.py +375 -0
- modal/cli/utils.py +1 -13
- modal/cli/volume.py +11 -17
- modal/client.py +16 -125
- modal/client.pyi +94 -144
- modal/cloud_bucket_mount.py +3 -1
- modal/cloud_bucket_mount.pyi +4 -0
- modal/cls.py +101 -64
- modal/cls.pyi +9 -8
- modal/config.py +21 -1
- modal/container_process.py +288 -12
- modal/container_process.pyi +99 -38
- modal/dict.py +72 -33
- modal/dict.pyi +88 -57
- modal/environments.py +16 -8
- modal/environments.pyi +6 -2
- modal/exception.py +154 -16
- modal/experimental/__init__.py +24 -53
- modal/experimental/flash.py +161 -74
- modal/experimental/flash.pyi +97 -49
- modal/file_io.py +50 -92
- modal/file_io.pyi +117 -89
- modal/functions.pyi +70 -87
- modal/image.py +82 -47
- modal/image.pyi +51 -30
- modal/io_streams.py +500 -149
- modal/io_streams.pyi +279 -189
- modal/mount.py +60 -46
- modal/mount.pyi +41 -17
- modal/network_file_system.py +19 -11
- modal/network_file_system.pyi +72 -39
- modal/object.pyi +114 -22
- modal/parallel_map.py +42 -44
- modal/parallel_map.pyi +9 -17
- modal/partial_function.pyi +4 -2
- modal/proxy.py +14 -6
- modal/proxy.pyi +10 -2
- modal/queue.py +45 -38
- modal/queue.pyi +88 -52
- modal/runner.py +96 -96
- modal/runner.pyi +44 -27
- modal/sandbox.py +225 -107
- modal/sandbox.pyi +226 -60
- modal/secret.py +58 -56
- modal/secret.pyi +28 -13
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +29 -15
- modal/snapshot.pyi +18 -10
- modal/token_flow.py +1 -1
- modal/token_flow.pyi +4 -6
- modal/volume.py +102 -55
- modal/volume.pyi +125 -66
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/METADATA +10 -9
- modal-1.3.1.dev8.dist-info/RECORD +189 -0
- modal_proto/api.proto +141 -70
- modal_proto/api_grpc.py +42 -26
- modal_proto/api_pb2.py +1123 -1103
- modal_proto/api_pb2.pyi +331 -83
- modal_proto/api_pb2_grpc.py +80 -48
- modal_proto/api_pb2_grpc.pyi +26 -18
- modal_proto/modal_api_grpc.py +175 -174
- modal_proto/task_command_router.proto +164 -0
- modal_proto/task_command_router_grpc.py +138 -0
- modal_proto/task_command_router_pb2.py +180 -0
- modal_proto/{sandbox_router_pb2.pyi → task_command_router_pb2.pyi} +148 -57
- modal_proto/task_command_router_pb2_grpc.py +272 -0
- modal_proto/task_command_router_pb2_grpc.pyi +100 -0
- modal_version/__init__.py +1 -1
- modal_version/__main__.py +1 -1
- modal/cli/programs/launch_instance_ssh.py +0 -94
- modal/cli/programs/run_marimo.py +0 -95
- modal-1.1.5.dev66.dist-info/RECORD +0 -191
- modal_proto/modal_options_grpc.py +0 -3
- modal_proto/options.proto +0 -19
- modal_proto/options_grpc.py +0 -3
- modal_proto/options_pb2.py +0 -35
- modal_proto/options_pb2.pyi +0 -20
- modal_proto/options_pb2_grpc.py +0 -4
- modal_proto/options_pb2_grpc.pyi +0 -7
- modal_proto/sandbox_router.proto +0 -125
- modal_proto/sandbox_router_grpc.py +0 -89
- modal_proto/sandbox_router_pb2.py +0 -128
- modal_proto/sandbox_router_pb2_grpc.py +0 -169
- modal_proto/sandbox_router_pb2_grpc.pyi +0 -63
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
- {modal-1.1.5.dev66.dist-info → modal-1.3.1.dev8.dist-info}/top_level.txt +0 -0
modal/runner.py
CHANGED
|
@@ -8,17 +8,17 @@ import asyncio
|
|
|
8
8
|
import dataclasses
|
|
9
9
|
import os
|
|
10
10
|
import time
|
|
11
|
-
import typing
|
|
12
11
|
from collections.abc import AsyncGenerator
|
|
13
12
|
from contextlib import nullcontext
|
|
14
13
|
from multiprocessing.synchronize import Event
|
|
15
14
|
from typing import TYPE_CHECKING, Any, Optional, TypeVar
|
|
16
15
|
|
|
17
|
-
from grpclib import GRPCError, Status
|
|
18
16
|
from synchronicity.async_wrap import asynccontextmanager
|
|
19
17
|
|
|
20
18
|
import modal._runtime.execution_context
|
|
21
19
|
import modal_proto.api_pb2
|
|
20
|
+
from modal._load_context import LoadContext
|
|
21
|
+
from modal._utils.grpc_utils import Retry
|
|
22
22
|
from modal_proto import api_pb2
|
|
23
23
|
|
|
24
24
|
from ._functions import _Function
|
|
@@ -29,13 +29,12 @@ from ._traceback import print_server_warnings, traceback_contains_remote_call
|
|
|
29
29
|
from ._utils.async_utils import TaskContext, gather_cancel_on_exc, synchronize_api
|
|
30
30
|
from ._utils.deprecation import warn_if_passing_namespace
|
|
31
31
|
from ._utils.git_utils import get_git_commit_info
|
|
32
|
-
from ._utils.grpc_utils import retry_transient_errors
|
|
33
32
|
from ._utils.name_utils import check_object_name, is_valid_tag
|
|
34
33
|
from .client import HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, _Client
|
|
35
34
|
from .cls import _Cls
|
|
36
35
|
from .config import config, logger
|
|
37
36
|
from .environments import _get_environment_cached
|
|
38
|
-
from .exception import InteractiveTimeoutError, InvalidError, RemoteError, _CliUserExecutionError
|
|
37
|
+
from .exception import ConnectionError, InteractiveTimeoutError, InvalidError, RemoteError, _CliUserExecutionError
|
|
39
38
|
from .output import _get_output_manager, enable_output
|
|
40
39
|
from .running_app import RunningApp, running_app_from_layout
|
|
41
40
|
from .sandbox import _Sandbox
|
|
@@ -43,9 +42,7 @@ from .secret import _Secret
|
|
|
43
42
|
from .stream_type import StreamType
|
|
44
43
|
|
|
45
44
|
if TYPE_CHECKING:
|
|
46
|
-
|
|
47
|
-
else:
|
|
48
|
-
_App = TypeVar("_App")
|
|
45
|
+
import modal.app
|
|
49
46
|
|
|
50
47
|
|
|
51
48
|
V = TypeVar("V")
|
|
@@ -56,14 +53,14 @@ async def _heartbeat(client: _Client, app_id: str) -> None:
|
|
|
56
53
|
# TODO(erikbern): we should capture exceptions here
|
|
57
54
|
# * if request fails: destroy the client
|
|
58
55
|
# * if server says the app is gone: print a helpful warning about detaching
|
|
59
|
-
await
|
|
56
|
+
await client.stub.AppHeartbeat(request, retry=Retry(attempt_timeout=HEARTBEAT_TIMEOUT))
|
|
60
57
|
|
|
61
58
|
|
|
62
59
|
async def _init_local_app_existing(client: _Client, existing_app_id: str, environment_name: str) -> RunningApp:
|
|
63
60
|
# Get all the objects first
|
|
64
61
|
obj_req = api_pb2.AppGetLayoutRequest(app_id=existing_app_id)
|
|
65
62
|
obj_resp, _ = await gather_cancel_on_exc(
|
|
66
|
-
|
|
63
|
+
client.stub.AppGetLayout(obj_req),
|
|
67
64
|
# Cache the environment associated with the app now as we will use it later
|
|
68
65
|
_get_environment_cached(environment_name, client),
|
|
69
66
|
)
|
|
@@ -78,6 +75,7 @@ async def _init_local_app_existing(client: _Client, existing_app_id: str, enviro
|
|
|
78
75
|
async def _init_local_app_new(
|
|
79
76
|
client: _Client,
|
|
80
77
|
description: str,
|
|
78
|
+
tags: dict[str, str],
|
|
81
79
|
app_state: int, # ValueType
|
|
82
80
|
environment_name: str = "",
|
|
83
81
|
interactive: bool = False,
|
|
@@ -86,9 +84,10 @@ async def _init_local_app_new(
|
|
|
86
84
|
description=description,
|
|
87
85
|
environment_name=environment_name,
|
|
88
86
|
app_state=app_state, # type: ignore
|
|
87
|
+
tags=tags,
|
|
89
88
|
)
|
|
90
89
|
app_resp, _ = await gather_cancel_on_exc( # TODO: use TaskGroup?
|
|
91
|
-
|
|
90
|
+
client.stub.AppCreate(app_req),
|
|
92
91
|
# Cache the environment associated with the app now as we will use it later
|
|
93
92
|
_get_environment_cached(environment_name, client),
|
|
94
93
|
)
|
|
@@ -104,6 +103,7 @@ async def _init_local_app_new(
|
|
|
104
103
|
async def _init_local_app_from_name(
|
|
105
104
|
client: _Client,
|
|
106
105
|
name: str,
|
|
106
|
+
tags: dict[str, str],
|
|
107
107
|
environment_name: str = "",
|
|
108
108
|
) -> RunningApp:
|
|
109
109
|
# Look up any existing deployment
|
|
@@ -111,7 +111,7 @@ async def _init_local_app_from_name(
|
|
|
111
111
|
name=name,
|
|
112
112
|
environment_name=environment_name,
|
|
113
113
|
)
|
|
114
|
-
app_resp = await
|
|
114
|
+
app_resp = await client.stub.AppGetByDeploymentName(app_req)
|
|
115
115
|
existing_app_id = app_resp.app_id or None
|
|
116
116
|
|
|
117
117
|
# Grab the app
|
|
@@ -119,24 +119,19 @@ async def _init_local_app_from_name(
|
|
|
119
119
|
return await _init_local_app_existing(client, existing_app_id, environment_name)
|
|
120
120
|
else:
|
|
121
121
|
return await _init_local_app_new(
|
|
122
|
-
client, name, api_pb2.APP_STATE_INITIALIZING, environment_name=environment_name
|
|
122
|
+
client, name, tags, api_pb2.APP_STATE_INITIALIZING, environment_name=environment_name
|
|
123
123
|
)
|
|
124
124
|
|
|
125
125
|
|
|
126
126
|
async def _create_all_objects(
|
|
127
|
-
client: _Client,
|
|
128
127
|
running_app: RunningApp,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
environment_name: str,
|
|
128
|
+
local_app_state: "modal.app._LocalAppState",
|
|
129
|
+
load_context: LoadContext,
|
|
132
130
|
) -> None:
|
|
133
131
|
"""Create objects that have been defined but not created on the server."""
|
|
134
|
-
indexed_objects: dict[str, _Object] = {**functions, **classes}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
environment_name=environment_name,
|
|
138
|
-
app_id=running_app.app_id,
|
|
139
|
-
)
|
|
132
|
+
indexed_objects: dict[str, _Object] = {**local_app_state.functions, **local_app_state.classes}
|
|
133
|
+
|
|
134
|
+
resolver = Resolver()
|
|
140
135
|
with resolver.display():
|
|
141
136
|
# Get current objects, and reset all objects
|
|
142
137
|
tag_to_object_id = {**running_app.function_ids, **running_app.class_ids}
|
|
@@ -159,7 +154,7 @@ async def _create_all_objects(
|
|
|
159
154
|
# Note: preload only currently implemented for Functions, returns None otherwise
|
|
160
155
|
# this is to ensure that directly referenced functions from the global scope has
|
|
161
156
|
# ids associated with them when they are serialized into other functions
|
|
162
|
-
await resolver.preload(obj, existing_object_id)
|
|
157
|
+
await resolver.preload(obj, load_context, existing_object_id)
|
|
163
158
|
if obj.is_hydrated:
|
|
164
159
|
tag_to_object_id[tag] = obj.object_id
|
|
165
160
|
|
|
@@ -167,7 +162,8 @@ async def _create_all_objects(
|
|
|
167
162
|
|
|
168
163
|
async def _load(tag, obj):
|
|
169
164
|
existing_object_id = tag_to_object_id.get(tag)
|
|
170
|
-
|
|
165
|
+
# Pass load_context so dependencies can inherit app_id, client, etc.
|
|
166
|
+
await resolver.load(obj, load_context, existing_object_id=existing_object_id)
|
|
171
167
|
if _Function._is_id_type(obj.object_id):
|
|
172
168
|
running_app.function_ids[tag] = obj.object_id
|
|
173
169
|
elif _Cls._is_id_type(obj.object_id):
|
|
@@ -182,21 +178,19 @@ async def _publish_app(
|
|
|
182
178
|
client: _Client,
|
|
183
179
|
running_app: RunningApp,
|
|
184
180
|
app_state: int, # api_pb2.AppState.value
|
|
185
|
-
|
|
186
|
-
classes: dict[str, _Cls],
|
|
181
|
+
app_local_state: "modal.app._LocalAppState",
|
|
187
182
|
name: str = "",
|
|
188
|
-
tags: dict[str, str] = {}, # Additional App metadata
|
|
189
183
|
deployment_tag: str = "", # Only relevant for deployments
|
|
190
184
|
commit_info: Optional[api_pb2.CommitInfo] = None, # Git commit information
|
|
191
185
|
) -> tuple[str, list[api_pb2.Warning]]:
|
|
192
186
|
"""Wrapper for AppPublish RPC."""
|
|
193
|
-
|
|
187
|
+
functions = app_local_state.functions
|
|
194
188
|
definition_ids = {obj.object_id: obj._get_metadata().definition_id for obj in functions.values()} # type: ignore
|
|
195
189
|
|
|
196
190
|
request = api_pb2.AppPublishRequest(
|
|
197
191
|
app_id=running_app.app_id,
|
|
198
192
|
name=name,
|
|
199
|
-
tags=tags,
|
|
193
|
+
tags=app_local_state.tags,
|
|
200
194
|
deployment_tag=deployment_tag,
|
|
201
195
|
commit_info=commit_info,
|
|
202
196
|
app_state=app_state, # type: ignore : should be a api_pb2.AppState.value
|
|
@@ -205,13 +199,7 @@ async def _publish_app(
|
|
|
205
199
|
definition_ids=definition_ids,
|
|
206
200
|
)
|
|
207
201
|
|
|
208
|
-
|
|
209
|
-
response = await retry_transient_errors(client.stub.AppPublish, request)
|
|
210
|
-
except GRPCError as exc:
|
|
211
|
-
if exc.status == Status.INVALID_ARGUMENT or exc.status == Status.FAILED_PRECONDITION:
|
|
212
|
-
raise InvalidError(exc.message)
|
|
213
|
-
raise
|
|
214
|
-
|
|
202
|
+
response = await client.stub.AppPublish(request)
|
|
215
203
|
print_server_warnings(response.server_warnings)
|
|
216
204
|
return response.url, response.server_warnings
|
|
217
205
|
|
|
@@ -230,7 +218,7 @@ async def _disconnect(
|
|
|
230
218
|
|
|
231
219
|
logger.debug("Sending app disconnect/stop request")
|
|
232
220
|
req_disconnect = api_pb2.AppClientDisconnectRequest(app_id=app_id, reason=reason, exception=exc_str)
|
|
233
|
-
await
|
|
221
|
+
await client.stub.AppClientDisconnect(req_disconnect)
|
|
234
222
|
logger.debug("App disconnected")
|
|
235
223
|
|
|
236
224
|
|
|
@@ -260,16 +248,17 @@ async def _status_based_disconnect(client: _Client, app_id: str, exc_info: Optio
|
|
|
260
248
|
|
|
261
249
|
@asynccontextmanager
|
|
262
250
|
async def _run_app(
|
|
263
|
-
app: _App,
|
|
251
|
+
app: "modal.app._App",
|
|
264
252
|
*,
|
|
265
253
|
client: Optional[_Client] = None,
|
|
266
254
|
detach: bool = False,
|
|
267
255
|
environment_name: Optional[str] = None,
|
|
268
256
|
interactive: bool = False,
|
|
269
|
-
) -> AsyncGenerator[_App, None]:
|
|
257
|
+
) -> AsyncGenerator["modal.app._App", None]:
|
|
270
258
|
"""mdmd:hidden"""
|
|
271
|
-
|
|
272
|
-
|
|
259
|
+
load_context = await app._root_load_context.reset().in_place_upgrade(
|
|
260
|
+
client=client, environment_name=environment_name
|
|
261
|
+
)
|
|
273
262
|
|
|
274
263
|
if modal._runtime.execution_context._is_currently_importing:
|
|
275
264
|
raise InvalidError("Can not run an app in global scope within a container")
|
|
@@ -290,9 +279,6 @@ async def _run_app(
|
|
|
290
279
|
# https://docs.python.org/3/library/__main__.html#import-main
|
|
291
280
|
app.set_description(__main__.__name__)
|
|
292
281
|
|
|
293
|
-
if client is None:
|
|
294
|
-
client = await _Client.from_env()
|
|
295
|
-
|
|
296
282
|
app_state = api_pb2.APP_STATE_DETACHED if detach else api_pb2.APP_STATE_EPHEMERAL
|
|
297
283
|
|
|
298
284
|
output_mgr = _get_output_manager()
|
|
@@ -300,21 +286,25 @@ async def _run_app(
|
|
|
300
286
|
msg = "Interactive mode requires output to be enabled. (Use the the `modal.enable_output()` context manager.)"
|
|
301
287
|
raise InvalidError(msg)
|
|
302
288
|
|
|
289
|
+
local_app_state = app._local_state
|
|
290
|
+
|
|
303
291
|
running_app: RunningApp = await _init_local_app_new(
|
|
304
|
-
client,
|
|
292
|
+
load_context.client,
|
|
305
293
|
app.description or "",
|
|
306
|
-
|
|
294
|
+
local_app_state.tags,
|
|
295
|
+
environment_name=load_context.environment_name,
|
|
307
296
|
app_state=app_state,
|
|
308
297
|
interactive=interactive,
|
|
309
298
|
)
|
|
299
|
+
await load_context.in_place_upgrade(app_id=running_app.app_id)
|
|
310
300
|
|
|
311
301
|
logs_timeout = config["logs_timeout"]
|
|
312
|
-
async with app._set_local_app(client, running_app), TaskContext(grace=logs_timeout) as tc:
|
|
302
|
+
async with app._set_local_app(load_context.client, running_app), TaskContext(grace=logs_timeout) as tc:
|
|
313
303
|
# Start heartbeats loop to keep the client alive
|
|
314
304
|
# we don't log heartbeat exceptions in detached mode
|
|
315
305
|
# as losing the local connection will not affect the running app
|
|
316
306
|
def heartbeat():
|
|
317
|
-
return _heartbeat(client, running_app.app_id)
|
|
307
|
+
return _heartbeat(load_context.client, running_app.app_id)
|
|
318
308
|
|
|
319
309
|
heartbeat_loop = tc.infinite_loop(heartbeat, sleep=HEARTBEAT_INTERVAL, log_exception=not detach)
|
|
320
310
|
logs_loop: Optional[asyncio.Task] = None
|
|
@@ -335,26 +325,37 @@ async def _run_app(
|
|
|
335
325
|
# Start logs loop
|
|
336
326
|
|
|
337
327
|
logs_loop = tc.create_task(
|
|
338
|
-
get_app_logs_loop(
|
|
328
|
+
get_app_logs_loop(
|
|
329
|
+
load_context.client, output_mgr, app_id=running_app.app_id, app_logs_url=running_app.app_logs_url
|
|
330
|
+
)
|
|
339
331
|
)
|
|
340
332
|
|
|
341
333
|
try:
|
|
342
334
|
# Create all members
|
|
343
|
-
await _create_all_objects(
|
|
335
|
+
await _create_all_objects(running_app, local_app_state, load_context)
|
|
344
336
|
|
|
345
337
|
# Publish the app
|
|
346
|
-
await _publish_app(client, running_app, app_state,
|
|
338
|
+
await _publish_app(load_context.client, running_app, app_state, local_app_state)
|
|
347
339
|
except asyncio.CancelledError as e:
|
|
348
340
|
# this typically happens on sigint/ctrl-C during setup (the KeyboardInterrupt happens in the main thread)
|
|
349
341
|
if output_mgr := _get_output_manager():
|
|
350
342
|
output_mgr.print("Aborting app initialization...\n")
|
|
351
343
|
|
|
352
|
-
await _status_based_disconnect(client, running_app.app_id, e)
|
|
344
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, e)
|
|
353
345
|
raise
|
|
354
346
|
except BaseException as e:
|
|
355
|
-
await _status_based_disconnect(client, running_app.app_id, e)
|
|
347
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, e)
|
|
356
348
|
raise
|
|
357
349
|
|
|
350
|
+
detached_disconnect_msg = (
|
|
351
|
+
"The detached App will keep running. You can track its progress on the Dashboard: "
|
|
352
|
+
f"[magenta underline]{running_app.app_page_url}[/magenta underline]"
|
|
353
|
+
"\n\nStream App logs:\n"
|
|
354
|
+
f"[green]modal app logs {running_app.app_id}[/green]"
|
|
355
|
+
"\n\nStop the App:\n"
|
|
356
|
+
f"[green]modal app stop {running_app.app_id}[/green]"
|
|
357
|
+
)
|
|
358
|
+
|
|
358
359
|
try:
|
|
359
360
|
# Show logs from dynamically created images.
|
|
360
361
|
# TODO: better way to do this
|
|
@@ -371,26 +372,22 @@ async def _run_app(
|
|
|
371
372
|
yield app
|
|
372
373
|
# successful completion!
|
|
373
374
|
heartbeat_loop.cancel()
|
|
374
|
-
await _status_based_disconnect(client, running_app.app_id, exc_info=None)
|
|
375
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, exc_info=None)
|
|
375
376
|
except KeyboardInterrupt as e:
|
|
376
377
|
# this happens only if sigint comes in during the yield block above
|
|
377
378
|
if detach:
|
|
378
379
|
if output_mgr := _get_output_manager():
|
|
379
380
|
output_mgr.print(output_mgr.step_completed("Shutting down Modal client."))
|
|
380
|
-
output_mgr.print(
|
|
381
|
-
"The detached app keeps running. You can track its progress at: "
|
|
382
|
-
f"[magenta]{running_app.app_page_url}[/magenta]"
|
|
383
|
-
""
|
|
384
|
-
)
|
|
381
|
+
output_mgr.print(detached_disconnect_msg)
|
|
385
382
|
if logs_loop:
|
|
386
383
|
logs_loop.cancel()
|
|
387
|
-
await _status_based_disconnect(client, running_app.app_id, e)
|
|
384
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, e)
|
|
388
385
|
else:
|
|
389
386
|
if output_mgr := _get_output_manager():
|
|
390
387
|
output_mgr.print(
|
|
391
388
|
"Disconnecting from Modal - This will terminate your Modal app in a few seconds.\n"
|
|
392
389
|
)
|
|
393
|
-
await _status_based_disconnect(client, running_app.app_id, e)
|
|
390
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, e)
|
|
394
391
|
if logs_loop:
|
|
395
392
|
try:
|
|
396
393
|
await asyncio.wait_for(logs_loop, timeout=logs_timeout)
|
|
@@ -405,9 +402,17 @@ async def _run_app(
|
|
|
405
402
|
)
|
|
406
403
|
)
|
|
407
404
|
return
|
|
405
|
+
except ConnectionError as e:
|
|
406
|
+
# If we lose connection to the server after a detached App has started running, it will continue
|
|
407
|
+
# I think we can only exit "nicely" if we are able to print output though, otherwise we should raise
|
|
408
|
+
if detach and (output_mgr := _get_output_manager()):
|
|
409
|
+
output_mgr.print(":white_exclamation_mark: Connection lost!")
|
|
410
|
+
output_mgr.print(detached_disconnect_msg)
|
|
411
|
+
return
|
|
412
|
+
raise
|
|
408
413
|
except BaseException as e:
|
|
409
414
|
logger.info("Exception during app run")
|
|
410
|
-
await _status_based_disconnect(client, running_app.app_id, e)
|
|
415
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, e)
|
|
411
416
|
raise
|
|
412
417
|
|
|
413
418
|
# wait for logs gracefully, even though the task context would do the same
|
|
@@ -428,34 +433,27 @@ async def _run_app(
|
|
|
428
433
|
|
|
429
434
|
|
|
430
435
|
async def _serve_update(
|
|
431
|
-
app: _App,
|
|
436
|
+
app: "modal.app._App",
|
|
432
437
|
existing_app_id: str,
|
|
433
438
|
is_ready: Event,
|
|
434
439
|
environment_name: str,
|
|
435
440
|
) -> None:
|
|
436
441
|
"""mdmd:hidden"""
|
|
437
442
|
# Used by child process to reinitialize a served app
|
|
438
|
-
|
|
443
|
+
load_context = await app._root_load_context.reset().in_place_upgrade(environment_name=environment_name)
|
|
439
444
|
try:
|
|
440
|
-
running_app: RunningApp = await _init_local_app_existing(client, existing_app_id, environment_name)
|
|
441
|
-
|
|
445
|
+
running_app: RunningApp = await _init_local_app_existing(load_context.client, existing_app_id, environment_name)
|
|
446
|
+
await load_context.in_place_upgrade(app_id=running_app.app_id)
|
|
447
|
+
local_app_state = app._local_state
|
|
442
448
|
# Create objects
|
|
443
|
-
await _create_all_objects(
|
|
444
|
-
client,
|
|
445
|
-
running_app,
|
|
446
|
-
app._functions,
|
|
447
|
-
app._classes,
|
|
448
|
-
environment_name,
|
|
449
|
-
)
|
|
449
|
+
await _create_all_objects(running_app, local_app_state, load_context)
|
|
450
450
|
|
|
451
451
|
# Publish the updated app
|
|
452
452
|
await _publish_app(
|
|
453
|
-
client,
|
|
453
|
+
load_context.client,
|
|
454
454
|
running_app,
|
|
455
455
|
app_state=api_pb2.APP_STATE_UNSPECIFIED,
|
|
456
|
-
|
|
457
|
-
classes=app._classes,
|
|
458
|
-
tags=app._tags,
|
|
456
|
+
app_local_state=local_app_state,
|
|
459
457
|
)
|
|
460
458
|
|
|
461
459
|
# Communicate to the parent process
|
|
@@ -476,7 +474,7 @@ class DeployResult:
|
|
|
476
474
|
|
|
477
475
|
|
|
478
476
|
async def _deploy_app(
|
|
479
|
-
app: _App,
|
|
477
|
+
app: "modal.app._App",
|
|
480
478
|
name: Optional[str] = None,
|
|
481
479
|
namespace: Any = None, # mdmd:line-hidden
|
|
482
480
|
client: Optional[_Client] = None,
|
|
@@ -487,9 +485,6 @@ async def _deploy_app(
|
|
|
487
485
|
|
|
488
486
|
Users should prefer the `modal deploy` CLI or the `App.deploy` method.
|
|
489
487
|
"""
|
|
490
|
-
if environment_name is None:
|
|
491
|
-
environment_name = typing.cast(str, config.get("environment"))
|
|
492
|
-
|
|
493
488
|
warn_if_passing_namespace(namespace, "modal.runner.deploy_app")
|
|
494
489
|
|
|
495
490
|
name = name or app.name or ""
|
|
@@ -515,12 +510,25 @@ async def _deploy_app(
|
|
|
515
510
|
if client is None:
|
|
516
511
|
client = await _Client.from_env()
|
|
517
512
|
|
|
513
|
+
local_app_state = app._local_state
|
|
518
514
|
t0 = time.time()
|
|
519
515
|
|
|
520
516
|
# Get git information to track deployment history
|
|
521
517
|
commit_info_task = asyncio.create_task(get_git_commit_info())
|
|
522
518
|
|
|
523
|
-
|
|
519
|
+
# We need to do in-place replacement of fields in self._root_load_context in case it has already "spread"
|
|
520
|
+
# to with_options() instances or similar before load
|
|
521
|
+
root_load_context = await app._root_load_context.reset().in_place_upgrade(
|
|
522
|
+
client=client,
|
|
523
|
+
environment_name=environment_name,
|
|
524
|
+
)
|
|
525
|
+
running_app: RunningApp = await _init_local_app_from_name(
|
|
526
|
+
root_load_context.client, name, local_app_state.tags, environment_name=root_load_context.environment_name
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
await root_load_context.in_place_upgrade(
|
|
530
|
+
app_id=running_app.app_id,
|
|
531
|
+
)
|
|
524
532
|
|
|
525
533
|
async with TaskContext(0) as tc:
|
|
526
534
|
# Start heartbeats loop to keep the client alive
|
|
@@ -531,13 +539,7 @@ async def _deploy_app(
|
|
|
531
539
|
|
|
532
540
|
try:
|
|
533
541
|
# Create all members
|
|
534
|
-
await _create_all_objects(
|
|
535
|
-
client,
|
|
536
|
-
running_app,
|
|
537
|
-
app._functions,
|
|
538
|
-
app._classes,
|
|
539
|
-
environment_name=environment_name,
|
|
540
|
-
)
|
|
542
|
+
await _create_all_objects(running_app, local_app_state, root_load_context)
|
|
541
543
|
|
|
542
544
|
commit_info = None
|
|
543
545
|
try:
|
|
@@ -548,11 +550,9 @@ async def _deploy_app(
|
|
|
548
550
|
app_url, warnings = await _publish_app(
|
|
549
551
|
client,
|
|
550
552
|
running_app,
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
classes=app._classes,
|
|
553
|
+
api_pb2.APP_STATE_DEPLOYED,
|
|
554
|
+
local_app_state,
|
|
554
555
|
name=name,
|
|
555
|
-
tags=app._tags,
|
|
556
556
|
deployment_tag=tag,
|
|
557
557
|
commit_info=commit_info,
|
|
558
558
|
)
|
|
@@ -574,7 +574,7 @@ async def _deploy_app(
|
|
|
574
574
|
|
|
575
575
|
|
|
576
576
|
async def _interactive_shell(
|
|
577
|
-
_app: _App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: Any
|
|
577
|
+
_app: "modal.app._App", cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: Any
|
|
578
578
|
) -> None:
|
|
579
579
|
"""Run an interactive shell (like `bash`) within the image for this app.
|
|
580
580
|
|
|
@@ -621,7 +621,7 @@ async def _interactive_shell(
|
|
|
621
621
|
try:
|
|
622
622
|
if pty:
|
|
623
623
|
container_process = await sandbox._exec(
|
|
624
|
-
*sandbox_cmds, pty_info=get_pty_info(shell=True) if pty else None
|
|
624
|
+
*sandbox_cmds, pty_info=get_pty_info(shell=True) if pty else None, text=False
|
|
625
625
|
)
|
|
626
626
|
await container_process.attach()
|
|
627
627
|
else:
|
|
@@ -632,7 +632,7 @@ async def _interactive_shell(
|
|
|
632
632
|
except InteractiveTimeoutError:
|
|
633
633
|
# Check on status of Sandbox. It may have crashed, causing connection failure.
|
|
634
634
|
req = api_pb2.SandboxWaitRequest(sandbox_id=sandbox._object_id, timeout=0)
|
|
635
|
-
resp = await
|
|
635
|
+
resp = await sandbox._client.stub.SandboxWait(req)
|
|
636
636
|
if resp.result.exception:
|
|
637
637
|
raise RemoteError(resp.result.exception)
|
|
638
638
|
else:
|
modal/runner.pyi
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import modal.
|
|
1
|
+
import modal._load_context
|
|
2
|
+
import modal.app
|
|
2
3
|
import modal.client
|
|
3
|
-
import modal.cls
|
|
4
4
|
import modal.running_app
|
|
5
5
|
import modal_proto.api_pb2
|
|
6
6
|
import multiprocessing.synchronize
|
|
@@ -8,8 +8,6 @@ import synchronicity.combined_types
|
|
|
8
8
|
import typing
|
|
9
9
|
import typing_extensions
|
|
10
10
|
|
|
11
|
-
_App = typing.TypeVar("_App")
|
|
12
|
-
|
|
13
11
|
V = typing.TypeVar("V")
|
|
14
12
|
|
|
15
13
|
async def _heartbeat(client: modal.client._Client, app_id: str) -> None: ...
|
|
@@ -19,19 +17,18 @@ async def _init_local_app_existing(
|
|
|
19
17
|
async def _init_local_app_new(
|
|
20
18
|
client: modal.client._Client,
|
|
21
19
|
description: str,
|
|
20
|
+
tags: dict[str, str],
|
|
22
21
|
app_state: int,
|
|
23
22
|
environment_name: str = "",
|
|
24
23
|
interactive: bool = False,
|
|
25
24
|
) -> modal.running_app.RunningApp: ...
|
|
26
25
|
async def _init_local_app_from_name(
|
|
27
|
-
client: modal.client._Client, name: str, environment_name: str = ""
|
|
26
|
+
client: modal.client._Client, name: str, tags: dict[str, str], environment_name: str = ""
|
|
28
27
|
) -> modal.running_app.RunningApp: ...
|
|
29
28
|
async def _create_all_objects(
|
|
30
|
-
client: modal.client._Client,
|
|
31
29
|
running_app: modal.running_app.RunningApp,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
environment_name: str,
|
|
30
|
+
local_app_state: modal.app._LocalAppState,
|
|
31
|
+
load_context: modal._load_context.LoadContext,
|
|
35
32
|
) -> None:
|
|
36
33
|
"""Create objects that have been defined but not created on the server."""
|
|
37
34
|
...
|
|
@@ -40,10 +37,8 @@ async def _publish_app(
|
|
|
40
37
|
client: modal.client._Client,
|
|
41
38
|
running_app: modal.running_app.RunningApp,
|
|
42
39
|
app_state: int,
|
|
43
|
-
|
|
44
|
-
classes: dict[str, modal.cls._Cls],
|
|
40
|
+
app_local_state: modal.app._LocalAppState,
|
|
45
41
|
name: str = "",
|
|
46
|
-
tags: dict[str, str] = {},
|
|
47
42
|
deployment_tag: str = "",
|
|
48
43
|
commit_info: typing.Optional[modal_proto.api_pb2.CommitInfo] = None,
|
|
49
44
|
) -> tuple[str, list[modal_proto.api_pb2.Warning]]:
|
|
@@ -66,18 +61,18 @@ async def _status_based_disconnect(
|
|
|
66
61
|
...
|
|
67
62
|
|
|
68
63
|
def _run_app(
|
|
69
|
-
app: _App,
|
|
64
|
+
app: modal.app._App,
|
|
70
65
|
*,
|
|
71
66
|
client: typing.Optional[modal.client._Client] = None,
|
|
72
67
|
detach: bool = False,
|
|
73
68
|
environment_name: typing.Optional[str] = None,
|
|
74
69
|
interactive: bool = False,
|
|
75
|
-
) -> typing.AsyncContextManager[_App]:
|
|
70
|
+
) -> typing.AsyncContextManager[modal.app._App]:
|
|
76
71
|
"""mdmd:hidden"""
|
|
77
72
|
...
|
|
78
73
|
|
|
79
74
|
async def _serve_update(
|
|
80
|
-
app: _App, existing_app_id: str, is_ready: multiprocessing.synchronize.Event, environment_name: str
|
|
75
|
+
app: modal.app._App, existing_app_id: str, is_ready: multiprocessing.synchronize.Event, environment_name: str
|
|
81
76
|
) -> None:
|
|
82
77
|
"""mdmd:hidden"""
|
|
83
78
|
...
|
|
@@ -115,7 +110,7 @@ class DeployResult:
|
|
|
115
110
|
...
|
|
116
111
|
|
|
117
112
|
async def _deploy_app(
|
|
118
|
-
app: _App,
|
|
113
|
+
app: modal.app._App,
|
|
119
114
|
name: typing.Optional[str] = None,
|
|
120
115
|
namespace: typing.Any = None,
|
|
121
116
|
client: typing.Optional[modal.client._Client] = None,
|
|
@@ -129,7 +124,7 @@ async def _deploy_app(
|
|
|
129
124
|
...
|
|
130
125
|
|
|
131
126
|
async def _interactive_shell(
|
|
132
|
-
_app: _App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: typing.Any
|
|
127
|
+
_app: modal.app._App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: typing.Any
|
|
133
128
|
) -> None:
|
|
134
129
|
"""Run an interactive shell (like `bash`) within the image for this app.
|
|
135
130
|
|
|
@@ -159,26 +154,26 @@ class __run_app_spec(typing_extensions.Protocol):
|
|
|
159
154
|
def __call__(
|
|
160
155
|
self,
|
|
161
156
|
/,
|
|
162
|
-
app:
|
|
157
|
+
app: modal.app.App,
|
|
163
158
|
*,
|
|
164
159
|
client: typing.Optional[modal.client.Client] = None,
|
|
165
160
|
detach: bool = False,
|
|
166
161
|
environment_name: typing.Optional[str] = None,
|
|
167
162
|
interactive: bool = False,
|
|
168
|
-
) -> synchronicity.combined_types.AsyncAndBlockingContextManager[
|
|
163
|
+
) -> synchronicity.combined_types.AsyncAndBlockingContextManager[modal.app.App]:
|
|
169
164
|
"""mdmd:hidden"""
|
|
170
165
|
...
|
|
171
166
|
|
|
172
167
|
def aio(
|
|
173
168
|
self,
|
|
174
169
|
/,
|
|
175
|
-
app:
|
|
170
|
+
app: modal.app.App,
|
|
176
171
|
*,
|
|
177
172
|
client: typing.Optional[modal.client.Client] = None,
|
|
178
173
|
detach: bool = False,
|
|
179
174
|
environment_name: typing.Optional[str] = None,
|
|
180
175
|
interactive: bool = False,
|
|
181
|
-
) -> typing.AsyncContextManager[
|
|
176
|
+
) -> typing.AsyncContextManager[modal.app.App]:
|
|
182
177
|
"""mdmd:hidden"""
|
|
183
178
|
...
|
|
184
179
|
|
|
@@ -186,13 +181,23 @@ run_app: __run_app_spec
|
|
|
186
181
|
|
|
187
182
|
class __serve_update_spec(typing_extensions.Protocol):
|
|
188
183
|
def __call__(
|
|
189
|
-
self,
|
|
184
|
+
self,
|
|
185
|
+
/,
|
|
186
|
+
app: modal.app.App,
|
|
187
|
+
existing_app_id: str,
|
|
188
|
+
is_ready: multiprocessing.synchronize.Event,
|
|
189
|
+
environment_name: str,
|
|
190
190
|
) -> None:
|
|
191
191
|
"""mdmd:hidden"""
|
|
192
192
|
...
|
|
193
193
|
|
|
194
194
|
async def aio(
|
|
195
|
-
self,
|
|
195
|
+
self,
|
|
196
|
+
/,
|
|
197
|
+
app: modal.app.App,
|
|
198
|
+
existing_app_id: str,
|
|
199
|
+
is_ready: multiprocessing.synchronize.Event,
|
|
200
|
+
environment_name: str,
|
|
196
201
|
) -> None:
|
|
197
202
|
"""mdmd:hidden"""
|
|
198
203
|
...
|
|
@@ -203,7 +208,7 @@ class __deploy_app_spec(typing_extensions.Protocol):
|
|
|
203
208
|
def __call__(
|
|
204
209
|
self,
|
|
205
210
|
/,
|
|
206
|
-
app:
|
|
211
|
+
app: modal.app.App,
|
|
207
212
|
name: typing.Optional[str] = None,
|
|
208
213
|
namespace: typing.Any = None,
|
|
209
214
|
client: typing.Optional[modal.client.Client] = None,
|
|
@@ -219,7 +224,7 @@ class __deploy_app_spec(typing_extensions.Protocol):
|
|
|
219
224
|
async def aio(
|
|
220
225
|
self,
|
|
221
226
|
/,
|
|
222
|
-
app:
|
|
227
|
+
app: modal.app.App,
|
|
223
228
|
name: typing.Optional[str] = None,
|
|
224
229
|
namespace: typing.Any = None,
|
|
225
230
|
client: typing.Optional[modal.client.Client] = None,
|
|
@@ -236,7 +241,13 @@ deploy_app: __deploy_app_spec
|
|
|
236
241
|
|
|
237
242
|
class __interactive_shell_spec(typing_extensions.Protocol):
|
|
238
243
|
def __call__(
|
|
239
|
-
self,
|
|
244
|
+
self,
|
|
245
|
+
/,
|
|
246
|
+
_app: modal.app.App,
|
|
247
|
+
cmds: list[str],
|
|
248
|
+
environment_name: str = "",
|
|
249
|
+
pty: bool = True,
|
|
250
|
+
**kwargs: typing.Any,
|
|
240
251
|
) -> None:
|
|
241
252
|
"""Run an interactive shell (like `bash`) within the image for this app.
|
|
242
253
|
|
|
@@ -263,7 +274,13 @@ class __interactive_shell_spec(typing_extensions.Protocol):
|
|
|
263
274
|
...
|
|
264
275
|
|
|
265
276
|
async def aio(
|
|
266
|
-
self,
|
|
277
|
+
self,
|
|
278
|
+
/,
|
|
279
|
+
_app: modal.app.App,
|
|
280
|
+
cmds: list[str],
|
|
281
|
+
environment_name: str = "",
|
|
282
|
+
pty: bool = True,
|
|
283
|
+
**kwargs: typing.Any,
|
|
267
284
|
) -> None:
|
|
268
285
|
"""Run an interactive shell (like `bash`) within the image for this app.
|
|
269
286
|
|