Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[App] Support for headless apps #15875

Merged
merged 30 commits into from Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0cb2146
Add `is_headless` when dispatching in the cloud
ethanwharris Nov 29, 2022
1dc42d8
Bump cloud version
ethanwharris Nov 30, 2022
c6ad5cc
Add tests
ethanwharris Nov 30, 2022
fb2a336
Merge branch 'master' into feature/is_headless
ethanwharris Dec 1, 2022
335f76d
Dont open app page for headless apps locally
ethanwharris Dec 1, 2022
c1dc0a6
Refactor
ethanwharris Dec 1, 2022
80ec62d
Update CHANGELOG.md
ethanwharris Dec 1, 2022
7dadc33
Support dynamic UIs at runtime
ethanwharris Dec 2, 2022
df7a7fc
Comments
ethanwharris Dec 2, 2022
6e52607
Fix
ethanwharris Dec 2, 2022
04efa22
Updates
ethanwharris Dec 2, 2022
3e57541
Fixes and cleanup
ethanwharris Dec 2, 2022
3e8f525
Fix tests
ethanwharris Dec 2, 2022
a95af08
Dont open view page for headless apps
ethanwharris Dec 2, 2022
1b137fe
Fix test, resolve URL the right way
ethanwharris Dec 2, 2022
a1ee356
Remove launch
ethanwharris Dec 5, 2022
9516395
Clean
ethanwharris Dec 5, 2022
bac282b
Cleanup tests
ethanwharris Dec 5, 2022
fe86b32
Fixes
ethanwharris Dec 5, 2022
e636bb1
Updates
ethanwharris Dec 5, 2022
67ef5b5
Add test
ethanwharris Dec 5, 2022
5280bab
Merge branch 'master' into feature/is_headless
ethanwharris Dec 5, 2022
26e0735
Increase app cloud tests timeout
ethanwharris Dec 5, 2022
7d8ebb9
Merge branch 'feature/is_headless' of https://github.com/Lightning-AI…
ethanwharris Dec 5, 2022
02ced7f
Increase timeout
ethanwharris Dec 5, 2022
4152c02
Wait for running
ethanwharris Dec 5, 2022
987e95e
Revert timeouts
ethanwharris Dec 5, 2022
11d16f4
Clean
ethanwharris Dec 5, 2022
175b53f
Dont update if it hasnt changed
ethanwharris Dec 5, 2022
24b810d
Increase timeout
ethanwharris Dec 5, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions .azure/app-cloud-e2e.yml
Expand Up @@ -93,7 +93,7 @@ jobs:
'App: custom_work_dependencies':
name: "custom_work_dependencies"
dir: "local"
timeoutInMinutes: "10"
timeoutInMinutes: "15"
cancelTimeoutInMinutes: "1"
# values: https://docs.microsoft.com/en-us/azure/devops/pipelines/process/phases?view=azure-devops&tabs=yaml#workspace
workspace:
Expand Down Expand Up @@ -164,7 +164,7 @@ jobs:
ls -l examples/${TEST_APP_NAME}
ls -l tests/tests_examples_app/public
python -m pytest tests/tests_examples_app/${TEST_APP_FOLDER}/test_${TEST_APP_NAME}.py::test_${TEST_APP_NAME}_example_cloud \
--timeout=540 --capture=no -v --color=yes
--timeout=660 --capture=no -v --color=yes
env:
HEADLESS: '1'
PACKAGE_LIGHTNING: '1'
Expand Down
2 changes: 1 addition & 1 deletion requirements/app/base.txt
@@ -1,4 +1,4 @@
lightning-cloud>=0.5.11
lightning-cloud>=0.5.12
packaging
typing-extensions>=4.0.0, <=4.4.0
deepdiff>=5.7.0, <=5.8.1
Expand Down
5 changes: 5 additions & 0 deletions src/lightning_app/CHANGELOG.md
Expand Up @@ -19,8 +19,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
### Changed

- The `MultiNode` components now warn the user when running with `num_nodes > 1` locally ([#15806](https://github.com/Lightning-AI/lightning/pull/15806))

- Cluster creation and deletion now waits by default [#15458](https://github.com/Lightning-AI/lightning/pull/15458)

- Running an app without a UI locally no longer opens the browser ([#15875](https://github.com/Lightning-AI/lightning/pull/15875))

- Apps without UIs no longer activate the "Open App" button when running in the cloud ([#15875](https://github.com/Lightning-AI/lightning/pull/15875))


### Deprecated

Expand Down
19 changes: 3 additions & 16 deletions src/lightning_app/cli/lightning_cli.py
Expand Up @@ -2,13 +2,13 @@
import shutil
import sys
from pathlib import Path
from typing import Any, Tuple, Union
from typing import Tuple, Union

import arrow
import click
import inquirer
import rich
from lightning_cloud.openapi import Externalv1LightningappInstance, V1LightningappInstanceState, V1LightningworkState
from lightning_cloud.openapi import V1LightningappInstanceState, V1LightningworkState
from lightning_cloud.openapi.rest import ApiException
from lightning_utilities.core.imports import RequirementCache
from requests.exceptions import ConnectionError
Expand Down Expand Up @@ -48,15 +48,6 @@
logger = Logger(__name__)


def get_app_url(runtime_type: RuntimeType, *args: Any, need_credits: bool = False) -> str:
if runtime_type == RuntimeType.CLOUD:
lit_app: Externalv1LightningappInstance = args[0]
action = "?action=add_credits" if need_credits else ""
return f"{get_lightning_cloud_url()}/me/apps/{lit_app.id}{action}"
else:
return os.getenv("APP_SERVER_HOST", "http://127.0.0.1:7501/view")


def main() -> None:
# Check environment and versions if not in the cloud
if "LIGHTNING_APP_STATE_URL" not in os.environ:
Expand Down Expand Up @@ -269,10 +260,6 @@ def _run_app(

secrets = _format_input_env_variables(secret)

def on_before_run(*args: Any, **kwargs: Any) -> None:
if open_ui and not without_server:
click.launch(get_app_url(runtime_type, *args, **kwargs))

click.echo("Your Lightning App is starting. This won't take long.")

# TODO: Fixme when Grid utilities are available.
Expand All @@ -284,7 +271,7 @@ def on_before_run(*args: Any, **kwargs: Any) -> None:
start_server=not without_server,
no_cache=no_cache,
blocking=blocking,
on_before_run=on_before_run,
open_ui=open_ui,
name=name,
env_vars=env_vars,
secrets=secrets,
Expand Down
15 changes: 15 additions & 0 deletions src/lightning_app/core/app.py
Expand Up @@ -29,6 +29,8 @@
from lightning_app.utilities import frontend
from lightning_app.utilities.app_helpers import (
_delta_to_app_state_delta,
_handle_is_headless,
_is_headless,
_LightningAppRef,
_should_dispatch_app,
Logger,
Expand Down Expand Up @@ -148,6 +150,8 @@ def __init__(

self._update_layout()

self.is_headless: Optional[bool] = None

self._original_state = None
self._last_state = self.state
self.state_accumulate_wait = STATE_ACCUMULATE_WAIT
Expand Down Expand Up @@ -412,6 +416,7 @@ def run_once(self):
self.backend.update_work_statuses(self.works)

self._update_layout()
self._update_is_headless()
self.maybe_apply_changes()

if self.checkpointing and self._should_snapshot():
Expand Down Expand Up @@ -510,6 +515,16 @@ def _update_layout(self) -> None:
layout = _collect_layout(self, component)
component._layout = layout

def _update_is_headless(self) -> None:
is_headless = _is_headless(self)

# If `is_headless` changed, handle it.
# This ensures support for apps which dynamically add a UI at runtime.
if self.is_headless != is_headless:
self.is_headless = is_headless

_handle_is_headless(self)

def _apply_restarting(self) -> bool:
self._reset_original_state()
# apply stage after restoring the original state.
Expand Down
20 changes: 14 additions & 6 deletions src/lightning_app/runners/cloud.py
Expand Up @@ -7,8 +7,9 @@
from dataclasses import dataclass
from pathlib import Path
from textwrap import dedent
from typing import Any, Callable, List, Optional, Union
from typing import Any, List, Optional, Union

import click
from lightning_cloud.openapi import (
Body3,
Body4,
Expand Down Expand Up @@ -56,12 +57,13 @@
ENABLE_MULTIPLE_WORKS_IN_NON_DEFAULT_CONTAINER,
ENABLE_PULLING_STATE_ENDPOINT,
ENABLE_PUSHING_STATE_ENDPOINT,
get_lightning_cloud_url,
)
from lightning_app.runners.backends.cloud import CloudBackend
from lightning_app.runners.runtime import Runtime
from lightning_app.source_code import LocalSourceCodeDir
from lightning_app.storage import Drive, Mount
from lightning_app.utilities.app_helpers import Logger
from lightning_app.utilities.app_helpers import _is_headless, Logger
from lightning_app.utilities.cloud import _get_project
from lightning_app.utilities.dependency_caching import get_hash
from lightning_app.utilities.load_app import load_app_from_file
Expand Down Expand Up @@ -192,9 +194,9 @@ class CloudRuntime(Runtime):

def dispatch(
self,
on_before_run: Optional[Callable] = None,
name: str = "",
cluster_id: str = None,
open_ui: bool = True,
**kwargs: Any,
) -> None:
"""Method to dispatch and run the :class:`~lightning_app.core.app.LightningApp` in the cloud."""
Expand Down Expand Up @@ -405,6 +407,7 @@ def dispatch(
local_source=True,
dependency_cache_key=app_spec.dependency_cache_key,
user_requested_flow_compute_config=app_spec.user_requested_flow_compute_config,
is_headless=_is_headless(self.app),
)

# create / upload the new app release
Expand Down Expand Up @@ -464,12 +467,12 @@ def dispatch(
logger.error(e.body)
sys.exit(1)

if on_before_run:
on_before_run(lightning_app_instance, need_credits=not has_sufficient_credits)

if lightning_app_instance.status.phase == V1LightningappInstanceState.FAILED:
raise RuntimeError("Failed to create the application. Cannot upload the source code.")

if open_ui:
click.launch(self._get_app_url(lightning_app_instance, not has_sufficient_credits))

if cleanup_handle:
cleanup_handle()

Expand Down Expand Up @@ -538,6 +541,11 @@ def load_app_from_file(cls, filepath: str) -> "LightningApp":
app = LightningApp(EmptyFlow())
return app

@staticmethod
def _get_app_url(lightning_app_instance: Externalv1LightningappInstance, need_credits: bool = False) -> str:
action = "?action=add_credits" if need_credits else ""
return f"{get_lightning_cloud_url()}/me/apps/{lightning_app_instance.id}{action}"


def _create_mount_drive_spec(work_name: str, mount: Mount) -> V1LightningworkDrives:
if mount.protocol == "s3://":
Expand Down
16 changes: 11 additions & 5 deletions src/lightning_app/runners/multiprocess.py
@@ -1,15 +1,17 @@
import multiprocessing
import os
from dataclasses import dataclass
from typing import Any, Callable, Optional, Union
from typing import Any, Union

import click

from lightning_app.api.http_methods import _add_tags_to_api, _validate_api
from lightning_app.core.api import start_server
from lightning_app.core.constants import APP_SERVER_IN_CLOUD
from lightning_app.runners.backends import Backend
from lightning_app.runners.runtime import Runtime
from lightning_app.storage.orchestrator import StorageOrchestrator
from lightning_app.utilities.app_helpers import is_overridden
from lightning_app.utilities.app_helpers import _is_headless, is_overridden
from lightning_app.utilities.commands.base import _commands_to_api, _prepare_commands
from lightning_app.utilities.component import _set_flow_context, _set_frontend_context
from lightning_app.utilities.load_app import extract_metadata_from_app
Expand All @@ -29,7 +31,7 @@ class MultiProcessRuntime(Runtime):
backend: Union[str, Backend] = "multiprocessing"
_has_triggered_termination: bool = False

def dispatch(self, *args: Any, on_before_run: Optional[Callable] = None, **kwargs: Any):
def dispatch(self, *args: Any, open_ui: bool = True, **kwargs: Any):
"""Method to dispatch and run the LightningApp."""
try:
_set_flow_context()
Expand Down Expand Up @@ -101,8 +103,8 @@ def dispatch(self, *args: Any, on_before_run: Optional[Callable] = None, **kwarg
# wait for server to be ready
has_started_queue.get()

if on_before_run:
on_before_run(self, self.app)
if open_ui and not _is_headless(self.app):
click.launch(self._get_app_url())

# Connect the runtime to the application.
self.app.connect(self)
Expand All @@ -125,3 +127,7 @@ def terminate(self):
for port in ports:
disable_port(port)
super().terminate()

@staticmethod
def _get_app_url() -> str:
return os.getenv("APP_SERVER_HOST", "http://127.0.0.1:7501/view")
8 changes: 4 additions & 4 deletions src/lightning_app/runners/runtime.py
Expand Up @@ -4,7 +4,7 @@
from dataclasses import dataclass, field
from pathlib import Path
from threading import Thread
from typing import Any, Callable, Dict, List, Optional, Type, TYPE_CHECKING, Union
from typing import Any, Dict, List, Optional, Type, TYPE_CHECKING, Union

from lightning_app import LightningApp, LightningFlow
from lightning_app.core.constants import APP_SERVER_HOST, APP_SERVER_PORT
Expand All @@ -28,7 +28,7 @@ def dispatch(
host: str = APP_SERVER_HOST,
port: int = APP_SERVER_PORT,
blocking: bool = True,
on_before_run: Optional[Callable] = None,
open_ui: bool = True,
name: str = "",
env_vars: Dict[str, str] = None,
secrets: Dict[str, str] = None,
Expand All @@ -45,7 +45,7 @@ def dispatch(
host: Server host address
port: Server port
blocking: Whether for the wait for the UI to start running.
on_before_run: Callable to be executed before run.
open_ui: Whether to open the UI in the browser.
name: Name of app execution
env_vars: Dict of env variables to be set on the app
secrets: Dict of secrets to be passed as environment variables to the app
Expand Down Expand Up @@ -82,7 +82,7 @@ def dispatch(
)
# a cloud dispatcher will return the result while local
# dispatchers will be running the app in the main process
return runtime.dispatch(on_before_run=on_before_run, name=name, no_cache=no_cache, cluster_id=cluster_id)
return runtime.dispatch(open_ui=open_ui, name=name, no_cache=no_cache, cluster_id=cluster_id)


@dataclass
Expand Down
16 changes: 12 additions & 4 deletions src/lightning_app/runners/singleprocess.py
@@ -1,9 +1,13 @@
import multiprocessing as mp
from typing import Any, Callable, Optional
import os
from typing import Any

import click

from lightning_app.core.api import start_server
from lightning_app.core.queues import QueuingSystem
from lightning_app.runners.runtime import Runtime
from lightning_app.utilities.app_helpers import _is_headless
from lightning_app.utilities.load_app import extract_metadata_from_app


Expand All @@ -13,7 +17,7 @@ class SingleProcessRuntime(Runtime):
def __post_init__(self):
pass

def dispatch(self, *args, on_before_run: Optional[Callable] = None, **kwargs: Any):
def dispatch(self, *args, open_ui: bool = True, **kwargs: Any):
"""Method to dispatch and run the LightningApp."""
queue = QueuingSystem.SINGLEPROCESS

Expand Down Expand Up @@ -42,8 +46,8 @@ def dispatch(self, *args, on_before_run: Optional[Callable] = None, **kwargs: An
# wait for server to be ready.
has_started_queue.get()

if on_before_run:
on_before_run()
if open_ui and not _is_headless(self.app):
click.launch(self._get_app_url())

try:
self.app._run()
Expand All @@ -52,3 +56,7 @@ def dispatch(self, *args, on_before_run: Optional[Callable] = None, **kwargs: An
raise
finally:
self.terminate()

@staticmethod
def _get_app_url() -> str:
return os.getenv("APP_SERVER_HOST", "http://127.0.0.1:7501/view")
21 changes: 12 additions & 9 deletions src/lightning_app/testing/testing.py
Expand Up @@ -394,15 +394,18 @@ def run_app_in_cloud(
process = Process(target=_print_logs, kwargs={"app_id": app_id})
process.start()

while True:
try:
with admin_page.context.expect_page() as page_catcher:
admin_page.locator('[data-cy="open"]').click()
view_page = page_catcher.value
view_page.wait_for_load_state(timeout=0)
break
except (playwright._impl._api_types.Error, playwright._impl._api_types.TimeoutError):
pass
if not app.spec.is_headless:
while True:
try:
with admin_page.context.expect_page() as page_catcher:
admin_page.locator('[data-cy="open"]').click()
view_page = page_catcher.value
view_page.wait_for_load_state(timeout=0)
break
except (playwright._impl._api_types.Error, playwright._impl._api_types.TimeoutError):
pass
else:
view_page = None

# TODO: is re-creating this redundant?
lit_apps = [
Expand Down