Skip to content

Commit

Permalink
Merge branch 'main' into cathy/batching-integration
Browse files Browse the repository at this point in the history
  • Loading branch information
cathyzbn committed Jul 31, 2024
2 parents c71d982 + 0c73d3e commit 3ebf9cb
Show file tree
Hide file tree
Showing 8 changed files with 64 additions and 77 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ We appreciate your patience while we speedily work towards a stable release of t

<!-- NEW CONTENT GENERATED BELOW. PLEASE PRESERVE THIS COMMENT. -->


### 0.63.87 (2024-07-24)

* The `_experimental_boost` argument can now be removed. Boost is now enabled on all modal Functions.
Expand Down
60 changes: 47 additions & 13 deletions modal/_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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}"
Expand Down
65 changes: 6 additions & 59 deletions modal/_resolver.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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)
4 changes: 2 additions & 2 deletions modal/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion modal/mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion modal/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down
4 changes: 4 additions & 0 deletions modal_proto/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion modal_version/_version_generated.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 3ebf9cb

Please sign in to comment.