modal 1.0.6.dev58__py3-none-any.whl → 1.2.3.dev7__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/__main__.py +3 -4
- modal/_billing.py +80 -0
- modal/_clustered_functions.py +7 -3
- modal/_clustered_functions.pyi +4 -2
- modal/_container_entrypoint.py +41 -49
- modal/_functions.py +424 -195
- modal/_grpc_client.py +171 -0
- modal/_load_context.py +105 -0
- modal/_object.py +68 -20
- modal/_output.py +58 -45
- modal/_partial_function.py +36 -11
- modal/_pty.py +7 -3
- modal/_resolver.py +21 -35
- modal/_runtime/asgi.py +4 -3
- modal/_runtime/container_io_manager.py +301 -186
- modal/_runtime/container_io_manager.pyi +70 -61
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +4 -1
- modal/_runtime/gpu_memory_snapshot.py +170 -63
- modal/_runtime/user_code_imports.py +28 -58
- modal/_serialization.py +57 -1
- modal/_utils/async_utils.py +33 -12
- modal/_utils/auth_token_manager.py +2 -5
- modal/_utils/blob_utils.py +110 -53
- modal/_utils/function_utils.py +49 -42
- modal/_utils/grpc_utils.py +80 -50
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +17 -3
- modal/_utils/task_command_router_client.py +536 -0
- modal/_utils/time_utils.py +34 -6
- modal/app.py +219 -83
- modal/app.pyi +229 -56
- modal/billing.py +5 -0
- modal/{requirements → builder}/2025.06.txt +1 -0
- modal/{requirements → builder}/PREVIEW.txt +1 -0
- modal/cli/_download.py +19 -3
- modal/cli/_traceback.py +3 -2
- modal/cli/app.py +4 -4
- modal/cli/cluster.py +15 -7
- modal/cli/config.py +5 -3
- modal/cli/container.py +7 -6
- modal/cli/dict.py +22 -16
- modal/cli/entry_point.py +12 -5
- modal/cli/environment.py +5 -4
- modal/cli/import_refs.py +3 -3
- modal/cli/launch.py +102 -5
- modal/cli/network_file_system.py +9 -13
- modal/cli/profile.py +3 -2
- modal/cli/programs/launch_instance_ssh.py +94 -0
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/run_marimo.py +95 -0
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +57 -26
- modal/cli/run.py +58 -16
- modal/cli/secret.py +48 -22
- modal/cli/utils.py +3 -4
- modal/cli/volume.py +28 -25
- modal/client.py +13 -116
- modal/client.pyi +9 -91
- modal/cloud_bucket_mount.py +5 -3
- modal/cloud_bucket_mount.pyi +5 -1
- modal/cls.py +130 -102
- modal/cls.pyi +45 -85
- modal/config.py +29 -10
- modal/container_process.py +291 -13
- modal/container_process.pyi +95 -32
- modal/dict.py +282 -63
- modal/dict.pyi +423 -73
- modal/environments.py +15 -27
- modal/environments.pyi +5 -15
- modal/exception.py +8 -0
- modal/experimental/__init__.py +143 -38
- modal/experimental/flash.py +247 -78
- modal/experimental/flash.pyi +137 -9
- modal/file_io.py +14 -28
- modal/file_io.pyi +2 -2
- modal/file_pattern_matcher.py +25 -16
- modal/functions.pyi +134 -61
- modal/image.py +255 -86
- modal/image.pyi +300 -62
- modal/io_streams.py +436 -126
- modal/io_streams.pyi +236 -171
- modal/mount.py +62 -157
- modal/mount.pyi +45 -172
- modal/network_file_system.py +30 -53
- modal/network_file_system.pyi +16 -76
- modal/object.pyi +42 -8
- modal/parallel_map.py +821 -113
- modal/parallel_map.pyi +134 -0
- modal/partial_function.pyi +4 -1
- modal/proxy.py +16 -7
- modal/proxy.pyi +10 -2
- modal/queue.py +263 -61
- modal/queue.pyi +409 -66
- modal/runner.py +112 -92
- modal/runner.pyi +45 -27
- modal/sandbox.py +451 -124
- modal/sandbox.pyi +513 -67
- modal/secret.py +291 -67
- modal/secret.pyi +425 -19
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +11 -8
- modal/token_flow.py +4 -4
- modal/volume.py +344 -98
- modal/volume.pyi +464 -68
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +9 -8
- modal-1.2.3.dev7.dist-info/RECORD +195 -0
- modal_docs/mdmd/mdmd.py +11 -1
- modal_proto/api.proto +399 -67
- modal_proto/api_grpc.py +241 -1
- modal_proto/api_pb2.py +1395 -1000
- modal_proto/api_pb2.pyi +1239 -79
- modal_proto/api_pb2_grpc.py +499 -4
- modal_proto/api_pb2_grpc.pyi +162 -14
- modal_proto/modal_api_grpc.py +175 -160
- modal_proto/sandbox_router.proto +145 -0
- modal_proto/sandbox_router_grpc.py +105 -0
- modal_proto/sandbox_router_pb2.py +149 -0
- modal_proto/sandbox_router_pb2.pyi +333 -0
- modal_proto/sandbox_router_pb2_grpc.py +203 -0
- modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
- modal_proto/task_command_router.proto +144 -0
- modal_proto/task_command_router_grpc.py +105 -0
- modal_proto/task_command_router_pb2.py +149 -0
- modal_proto/task_command_router_pb2.pyi +333 -0
- modal_proto/task_command_router_pb2_grpc.py +203 -0
- modal_proto/task_command_router_pb2_grpc.pyi +75 -0
- modal_version/__init__.py +1 -1
- modal-1.0.6.dev58.dist-info/RECORD +0 -183
- 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/{requirements → builder}/2023.12.312.txt +0 -0
- /modal/{requirements → builder}/2023.12.txt +0 -0
- /modal/{requirements → builder}/2024.04.txt +0 -0
- /modal/{requirements → builder}/2024.10.txt +0 -0
- /modal/{requirements → builder}/README.md +0 -0
- /modal/{requirements → builder}/base-images.json +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/runner.py
CHANGED
|
@@ -8,8 +8,8 @@ import asyncio
|
|
|
8
8
|
import dataclasses
|
|
9
9
|
import os
|
|
10
10
|
import time
|
|
11
|
-
import typing
|
|
12
11
|
from collections.abc import AsyncGenerator
|
|
12
|
+
from contextlib import nullcontext
|
|
13
13
|
from multiprocessing.synchronize import Event
|
|
14
14
|
from typing import TYPE_CHECKING, Any, Optional, TypeVar
|
|
15
15
|
|
|
@@ -18,6 +18,8 @@ from synchronicity.async_wrap import asynccontextmanager
|
|
|
18
18
|
|
|
19
19
|
import modal._runtime.execution_context
|
|
20
20
|
import modal_proto.api_pb2
|
|
21
|
+
from modal._load_context import LoadContext
|
|
22
|
+
from modal._utils.grpc_utils import Retry
|
|
21
23
|
from modal_proto import api_pb2
|
|
22
24
|
|
|
23
25
|
from ._functions import _Function
|
|
@@ -28,13 +30,12 @@ from ._traceback import print_server_warnings, traceback_contains_remote_call
|
|
|
28
30
|
from ._utils.async_utils import TaskContext, gather_cancel_on_exc, synchronize_api
|
|
29
31
|
from ._utils.deprecation import warn_if_passing_namespace
|
|
30
32
|
from ._utils.git_utils import get_git_commit_info
|
|
31
|
-
from ._utils.grpc_utils import retry_transient_errors
|
|
32
33
|
from ._utils.name_utils import check_object_name, is_valid_tag
|
|
33
34
|
from .client import HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, _Client
|
|
34
35
|
from .cls import _Cls
|
|
35
36
|
from .config import config, logger
|
|
36
37
|
from .environments import _get_environment_cached
|
|
37
|
-
from .exception import InteractiveTimeoutError, InvalidError, RemoteError, _CliUserExecutionError
|
|
38
|
+
from .exception import ConnectionError, InteractiveTimeoutError, InvalidError, RemoteError, _CliUserExecutionError
|
|
38
39
|
from .output import _get_output_manager, enable_output
|
|
39
40
|
from .running_app import RunningApp, running_app_from_layout
|
|
40
41
|
from .sandbox import _Sandbox
|
|
@@ -42,9 +43,7 @@ from .secret import _Secret
|
|
|
42
43
|
from .stream_type import StreamType
|
|
43
44
|
|
|
44
45
|
if TYPE_CHECKING:
|
|
45
|
-
|
|
46
|
-
else:
|
|
47
|
-
_App = TypeVar("_App")
|
|
46
|
+
import modal.app
|
|
48
47
|
|
|
49
48
|
|
|
50
49
|
V = TypeVar("V")
|
|
@@ -55,14 +54,14 @@ async def _heartbeat(client: _Client, app_id: str) -> None:
|
|
|
55
54
|
# TODO(erikbern): we should capture exceptions here
|
|
56
55
|
# * if request fails: destroy the client
|
|
57
56
|
# * if server says the app is gone: print a helpful warning about detaching
|
|
58
|
-
await
|
|
57
|
+
await client.stub.AppHeartbeat(request, retry=Retry(attempt_timeout=HEARTBEAT_TIMEOUT))
|
|
59
58
|
|
|
60
59
|
|
|
61
60
|
async def _init_local_app_existing(client: _Client, existing_app_id: str, environment_name: str) -> RunningApp:
|
|
62
61
|
# Get all the objects first
|
|
63
62
|
obj_req = api_pb2.AppGetLayoutRequest(app_id=existing_app_id)
|
|
64
63
|
obj_resp, _ = await gather_cancel_on_exc(
|
|
65
|
-
|
|
64
|
+
client.stub.AppGetLayout(obj_req),
|
|
66
65
|
# Cache the environment associated with the app now as we will use it later
|
|
67
66
|
_get_environment_cached(environment_name, client),
|
|
68
67
|
)
|
|
@@ -77,6 +76,7 @@ async def _init_local_app_existing(client: _Client, existing_app_id: str, enviro
|
|
|
77
76
|
async def _init_local_app_new(
|
|
78
77
|
client: _Client,
|
|
79
78
|
description: str,
|
|
79
|
+
tags: dict[str, str],
|
|
80
80
|
app_state: int, # ValueType
|
|
81
81
|
environment_name: str = "",
|
|
82
82
|
interactive: bool = False,
|
|
@@ -85,9 +85,10 @@ async def _init_local_app_new(
|
|
|
85
85
|
description=description,
|
|
86
86
|
environment_name=environment_name,
|
|
87
87
|
app_state=app_state, # type: ignore
|
|
88
|
+
tags=tags,
|
|
88
89
|
)
|
|
89
90
|
app_resp, _ = await gather_cancel_on_exc( # TODO: use TaskGroup?
|
|
90
|
-
|
|
91
|
+
client.stub.AppCreate(app_req),
|
|
91
92
|
# Cache the environment associated with the app now as we will use it later
|
|
92
93
|
_get_environment_cached(environment_name, client),
|
|
93
94
|
)
|
|
@@ -103,6 +104,7 @@ async def _init_local_app_new(
|
|
|
103
104
|
async def _init_local_app_from_name(
|
|
104
105
|
client: _Client,
|
|
105
106
|
name: str,
|
|
107
|
+
tags: dict[str, str],
|
|
106
108
|
environment_name: str = "",
|
|
107
109
|
) -> RunningApp:
|
|
108
110
|
# Look up any existing deployment
|
|
@@ -110,7 +112,7 @@ async def _init_local_app_from_name(
|
|
|
110
112
|
name=name,
|
|
111
113
|
environment_name=environment_name,
|
|
112
114
|
)
|
|
113
|
-
app_resp = await
|
|
115
|
+
app_resp = await client.stub.AppGetByDeploymentName(app_req)
|
|
114
116
|
existing_app_id = app_resp.app_id or None
|
|
115
117
|
|
|
116
118
|
# Grab the app
|
|
@@ -118,24 +120,19 @@ async def _init_local_app_from_name(
|
|
|
118
120
|
return await _init_local_app_existing(client, existing_app_id, environment_name)
|
|
119
121
|
else:
|
|
120
122
|
return await _init_local_app_new(
|
|
121
|
-
client, name, api_pb2.APP_STATE_INITIALIZING, environment_name=environment_name
|
|
123
|
+
client, name, tags, api_pb2.APP_STATE_INITIALIZING, environment_name=environment_name
|
|
122
124
|
)
|
|
123
125
|
|
|
124
126
|
|
|
125
127
|
async def _create_all_objects(
|
|
126
|
-
client: _Client,
|
|
127
128
|
running_app: RunningApp,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
environment_name: str,
|
|
129
|
+
local_app_state: "modal.app._LocalAppState",
|
|
130
|
+
load_context: LoadContext,
|
|
131
131
|
) -> None:
|
|
132
132
|
"""Create objects that have been defined but not created on the server."""
|
|
133
|
-
indexed_objects: dict[str, _Object] = {**functions, **classes}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
environment_name=environment_name,
|
|
137
|
-
app_id=running_app.app_id,
|
|
138
|
-
)
|
|
133
|
+
indexed_objects: dict[str, _Object] = {**local_app_state.functions, **local_app_state.classes}
|
|
134
|
+
|
|
135
|
+
resolver = Resolver()
|
|
139
136
|
with resolver.display():
|
|
140
137
|
# Get current objects, and reset all objects
|
|
141
138
|
tag_to_object_id = {**running_app.function_ids, **running_app.class_ids}
|
|
@@ -158,7 +155,7 @@ async def _create_all_objects(
|
|
|
158
155
|
# Note: preload only currently implemented for Functions, returns None otherwise
|
|
159
156
|
# this is to ensure that directly referenced functions from the global scope has
|
|
160
157
|
# ids associated with them when they are serialized into other functions
|
|
161
|
-
await resolver.preload(obj, existing_object_id)
|
|
158
|
+
await resolver.preload(obj, load_context, existing_object_id)
|
|
162
159
|
if obj.is_hydrated:
|
|
163
160
|
tag_to_object_id[tag] = obj.object_id
|
|
164
161
|
|
|
@@ -166,7 +163,8 @@ async def _create_all_objects(
|
|
|
166
163
|
|
|
167
164
|
async def _load(tag, obj):
|
|
168
165
|
existing_object_id = tag_to_object_id.get(tag)
|
|
169
|
-
|
|
166
|
+
# Pass load_context so dependencies can inherit app_id, client, etc.
|
|
167
|
+
await resolver.load(obj, load_context, existing_object_id=existing_object_id)
|
|
170
168
|
if _Function._is_id_type(obj.object_id):
|
|
171
169
|
running_app.function_ids[tag] = obj.object_id
|
|
172
170
|
elif _Cls._is_id_type(obj.object_id):
|
|
@@ -181,29 +179,29 @@ async def _publish_app(
|
|
|
181
179
|
client: _Client,
|
|
182
180
|
running_app: RunningApp,
|
|
183
181
|
app_state: int, # api_pb2.AppState.value
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
tag: str = "", # Only relevant for deployments
|
|
182
|
+
app_local_state: "modal.app._LocalAppState",
|
|
183
|
+
name: str = "",
|
|
184
|
+
deployment_tag: str = "", # Only relevant for deployments
|
|
188
185
|
commit_info: Optional[api_pb2.CommitInfo] = None, # Git commit information
|
|
189
186
|
) -> tuple[str, list[api_pb2.Warning]]:
|
|
190
187
|
"""Wrapper for AppPublish RPC."""
|
|
191
|
-
|
|
188
|
+
functions = app_local_state.functions
|
|
192
189
|
definition_ids = {obj.object_id: obj._get_metadata().definition_id for obj in functions.values()} # type: ignore
|
|
193
190
|
|
|
194
191
|
request = api_pb2.AppPublishRequest(
|
|
195
192
|
app_id=running_app.app_id,
|
|
196
193
|
name=name,
|
|
197
|
-
|
|
194
|
+
tags=app_local_state.tags,
|
|
195
|
+
deployment_tag=deployment_tag,
|
|
196
|
+
commit_info=commit_info,
|
|
198
197
|
app_state=app_state, # type: ignore : should be a api_pb2.AppState.value
|
|
199
198
|
function_ids=running_app.function_ids,
|
|
200
199
|
class_ids=running_app.class_ids,
|
|
201
200
|
definition_ids=definition_ids,
|
|
202
|
-
commit_info=commit_info,
|
|
203
201
|
)
|
|
204
202
|
|
|
205
203
|
try:
|
|
206
|
-
response = await
|
|
204
|
+
response = await client.stub.AppPublish(request)
|
|
207
205
|
except GRPCError as exc:
|
|
208
206
|
if exc.status == Status.INVALID_ARGUMENT or exc.status == Status.FAILED_PRECONDITION:
|
|
209
207
|
raise InvalidError(exc.message)
|
|
@@ -227,7 +225,7 @@ async def _disconnect(
|
|
|
227
225
|
|
|
228
226
|
logger.debug("Sending app disconnect/stop request")
|
|
229
227
|
req_disconnect = api_pb2.AppClientDisconnectRequest(app_id=app_id, reason=reason, exception=exc_str)
|
|
230
|
-
await
|
|
228
|
+
await client.stub.AppClientDisconnect(req_disconnect)
|
|
231
229
|
logger.debug("App disconnected")
|
|
232
230
|
|
|
233
231
|
|
|
@@ -257,16 +255,17 @@ async def _status_based_disconnect(client: _Client, app_id: str, exc_info: Optio
|
|
|
257
255
|
|
|
258
256
|
@asynccontextmanager
|
|
259
257
|
async def _run_app(
|
|
260
|
-
app: _App,
|
|
258
|
+
app: "modal.app._App",
|
|
261
259
|
*,
|
|
262
260
|
client: Optional[_Client] = None,
|
|
263
261
|
detach: bool = False,
|
|
264
262
|
environment_name: Optional[str] = None,
|
|
265
263
|
interactive: bool = False,
|
|
266
|
-
) -> AsyncGenerator[_App, None]:
|
|
264
|
+
) -> AsyncGenerator["modal.app._App", None]:
|
|
267
265
|
"""mdmd:hidden"""
|
|
268
|
-
|
|
269
|
-
|
|
266
|
+
load_context = await app._root_load_context.reset().in_place_upgrade(
|
|
267
|
+
client=client, environment_name=environment_name
|
|
268
|
+
)
|
|
270
269
|
|
|
271
270
|
if modal._runtime.execution_context._is_currently_importing:
|
|
272
271
|
raise InvalidError("Can not run an app in global scope within a container")
|
|
@@ -287,9 +286,6 @@ async def _run_app(
|
|
|
287
286
|
# https://docs.python.org/3/library/__main__.html#import-main
|
|
288
287
|
app.set_description(__main__.__name__)
|
|
289
288
|
|
|
290
|
-
if client is None:
|
|
291
|
-
client = await _Client.from_env()
|
|
292
|
-
|
|
293
289
|
app_state = api_pb2.APP_STATE_DETACHED if detach else api_pb2.APP_STATE_EPHEMERAL
|
|
294
290
|
|
|
295
291
|
output_mgr = _get_output_manager()
|
|
@@ -297,21 +293,25 @@ async def _run_app(
|
|
|
297
293
|
msg = "Interactive mode requires output to be enabled. (Use the the `modal.enable_output()` context manager.)"
|
|
298
294
|
raise InvalidError(msg)
|
|
299
295
|
|
|
296
|
+
local_app_state = app._local_state
|
|
297
|
+
|
|
300
298
|
running_app: RunningApp = await _init_local_app_new(
|
|
301
|
-
client,
|
|
299
|
+
load_context.client,
|
|
302
300
|
app.description or "",
|
|
303
|
-
|
|
301
|
+
local_app_state.tags,
|
|
302
|
+
environment_name=load_context.environment_name,
|
|
304
303
|
app_state=app_state,
|
|
305
304
|
interactive=interactive,
|
|
306
305
|
)
|
|
306
|
+
await load_context.in_place_upgrade(app_id=running_app.app_id)
|
|
307
307
|
|
|
308
308
|
logs_timeout = config["logs_timeout"]
|
|
309
|
-
async with app._set_local_app(client, running_app), TaskContext(grace=logs_timeout) as tc:
|
|
309
|
+
async with app._set_local_app(load_context.client, running_app), TaskContext(grace=logs_timeout) as tc:
|
|
310
310
|
# Start heartbeats loop to keep the client alive
|
|
311
311
|
# we don't log heartbeat exceptions in detached mode
|
|
312
312
|
# as losing the local connection will not affect the running app
|
|
313
313
|
def heartbeat():
|
|
314
|
-
return _heartbeat(client, running_app.app_id)
|
|
314
|
+
return _heartbeat(load_context.client, running_app.app_id)
|
|
315
315
|
|
|
316
316
|
heartbeat_loop = tc.infinite_loop(heartbeat, sleep=HEARTBEAT_INTERVAL, log_exception=not detach)
|
|
317
317
|
logs_loop: Optional[asyncio.Task] = None
|
|
@@ -332,26 +332,37 @@ async def _run_app(
|
|
|
332
332
|
# Start logs loop
|
|
333
333
|
|
|
334
334
|
logs_loop = tc.create_task(
|
|
335
|
-
get_app_logs_loop(
|
|
335
|
+
get_app_logs_loop(
|
|
336
|
+
load_context.client, output_mgr, app_id=running_app.app_id, app_logs_url=running_app.app_logs_url
|
|
337
|
+
)
|
|
336
338
|
)
|
|
337
339
|
|
|
338
340
|
try:
|
|
339
341
|
# Create all members
|
|
340
|
-
await _create_all_objects(
|
|
342
|
+
await _create_all_objects(running_app, local_app_state, load_context)
|
|
341
343
|
|
|
342
344
|
# Publish the app
|
|
343
|
-
await _publish_app(client, running_app, app_state,
|
|
345
|
+
await _publish_app(load_context.client, running_app, app_state, local_app_state)
|
|
344
346
|
except asyncio.CancelledError as e:
|
|
345
347
|
# this typically happens on sigint/ctrl-C during setup (the KeyboardInterrupt happens in the main thread)
|
|
346
348
|
if output_mgr := _get_output_manager():
|
|
347
349
|
output_mgr.print("Aborting app initialization...\n")
|
|
348
350
|
|
|
349
|
-
await _status_based_disconnect(client, running_app.app_id, e)
|
|
351
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, e)
|
|
350
352
|
raise
|
|
351
353
|
except BaseException as e:
|
|
352
|
-
await _status_based_disconnect(client, running_app.app_id, e)
|
|
354
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, e)
|
|
353
355
|
raise
|
|
354
356
|
|
|
357
|
+
detached_disconnect_msg = (
|
|
358
|
+
"The detached App will keep running. You can track its progress on the Dashboard: "
|
|
359
|
+
f"[magenta underline]{running_app.app_page_url}[/magenta underline]"
|
|
360
|
+
"\n\nStream App logs:\n"
|
|
361
|
+
f"[green]modal app logs {running_app.app_id}[/green]"
|
|
362
|
+
"\n\nStop the App:\n"
|
|
363
|
+
f"[green]modal app stop {running_app.app_id}[/green]"
|
|
364
|
+
)
|
|
365
|
+
|
|
355
366
|
try:
|
|
356
367
|
# Show logs from dynamically created images.
|
|
357
368
|
# TODO: better way to do this
|
|
@@ -360,32 +371,30 @@ async def _run_app(
|
|
|
360
371
|
|
|
361
372
|
# Yield to context
|
|
362
373
|
if output_mgr := _get_output_manager():
|
|
363
|
-
with
|
|
374
|
+
# Don't show status spinner in interactive mode to avoid interfering with breakpoints
|
|
375
|
+
spinner_ctx = nullcontext() if interactive else output_mgr.show_status_spinner()
|
|
376
|
+
with spinner_ctx:
|
|
364
377
|
yield app
|
|
365
378
|
else:
|
|
366
379
|
yield app
|
|
367
380
|
# successful completion!
|
|
368
381
|
heartbeat_loop.cancel()
|
|
369
|
-
await _status_based_disconnect(client, running_app.app_id, exc_info=None)
|
|
382
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, exc_info=None)
|
|
370
383
|
except KeyboardInterrupt as e:
|
|
371
384
|
# this happens only if sigint comes in during the yield block above
|
|
372
385
|
if detach:
|
|
373
386
|
if output_mgr := _get_output_manager():
|
|
374
387
|
output_mgr.print(output_mgr.step_completed("Shutting down Modal client."))
|
|
375
|
-
output_mgr.print(
|
|
376
|
-
"The detached app keeps running. You can track its progress at: "
|
|
377
|
-
f"[magenta]{running_app.app_page_url}[/magenta]"
|
|
378
|
-
""
|
|
379
|
-
)
|
|
388
|
+
output_mgr.print(detached_disconnect_msg)
|
|
380
389
|
if logs_loop:
|
|
381
390
|
logs_loop.cancel()
|
|
382
|
-
await _status_based_disconnect(client, running_app.app_id, e)
|
|
391
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, e)
|
|
383
392
|
else:
|
|
384
393
|
if output_mgr := _get_output_manager():
|
|
385
394
|
output_mgr.print(
|
|
386
395
|
"Disconnecting from Modal - This will terminate your Modal app in a few seconds.\n"
|
|
387
396
|
)
|
|
388
|
-
await _status_based_disconnect(client, running_app.app_id, e)
|
|
397
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, e)
|
|
389
398
|
if logs_loop:
|
|
390
399
|
try:
|
|
391
400
|
await asyncio.wait_for(logs_loop, timeout=logs_timeout)
|
|
@@ -400,9 +409,17 @@ async def _run_app(
|
|
|
400
409
|
)
|
|
401
410
|
)
|
|
402
411
|
return
|
|
412
|
+
except ConnectionError as e:
|
|
413
|
+
# If we lose connection to the server after a detached App has started running, it will continue
|
|
414
|
+
# I think we can only exit "nicely" if we are able to print output though, otherwise we should raise
|
|
415
|
+
if detach and (output_mgr := _get_output_manager()):
|
|
416
|
+
output_mgr.print(":white_exclamation_mark: Connection lost!")
|
|
417
|
+
output_mgr.print(detached_disconnect_msg)
|
|
418
|
+
return
|
|
419
|
+
raise
|
|
403
420
|
except BaseException as e:
|
|
404
421
|
logger.info("Exception during app run")
|
|
405
|
-
await _status_based_disconnect(client, running_app.app_id, e)
|
|
422
|
+
await _status_based_disconnect(load_context.client, running_app.app_id, e)
|
|
406
423
|
raise
|
|
407
424
|
|
|
408
425
|
# wait for logs gracefully, even though the task context would do the same
|
|
@@ -423,28 +440,28 @@ async def _run_app(
|
|
|
423
440
|
|
|
424
441
|
|
|
425
442
|
async def _serve_update(
|
|
426
|
-
app: _App,
|
|
443
|
+
app: "modal.app._App",
|
|
427
444
|
existing_app_id: str,
|
|
428
445
|
is_ready: Event,
|
|
429
446
|
environment_name: str,
|
|
430
447
|
) -> None:
|
|
431
448
|
"""mdmd:hidden"""
|
|
432
449
|
# Used by child process to reinitialize a served app
|
|
433
|
-
|
|
450
|
+
load_context = await app._root_load_context.reset().in_place_upgrade(environment_name=environment_name)
|
|
434
451
|
try:
|
|
435
|
-
running_app: RunningApp = await _init_local_app_existing(client, existing_app_id, environment_name)
|
|
436
|
-
|
|
452
|
+
running_app: RunningApp = await _init_local_app_existing(load_context.client, existing_app_id, environment_name)
|
|
453
|
+
await load_context.in_place_upgrade(app_id=running_app.app_id)
|
|
454
|
+
local_app_state = app._local_state
|
|
437
455
|
# Create objects
|
|
438
|
-
await _create_all_objects(
|
|
439
|
-
client,
|
|
440
|
-
running_app,
|
|
441
|
-
app._functions,
|
|
442
|
-
app._classes,
|
|
443
|
-
environment_name,
|
|
444
|
-
)
|
|
456
|
+
await _create_all_objects(running_app, local_app_state, load_context)
|
|
445
457
|
|
|
446
458
|
# Publish the updated app
|
|
447
|
-
await _publish_app(
|
|
459
|
+
await _publish_app(
|
|
460
|
+
load_context.client,
|
|
461
|
+
running_app,
|
|
462
|
+
app_state=api_pb2.APP_STATE_UNSPECIFIED,
|
|
463
|
+
app_local_state=local_app_state,
|
|
464
|
+
)
|
|
448
465
|
|
|
449
466
|
# Communicate to the parent process
|
|
450
467
|
is_ready.set()
|
|
@@ -464,7 +481,7 @@ class DeployResult:
|
|
|
464
481
|
|
|
465
482
|
|
|
466
483
|
async def _deploy_app(
|
|
467
|
-
app: _App,
|
|
484
|
+
app: "modal.app._App",
|
|
468
485
|
name: Optional[str] = None,
|
|
469
486
|
namespace: Any = None, # mdmd:line-hidden
|
|
470
487
|
client: Optional[_Client] = None,
|
|
@@ -475,9 +492,6 @@ async def _deploy_app(
|
|
|
475
492
|
|
|
476
493
|
Users should prefer the `modal deploy` CLI or the `App.deploy` method.
|
|
477
494
|
"""
|
|
478
|
-
if environment_name is None:
|
|
479
|
-
environment_name = typing.cast(str, config.get("environment"))
|
|
480
|
-
|
|
481
495
|
warn_if_passing_namespace(namespace, "modal.runner.deploy_app")
|
|
482
496
|
|
|
483
497
|
name = name or app.name or ""
|
|
@@ -493,7 +507,7 @@ async def _deploy_app(
|
|
|
493
507
|
else:
|
|
494
508
|
check_object_name(name, "App")
|
|
495
509
|
|
|
496
|
-
if tag and not is_valid_tag(tag):
|
|
510
|
+
if tag and not is_valid_tag(tag, max_length=50):
|
|
497
511
|
raise InvalidError(
|
|
498
512
|
f"Deployment tag {tag!r} is invalid."
|
|
499
513
|
"\n\nTags may only contain alphanumeric characters, dashes, periods, and underscores, "
|
|
@@ -503,12 +517,25 @@ async def _deploy_app(
|
|
|
503
517
|
if client is None:
|
|
504
518
|
client = await _Client.from_env()
|
|
505
519
|
|
|
520
|
+
local_app_state = app._local_state
|
|
506
521
|
t0 = time.time()
|
|
507
522
|
|
|
508
523
|
# Get git information to track deployment history
|
|
509
524
|
commit_info_task = asyncio.create_task(get_git_commit_info())
|
|
510
525
|
|
|
511
|
-
|
|
526
|
+
# We need to do in-place replacement of fields in self._root_load_context in case it has already "spread"
|
|
527
|
+
# to with_options() instances or similar before load
|
|
528
|
+
root_load_context = await app._root_load_context.reset().in_place_upgrade(
|
|
529
|
+
client=client,
|
|
530
|
+
environment_name=environment_name,
|
|
531
|
+
)
|
|
532
|
+
running_app: RunningApp = await _init_local_app_from_name(
|
|
533
|
+
root_load_context.client, name, local_app_state.tags, environment_name=root_load_context.environment_name
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
await root_load_context.in_place_upgrade(
|
|
537
|
+
app_id=running_app.app_id,
|
|
538
|
+
)
|
|
512
539
|
|
|
513
540
|
async with TaskContext(0) as tc:
|
|
514
541
|
# Start heartbeats loop to keep the client alive
|
|
@@ -519,13 +546,7 @@ async def _deploy_app(
|
|
|
519
546
|
|
|
520
547
|
try:
|
|
521
548
|
# Create all members
|
|
522
|
-
await _create_all_objects(
|
|
523
|
-
client,
|
|
524
|
-
running_app,
|
|
525
|
-
app._functions,
|
|
526
|
-
app._classes,
|
|
527
|
-
environment_name=environment_name,
|
|
528
|
-
)
|
|
549
|
+
await _create_all_objects(running_app, local_app_state, root_load_context)
|
|
529
550
|
|
|
530
551
|
commit_info = None
|
|
531
552
|
try:
|
|
@@ -537,11 +558,10 @@ async def _deploy_app(
|
|
|
537
558
|
client,
|
|
538
559
|
running_app,
|
|
539
560
|
api_pb2.APP_STATE_DEPLOYED,
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
commit_info,
|
|
561
|
+
local_app_state,
|
|
562
|
+
name=name,
|
|
563
|
+
deployment_tag=tag,
|
|
564
|
+
commit_info=commit_info,
|
|
545
565
|
)
|
|
546
566
|
except Exception as e:
|
|
547
567
|
# Note that AppClientDisconnect only stops the app if it's still initializing, and is a no-op otherwise.
|
|
@@ -561,7 +581,7 @@ async def _deploy_app(
|
|
|
561
581
|
|
|
562
582
|
|
|
563
583
|
async def _interactive_shell(
|
|
564
|
-
_app: _App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: Any
|
|
584
|
+
_app: "modal.app._App", cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: Any
|
|
565
585
|
) -> None:
|
|
566
586
|
"""Run an interactive shell (like `bash`) within the image for this app.
|
|
567
587
|
|
|
@@ -607,19 +627,19 @@ async def _interactive_shell(
|
|
|
607
627
|
|
|
608
628
|
try:
|
|
609
629
|
if pty:
|
|
610
|
-
container_process = await sandbox.
|
|
630
|
+
container_process = await sandbox._exec(
|
|
611
631
|
*sandbox_cmds, pty_info=get_pty_info(shell=True) if pty else None
|
|
612
632
|
)
|
|
613
633
|
await container_process.attach()
|
|
614
634
|
else:
|
|
615
|
-
container_process = await sandbox.
|
|
635
|
+
container_process = await sandbox._exec(
|
|
616
636
|
*sandbox_cmds, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
|
|
617
637
|
)
|
|
618
638
|
await container_process.wait()
|
|
619
639
|
except InteractiveTimeoutError:
|
|
620
640
|
# Check on status of Sandbox. It may have crashed, causing connection failure.
|
|
621
641
|
req = api_pb2.SandboxWaitRequest(sandbox_id=sandbox._object_id, timeout=0)
|
|
622
|
-
resp = await
|
|
642
|
+
resp = await sandbox._client.stub.SandboxWait(req)
|
|
623
643
|
if resp.result.exception:
|
|
624
644
|
raise RemoteError(resp.result.exception)
|
|
625
645
|
else:
|