plugin app manager: start doesn't wait anymore for actual app start:
Application may be long to start (e.g. a Docker app may have to download images first, and even without the downloading, the starting could be long), which may lead to UI blocking or bridge time out. To prevent that, `start` is now returning immediately, and 2 new signals are used to indicate when the application is started, of if something wrong happened. `start` now returns initial app data, including exposed data without the computed exposed data. The computed data must be retrieved after the app has been started.
This commit is contained in:
parent
ab3f76a559
commit
6ad9431678
2 changed files with 101 additions and 41 deletions
|
|
@ -57,7 +57,7 @@ class AppManagerDocker:
|
|||
self._am.register(self)
|
||||
|
||||
async def start(self, app_data: dict) -> None:
|
||||
await self._am.startCommon(app_data)
|
||||
await self._am.start_common(app_data)
|
||||
working_dir = app_data['_instance_dir_path']
|
||||
try:
|
||||
override = app_data['override']
|
||||
|
|
@ -83,7 +83,7 @@ class AppManagerDocker:
|
|||
path=str(working_dir),
|
||||
)
|
||||
|
||||
async def computeExpose(self, app_data: dict) -> dict:
|
||||
async def compute_expose(self, app_data: dict) -> dict:
|
||||
working_dir = app_data['_instance_dir_path']
|
||||
expose = app_data['expose']
|
||||
ports = expose.get('ports', {})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# SàT plugin to manage external applications
|
||||
# Libervia plugin to manage external applications
|
||||
# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Callable
|
||||
from functools import partial, reduce
|
||||
import tempfile
|
||||
import secrets
|
||||
|
|
@ -95,7 +95,7 @@ class AppManager:
|
|||
"applicationStart",
|
||||
".plugin",
|
||||
in_sign="ss",
|
||||
out_sign="",
|
||||
out_sign="s",
|
||||
method=self._start,
|
||||
async_=True,
|
||||
)
|
||||
|
|
@ -112,9 +112,19 @@ class AppManager:
|
|||
".plugin",
|
||||
in_sign="sss",
|
||||
out_sign="s",
|
||||
method=self._getExposed,
|
||||
method=self._get_exposed,
|
||||
async_=True,
|
||||
)
|
||||
# application has been started succeesfully,
|
||||
# args: name, instance_id, extra
|
||||
host.bridge.addSignal(
|
||||
"application_started", ".plugin", signature="sss"
|
||||
)
|
||||
# application went wrong with the application
|
||||
# args: name, instance_id, extra
|
||||
host.bridge.addSignal(
|
||||
"application_error", ".plugin", signature="sss"
|
||||
)
|
||||
yaml.add_constructor(
|
||||
"!sat_conf", self._sat_conf_constr, Loader=Loader)
|
||||
yaml.add_constructor(
|
||||
|
|
@ -133,7 +143,7 @@ class AppManager:
|
|||
data['_instance_dir_obj'].cleanup()
|
||||
|
||||
def _sat_conf_constr(self, loader, node):
|
||||
"""Get a value from SàT configuration
|
||||
"""Get a value from Libervia configuration
|
||||
|
||||
A list is expected with either "name" of a config parameter, a one or more of
|
||||
those parameters:
|
||||
|
|
@ -208,7 +218,7 @@ class AppManager:
|
|||
if hasattr(manager, "discover_path"):
|
||||
self.discover(manager.discover_path, manager)
|
||||
|
||||
def getManager(self, app_data: dict) -> object:
|
||||
def get_manager(self, app_data: dict) -> object:
|
||||
"""Get manager instance needed for this app
|
||||
|
||||
@raise exceptions.DataError: something is wrong with the type
|
||||
|
|
@ -231,7 +241,7 @@ class AppManager:
|
|||
raise exceptions.NotFound(
|
||||
f"No manager found to manage app of type {app_type!r}")
|
||||
|
||||
def getAppData(
|
||||
def get_app_data(
|
||||
self,
|
||||
id_type: Optional[str],
|
||||
identifier: str
|
||||
|
|
@ -277,7 +287,7 @@ class AppManager:
|
|||
if manager is None:
|
||||
try:
|
||||
app_data = self.parse(file_path)
|
||||
manager = self.getManager(app_data)
|
||||
manager = self.get_manager(app_data)
|
||||
except (exceptions.DataError, exceptions.NotFound) as e:
|
||||
log.warning(
|
||||
f"Can't parse {file_path}, skipping: {e}")
|
||||
|
|
@ -294,7 +304,7 @@ class AppManager:
|
|||
)
|
||||
|
||||
def parse(self, file_path: Path, params: Optional[dict] = None) -> dict:
|
||||
"""Parse SàT application file
|
||||
"""Parse Libervia application file
|
||||
|
||||
@param params: parameters for running this instance
|
||||
@raise exceptions.DataError: something is wrong in the file
|
||||
|
|
@ -341,13 +351,30 @@ class AppManager:
|
|||
|
||||
def _start(self, app_name, extra):
|
||||
extra = data_format.deserialise(extra)
|
||||
return defer.ensureDeferred(self.start(str(app_name), extra))
|
||||
d = defer.ensureDeferred(self.start(str(app_name), extra))
|
||||
d.addCallback(data_format.serialise)
|
||||
return d
|
||||
|
||||
async def start(
|
||||
self,
|
||||
app_name: str,
|
||||
extra: Optional[dict] = None,
|
||||
) -> None:
|
||||
) -> dict:
|
||||
"""Start an application
|
||||
|
||||
@param app_name: name of the application to start
|
||||
@param extra: extra parameters
|
||||
@return: data with following keys:
|
||||
- name (str): canonical application name
|
||||
- instance (str): instance ID
|
||||
- started (bool): True if the application is already started
|
||||
if False, the "application_started" signal should be used to get notificed
|
||||
when the application is actually started
|
||||
- expose (dict): exposed data as given by [self.get_exposed]
|
||||
exposed data which need to be computed are NOT returned, they will
|
||||
available when the app will be fully started, throught the
|
||||
[self.get_exposed] method.
|
||||
"""
|
||||
# FIXME: for now we use the first app manager available for the requested app_name
|
||||
# TODO: implement running multiple instance of the same app if some metadata
|
||||
# to be defined in app_data allows explicitly it.
|
||||
|
|
@ -358,15 +385,28 @@ class AppManager:
|
|||
raise exceptions.NotFound(
|
||||
f"No application found with the name {app_name!r}"
|
||||
)
|
||||
log.info(f"starting {app_name!r}")
|
||||
started_data = self._started.setdefault(app_name, [])
|
||||
app_data = self.parse(app_file_path, extra)
|
||||
app_data["_started"] = False
|
||||
app_data['_file_path'] = app_file_path
|
||||
app_data['_name_canonical'] = app_name
|
||||
single_instance = app_data['single_instance']
|
||||
ret_data = {
|
||||
"name": app_name,
|
||||
"started": False
|
||||
}
|
||||
if single_instance:
|
||||
if started_data:
|
||||
log.info(f"{app_name!r} is already started")
|
||||
return
|
||||
instance_data = started_data[0]
|
||||
instance_id = instance_data["_instance_id"]
|
||||
ret_data["instance"] = instance_id
|
||||
ret_data["started"] = instance_data["_started"]
|
||||
ret_data["expose"] = await self.get_exposed(
|
||||
instance_id, "instance", {"skip_compute": True}
|
||||
)
|
||||
log.info(f"{app_name!r} is already started or being started")
|
||||
return ret_data
|
||||
else:
|
||||
cache_path = self.host.memory.getCachePath(
|
||||
PLUGIN_INFO[C.PI_IMPORT_NAME], app_name
|
||||
|
|
@ -377,11 +417,16 @@ class AppManager:
|
|||
dest_dir_obj = tempfile.TemporaryDirectory(prefix="sat_app_")
|
||||
app_data['_instance_dir_obj'] = dest_dir_obj
|
||||
app_data['_instance_dir_path'] = Path(dest_dir_obj.name)
|
||||
instance_id = app_data['_instance_id'] = shortuuid.uuid()
|
||||
manager = self.getManager(app_data)
|
||||
instance_id = ret_data["instance"] = app_data['_instance_id'] = shortuuid.uuid()
|
||||
manager = self.get_manager(app_data)
|
||||
app_data['_manager'] = manager
|
||||
started_data.append(app_data)
|
||||
self._instances[instance_id] = app_data
|
||||
# we retrieve exposed data such as url_prefix which can be useful computed exposed
|
||||
# data must wait for the app to be started, so we skip them for now
|
||||
ret_data["expose"] = await self.get_exposed(
|
||||
instance_id, "instance", {"skip_compute": True}
|
||||
)
|
||||
|
||||
try:
|
||||
start = manager.start
|
||||
|
|
@ -390,8 +435,29 @@ class AppManager:
|
|||
f"{manager.name} doesn't have the mandatory \"start\" method"
|
||||
)
|
||||
else:
|
||||
await start(app_data)
|
||||
log.info(f"{app_name!r} started")
|
||||
defer.ensureDeferred(self.start_app(start, app_data))
|
||||
return ret_data
|
||||
|
||||
async def start_app(self, start_cb: Callable, app_data: dict) -> None:
|
||||
app_name = app_data["_name_canonical"]
|
||||
instance_id = app_data["_instance_id"]
|
||||
try:
|
||||
await start_cb(app_data)
|
||||
except Exception as e:
|
||||
log.exception(f"Can't start libervia app {app_name!r}")
|
||||
self.host.bridge.application_error(
|
||||
app_name,
|
||||
instance_id,
|
||||
data_format.serialise(
|
||||
{
|
||||
"class": str(type(e)),
|
||||
"msg": str(e)
|
||||
}
|
||||
))
|
||||
else:
|
||||
app_data["_started"] = True
|
||||
self.host.bridge.application_started(app_name, instance_id, "")
|
||||
log.info(f"{app_name!r} started")
|
||||
|
||||
def _stop(self, identifier, id_type, extra):
|
||||
extra = data_format.deserialise(extra)
|
||||
|
|
@ -407,7 +473,7 @@ class AppManager:
|
|||
if extra is None:
|
||||
extra = {}
|
||||
|
||||
app_data = self.getAppData(id_type, identifier)
|
||||
app_data = self.get_app_data(id_type, identifier)
|
||||
|
||||
log.info(f"stopping {app_data['name']!r}")
|
||||
|
||||
|
|
@ -447,21 +513,13 @@ class AppManager:
|
|||
|
||||
log.info(f"{app_name!r} stopped")
|
||||
|
||||
def _getExposed(self, identifier, id_type, extra):
|
||||
def _get_exposed(self, identifier, id_type, extra):
|
||||
extra = data_format.deserialise(extra)
|
||||
d = defer.ensureDeferred(self.getExposed(identifier, id_type, extra))
|
||||
d = defer.ensureDeferred(self.get_exposed(identifier, id_type, extra))
|
||||
d.addCallback(lambda d: data_format.serialise(d))
|
||||
return d
|
||||
|
||||
def getValueFromPath(self, app_data: dict, path: List[str]) -> any:
|
||||
"""Retrieve a value set in the data from it path
|
||||
|
||||
@param path: list of key to use in app_data to retrieve the value
|
||||
@return: found value
|
||||
@raise NotFound: the value can't be found
|
||||
"""
|
||||
|
||||
async def getExposed(
|
||||
async def get_exposed(
|
||||
self,
|
||||
identifier: str,
|
||||
id_type: Optional[str] = None,
|
||||
|
|
@ -469,10 +527,10 @@ class AppManager:
|
|||
) -> dict:
|
||||
"""Get data exposed by the application
|
||||
|
||||
The manager's "computeExpose" method will be called if it exists. It can be used
|
||||
The manager's "compute_expose" method will be called if it exists. It can be used
|
||||
to handle manager specific conventions.
|
||||
"""
|
||||
app_data = self.getAppData(id_type, identifier)
|
||||
app_data = self.get_app_data(id_type, identifier)
|
||||
if app_data.get('_exposed_computed', False):
|
||||
return app_data['expose']
|
||||
if extra is None:
|
||||
|
|
@ -500,17 +558,20 @@ class AppManager:
|
|||
f"Can't retrieve exposed value for url_prefix: {e}")
|
||||
del expose["url_prefix"]
|
||||
|
||||
if extra.get("skip_compute", False):
|
||||
return expose
|
||||
|
||||
try:
|
||||
computeExpose = app_data['_manager'].computeExpose
|
||||
compute_expose = app_data['_manager'].compute_expose
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
await computeExpose(app_data)
|
||||
await compute_expose(app_data)
|
||||
|
||||
app_data['_exposed_computed'] = True
|
||||
return expose
|
||||
|
||||
async def _doPrepare(
|
||||
async def _do_prepare(
|
||||
self,
|
||||
app_data: dict,
|
||||
) -> None:
|
||||
|
|
@ -548,7 +609,7 @@ class AppManager:
|
|||
if prepare:
|
||||
raise exceptions.InternalError('"prepare" should be empty')
|
||||
|
||||
async def _doCreateFiles(
|
||||
async def _do_create_files(
|
||||
self,
|
||||
app_data: dict,
|
||||
) -> None:
|
||||
|
|
@ -566,11 +627,10 @@ class AppManager:
|
|||
f.write(data.get("content", ""))
|
||||
log.debug(f"{path} created")
|
||||
|
||||
async def startCommon(self, app_data: dict) -> None:
|
||||
async def start_common(self, app_data: dict) -> None:
|
||||
"""Method running common action when starting a manager
|
||||
|
||||
It should be called by managers in "start" method.
|
||||
"""
|
||||
log.info(f"starting {app_data['name']!r}")
|
||||
await self._doPrepare(app_data)
|
||||
await self._doCreateFiles(app_data)
|
||||
await self._do_prepare(app_data)
|
||||
await self._do_create_files(app_data)
|
||||
|
|
|
|||
Loading…
Reference in a new issue