From eca76962e2feb4de8238be6ae1bfbd19cbca1fd4 Mon Sep 17 00:00:00 2001 From: Dominik Haentsch Date: Thu, 26 Oct 2023 11:00:05 +0200 Subject: [PATCH] feat: send viewer update errors to and re-raise in parent process --- renumics/spotlight/app.py | 75 ++++++++++++++++++------------------ renumics/spotlight/server.py | 27 +++++++++---- renumics/spotlight/viewer.py | 6 ++- 3 files changed, 62 insertions(+), 46 deletions(-) diff --git a/renumics/spotlight/app.py b/renumics/spotlight/app.py index fc6993d4..5baa724d 100644 --- a/renumics/spotlight/app.py +++ b/renumics/spotlight/app.py @@ -89,7 +89,6 @@ class SpotlightApp(FastAPI): """ # lifecycle - _startup_complete: bool _loop: asyncio.AbstractEventLoop # connection @@ -120,7 +119,6 @@ class SpotlightApp(FastAPI): def __init__(self) -> None: super().__init__() - self._startup_complete = False self.task_manager = TaskManager() self.websocket_manager = None self.config = Config() @@ -313,44 +311,45 @@ def update(self, config: AppConfig) -> None: """ Update application config. """ - if config.project_root is not None: - self.project_root = config.project_root - if config.dtypes is not None: - self._user_dtypes = config.dtypes - if config.analyze is not None: - self.analyze_columns = config.analyze - if config.custom_issues is not None: - self.custom_issues = config.custom_issues - if config.dataset is not None: - self._dataset = config.dataset - self._data_source = create_datasource(self._dataset) - if config.layout is not None: - self._layout = config.layout or layouts.default() - if config.filebrowsing_allowed is not None: - self.filebrowsing_allowed = config.filebrowsing_allowed - - if config.dtypes is not None or config.dataset is not None: - data_source = self._data_source - assert data_source is not None - self._data_store = DataStore(data_source, self._user_dtypes) - self._broadcast(RefreshMessage()) - self._update_issues() - if config.layout is not None: - if self._data_store is not None: - dataset_uid = self._data_store.uid - future = asyncio.run_coroutine_threadsafe( - self.config.remove_all(CURRENT_LAYOUT_KEY, dataset=dataset_uid), - self._loop, - ) - future.result() - self._broadcast(ResetLayoutMessage()) + try: + if config.project_root is not None: + self.project_root = config.project_root + if config.dtypes is not None: + self._user_dtypes = config.dtypes + if config.analyze is not None: + self.analyze_columns = config.analyze + if config.custom_issues is not None: + self.custom_issues = config.custom_issues + if config.dataset is not None: + self._dataset = config.dataset + self._data_source = create_datasource(self._dataset) + if config.layout is not None: + self._layout = config.layout or layouts.default() + if config.filebrowsing_allowed is not None: + self.filebrowsing_allowed = config.filebrowsing_allowed + + if config.dtypes is not None or config.dataset is not None: + data_source = self._data_source + assert data_source is not None + self._data_store = DataStore(data_source, self._user_dtypes) + self._broadcast(RefreshMessage()) + self._update_issues() + if config.layout is not None: + if self._data_store is not None: + dataset_uid = self._data_store.uid + future = asyncio.run_coroutine_threadsafe( + self.config.remove_all(CURRENT_LAYOUT_KEY, dataset=dataset_uid), + self._loop, + ) + future.result() + self._broadcast(ResetLayoutMessage()) - for plugin in load_plugins(): - plugin.update(self, config) + for plugin in load_plugins(): + plugin.update(self, config) + except Exception as e: + self._connection.send({"kind": "update_complete", "error": e}) - if not self._startup_complete: - self._startup_complete = True - self._connection.send({"kind": "startup_complete"}) + self._connection.send({"kind": "update_complete"}) def _handle_message(self, message: Any) -> None: kind = message.get("kind") diff --git a/renumics/spotlight/server.py b/renumics/spotlight/server.py index 83d688db..bf56918d 100644 --- a/renumics/spotlight/server.py +++ b/renumics/spotlight/server.py @@ -41,6 +41,8 @@ class Server: process: Optional[subprocess.Popen] _startup_event: threading.Event + _update_complete_event: threading.Event + _update_error: Optional[Exception] connection: Optional[multiprocessing.connection.Connection] _connection_message_queue: Queue @@ -77,7 +79,7 @@ def __init__(self, host: str = "127.0.0.1", port: int = 8000) -> None: ) self._startup_event = threading.Event() - self._startup_complete_event = threading.Event() + self._update_complete_event = threading.Event() self._connection_thread_online = threading.Event() self._connection_thread = threading.Thread( @@ -151,7 +153,6 @@ def start(self, config: AppConfig) -> None: command.append("--reload") # start uvicorn - self.process = subprocess.Popen( command, env=env, @@ -164,7 +165,7 @@ def start(self, config: AppConfig) -> None: ) if platform.system() != "Windows": sock.close() - self._startup_complete_event.wait(timeout=120) + self._wait_for_update() def stop(self) -> None: """ @@ -197,7 +198,6 @@ def stop(self) -> None: self._port = self._requested_port self._startup_event.clear() - self._startup_complete_event.clear() @property def running(self) -> bool: @@ -217,9 +217,21 @@ def update(self, config: AppConfig) -> None: """ Update app config """ + self._update(config) + self._wait_for_update() + + def _update(self, config: AppConfig) -> None: self._app_config = config self.send({"kind": "update", "data": config}) + def _wait_for_update(self) -> None: + self._update_complete_event.wait(timeout=120) + self._update_complete_event.clear() + err = self._update_error + self._update_error = None + if err: + raise err + def get_df(self) -> Optional[pd.DataFrame]: """ Request and return the current DafaFrame from the server process (if possible) @@ -236,9 +248,10 @@ def _handle_message(self, message: Any) -> None: if kind == "startup": self._startup_event.set() - self.update(self._app_config) - elif kind == "startup_complete": - self._startup_complete_event.set() + self._update(self._app_config) + elif kind == "update_complete": + self._update_error = message.get("error") + self._update_complete_event.set() elif kind == "frontend_connected": self.connected_frontends = message["data"] self._all_frontends_disconnected.clear() diff --git a/renumics/spotlight/viewer.py b/renumics/spotlight/viewer.py index 8f859ad1..817f31b2 100644 --- a/renumics/spotlight/viewer.py +++ b/renumics/spotlight/viewer.py @@ -176,7 +176,11 @@ def show( if self not in _VIEWERS: _VIEWERS.append(self) else: - self._server.update(config) + try: + self._server.update(config) + except Exception as e: + self.close() + raise e if not no_browser and self._server.connected_frontends == 0: self.open_browser()