diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e18a33fa..1fac9dcc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ We appreciate your patience while we speedily work towards a stable release of t + ### 0.63.87 (2024-07-24) * The `_experimental_boost` argument can now be removed. Boost is now enabled on all modal Functions. diff --git a/modal/_output.py b/modal/_output.py index a7c6e7c89..0c9bb4e4d 100644 --- a/modal/_output.py +++ b/modal/_output.py @@ -30,6 +30,7 @@ ) from rich.spinner import Spinner from rich.text import Text +from rich.tree import Tree from modal_proto import api_pb2 @@ -66,18 +67,30 @@ def step_progress(text: str = "") -> Spinner: return Spinner(default_spinner, text, style="blue") -def step_progress_update(spinner: Spinner, message: str): - spinner.update(text=message) +def step_completed(message: str) -> RenderableType: + return f"[green]✓[/green] {message}" -def step_completed(message: str, is_substep: bool = False) -> RenderableType: - """Returns the element to be rendered when a step is completed.""" +def substep_completed(message: str) -> RenderableType: + return f"🔨 {message}" - STEP_COMPLETED = "[green]✓[/green]" - SUBSTEP_COMPLETED = "🔨" - symbol = SUBSTEP_COMPLETED if is_substep else STEP_COMPLETED - return f"{symbol} {message}" +class StatusRow: + def __init__(self, progress: "Optional[Tree]"): + self._spinner = None + self._step_node = None + if progress is not None: + self._spinner = step_progress() + self._step_node = progress.add(self._spinner) + + def message(self, message): + if self._spinner is not None: + self._spinner.update(text=message) + + def finish(self, message): + if self._step_node is not None: + self._spinner.update(text=message) + self._step_node.label = substep_completed(message) def download_progress_bar() -> Progress: @@ -141,6 +154,7 @@ def finalize(self): class OutputManager: _instance: ClassVar[Optional["OutputManager"]] = None + _tree: ClassVar[Optional[Tree]] = None _console: Console _task_states: Dict[str, int] @@ -313,7 +327,7 @@ def tasks_at_state(state): message = f"[blue]{message}[/blue] [grey70]View app at [underline]{self._app_page_url}[/underline][/grey70]" # Set the new message - step_progress_update(self._status_spinner, message) + self._status_spinner.update(text=message) def update_snapshot_progress(self, image_id: str, task_progress: api_pb2.TaskProgress) -> None: # TODO(erikbern): move this to sit on the resolver object, mostly @@ -372,6 +386,27 @@ def show_status_spinner(self): with self._status_spinner_live: yield + @classmethod + @contextlib.contextmanager + def make_tree(cls): + # Note: If the output isn't enabled, don't actually show the tree. + cls._tree = Tree(step_progress("Creating objects..."), guide_style="gray50") + + if output_mgr := OutputManager.get(): + with output_mgr.make_live(cls._tree): + yield + cls._tree.label = step_completed("Created objects.") + output_mgr.print(output_mgr._tree) + else: + yield + + @classmethod + def add_status_row(cls) -> "StatusRow": + # Return a status row to be used for object creation. + # If output isn't enabled, the status row might be invisible. + assert cls._tree, "Output manager has no tree yet" + return StatusRow(cls._tree) + class ProgressHandler: live: Live @@ -664,12 +699,11 @@ class FunctionCreationStatus: tag: str response: Optional[api_pb2.FunctionCreateResponse] = None - def __init__(self, resolver, tag): - self.resolver = resolver + def __init__(self, tag): self.tag = tag def __enter__(self): - self.status_row = self.resolver.add_status_row() + self.status_row = OutputManager.add_status_row() self.status_row.message(f"Creating function {self.tag}...") return self @@ -700,7 +734,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): # Print custom domain in terminal for custom_domain in self.response.function.custom_domain_info: - custom_domain_status_row = self.resolver.add_status_row() + custom_domain_status_row = OutputManager.add_status_row() custom_domain_status_row.finish( f"Custom domain for {self.tag} => [magenta underline]" f"{custom_domain.url}[/magenta underline]{suffix}" diff --git a/modal/_resolver.py b/modal/_resolver.py index 7dd7cde58..ae3793918 100644 --- a/modal/_resolver.py +++ b/modal/_resolver.py @@ -1,6 +1,5 @@ # Copyright Modal Labs 2023 import asyncio -import contextlib from asyncio import Future from typing import TYPE_CHECKING, Dict, Hashable, List, Optional @@ -12,37 +11,9 @@ from .exception import NotFoundError if TYPE_CHECKING: - from rich.tree import Tree - from modal.object import _Object -class StatusRow: - def __init__(self, progress: "Optional[Tree]"): - from ._output import ( - step_progress, - ) - - self._spinner = None - self._step_node = None - if progress is not None: - self._spinner = step_progress() - self._step_node = progress.add(self._spinner) - - def message(self, message): - from ._output import step_progress_update - - if self._spinner is not None: - step_progress_update(self._spinner, message) - - def finish(self, message): - from ._output import step_completed, step_progress_update - - if self._step_node is not None: - step_progress_update(self._spinner, message) - self._step_node.label = step_completed(message, is_substep=True) - - class Resolver: _local_uuid_to_future: Dict[str, Future] _environment_name: Optional[str] @@ -57,12 +28,7 @@ def __init__( environment_name: Optional[str] = None, app_id: Optional[str] = None, ): - from rich.tree import Tree - - from ._output import step_progress - self._local_uuid_to_future = {} - self._tree = Tree(step_progress("Creating objects..."), guide_style="gray50") self._client = client self._app_id = app_id self._environment_name = environment_name @@ -127,15 +93,12 @@ async def loader(): raise # Check that the id of functions and classes didn't change - # TODO(erikbern): revisit this once stub assignments have been disallowed - if not obj._is_another_app and (obj.object_id.startswith("fu-") or obj.object_id.startswith("cs-")): - # Persisted refs are ignored because their life cycle is managed independently. - # The same tag on an app can be pointed at different objects. - if existing_object_id is not None and obj.object_id != existing_object_id: - raise Exception( - f"Tried creating an object using existing id {existing_object_id}" - f" but it has id {obj.object_id}" - ) + # Persisted refs are ignored because their life cycle is managed independently. + if not obj._is_another_app and existing_object_id is not None and obj.object_id != existing_object_id: + raise Exception( + f"Tried creating an object using existing id {existing_object_id}" + f" but it has id {obj.object_id}" + ) return obj @@ -160,19 +123,3 @@ def objects(self) -> List["_Object"]: obj = fut.result() unique_objects.setdefault(obj.object_id, obj) return list(unique_objects.values()) - - @contextlib.contextmanager - def display(self): - # TODO(erikbern): get rid of this wrapper - from ._output import OutputManager, step_completed - - if output_mgr := OutputManager.get(): - with output_mgr.make_live(self._tree): - yield - self._tree.label = step_completed("Created objects.") - output_mgr.print(self._tree) - else: - yield - - def add_status_row(self) -> StatusRow: - return StatusRow(self._tree) diff --git a/modal/functions.py b/modal/functions.py index 89423cee7..bda30c1a4 100644 --- a/modal/functions.py +++ b/modal/functions.py @@ -355,7 +355,7 @@ async def _load(method_bound_function: "_Function", resolver: Resolver, existing existing_function_id=existing_object_id or method_bound_function.object_id or "", ) assert resolver.client.stub is not None # client should be connected when load is called - with FunctionCreationStatus(resolver, full_name) as function_creation_status: + with FunctionCreationStatus(full_name) as function_creation_status: response = await resolver.client.stub.FunctionCreate(request) method_bound_function._hydrate( response.function_id, @@ -721,7 +721,7 @@ async def _preload(self: _Function, resolver: Resolver, existing_object_id: Opti async def _load(self: _Function, resolver: Resolver, existing_object_id: Optional[str]): assert resolver.client and resolver.client.stub - with FunctionCreationStatus(resolver, tag) as function_creation_status: + with FunctionCreationStatus(tag) as function_creation_status: if is_generator: function_type = api_pb2.Function.FUNCTION_TYPE_GENERATOR else: diff --git a/modal/mount.py b/modal/mount.py index 4344e6b5a..779c743e4 100644 --- a/modal/mount.py +++ b/modal/mount.py @@ -19,6 +19,7 @@ from modal_proto import api_pb2 from modal_version import __version__ +from ._output import OutputManager from ._resolver import Resolver from ._utils.async_utils import synchronize_api from ._utils.blob_utils import FileUploadSpec, blob_upload_file, get_file_upload_spec_from_path @@ -429,7 +430,7 @@ async def _load_mount( accounted_hashes: set[str] = set() message_label = _Mount._description(self._entries) blob_upload_concurrency = asyncio.Semaphore(16) # Limit uploads of large files. - status_row = resolver.add_status_row() + status_row = OutputManager.add_status_row() async def _put_file(file_spec: FileUploadSpec) -> api_pb2.MountFile: nonlocal n_seen, n_finished, total_uploads, total_bytes diff --git a/modal/runner.py b/modal/runner.py index 116fa063d..905a9db4b 100644 --- a/modal/runner.py +++ b/modal/runner.py @@ -116,7 +116,7 @@ async def _create_all_objects( environment_name=environment_name, app_id=running_app.app_id, ) - with resolver.display(): + with OutputManager.make_tree(): # Get current objects, and reset all objects tag_to_object_id = running_app.tag_to_object_id running_app.tag_to_object_id = {} diff --git a/modal_proto/api.proto b/modal_proto/api.proto index a023270d1..4e65d3a8b 100644 --- a/modal_proto/api.proto +++ b/modal_proto/api.proto @@ -1066,6 +1066,10 @@ message FunctionCreateRequest { string app_id = 2 [ (modal.options.audit_target_attr) = true ]; Schedule schedule = 6; string existing_function_id = 7; + // This flag tells the server to avoid doing updates in FunctionCreate that should now + // be done in AppPublish. Provides a smoother migration onto atomic deployments with 0.64, + // and can be deprecated once we no longer support ealier versions. + bool defer_updates = 8; } message FunctionCreateResponse { diff --git a/modal_version/_version_generated.py b/modal_version/_version_generated.py index 52d16b8a6..640befa91 100644 --- a/modal_version/_version_generated.py +++ b/modal_version/_version_generated.py @@ -1,4 +1,4 @@ # Copyright Modal Labs 2024 # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client. -build_number = 90 # git: 0bc7824 +build_number = 96 # git: f1140b1