From 0cb21469fa492736c6e2f8cbbd11ac7e59a43261 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Tue, 29 Nov 2022 19:50:01 +0000 Subject: [PATCH 01/27] Add `is_headless` when dispatching in the cloud --- src/lightning_app/runners/cloud.py | 3 ++- src/lightning_app/utilities/app_helpers.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/lightning_app/runners/cloud.py b/src/lightning_app/runners/cloud.py index 0752a3b5be8a8..4d9b2af0c6bd9 100644 --- a/src/lightning_app/runners/cloud.py +++ b/src/lightning_app/runners/cloud.py @@ -60,7 +60,7 @@ 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 @@ -395,6 +395,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 diff --git a/src/lightning_app/utilities/app_helpers.py b/src/lightning_app/utilities/app_helpers.py index d63e33db6addb..1a57423ecc4c1 100644 --- a/src/lightning_app/utilities/app_helpers.py +++ b/src/lightning_app/utilities/app_helpers.py @@ -22,6 +22,7 @@ import lightning_app from lightning_app.utilities.exceptions import LightningAppStateException +from lightning_app.utilities.tree import breadth_first if TYPE_CHECKING: from lightning_app.core.app import LightningApp @@ -527,3 +528,15 @@ def _should_dispatch_app() -> bool: and not bool(int(os.getenv("LIGHTNING_DISPATCHED", "0"))) and "LIGHTNING_APP_STATE_URL" not in os.environ ) + + +def _is_headless(app: "LightningApp") -> bool: + """Utility which returns True if the given App has no ``Frontend`` objects or URLs exposed through + ``configure_layout``.""" + if app.frontends: + return False + for component in breadth_first(app.root, types=(lightning_app.LightningFlow,)): + for entry in component._layout: + if "target" in entry: + return False + return True From 1dc42d85a2ffeaf1f6d0d3ff836216a25ac2e293 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Wed, 30 Nov 2022 14:14:56 +0000 Subject: [PATCH 02/27] Bump cloud version --- requirements/app/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/app/base.txt b/requirements/app/base.txt index a3da6958b6688..f4331ff5bc536 100644 --- a/requirements/app/base.txt +++ b/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 From c6ad5cce9f78500cfdfafddc56a74ba1158c0afc Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Wed, 30 Nov 2022 15:05:52 +0000 Subject: [PATCH 03/27] Add tests --- src/lightning_app/utilities/layout.py | 10 +-- tests/tests_app/utilities/test_app_helpers.py | 61 ++++++++++++++++++- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/lightning_app/utilities/layout.py b/src/lightning_app/utilities/layout.py index 9235993ad31d1..a66bcbf167244 100644 --- a/src/lightning_app/utilities/layout.py +++ b/src/lightning_app/utilities/layout.py @@ -41,8 +41,9 @@ def _collect_layout(app: "lightning_app.LightningApp", flow: "lightning_app.Ligh # When running in the cloud, the frontend code will construct the URL based on the flow name return flow._layout elif isinstance(layout, _MagicMockJsonSerializable): - # Do nothing - pass + # The import was mocked, we assume it is a `Frontend` + app.frontends.setdefault(flow.name, "mock") + return flow._layout elif isinstance(layout, dict): layout = _collect_content_layout([layout], flow) elif isinstance(layout, (list, tuple)) and all(isinstance(item, dict) for item in layout): @@ -108,8 +109,9 @@ def _collect_content_layout(layout: List[Dict], flow: "lightning_app.LightningFl entry["content"] = "" entry["target"] = "" elif isinstance(entry["content"], _MagicMockJsonSerializable): - # Do nothing - pass + # The import was mocked, so we just record dummy content + entry["content"] = "mock" + entry["target"] = "mock" else: m = f""" A dictionary returned by `{flow.__class__.__name__}.configure_layout()` contains an unsupported entry. diff --git a/tests/tests_app/utilities/test_app_helpers.py b/tests/tests_app/utilities/test_app_helpers.py index cdf526b74f1c7..9d66dc886ae08 100644 --- a/tests/tests_app/utilities/test_app_helpers.py +++ b/tests/tests_app/utilities/test_app_helpers.py @@ -2,8 +2,10 @@ import pytest -from lightning_app import LightningFlow, LightningWork +from lightning_app import LightningApp, LightningFlow, LightningWork from lightning_app.utilities.app_helpers import ( + _is_headless, + _MagicMockJsonSerializable, AppStatePlugin, BaseStatePlugin, InMemoryStateStore, @@ -102,3 +104,60 @@ def c(self): assert is_static_method(A, "a") assert is_static_method(A, "b") assert not is_static_method(A, "c") + + +class FlowWithURLLayout(Flow): + def configure_layout(self): + return {"name": "test", "content": "https://appurl"} + + +class FlowWithWorkLayout(Flow): + def __init__(self): + super().__init__() + + self.work = Work() + + def configure_layout(self): + return {"name": "test", "content": self.work} + + +class FlowWithMockedFrontend(Flow): + def configure_layout(self): + return _MagicMockJsonSerializable() + + +class FlowWithMockedContent(Flow): + def configure_layout(self): + return [{"name": "test", "content": _MagicMockJsonSerializable()}] + + +class NestedFlow(Flow): + def __init__(self): + super().__init__() + + self.flow = Flow() + + +class NestedFlowWithURLLayout(Flow): + def __init__(self): + super().__init__() + + self.flow = FlowWithURLLayout() + + +@pytest.mark.parametrize( + "flow,expected", + [ + (Flow, True), + (FlowWithURLLayout, False), + (FlowWithWorkLayout, False), + (FlowWithMockedFrontend, False), + (FlowWithMockedContent, False), + (NestedFlow, True), + (NestedFlowWithURLLayout, False), + ], +) +def test_is_headless(flow, expected): + flow = flow() + app = LightningApp(flow) + assert _is_headless(app) == expected From 335f76d595c9d813a64e814c6bd380c134001f9f Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Thu, 1 Dec 2022 21:59:56 +0000 Subject: [PATCH 04/27] Dont open app page for headless apps locally --- src/lightning_app/cli/lightning_cli.py | 4 ++-- src/lightning_app/runners/multiprocess.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index 0205405c20560..5a5c907cf7581 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -269,8 +269,8 @@ 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: + def on_before_run(*args: Any, has_ui: bool = True, **kwargs: Any) -> None: + if open_ui and has_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.") diff --git a/src/lightning_app/runners/multiprocess.py b/src/lightning_app/runners/multiprocess.py index 343996cbdd732..64a70461a79e9 100644 --- a/src/lightning_app/runners/multiprocess.py +++ b/src/lightning_app/runners/multiprocess.py @@ -9,7 +9,7 @@ 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 @@ -102,7 +102,7 @@ def dispatch(self, *args: Any, on_before_run: Optional[Callable] = None, **kwarg has_started_queue.get() if on_before_run: - on_before_run(self, self.app) + on_before_run(self, self.app, has_ui=not _is_headless(self.app)) # Connect the runtime to the application. self.app.connect(self) From c1dc0a62d88dfd489c23e536a3f8dea2a2135743 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Thu, 1 Dec 2022 22:32:22 +0000 Subject: [PATCH 05/27] Refactor --- src/lightning_app/cli/lightning_cli.py | 18 ++--------- src/lightning_app/runners/cloud.py | 17 +++++++--- src/lightning_app/runners/multiprocess.py | 14 +++++--- src/lightning_app/runners/singleprocess.py | 16 +++++++--- tests/tests_app/cli/test_cli.py | 32 +------------------ tests/tests_app/runners/test_cloud.py | 23 +++++++++++++ tests/tests_app/runners/test_multiprocess.py | 13 ++++++++ tests/tests_app/runners/test_singleprocess.py | 13 ++++++++ 8 files changed, 86 insertions(+), 60 deletions(-) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index 5a5c907cf7581..bdea388d9b3b2 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -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 @@ -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: @@ -269,10 +260,6 @@ def _run_app( secrets = _format_input_env_variables(secret) - def on_before_run(*args: Any, has_ui: bool = True, **kwargs: Any) -> None: - if open_ui and has_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. @@ -284,7 +271,6 @@ def on_before_run(*args: Any, has_ui: bool = True, **kwargs: Any) -> None: start_server=not without_server, no_cache=no_cache, blocking=blocking, - on_before_run=on_before_run, name=name, env_vars=env_vars, secrets=secrets, diff --git a/src/lightning_app/runners/cloud.py b/src/lightning_app/runners/cloud.py index ef72fb1b330d4..6ef7770124aae 100644 --- a/src/lightning_app/runners/cloud.py +++ b/src/lightning_app/runners/cloud.py @@ -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, @@ -56,6 +57,7 @@ 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 @@ -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.""" @@ -465,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() @@ -539,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://": diff --git a/src/lightning_app/runners/multiprocess.py b/src/lightning_app/runners/multiprocess.py index 64a70461a79e9..673e8601043d7 100644 --- a/src/lightning_app/runners/multiprocess.py +++ b/src/lightning_app/runners/multiprocess.py @@ -1,7 +1,9 @@ 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 @@ -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() @@ -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, has_ui=not _is_headless(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) @@ -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") diff --git a/src/lightning_app/runners/singleprocess.py b/src/lightning_app/runners/singleprocess.py index b12b86e8625aa..61a67ce9ba904 100644 --- a/src/lightning_app/runners/singleprocess.py +++ b/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 @@ -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 @@ -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() @@ -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") diff --git a/tests/tests_app/cli/test_cli.py b/tests/tests_app/cli/test_cli.py index d5a3c4780a248..fb5ab2ab95ae4 100644 --- a/tests/tests_app/cli/test_cli.py +++ b/tests/tests_app/cli/test_cli.py @@ -4,45 +4,15 @@ import pytest from click.testing import CliRunner -from lightning_cloud.openapi import Externalv1LightningappInstance from lightning_app import __version__ -from lightning_app.cli.lightning_cli import _main, get_app_url, login, logout, run +from lightning_app.cli.lightning_cli import _main, login, logout, run from lightning_app.cli.lightning_cli_create import create, create_cluster from lightning_app.cli.lightning_cli_delete import delete, delete_cluster from lightning_app.cli.lightning_cli_list import get_list, list_apps, list_clusters -from lightning_app.runners.runtime_type import RuntimeType from lightning_app.utilities.exceptions import _ApiExceptionHandler -@pytest.mark.parametrize( - "runtime_type, extra_args, lightning_cloud_url, expected_url", - [ - ( - RuntimeType.CLOUD, - (Externalv1LightningappInstance(id="test-app-id"),), - "https://b975913c4b22eca5f0f9e8eff4c4b1c315340a0d.staging.lightning.ai", - "https://b975913c4b22eca5f0f9e8eff4c4b1c315340a0d.staging.lightning.ai/me/apps/test-app-id", - ), - ( - RuntimeType.CLOUD, - (Externalv1LightningappInstance(id="test-app-id"),), - "http://localhost:9800", - "http://localhost:9800/me/apps/test-app-id", - ), - (RuntimeType.SINGLEPROCESS, tuple(), "", "http://127.0.0.1:7501/view"), - (RuntimeType.SINGLEPROCESS, tuple(), "http://localhost:9800", "http://127.0.0.1:7501/view"), - (RuntimeType.MULTIPROCESS, tuple(), "", "http://127.0.0.1:7501/view"), - (RuntimeType.MULTIPROCESS, tuple(), "http://localhost:9800", "http://127.0.0.1:7501/view"), - ], -) -def test_start_target_url(runtime_type, extra_args, lightning_cloud_url, expected_url): - with mock.patch( - "lightning_app.cli.lightning_cli.get_lightning_cloud_url", mock.MagicMock(return_value=lightning_cloud_url) - ): - assert get_app_url(runtime_type, *extra_args) == expected_url - - @pytest.mark.parametrize("command", [_main, run, get_list, create, delete]) def test_commands(command): runner = CliRunner() diff --git a/tests/tests_app/runners/test_cloud.py b/tests/tests_app/runners/test_cloud.py index 1f525341224eb..30beae237a40a 100644 --- a/tests/tests_app/runners/test_cloud.py +++ b/tests/tests_app/runners/test_cloud.py @@ -13,6 +13,7 @@ Body8, Body9, Externalv1Cluster, + Externalv1LightningappInstance, Gridv1ImageSpec, IdGetBody, V1BuildSpec, @@ -1414,3 +1415,25 @@ def run(self): with pytest.raises(ValueError, match="You requested a custom base image for the Work with name"): _validate_build_spec_and_compute(Work()) + + +@pytest.mark.parametrize( + "lightning_app_instance, lightning_cloud_url, expected_url", + [ + ( + Externalv1LightningappInstance(id="test-app-id"), + "https://b975913c4b22eca5f0f9e8eff4c4b1c315340a0d.staging.lightning.ai", + "https://b975913c4b22eca5f0f9e8eff4c4b1c315340a0d.staging.lightning.ai/me/apps/test-app-id", + ), + ( + Externalv1LightningappInstance(id="test-app-id"), + "http://localhost:9800", + "http://localhost:9800/me/apps/test-app-id", + ), + ], +) +def test_get_app_url(lightning_app_instance, lightning_cloud_url, expected_url): + with mock.patch( + "lightning_app.runners.cloud.get_lightning_cloud_url", mock.MagicMock(return_value=lightning_cloud_url) + ): + assert CloudRuntime._get_app_url(lightning_app_instance) == expected_url diff --git a/tests/tests_app/runners/test_multiprocess.py b/tests/tests_app/runners/test_multiprocess.py index 0693e38a35f77..4ae13ebe64106 100644 --- a/tests/tests_app/runners/test_multiprocess.py +++ b/tests/tests_app/runners/test_multiprocess.py @@ -1,6 +1,8 @@ from unittest import mock from unittest.mock import Mock +import pytest + from lightning_app import LightningApp, LightningFlow, LightningWork from lightning_app.frontend import StaticWebFrontend, StreamlitFrontend from lightning_app.runners import MultiProcessRuntime @@ -81,3 +83,14 @@ def run(self): def test_multiprocess_runtime_sets_context(): """Test that the runtime sets the global variable COMPONENT_CONTEXT in Flow and Work.""" MultiProcessRuntime(LightningApp(ContxtFlow())).dispatch() + + +@pytest.mark.parametrize( + "expected_url", + [ + "http://127.0.0.1:7501/view", + "http://127.0.0.1:7501/view", + ], +) +def test_get_app_url(expected_url): + assert MultiProcessRuntime._get_app_url() == expected_url diff --git a/tests/tests_app/runners/test_singleprocess.py b/tests/tests_app/runners/test_singleprocess.py index 3b2ad69185077..60005c88e2e08 100644 --- a/tests/tests_app/runners/test_singleprocess.py +++ b/tests/tests_app/runners/test_singleprocess.py @@ -1,3 +1,5 @@ +import pytest + from lightning_app import LightningFlow from lightning_app.core.app import LightningApp from lightning_app.runners import SingleProcessRuntime @@ -16,3 +18,14 @@ def test_single_process_runtime(tmpdir): app = LightningApp(Flow()) SingleProcessRuntime(app, start_server=False).dispatch(on_before_run=on_before_run) + + +@pytest.mark.parametrize( + "expected_url", + [ + "http://127.0.0.1:7501/view", + "http://127.0.0.1:7501/view", + ], +) +def test_get_app_url(expected_url): + assert SingleProcessRuntime._get_app_url() == expected_url From 80ec62db57463650254978d5f22e8faeaa400c31 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Thu, 1 Dec 2022 22:38:16 +0000 Subject: [PATCH 06/27] Update CHANGELOG.md --- src/lightning_app/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lightning_app/CHANGELOG.md b/src/lightning_app/CHANGELOG.md index b7e01e77224cb..ccabdc4079c1c 100644 --- a/src/lightning_app/CHANGELOG.md +++ b/src/lightning_app/CHANGELOG.md @@ -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 From 7dadc33d9975e259b30e9ca9e2d203485844a6f3 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Fri, 2 Dec 2022 16:36:36 +0000 Subject: [PATCH 07/27] Support dynamic UIs at runtime --- src/lightning_app/core/app.py | 21 +++++++++++ src/lightning_app/runners/cloud.py | 1 + src/lightning_app/runners/multiprocess.py | 1 + src/lightning_app/runners/singleprocess.py | 1 + src/lightning_app/utilities/app_helpers.py | 41 ++++++++++++++++++++++ tests/tests_app/runners/test_cloud.py | 9 +++++ 6 files changed, 74 insertions(+) diff --git a/src/lightning_app/core/app.py b/src/lightning_app/core/app.py index ce1e81d57d9ea..e52e97e1e601e 100644 --- a/src/lightning_app/core/app.py +++ b/src/lightning_app/core/app.py @@ -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, @@ -140,6 +142,9 @@ def __init__( self.exception = None self.collect_changes: bool = True + self.is_headless: Optional[bool] = None + self._has_launched_browser = False + # NOTE: Checkpointing is disabled by default for the time being. We # will enable it when resuming from full checkpoint is supported. Also, # we will need to revisit the logic at _should_snapshot, since right now @@ -412,6 +417,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(): @@ -510,6 +516,21 @@ 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` hasn't been set before, set it and return. + if self.is_headless is None: + self.is_headless = is_headless + return + + # 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. diff --git a/src/lightning_app/runners/cloud.py b/src/lightning_app/runners/cloud.py index 6ef7770124aae..9280a6ecd30ef 100644 --- a/src/lightning_app/runners/cloud.py +++ b/src/lightning_app/runners/cloud.py @@ -472,6 +472,7 @@ def dispatch( if open_ui: click.launch(self._get_app_url(lightning_app_instance, not has_sufficient_credits)) + self.app._has_launched_browser = True if cleanup_handle: cleanup_handle() diff --git a/src/lightning_app/runners/multiprocess.py b/src/lightning_app/runners/multiprocess.py index 673e8601043d7..b2440509ec658 100644 --- a/src/lightning_app/runners/multiprocess.py +++ b/src/lightning_app/runners/multiprocess.py @@ -105,6 +105,7 @@ def dispatch(self, *args: Any, open_ui: bool = True, **kwargs: Any): if open_ui and not _is_headless(self.app): click.launch(self._get_app_url()) + self.app._has_launched_browser = True # Connect the runtime to the application. self.app.connect(self) diff --git a/src/lightning_app/runners/singleprocess.py b/src/lightning_app/runners/singleprocess.py index 61a67ce9ba904..51a293abd09d1 100644 --- a/src/lightning_app/runners/singleprocess.py +++ b/src/lightning_app/runners/singleprocess.py @@ -48,6 +48,7 @@ def dispatch(self, *args, open_ui: bool = True, **kwargs: Any): if open_ui and not _is_headless(self.app): click.launch(self._get_app_url()) + self.app._has_launched_browser = True try: self.app._run() diff --git a/src/lightning_app/utilities/app_helpers.py b/src/lightning_app/utilities/app_helpers.py index 1a57423ecc4c1..b212175ab1b33 100644 --- a/src/lightning_app/utilities/app_helpers.py +++ b/src/lightning_app/utilities/app_helpers.py @@ -19,6 +19,7 @@ import websockets from deepdiff import Delta +from lightning_cloud.openapi import AppinstancesIdBody, Externalv1LightningappInstance import lightning_app from lightning_app.utilities.exceptions import LightningAppStateException @@ -540,3 +541,43 @@ def _is_headless(app: "LightningApp") -> bool: if "target" in entry: return False return True + + +def _handle_is_headless(app: "LightningApp"): + """Utility for runtime-specific handling of changes to the ``is_headless`` property.""" + app_id = os.getenv("LIGHTNING_CLOUD_APP_ID", None) + project_id = os.getenv("LIGHTNING_CLOUD_PROJECT_ID", None) + + if app_id and project_id: + from lightning_app.utilities.network import LightningClient + + client = LightningClient() + list_apps_response = client.lightningapp_instance_service_list_lightningapp_instances(project_id=project_id) + + current_lightningapp_instance: Optional[Externalv1LightningappInstance] = None + for lightningapp_instance in list_apps_response.lightningapps: + if lightningapp_instance.id == app_id: + current_lightningapp_instance = lightningapp_instance + break + + if not current_lightningapp_instance: + raise RuntimeError( + "App was not found. Please open an issue at https://github.com/lightning-AI/lightning/issues." + ) + + current_lightningapp_instance.spec.is_headless = app.is_headless + + client.lightningapp_instance_service_update_lightningapp_instance( + project_id=project_id, + id=current_lightningapp_instance.id, + body=AppinstancesIdBody(name=current_lightningapp_instance.name, spec=current_lightningapp_instance.spec), + ) + elif not app.is_headless and not app._has_launched_browser: + # If running locally, the app now has a UI, and we haven't opened the browser yet, open the browser. + # TODO: This could cause browsers to be opened at strange times + import click + + from lightning_app.runners.multiprocess import MultiProcessRuntime + + click.launch(MultiProcessRuntime._get_app_url()) + app._has_launched_browser = True diff --git a/tests/tests_app/runners/test_cloud.py b/tests/tests_app/runners/test_cloud.py index 30beae237a40a..28c2ca2f3c710 100644 --- a/tests/tests_app/runners/test_cloud.py +++ b/tests/tests_app/runners/test_cloud.py @@ -85,6 +85,7 @@ def get_cloud_runtime_request_body(**kwargs) -> "Body8": default_request_body = dict( app_entrypoint_file=mock.ANY, enable_app_server=True, + is_headless=True, flow_servers=[], image_spec=None, works=[], @@ -299,6 +300,7 @@ def test_run_on_byoc_cluster(self, monkeypatch): cluster_id="test1234", app_entrypoint_file=mock.ANY, enable_app_server=True, + is_headless=False, flow_servers=[], image_spec=None, works=[], @@ -346,6 +348,7 @@ def test_requirements_file(self, monkeypatch): body = Body8( app_entrypoint_file=mock.ANY, enable_app_server=True, + is_headless=False, flow_servers=[], image_spec=None, works=[], @@ -479,6 +482,7 @@ def test_call_with_work_app(self, lightningapps, start_with_flow, monkeypatch, t local_source=True, app_entrypoint_file="entrypoint.py", enable_app_server=True, + is_headless=False, flow_servers=[], dependency_cache_key=get_hash(requirements_file), user_requested_flow_compute_config=mock.ANY, @@ -663,6 +667,7 @@ def test_call_with_work_app_and_attached_drives(self, lightningapps, monkeypatch local_source=True, app_entrypoint_file="entrypoint.py", enable_app_server=True, + is_headless=False, flow_servers=[], dependency_cache_key=get_hash(requirements_file), user_requested_flow_compute_config=mock.ANY, @@ -786,6 +791,7 @@ def test_call_with_work_app_and_app_comment_command_execution_set(self, lightnin local_source=True, app_entrypoint_file="entrypoint.py", enable_app_server=True, + is_headless=False, flow_servers=[], dependency_cache_key=get_hash(requirements_file), user_requested_flow_compute_config=mock.ANY, @@ -943,6 +949,7 @@ def test_call_with_work_app_and_multiple_attached_drives(self, lightningapps, mo local_source=True, app_entrypoint_file="entrypoint.py", enable_app_server=True, + is_headless=False, flow_servers=[], dependency_cache_key=get_hash(requirements_file), user_requested_flow_compute_config=mock.ANY, @@ -982,6 +989,7 @@ def test_call_with_work_app_and_multiple_attached_drives(self, lightningapps, mo local_source=True, app_entrypoint_file="entrypoint.py", enable_app_server=True, + is_headless=False, flow_servers=[], dependency_cache_key=get_hash(requirements_file), user_requested_flow_compute_config=mock.ANY, @@ -1119,6 +1127,7 @@ def test_call_with_work_app_and_attached_mount_and_drive(self, lightningapps, mo local_source=True, app_entrypoint_file="entrypoint.py", enable_app_server=True, + is_headless=False, flow_servers=[], dependency_cache_key=get_hash(requirements_file), image_spec=Gridv1ImageSpec( From df7a7fcce70058fd490180e2572aaacb87da181d Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Fri, 2 Dec 2022 16:38:50 +0000 Subject: [PATCH 08/27] Comments --- src/lightning_app/utilities/layout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lightning_app/utilities/layout.py b/src/lightning_app/utilities/layout.py index a66bcbf167244..15079fcb6964b 100644 --- a/src/lightning_app/utilities/layout.py +++ b/src/lightning_app/utilities/layout.py @@ -41,7 +41,7 @@ def _collect_layout(app: "lightning_app.LightningApp", flow: "lightning_app.Ligh # When running in the cloud, the frontend code will construct the URL based on the flow name return flow._layout elif isinstance(layout, _MagicMockJsonSerializable): - # The import was mocked, we assume it is a `Frontend` + # The import was mocked, we set a dummy `Frontend` so that `is_headless` knows there is a UI app.frontends.setdefault(flow.name, "mock") return flow._layout elif isinstance(layout, dict): @@ -109,7 +109,7 @@ def _collect_content_layout(layout: List[Dict], flow: "lightning_app.LightningFl entry["content"] = "" entry["target"] = "" elif isinstance(entry["content"], _MagicMockJsonSerializable): - # The import was mocked, so we just record dummy content + # The import was mocked, we just record dummy content so that `is_headless` knows there is a UI entry["content"] = "mock" entry["target"] = "mock" else: From 6e526074e73151c949ef4ccc3b9d8baee7f6db4c Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Fri, 2 Dec 2022 16:48:45 +0000 Subject: [PATCH 09/27] Fix --- src/lightning_app/cli/lightning_cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lightning_app/cli/lightning_cli.py b/src/lightning_app/cli/lightning_cli.py index bdea388d9b3b2..58541670d7df8 100644 --- a/src/lightning_app/cli/lightning_cli.py +++ b/src/lightning_app/cli/lightning_cli.py @@ -271,6 +271,7 @@ def _run_app( start_server=not without_server, no_cache=no_cache, blocking=blocking, + open_ui=open_ui, name=name, env_vars=env_vars, secrets=secrets, From 04efa225925f65bdf7610437d8e92d7c66a0f6c3 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Fri, 2 Dec 2022 16:51:36 +0000 Subject: [PATCH 10/27] Updates --- src/lightning_app/core/app.py | 1 + src/lightning_app/runners/cloud.py | 4 ++-- src/lightning_app/runners/multiprocess.py | 4 ++-- src/lightning_app/runners/singleprocess.py | 3 +-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lightning_app/core/app.py b/src/lightning_app/core/app.py index e52e97e1e601e..30355651b7e97 100644 --- a/src/lightning_app/core/app.py +++ b/src/lightning_app/core/app.py @@ -152,6 +152,7 @@ def __init__( self.checkpointing: bool = False self._update_layout() + self._update_is_headless() self._original_state = None self._last_state = self.state diff --git a/src/lightning_app/runners/cloud.py b/src/lightning_app/runners/cloud.py index 9280a6ecd30ef..6fd10e18e0146 100644 --- a/src/lightning_app/runners/cloud.py +++ b/src/lightning_app/runners/cloud.py @@ -63,7 +63,7 @@ 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 _is_headless, Logger +from lightning_app.utilities.app_helpers import 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 @@ -407,7 +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), + is_headless=self.app.is_headless, ) # create / upload the new app release diff --git a/src/lightning_app/runners/multiprocess.py b/src/lightning_app/runners/multiprocess.py index b2440509ec658..372374b07262c 100644 --- a/src/lightning_app/runners/multiprocess.py +++ b/src/lightning_app/runners/multiprocess.py @@ -11,7 +11,7 @@ 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_headless, is_overridden +from lightning_app.utilities.app_helpers import 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 @@ -103,7 +103,7 @@ def dispatch(self, *args: Any, open_ui: bool = True, **kwargs: Any): # wait for server to be ready has_started_queue.get() - if open_ui and not _is_headless(self.app): + if open_ui and not self.app.is_headless: click.launch(self._get_app_url()) self.app._has_launched_browser = True diff --git a/src/lightning_app/runners/singleprocess.py b/src/lightning_app/runners/singleprocess.py index 51a293abd09d1..4fa1f30b880c4 100644 --- a/src/lightning_app/runners/singleprocess.py +++ b/src/lightning_app/runners/singleprocess.py @@ -7,7 +7,6 @@ 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 @@ -46,7 +45,7 @@ def dispatch(self, *args, open_ui: bool = True, **kwargs: Any): # wait for server to be ready. has_started_queue.get() - if open_ui and not _is_headless(self.app): + if open_ui and not self.app.is_headless: click.launch(self._get_app_url()) self.app._has_launched_browser = True From 3e5754199f48ef7203475bc1aec2a656b05c17f4 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Fri, 2 Dec 2022 16:58:00 +0000 Subject: [PATCH 11/27] Fixes and cleanup --- src/lightning_app/core/app.py | 12 +++--------- src/lightning_app/runners/runtime.py | 8 ++++---- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/lightning_app/core/app.py b/src/lightning_app/core/app.py index 30355651b7e97..32d817b2baf21 100644 --- a/src/lightning_app/core/app.py +++ b/src/lightning_app/core/app.py @@ -142,9 +142,6 @@ def __init__( self.exception = None self.collect_changes: bool = True - self.is_headless: Optional[bool] = None - self._has_launched_browser = False - # NOTE: Checkpointing is disabled by default for the time being. We # will enable it when resuming from full checkpoint is supported. Also, # we will need to revisit the logic at _should_snapshot, since right now @@ -152,7 +149,9 @@ def __init__( self.checkpointing: bool = False self._update_layout() - self._update_is_headless() + + self.is_headless: bool = _is_headless(self) + self._has_launched_browser = False self._original_state = None self._last_state = self.state @@ -520,11 +519,6 @@ def _update_layout(self) -> None: def _update_is_headless(self) -> None: is_headless = _is_headless(self) - # If `is_headless` hasn't been set before, set it and return. - if self.is_headless is None: - self.is_headless = is_headless - return - # If `is_headless` changed, handle it. # This ensures support for apps which dynamically add a UI at runtime. if self.is_headless != is_headless: diff --git a/src/lightning_app/runners/runtime.py b/src/lightning_app/runners/runtime.py index b892e27f8ae93..ab552f136fde0 100644 --- a/src/lightning_app/runners/runtime.py +++ b/src/lightning_app/runners/runtime.py @@ -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 @@ -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, @@ -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 @@ -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 From 3e8f5254d6e69dc4a6f379add88f0d502e3a367a Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Fri, 2 Dec 2022 17:46:46 +0000 Subject: [PATCH 12/27] Fix tests --- tests/tests_app/cli/test_run_app.py | 4 ++-- tests/tests_app/runners/test_cloud.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/tests_app/cli/test_run_app.py b/tests/tests_app/cli/test_run_app.py index f4d90c9ed58fc..184e7311dd6b4 100644 --- a/tests/tests_app/cli/test_run_app.py +++ b/tests/tests_app/cli/test_run_app.py @@ -105,7 +105,7 @@ def test_lightning_run_app_cloud(mock_dispatch: mock.MagicMock, open_ui, caplog, RuntimeType.CLOUD, start_server=True, blocking=False, - on_before_run=mock.ANY, + open_ui=open_ui, name="", no_cache=True, env_vars={"FOO": "bar"}, @@ -148,7 +148,7 @@ def test_lightning_run_app_cloud_with_run_app_commands(mock_dispatch: mock.Magic RuntimeType.CLOUD, start_server=True, blocking=False, - on_before_run=mock.ANY, + open_ui=open_ui, name="", no_cache=True, env_vars={"FOO": "bar"}, diff --git a/tests/tests_app/runners/test_cloud.py b/tests/tests_app/runners/test_cloud.py index 28c2ca2f3c710..e89e1e8aa468d 100644 --- a/tests/tests_app/runners/test_cloud.py +++ b/tests/tests_app/runners/test_cloud.py @@ -286,6 +286,7 @@ def test_run_on_byoc_cluster(self, monkeypatch): monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock()) monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock()) app = mock.MagicMock() + app.is_headless = False app.flows = [] app.frontend = {} cloud_runtime = cloud.CloudRuntime(app=app, entrypoint_file="entrypoint.py") @@ -335,6 +336,7 @@ def test_requirements_file(self, monkeypatch): monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock()) monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock()) app = mock.MagicMock() + app.is_headless = False app.flows = [] app.frontend = {} cloud_runtime = cloud.CloudRuntime(app=app, entrypoint_file="entrypoint.py") @@ -459,6 +461,7 @@ def test_call_with_work_app(self, lightningapps, start_with_flow, monkeypatch, t monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock()) monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock()) app = mock.MagicMock() + app.is_headless = False work = MyWork(start_with_flow=start_with_flow, cloud_compute=CloudCompute("custom")) work._name = "test-work" @@ -631,6 +634,7 @@ def test_call_with_work_app_and_attached_drives(self, lightningapps, monkeypatch monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock()) monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock()) app = mock.MagicMock() + app.is_headless = False mocked_drive = MagicMock(spec=Drive) setattr(mocked_drive, "id", "foobar") @@ -766,6 +770,7 @@ def test_call_with_work_app_and_app_comment_command_execution_set(self, lightnin monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock()) monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock()) app = mock.MagicMock() + app.is_headless = False work = MyWork(cloud_compute=CloudCompute("custom")) work._state = {"_port"} @@ -882,6 +887,7 @@ def test_call_with_work_app_and_multiple_attached_drives(self, lightningapps, mo monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock()) monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock()) app = mock.MagicMock() + app.is_headless = False mocked_lit_drive = MagicMock(spec=Drive) setattr(mocked_lit_drive, "id", "foobar") @@ -1085,6 +1091,7 @@ def test_call_with_work_app_and_attached_mount_and_drive(self, lightningapps, mo monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock()) monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock()) app = mock.MagicMock() + app.is_headless = False mocked_drive = MagicMock(spec=Drive) setattr(mocked_drive, "id", "foobar") From a95af08e6fe34babfb3bb030877489052e3262e6 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Fri, 2 Dec 2022 18:00:32 +0000 Subject: [PATCH 13/27] Dont open view page for headless apps --- src/lightning_app/testing/testing.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/lightning_app/testing/testing.py b/src/lightning_app/testing/testing.py index 2ce426ecf109e..08fc0f5e14f52 100644 --- a/src/lightning_app/testing/testing.py +++ b/src/lightning_app/testing/testing.py @@ -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 = [ From 1b137fe39c887ec6e3f5926fc2eb6d9ab40cfd64 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Fri, 2 Dec 2022 19:56:08 +0000 Subject: [PATCH 14/27] Fix test, resolve URL the right way --- .../public/test_commands_and_api.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/tests_examples_app/public/test_commands_and_api.py b/tests/tests_examples_app/public/test_commands_and_api.py index 3105624432b78..a6a015d02a84a 100644 --- a/tests/tests_examples_app/public/test_commands_and_api.py +++ b/tests/tests_examples_app/public/test_commands_and_api.py @@ -7,6 +7,8 @@ from tests_examples_app.public import _PATH_EXAMPLES from lightning_app.testing.testing import run_app_in_cloud +from lightning_app.utilities.cloud import _get_project +from lightning_app.utilities.network import LightningClient @pytest.mark.timeout(300) @@ -14,7 +16,7 @@ def test_commands_and_api_example_cloud() -> None: with run_app_in_cloud(os.path.join(_PATH_EXAMPLES, "app_commands_and_api")) as ( admin_page, - view_page, + _, fetch_logs, _, ): @@ -34,7 +36,19 @@ def test_commands_and_api_example_cloud() -> None: sleep(5) # 5: Send a request to the Rest API directly. - base_url = view_page.url.replace("/view", "").replace("/child_flow", "") + client = LightningClient() + project = _get_project(client) + + lit_apps = [ + app + for app in client.lightningapp_instance_service_list_lightningapp_instances( + project_id=project.project_id + ).lightningapps + if app.id == app_id + ] + app = lit_apps[0] + + base_url = app.status.url resp = requests.post(base_url + "/user/command_without_client?name=awesome") assert resp.status_code == 200, resp.json() From a1ee356c1230785942511b272e984eeb62233257 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Mon, 5 Dec 2022 11:40:23 +0000 Subject: [PATCH 15/27] Remove launch --- src/lightning_app/core/app.py | 1 - src/lightning_app/runners/multiprocess.py | 1 - src/lightning_app/runners/singleprocess.py | 1 - src/lightning_app/utilities/app_helpers.py | 9 --------- 4 files changed, 12 deletions(-) diff --git a/src/lightning_app/core/app.py b/src/lightning_app/core/app.py index 32d817b2baf21..a0735e768ad80 100644 --- a/src/lightning_app/core/app.py +++ b/src/lightning_app/core/app.py @@ -151,7 +151,6 @@ def __init__( self._update_layout() self.is_headless: bool = _is_headless(self) - self._has_launched_browser = False self._original_state = None self._last_state = self.state diff --git a/src/lightning_app/runners/multiprocess.py b/src/lightning_app/runners/multiprocess.py index 372374b07262c..38c8b7187ef90 100644 --- a/src/lightning_app/runners/multiprocess.py +++ b/src/lightning_app/runners/multiprocess.py @@ -105,7 +105,6 @@ def dispatch(self, *args: Any, open_ui: bool = True, **kwargs: Any): if open_ui and not self.app.is_headless: click.launch(self._get_app_url()) - self.app._has_launched_browser = True # Connect the runtime to the application. self.app.connect(self) diff --git a/src/lightning_app/runners/singleprocess.py b/src/lightning_app/runners/singleprocess.py index 4fa1f30b880c4..74a0bec8f32ef 100644 --- a/src/lightning_app/runners/singleprocess.py +++ b/src/lightning_app/runners/singleprocess.py @@ -47,7 +47,6 @@ def dispatch(self, *args, open_ui: bool = True, **kwargs: Any): if open_ui and not self.app.is_headless: click.launch(self._get_app_url()) - self.app._has_launched_browser = True try: self.app._run() diff --git a/src/lightning_app/utilities/app_helpers.py b/src/lightning_app/utilities/app_helpers.py index b212175ab1b33..adb9d3f488c9b 100644 --- a/src/lightning_app/utilities/app_helpers.py +++ b/src/lightning_app/utilities/app_helpers.py @@ -572,12 +572,3 @@ def _handle_is_headless(app: "LightningApp"): id=current_lightningapp_instance.id, body=AppinstancesIdBody(name=current_lightningapp_instance.name, spec=current_lightningapp_instance.spec), ) - elif not app.is_headless and not app._has_launched_browser: - # If running locally, the app now has a UI, and we haven't opened the browser yet, open the browser. - # TODO: This could cause browsers to be opened at strange times - import click - - from lightning_app.runners.multiprocess import MultiProcessRuntime - - click.launch(MultiProcessRuntime._get_app_url()) - app._has_launched_browser = True From 951639574885f883096fdf4e70205febd6fb0abf Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Mon, 5 Dec 2022 11:41:51 +0000 Subject: [PATCH 16/27] Clean --- src/lightning_app/runners/cloud.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lightning_app/runners/cloud.py b/src/lightning_app/runners/cloud.py index 6fd10e18e0146..9ea2f88410084 100644 --- a/src/lightning_app/runners/cloud.py +++ b/src/lightning_app/runners/cloud.py @@ -472,7 +472,6 @@ def dispatch( if open_ui: click.launch(self._get_app_url(lightning_app_instance, not has_sufficient_credits)) - self.app._has_launched_browser = True if cleanup_handle: cleanup_handle() From bac282b0d8bf5b4bf88c81b0996e4e3cbf0304a6 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Mon, 5 Dec 2022 11:48:29 +0000 Subject: [PATCH 17/27] Cleanup tests --- tests/tests_app/runners/test_multiprocess.py | 12 ++++++++---- tests/tests_app/runners/test_singleprocess.py | 13 +++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/tests/tests_app/runners/test_multiprocess.py b/tests/tests_app/runners/test_multiprocess.py index 4ae13ebe64106..121b5f6d7d92a 100644 --- a/tests/tests_app/runners/test_multiprocess.py +++ b/tests/tests_app/runners/test_multiprocess.py @@ -1,3 +1,4 @@ +import os from unittest import mock from unittest.mock import Mock @@ -86,11 +87,14 @@ def test_multiprocess_runtime_sets_context(): @pytest.mark.parametrize( - "expected_url", + "env,expected_url", [ - "http://127.0.0.1:7501/view", - "http://127.0.0.1:7501/view", + ({}, "http://127.0.0.1:7501/view"), + ({"APP_SERVER_HOST": "http://test"}, "http://test"), ], ) -def test_get_app_url(expected_url): +def test_get_app_url(env, expected_url): + old_env = os.environ.copy() + os.environ.update(**env) assert MultiProcessRuntime._get_app_url() == expected_url + os.environ = old_env diff --git a/tests/tests_app/runners/test_singleprocess.py b/tests/tests_app/runners/test_singleprocess.py index 60005c88e2e08..8bb7654685df6 100644 --- a/tests/tests_app/runners/test_singleprocess.py +++ b/tests/tests_app/runners/test_singleprocess.py @@ -1,3 +1,5 @@ +import os + import pytest from lightning_app import LightningFlow @@ -21,11 +23,14 @@ def test_single_process_runtime(tmpdir): @pytest.mark.parametrize( - "expected_url", + "env,expected_url", [ - "http://127.0.0.1:7501/view", - "http://127.0.0.1:7501/view", + ({}, "http://127.0.0.1:7501/view"), + ({"APP_SERVER_HOST": "http://test"}, "http://test"), ], ) -def test_get_app_url(expected_url): +def test_get_app_url(env, expected_url): + old_env = os.environ.copy() + os.environ.update(**env) assert SingleProcessRuntime._get_app_url() == expected_url + os.environ = old_env From fe86b327ea9e4e9c07dfeb27f4e23e697659a220 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Mon, 5 Dec 2022 12:20:09 +0000 Subject: [PATCH 18/27] Fixes --- tests/tests_app/runners/test_multiprocess.py | 6 ++---- tests/tests_app/runners/test_singleprocess.py | 7 +++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/tests_app/runners/test_multiprocess.py b/tests/tests_app/runners/test_multiprocess.py index 121b5f6d7d92a..2e1a34ab38677 100644 --- a/tests/tests_app/runners/test_multiprocess.py +++ b/tests/tests_app/runners/test_multiprocess.py @@ -94,7 +94,5 @@ def test_multiprocess_runtime_sets_context(): ], ) def test_get_app_url(env, expected_url): - old_env = os.environ.copy() - os.environ.update(**env) - assert MultiProcessRuntime._get_app_url() == expected_url - os.environ = old_env + with mock.patch.dict(os.environ, env): + assert MultiProcessRuntime._get_app_url() == expected_url diff --git a/tests/tests_app/runners/test_singleprocess.py b/tests/tests_app/runners/test_singleprocess.py index 8bb7654685df6..998f23e66296f 100644 --- a/tests/tests_app/runners/test_singleprocess.py +++ b/tests/tests_app/runners/test_singleprocess.py @@ -1,4 +1,5 @@ import os +from unittest import mock import pytest @@ -30,7 +31,5 @@ def test_single_process_runtime(tmpdir): ], ) def test_get_app_url(env, expected_url): - old_env = os.environ.copy() - os.environ.update(**env) - assert SingleProcessRuntime._get_app_url() == expected_url - os.environ = old_env + with mock.patch.dict(os.environ, env): + assert SingleProcessRuntime._get_app_url() == expected_url From e636bb114318277c21da89e1fe2a10b71a56efd2 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Mon, 5 Dec 2022 12:22:33 +0000 Subject: [PATCH 19/27] Updates --- src/lightning_app/core/app.py | 2 +- src/lightning_app/runners/cloud.py | 4 ++-- src/lightning_app/runners/multiprocess.py | 4 ++-- src/lightning_app/runners/singleprocess.py | 3 ++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/lightning_app/core/app.py b/src/lightning_app/core/app.py index a0735e768ad80..377a6a0d9e220 100644 --- a/src/lightning_app/core/app.py +++ b/src/lightning_app/core/app.py @@ -150,7 +150,7 @@ def __init__( self._update_layout() - self.is_headless: bool = _is_headless(self) + self.is_headless: Optional[bool] = None self._original_state = None self._last_state = self.state diff --git a/src/lightning_app/runners/cloud.py b/src/lightning_app/runners/cloud.py index 9ea2f88410084..6ef7770124aae 100644 --- a/src/lightning_app/runners/cloud.py +++ b/src/lightning_app/runners/cloud.py @@ -63,7 +63,7 @@ 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 @@ -407,7 +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=self.app.is_headless, + is_headless=_is_headless(self.app), ) # create / upload the new app release diff --git a/src/lightning_app/runners/multiprocess.py b/src/lightning_app/runners/multiprocess.py index 38c8b7187ef90..673e8601043d7 100644 --- a/src/lightning_app/runners/multiprocess.py +++ b/src/lightning_app/runners/multiprocess.py @@ -11,7 +11,7 @@ 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 @@ -103,7 +103,7 @@ def dispatch(self, *args: Any, open_ui: bool = True, **kwargs: Any): # wait for server to be ready has_started_queue.get() - if open_ui and not self.app.is_headless: + if open_ui and not _is_headless(self.app): click.launch(self._get_app_url()) # Connect the runtime to the application. diff --git a/src/lightning_app/runners/singleprocess.py b/src/lightning_app/runners/singleprocess.py index 74a0bec8f32ef..61a67ce9ba904 100644 --- a/src/lightning_app/runners/singleprocess.py +++ b/src/lightning_app/runners/singleprocess.py @@ -7,6 +7,7 @@ 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 @@ -45,7 +46,7 @@ def dispatch(self, *args, open_ui: bool = True, **kwargs: Any): # wait for server to be ready. has_started_queue.get() - if open_ui and not self.app.is_headless: + if open_ui and not _is_headless(self.app): click.launch(self._get_app_url()) try: From 67ef5b5d1f9e15a63a96f7aeab59a7f990f43a8f Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Mon, 5 Dec 2022 12:59:47 +0000 Subject: [PATCH 20/27] Add test --- tests/tests_app/utilities/test_app_helpers.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/tests_app/utilities/test_app_helpers.py b/tests/tests_app/utilities/test_app_helpers.py index 9d66dc886ae08..791d2011f7651 100644 --- a/tests/tests_app/utilities/test_app_helpers.py +++ b/tests/tests_app/utilities/test_app_helpers.py @@ -1,9 +1,17 @@ +import os from unittest import mock import pytest +from lightning_cloud.openapi import ( + AppinstancesIdBody, + V1LightningappInstanceSpec, + V1LightningappInstanceState, + V1ListLightningappInstancesResponse, +) from lightning_app import LightningApp, LightningFlow, LightningWork from lightning_app.utilities.app_helpers import ( + _handle_is_headless, _is_headless, _MagicMockJsonSerializable, AppStatePlugin, @@ -161,3 +169,32 @@ def test_is_headless(flow, expected): flow = flow() app = LightningApp(flow) assert _is_headless(app) == expected + + +@mock.patch("lightning_app.utilities.network.LightningClient") +def test_handle_is_headless(mock_client): + project_id = "test_project_id" + app_id = "test_app_id" + app_name = "test_app_name" + + lightningapps = [mock.MagicMock()] + lightningapps[0].id = app_id + lightningapps[0].name = app_name + lightningapps[0].status.phase = V1LightningappInstanceState.RUNNING + lightningapps[0].spec = V1LightningappInstanceSpec(app_id=app_id) + + mock_client().lightningapp_instance_service_list_lightningapp_instances.return_value = ( + V1ListLightningappInstancesResponse(lightningapps=lightningapps) + ) + + app = mock.MagicMock() + app.is_headless = True + + with mock.patch.dict(os.environ, {"LIGHTNING_CLOUD_APP_ID": app_id, "LIGHTNING_CLOUD_PROJECT_ID": project_id}): + _handle_is_headless(app) + + mock_client().lightningapp_instance_service_update_lightningapp_instance.assert_called_once_with( + project_id=project_id, + id=app_id, + body=AppinstancesIdBody(name="test_app_name", spec=V1LightningappInstanceSpec(app_id=app_id, is_headless=True)), + ) From 26e0735eaca5bc911b34590f7a49dc88fb6d0bc0 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Mon, 5 Dec 2022 13:20:41 +0000 Subject: [PATCH 21/27] Increase app cloud tests timeout --- .azure/app-cloud-e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/app-cloud-e2e.yml b/.azure/app-cloud-e2e.yml index 971210d840fe0..821ffc8c426f1 100644 --- a/.azure/app-cloud-e2e.yml +++ b/.azure/app-cloud-e2e.yml @@ -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: From 02ced7fe14bc5f1f2e69b01e74a0f8ebd61e861d Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Mon, 5 Dec 2022 14:02:27 +0000 Subject: [PATCH 22/27] Increase timeout --- .azure/app-cloud-e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/app-cloud-e2e.yml b/.azure/app-cloud-e2e.yml index 821ffc8c426f1..add8bca9e936c 100644 --- a/.azure/app-cloud-e2e.yml +++ b/.azure/app-cloud-e2e.yml @@ -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' From 4152c021aa674fb6be9290292a2194087aa4fb72 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Mon, 5 Dec 2022 16:49:36 +0000 Subject: [PATCH 23/27] Wait for running --- src/lightning_app/testing/testing.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/lightning_app/testing/testing.py b/src/lightning_app/testing/testing.py index 08fc0f5e14f52..3e09012ac431c 100644 --- a/src/lightning_app/testing/testing.py +++ b/src/lightning_app/testing/testing.py @@ -14,6 +14,7 @@ from typing import Any, Callable, Dict, Generator, List, Optional, Type import requests +from lightning_cloud.openapi import V1LightningappInstanceState from lightning_cloud.openapi.rest import ApiException from requests import Session from rich import print @@ -407,6 +408,22 @@ def run_app_in_cloud( else: view_page = None + # Wait until the app is running + while True: + sleep(1) + + lit_apps = [ + app + for app in client.lightningapp_instance_service_list_lightningapp_instances( + project_id=project.project_id + ).lightningapps + if app.name == name + ] + app = lit_apps[0] + + if app.status.phase == V1LightningappInstanceState.RUNNING: + break + # TODO: is re-creating this redundant? lit_apps = [ app From 987e95ec9802bb2d9d1daded6485c846880b51cc Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Mon, 5 Dec 2022 17:16:03 +0000 Subject: [PATCH 24/27] Revert timeouts --- .azure/app-cloud-e2e.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.azure/app-cloud-e2e.yml b/.azure/app-cloud-e2e.yml index add8bca9e936c..971210d840fe0 100644 --- a/.azure/app-cloud-e2e.yml +++ b/.azure/app-cloud-e2e.yml @@ -93,7 +93,7 @@ jobs: 'App: custom_work_dependencies': name: "custom_work_dependencies" dir: "local" - timeoutInMinutes: "15" + timeoutInMinutes: "10" cancelTimeoutInMinutes: "1" # values: https://docs.microsoft.com/en-us/azure/devops/pipelines/process/phases?view=azure-devops&tabs=yaml#workspace workspace: @@ -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=660 --capture=no -v --color=yes + --timeout=540 --capture=no -v --color=yes env: HEADLESS: '1' PACKAGE_LIGHTNING: '1' From 11d16f4a8f2311699388df5cec7dc39ba36880a0 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Mon, 5 Dec 2022 17:17:17 +0000 Subject: [PATCH 25/27] Clean --- src/lightning_app/utilities/app_helpers.py | 40 ++++++++++++---------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/lightning_app/utilities/app_helpers.py b/src/lightning_app/utilities/app_helpers.py index adb9d3f488c9b..1d4825700aed3 100644 --- a/src/lightning_app/utilities/app_helpers.py +++ b/src/lightning_app/utilities/app_helpers.py @@ -548,27 +548,29 @@ def _handle_is_headless(app: "LightningApp"): app_id = os.getenv("LIGHTNING_CLOUD_APP_ID", None) project_id = os.getenv("LIGHTNING_CLOUD_PROJECT_ID", None) - if app_id and project_id: - from lightning_app.utilities.network import LightningClient + if app_id is None or project_id is None: + return - client = LightningClient() - list_apps_response = client.lightningapp_instance_service_list_lightningapp_instances(project_id=project_id) + from lightning_app.utilities.network import LightningClient - current_lightningapp_instance: Optional[Externalv1LightningappInstance] = None - for lightningapp_instance in list_apps_response.lightningapps: - if lightningapp_instance.id == app_id: - current_lightningapp_instance = lightningapp_instance - break - - if not current_lightningapp_instance: - raise RuntimeError( - "App was not found. Please open an issue at https://github.com/lightning-AI/lightning/issues." - ) + client = LightningClient() + list_apps_response = client.lightningapp_instance_service_list_lightningapp_instances(project_id=project_id) - current_lightningapp_instance.spec.is_headless = app.is_headless + current_lightningapp_instance: Optional[Externalv1LightningappInstance] = None + for lightningapp_instance in list_apps_response.lightningapps: + if lightningapp_instance.id == app_id: + current_lightningapp_instance = lightningapp_instance + break - client.lightningapp_instance_service_update_lightningapp_instance( - project_id=project_id, - id=current_lightningapp_instance.id, - body=AppinstancesIdBody(name=current_lightningapp_instance.name, spec=current_lightningapp_instance.spec), + if not current_lightningapp_instance: + raise RuntimeError( + "App was not found. Please open an issue at https://github.com/lightning-AI/lightning/issues." ) + + current_lightningapp_instance.spec.is_headless = app.is_headless + + client.lightningapp_instance_service_update_lightningapp_instance( + project_id=project_id, + id=current_lightningapp_instance.id, + body=AppinstancesIdBody(name=current_lightningapp_instance.name, spec=current_lightningapp_instance.spec), + ) From 175b53f156c17c5a3b27ddaba5267adf75eb7d67 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Mon, 5 Dec 2022 21:22:27 +0000 Subject: [PATCH 26/27] Dont update if it hasnt changed --- src/lightning_app/utilities/app_helpers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lightning_app/utilities/app_helpers.py b/src/lightning_app/utilities/app_helpers.py index 1d4825700aed3..a000af3e71fe6 100644 --- a/src/lightning_app/utilities/app_helpers.py +++ b/src/lightning_app/utilities/app_helpers.py @@ -567,6 +567,9 @@ def _handle_is_headless(app: "LightningApp"): "App was not found. Please open an issue at https://github.com/lightning-AI/lightning/issues." ) + if current_lightningapp_instance.spec.is_headless == app.is_headless: + return + current_lightningapp_instance.spec.is_headless = app.is_headless client.lightningapp_instance_service_update_lightningapp_instance( From 24b810d1068fe2b96b7a86235f4bb94a8c449309 Mon Sep 17 00:00:00 2001 From: Ethan Harris Date: Mon, 5 Dec 2022 21:40:43 +0000 Subject: [PATCH 27/27] Increase timeout --- .azure/app-cloud-e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/app-cloud-e2e.yml b/.azure/app-cloud-e2e.yml index 971210d840fe0..821ffc8c426f1 100644 --- a/.azure/app-cloud-e2e.yml +++ b/.azure/app-cloud-e2e.yml @@ -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: