From ac7a53573813b4643f9bd412b59b4f3024ec4cd1 Mon Sep 17 00:00:00 2001 From: TheBurchLog <5104941+TheBurchLog@users.noreply.github.com> Date: Wed, 30 Oct 2024 05:20:36 -0400 Subject: [PATCH 01/11] Add support for shutdown annotation --- CHANGELOG.rst | 7 +++++ brewtils/decorators.py | 33 ++++++++++++++++++++ brewtils/plugin.py | 64 +++++++++++++++++++++++++++++++++++++-- brewtils/specification.py | 12 ++++++++ 4 files changed, 114 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cb72571d..18c6bf4b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Brewtils Changelog ================== +TBD +------ +TBD + +- Added new annotation/configuration support for shutdown functions. These functions will be executed at the start + of the shutdown process. + 3.28.0 ------ 10/9/24 diff --git a/brewtils/decorators.py b/brewtils/decorators.py index d6e332ba..54e88569 100644 --- a/brewtils/decorators.py +++ b/brewtils/decorators.py @@ -447,6 +447,23 @@ def cmd1(self, **kwargs): return _wrapped +def shutdown(_wrapped=None): + """Decorator for specifying a function to run before a plugin is shutdown + + for example:: + + @shutdown + def pre_shutdown(self): + # Run pre-shutdown processing + return + + Args: + _wrapped: The function to decorate. This is handled as a positional argument and + shouldn't be explicitly set. + """ + _wrapped._shutdown = True + return _wrapped + def subscribe(_wrapped=None, topic: str = None, topics=[]): """Decorator for specifiying topic to listen to. @@ -489,6 +506,22 @@ def returnTrue(self): return _wrapped +def _parse_shutdown_functions(client): + # type: (object) -> List[Callable] + """Get a list of callable fields labeled with the shutdown annotation + + This will iterate over everything returned from dir, looking for metadata added + by the shutdown decorator. + """ + + shutdown_functions = [] + + for attr in dir(client): + if callable(attr) and getattr(attr, "_shutdown", False): + shutdown_functions.append(attr) + + return shutdown_functions + def _parse_client(client): # type: (object) -> List[Command] diff --git a/brewtils/plugin.py b/brewtils/plugin.py index 87458ad2..b7dc8fcf 100644 --- a/brewtils/plugin.py +++ b/brewtils/plugin.py @@ -17,7 +17,7 @@ import brewtils from brewtils.config import load_config -from brewtils.decorators import _parse_client +from brewtils.decorators import _parse_client, _parse_shutdown_functions from brewtils.display import resolve_template from brewtils.errors import ( ConflictError, @@ -183,6 +183,9 @@ class Plugin(object): group (str): Grouping label applied to plugin groups (list): Grouping labels applied to plugin + shutdown_function (func): Function to be executed at start of shutdown + shutdown_functions (list): Functions to be executed at start of shutdown + prefix_topic (str): Prefix for Generated Command Topics logger (:py:class:`logging.Logger`): Logger that will be used by the Plugin. @@ -218,9 +221,20 @@ def __init__(self, client=None, system=None, logger=None, **kwargs): self._custom_logger = False self._logger = self._setup_logging(logger=logger, **kwargs) + # Need to pop out shutdown functions because String is expected from + # config files, not callable functions + shutdown_functions = kwargs.pop("shutdown_functions", []) + if hasattr(kwargs, "shutdown_function"): + shutdown_functions.append(kwargs.pop("shutdown_function")) + # Now that logging is configured we can load the real config self._config = load_config(**kwargs) + # Map over single shutdown provided functions from kwargs + for shutdown_function in shutdown_functions: + if shutdown_function not in self._config.shutdown_functions: + self._config.shutdown_functions.append(shutdown_function) + # If global config has already been set that's a warning global CONFIG if len(CONFIG): @@ -233,7 +247,7 @@ def __init__(self, client=None, system=None, logger=None, **kwargs): # Now set up the system self._system = self._setup_system(system, kwargs) - + global CLIENT # Make sure this is set after self._system if client: @@ -296,9 +310,42 @@ def client(self, new_client): raise AttributeError("Sorry, you can't change a plugin's client once set") if new_client is None: + self._set_shutdown_functions() return self._set_client(new_client) + self._set_shutdown_functions() + + + def _set_shutdown_functions(self): + + if self._client: + self._shutdown_functions = self._client._shutdown_functions + else: + self._shutdown_functions = [] + + if hasattr(self._config, "shutdown_function"): + if self._config.shutdown_function not in self._config.shutdown_functions: + self._config.shutdown_functions.append(self._config.shutdown_function) + + + for add_shutdown_function in self._config.shutdown_functions: + if callable(add_shutdown_function): + if add_shutdown_function not in self._shutdown_functions: + self._shutdown_functions.append(add_shutdown_function) + + elif self._client and hasattr(self._client, add_shutdown_function): + client_function = getattr(self._client, add_shutdown_function) + if callable(client_function): + if client_function not in self._shutdown_functions: + self._shutdown_functions.append(client_function) + else: + raise PluginValidationError(f"Provided non callable function for shutdown function: {add_shutdown_function}") + elif self._client: + raise PluginValidationError(f"Provided function not existing on client for shutdown function: {add_shutdown_function}") + else: + self._logger.warning(f"No client provided to check for shutdown function: {add_shutdown_function}") + def _set_client(self, new_client): # Several _system properties can come from the client, so update if needed @@ -319,6 +366,8 @@ def _set_client(self, new_client): # Now roll up / interpret all metadata to get the Commands self._system.commands = _parse_client(new_client) + self._shutdown_functions = _parse_shutdown_functions(new_client) + try: # Put some attributes on the Client class client_clazz = type(new_client) @@ -490,7 +539,18 @@ def _shutdown(self, status="STOPPED"): considered in a "stopped" state - the message processors shut down and all connections closed. """ + self._logger.debug("About to shut down plugin %s", self.unique_name) + + if len(self._shutdown_functions) > 0: + + # Run shutdown functions prior to setting shutdown event to allow for + # any functions that might generate Requests + self._logger.info("About to run provided shutdown functions") + + for shutdown_function in self._shutdown_functions: + shutdown_function() + self._shutdown_event.set() self._logger.debug("Shutting down processors") diff --git a/brewtils/specification.py b/brewtils/specification.py index e3829563..0434ecbd 100644 --- a/brewtils/specification.py +++ b/brewtils/specification.py @@ -188,6 +188,18 @@ def _is_json_dict(s): "description": "The dependency timeout to use", "default": 300, }, + "shutdown_function": { + "type": "str", + "description": "The function in client to be executed at shutdown", + "required": False, + }, + "shutdown_functions": { + "type": "list", + "description": "The functions in client to be executed at shutdown", + "items": {"name": {"type": "str"}}, + "required": False, + "default": [], + } } _PLUGIN_SPEC = { From 73422a1332848644e1dda0c7f94f46992301a5c6 Mon Sep 17 00:00:00 2001 From: TheBurchLog <5104941+TheBurchLog@users.noreply.github.com> Date: Wed, 30 Oct 2024 06:18:16 -0400 Subject: [PATCH 02/11] Add shutdown to the init --- brewtils/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/brewtils/__init__.py b/brewtils/__init__.py index 0fe5ec1d..3441f535 100644 --- a/brewtils/__init__.py +++ b/brewtils/__init__.py @@ -2,7 +2,7 @@ from brewtils.__version__ import __version__ from brewtils.auto_decorator import AutoDecorator from brewtils.config import get_argument_parser, get_connection_info, load_config -from brewtils.decorators import client, command, parameter, subscribe, system +from brewtils.decorators import client, command, parameter, shutdown, subscribe, system from brewtils.log import configure_logging from brewtils.plugin import ( get_current_request_read_only, @@ -19,6 +19,7 @@ "client", "command", "parameter", + "shutdown", "system", "subscribe", "Plugin", From 7fa7bcb6bff38c3ccaada042eebac9b81c4abd54 Mon Sep 17 00:00:00 2001 From: TheBurchLog <5104941+TheBurchLog@users.noreply.github.com> Date: Wed, 30 Oct 2024 06:23:21 -0400 Subject: [PATCH 03/11] Set shutdown functions in set_client --- brewtils/decorators.py | 2 ++ brewtils/plugin.py | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/brewtils/decorators.py b/brewtils/decorators.py index 54e88569..d8b28cd9 100644 --- a/brewtils/decorators.py +++ b/brewtils/decorators.py @@ -106,6 +106,8 @@ def client( if require: _wrapped._requires.append(require) + _wrapped._shutdown_functions = _parse_shutdown_functions(_wrapped) + return _wrapped diff --git a/brewtils/plugin.py b/brewtils/plugin.py index b7dc8fcf..6bf90443 100644 --- a/brewtils/plugin.py +++ b/brewtils/plugin.py @@ -366,8 +366,6 @@ def _set_client(self, new_client): # Now roll up / interpret all metadata to get the Commands self._system.commands = _parse_client(new_client) - self._shutdown_functions = _parse_shutdown_functions(new_client) - try: # Put some attributes on the Client class client_clazz = type(new_client) From 39729ca5569cc45989cadc4d091dda561607505a Mon Sep 17 00:00:00 2001 From: TheBurchLog <5104941+TheBurchLog@users.noreply.github.com> Date: Wed, 30 Oct 2024 08:49:53 -0400 Subject: [PATCH 04/11] Testing --- brewtils/decorators.py | 8 +-- brewtils/plugin.py | 88 +++++++++++++++++---------------- brewtils/rest/publish_client.py | 1 - brewtils/specification.py | 2 +- test/plugin_test.py | 20 ++++++++ 5 files changed, 71 insertions(+), 48 deletions(-) diff --git a/brewtils/decorators.py b/brewtils/decorators.py index d8b28cd9..29cb0331 100644 --- a/brewtils/decorators.py +++ b/brewtils/decorators.py @@ -106,8 +106,6 @@ def client( if require: _wrapped._requires.append(require) - _wrapped._shutdown_functions = _parse_shutdown_functions(_wrapped) - return _wrapped @@ -449,6 +447,7 @@ def cmd1(self, **kwargs): return _wrapped + def shutdown(_wrapped=None): """Decorator for specifying a function to run before a plugin is shutdown @@ -458,7 +457,7 @@ def shutdown(_wrapped=None): def pre_shutdown(self): # Run pre-shutdown processing return - + Args: _wrapped: The function to decorate. This is handled as a positional argument and shouldn't be explicitly set. @@ -508,10 +507,11 @@ def returnTrue(self): return _wrapped + def _parse_shutdown_functions(client): # type: (object) -> List[Callable] """Get a list of callable fields labeled with the shutdown annotation - + This will iterate over everything returned from dir, looking for metadata added by the shutdown decorator. """ diff --git a/brewtils/plugin.py b/brewtils/plugin.py index 6bf90443..19bc7c8d 100644 --- a/brewtils/plugin.py +++ b/brewtils/plugin.py @@ -221,20 +221,15 @@ def __init__(self, client=None, system=None, logger=None, **kwargs): self._custom_logger = False self._logger = self._setup_logging(logger=logger, **kwargs) - # Need to pop out shutdown functions because String is expected from - # config files, not callable functions - shutdown_functions = kwargs.pop("shutdown_functions", []) + # Need to pop out shutdown functions because these are not processed + # until shutdown + self._arg_shutdown_functions = kwargs.pop("shutdown_functions", []) if hasattr(kwargs, "shutdown_function"): - shutdown_functions.append(kwargs.pop("shutdown_function")) - + self._arg_shutdown_functions.append(kwargs.pop("shutdown_function")) + # Now that logging is configured we can load the real config self._config = load_config(**kwargs) - # Map over single shutdown provided functions from kwargs - for shutdown_function in shutdown_functions: - if shutdown_function not in self._config.shutdown_functions: - self._config.shutdown_functions.append(shutdown_function) - # If global config has already been set that's a warning global CONFIG if len(CONFIG): @@ -247,7 +242,7 @@ def __init__(self, client=None, system=None, logger=None, **kwargs): # Now set up the system self._system = self._setup_system(system, kwargs) - + global CLIENT # Make sure this is set after self._system if client: @@ -316,36 +311,36 @@ def client(self, new_client): self._set_client(new_client) self._set_shutdown_functions() + def _run_shutdown_functions( + self, shutdown_functions, executed_shutdown_functions=None + ): + if executed_shutdown_functions is None: + executed_shutdown_functions = [] - def _set_shutdown_functions(self): + for shutdown_function in shutdown_functions: + if callable(shutdown_function): + if shutdown_function not in executed_shutdown_functions: + shutdown_function() + executed_shutdown_functions.append(shutdown_function) - if self._client: - self._shutdown_functions = self._client._shutdown_functions - else: - self._shutdown_functions = [] - - if hasattr(self._config, "shutdown_function"): - if self._config.shutdown_function not in self._config.shutdown_functions: - self._config.shutdown_functions.append(self._config.shutdown_function) - - - for add_shutdown_function in self._config.shutdown_functions: - if callable(add_shutdown_function): - if add_shutdown_function not in self._shutdown_functions: - self._shutdown_functions.append(add_shutdown_function) - - elif self._client and hasattr(self._client, add_shutdown_function): - client_function = getattr(self._client, add_shutdown_function) + elif self._client and hasattr(self._client, shutdown_function): + client_function = getattr(self._client, shutdown_function) if callable(client_function): - if client_function not in self._shutdown_functions: - self._shutdown_functions.append(client_function) + if client_function not in executed_shutdown_functions: + client_function() + executed_shutdown_functions.append(client_function) else: - raise PluginValidationError(f"Provided non callable function for shutdown function: {add_shutdown_function}") + self._logger.error( + f"Provided non callable function for shutdown function: {shutdown_function}" + ) elif self._client: - raise PluginValidationError(f"Provided function not existing on client for shutdown function: {add_shutdown_function}") + self._logger.error( + f"Provided function not existing on client for shutdown function: {shutdown_function}" + ) else: - self._logger.warning(f"No client provided to check for shutdown function: {add_shutdown_function}") - + self._logger.error( + f"No client provided to check for shutdown function: {shutdown_function}" + ) def _set_client(self, new_client): # Several _system properties can come from the client, so update if needed @@ -539,15 +534,24 @@ def _shutdown(self, status="STOPPED"): """ self._logger.debug("About to shut down plugin %s", self.unique_name) - - if len(self._shutdown_functions) > 0: - # Run shutdown functions prior to setting shutdown event to allow for - # any functions that might generate Requests - self._logger.info("About to run provided shutdown functions") + # Run shutdown functions prior to setting shutdown event to allow for + # any functions that might generate Requests + + self._logger.debug("About to run annotated shutdown functions") + executed_shutdown_functions = self._run_shutdown_functions( + _parse_shutdown_functions(self.client) + ) - for shutdown_function in self._shutdown_functions: - shutdown_function() + self._logger.debug("About to run plugin shutdown functions") + executed_shutdown_functions = self._run_shutdown_functions( + self._arg_shutdown_functions, executed_shutdown_functions + ) + + self._logger.debug("About to run config shutdown functions") + executed_shutdown_functions = self._run_shutdown_functions( + self._config.shutdown_functions, executed_shutdown_functions + ) self._shutdown_event.set() diff --git a/brewtils/rest/publish_client.py b/brewtils/rest/publish_client.py index be96fac2..fc4fdfe3 100644 --- a/brewtils/rest/publish_client.py +++ b/brewtils/rest/publish_client.py @@ -90,7 +90,6 @@ def publish( """ if _topic is None: - if brewtils.plugin._system.prefix_topic: _topic = brewtils.plugin._system.prefix_topic elif ( diff --git a/brewtils/specification.py b/brewtils/specification.py index 0434ecbd..a2d5cc77 100644 --- a/brewtils/specification.py +++ b/brewtils/specification.py @@ -199,7 +199,7 @@ def _is_json_dict(s): "items": {"name": {"type": "str"}}, "required": False, "default": [], - } + }, } _PLUGIN_SPEC = { diff --git a/test/plugin_test.py b/test/plugin_test.py index 51ec4688..4851893f 100644 --- a/test/plugin_test.py +++ b/test/plugin_test.py @@ -429,6 +429,26 @@ def test_update_error(self, caplog, plugin, ez_client, bg_instance): assert len(caplog.records) == 1 + def test_shutdown_plugin_shutdown_function_executed(self, plugin): + plugin._request_processor = Mock() + plugin._admin_processor = Mock() + + mock_shutdown_function = Mock() + plugin._arg_shutdown_functions = [mock_shutdown_function] + + plugin._shutdown() + assert mock_shutdown_function.called is True + + def test_shutdown_config_shutdown_function_executed(self, plugin): + plugin._request_processor = Mock() + plugin._admin_processor = Mock() + + mock_shutdown_function = Mock() + plugin._config.shutdown_functions = [mock_shutdown_function] + + plugin._shutdown() + assert mock_shutdown_function.called is True + class TestInitializeLogging(object): @pytest.fixture(autouse=True) From c91c9d3677e2c5e4a641e5294387c2d96cf271cf Mon Sep 17 00:00:00 2001 From: TheBurchLog <5104941+TheBurchLog@users.noreply.github.com> Date: Wed, 30 Oct 2024 08:54:38 -0400 Subject: [PATCH 05/11] decorator testing --- brewtils/plugin.py | 2 +- test/decorators_test.py | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/brewtils/plugin.py b/brewtils/plugin.py index 19bc7c8d..c29369a6 100644 --- a/brewtils/plugin.py +++ b/brewtils/plugin.py @@ -7,11 +7,11 @@ import signal import sys import threading +from datetime import datetime, timezone from pathlib import Path import appdirs from box import Box -from datetime import datetime, timezone from packaging.version import Version from requests import ConnectionError as RequestsConnectionError diff --git a/test/decorators_test.py b/test/decorators_test.py index ed01ca8a..85753689 100644 --- a/test/decorators_test.py +++ b/test/decorators_test.py @@ -28,6 +28,7 @@ parameters, plugin_param, register, + shutdown, system, ) from brewtils.errors import PluginParamError @@ -739,7 +740,6 @@ def cmd3(foo): assert cmd3.parameters[0].type == "String" def test_literal_mapping(self, basic_param): - del basic_param["type"] @parameter(**basic_param, type=str) @@ -1439,3 +1439,23 @@ def test_plugin_param(self, cmd, parameter_dict): _initialize_parameter(cmd.parameters[0]), _initialize_parameter(**parameter_dict), ) + + +class TestShutdown(object): + """Test shutdown decorator""" + + def test_shutdown(self): + @shutdown + def cmd(): + return True + + assert hasattr(cmd, "_shutdown") + assert cmd._shutdown + + def test_missing_shutdown(self): + @command + def cmd(): + return True + + assert not hasattr(cmd, "_shutdown") + assert not getattr(cmd, "_shutdown", False) From d96ec7556248a6459ecd911a9947a3288feeeecd Mon Sep 17 00:00:00 2001 From: TheBurchLog <5104941+TheBurchLog@users.noreply.github.com> Date: Wed, 30 Oct 2024 08:55:41 -0400 Subject: [PATCH 06/11] cleanup --- brewtils/plugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/brewtils/plugin.py b/brewtils/plugin.py index c29369a6..f00750d6 100644 --- a/brewtils/plugin.py +++ b/brewtils/plugin.py @@ -305,11 +305,9 @@ def client(self, new_client): raise AttributeError("Sorry, you can't change a plugin's client once set") if new_client is None: - self._set_shutdown_functions() return self._set_client(new_client) - self._set_shutdown_functions() def _run_shutdown_functions( self, shutdown_functions, executed_shutdown_functions=None From 3fc2dc4b49c7c278fcf528c5c90b6c2ff7d9a7bf Mon Sep 17 00:00:00 2001 From: TheBurchLog <5104941+TheBurchLog@users.noreply.github.com> Date: Wed, 30 Oct 2024 09:16:17 -0400 Subject: [PATCH 07/11] formatting --- brewtils/plugin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/brewtils/plugin.py b/brewtils/plugin.py index f00750d6..cb50a33b 100644 --- a/brewtils/plugin.py +++ b/brewtils/plugin.py @@ -333,7 +333,10 @@ def _run_shutdown_functions( ) elif self._client: self._logger.error( - f"Provided function not existing on client for shutdown function: {shutdown_function}" + ( + "Provided function not existing on client " + f"for shutdown function: {shutdown_function}" + ) ) else: self._logger.error( From a37cbb32c4b001b42ae358fd7d0bdf15dd28f85d Mon Sep 17 00:00:00 2001 From: TheBurchLog <5104941+TheBurchLog@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:45:41 -0400 Subject: [PATCH 08/11] fixed logic in client parsing --- brewtils/decorators.py | 5 +++-- brewtils/plugin.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/brewtils/decorators.py b/brewtils/decorators.py index 29cb0331..08781c88 100644 --- a/brewtils/decorators.py +++ b/brewtils/decorators.py @@ -519,8 +519,9 @@ def _parse_shutdown_functions(client): shutdown_functions = [] for attr in dir(client): - if callable(attr) and getattr(attr, "_shutdown", False): - shutdown_functions.append(attr) + method = getattr(client, attr) + if callable(method) and getattr(method, "_shutdown", False): + shutdown_functions.append(method) return shutdown_functions diff --git a/brewtils/plugin.py b/brewtils/plugin.py index cb50a33b..16118164 100644 --- a/brewtils/plugin.py +++ b/brewtils/plugin.py @@ -224,7 +224,7 @@ def __init__(self, client=None, system=None, logger=None, **kwargs): # Need to pop out shutdown functions because these are not processed # until shutdown self._arg_shutdown_functions = kwargs.pop("shutdown_functions", []) - if hasattr(kwargs, "shutdown_function"): + if "shutdown_function" in kwargs: self._arg_shutdown_functions.append(kwargs.pop("shutdown_function")) # Now that logging is configured we can load the real config @@ -541,7 +541,7 @@ def _shutdown(self, status="STOPPED"): self._logger.debug("About to run annotated shutdown functions") executed_shutdown_functions = self._run_shutdown_functions( - _parse_shutdown_functions(self.client) + _parse_shutdown_functions(self._client) ) self._logger.debug("About to run plugin shutdown functions") From 22d6552a3b51f6d9bc05ade62599d1a70feaac65 Mon Sep 17 00:00:00 2001 From: TheBurchLog <5104941+TheBurchLog@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:49:04 -0400 Subject: [PATCH 09/11] updating comments --- brewtils/decorators.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/brewtils/decorators.py b/brewtils/decorators.py index 08781c88..2bbddb87 100644 --- a/brewtils/decorators.py +++ b/brewtils/decorators.py @@ -449,7 +449,10 @@ def cmd1(self, **kwargs): def shutdown(_wrapped=None): - """Decorator for specifying a function to run before a plugin is shutdown + """Decorator for specifying a function to run before a plugin is shutdown. + + Functions called should short actions. Locally hosted plugin threads will be + pruned if not stopped within the plugin.timeout.shutdown time window for example:: From 4b89b59849ccec7508112d30c11445488332ff38 Mon Sep 17 00:00:00 2001 From: TheBurchLog <5104941+TheBurchLog@users.noreply.github.com> Date: Thu, 31 Oct 2024 09:58:59 -0400 Subject: [PATCH 10/11] Add Startup Annotation Support --- CHANGELOG.rst | 3 + brewtils/__init__.py | 17 ++++-- brewtils/decorators.py | 36 ++++++++++++ brewtils/plugin.py | 118 +++++++++++++++++++++++++++----------- brewtils/specification.py | 16 +++++- test/decorators_test.py | 21 +++++++ test/plugin_test.py | 4 +- 7 files changed, 175 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 18c6bf4b..d325b1d0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,9 @@ TBD - Added new annotation/configuration support for shutdown functions. These functions will be executed at the start of the shutdown process. +- Added new annotation/configuration support for startup functions. These functions will be executed after `Plugin().run()` + has completed startup processes + 3.28.0 ------ diff --git a/brewtils/__init__.py b/brewtils/__init__.py index 3441f535..fb513096 100644 --- a/brewtils/__init__.py +++ b/brewtils/__init__.py @@ -2,13 +2,21 @@ from brewtils.__version__ import __version__ from brewtils.auto_decorator import AutoDecorator from brewtils.config import get_argument_parser, get_connection_info, load_config -from brewtils.decorators import client, command, parameter, shutdown, subscribe, system +from brewtils.decorators import ( + client, + command, + parameter, + shutdown, + startup, + subscribe, + system, +) from brewtils.log import configure_logging -from brewtils.plugin import ( - get_current_request_read_only, +from brewtils.plugin import ( # noqa F401 Plugin, RemotePlugin, -) # noqa F401 + get_current_request_read_only, +) from brewtils.rest import normalize_url_prefix from brewtils.rest.easy_client import EasyClient, get_easy_client from brewtils.rest.publish_client import PublishClient @@ -20,6 +28,7 @@ "command", "parameter", "shutdown", + "startup", "system", "subscribe", "Plugin", diff --git a/brewtils/decorators.py b/brewtils/decorators.py index 2bbddb87..dde4da9a 100644 --- a/brewtils/decorators.py +++ b/brewtils/decorators.py @@ -469,6 +469,24 @@ def pre_shutdown(self): return _wrapped +def startup(_wrapped=None): + """Decorator for specifying a function to run before a plugin is running. + + for example:: + + @startup + def pre_running(self): + # Run pre-running processing + return + + Args: + _wrapped: The function to decorate. This is handled as a positional argument and + shouldn't be explicitly set. + """ + _wrapped._startup = True + return _wrapped + + def subscribe(_wrapped=None, topic: str = None, topics=[]): """Decorator for specifiying topic to listen to. @@ -529,6 +547,24 @@ def _parse_shutdown_functions(client): return shutdown_functions +def _parse_startup_functions(client): + # type: (object) -> List[Callable] + """Get a list of callable fields labeled with the startup annotation + + This will iterate over everything returned from dir, looking for metadata added + by the startup decorator. + """ + + startup_functions = [] + + for attr in dir(client): + method = getattr(client, attr) + if callable(method) and getattr(method, "_startup", False): + startup_functions.append(method) + + return startup_functions + + def _parse_client(client): # type: (object) -> List[Command] """Get a list of Beergarden Commands from a client object diff --git a/brewtils/plugin.py b/brewtils/plugin.py index 16118164..c7cfd1f4 100644 --- a/brewtils/plugin.py +++ b/brewtils/plugin.py @@ -17,7 +17,11 @@ import brewtils from brewtils.config import load_config -from brewtils.decorators import _parse_client, _parse_shutdown_functions +from brewtils.decorators import ( + _parse_client, + _parse_shutdown_functions, + _parse_startup_functions, +) from brewtils.display import resolve_template from brewtils.errors import ( ConflictError, @@ -183,9 +187,22 @@ class Plugin(object): group (str): Grouping label applied to plugin groups (list): Grouping labels applied to plugin + client_shutdown_function (str): Function to be executed at start of shutdown + within client code + client_shutdown_functions (list): Functions to be executed at start of shutdown + within client code + shutdown_function (func): Function to be executed at start of shutdown shutdown_functions (list): Functions to be executed at start of shutdown + client_startup_function (str): Function to be executed at start of running plugin + within client code + client_startup_functions (list): Functions to be executed at start of running plugin + within client code + + startup_function (func): Function to be executed at start of running plugin + startup_functions (list): Functions to be executed at start of running plugin + prefix_topic (str): Prefix for Generated Command Topics logger (:py:class:`logging.Logger`): Logger that will be used by the Plugin. @@ -223,9 +240,42 @@ def __init__(self, client=None, system=None, logger=None, **kwargs): # Need to pop out shutdown functions because these are not processed # until shutdown - self._arg_shutdown_functions = kwargs.pop("shutdown_functions", []) + self.shutdown_functions = [] + for shutdown_function in kwargs.pop("shutdown_functions", []): + if callable(shutdown_function): + self.shutdown_functions.append(shutdown_function) + else: + raise PluginValidationError( + f"Provided un-callable shutdown function {shutdown_function}" + ) + if "shutdown_function" in kwargs: - self._arg_shutdown_functions.append(kwargs.pop("shutdown_function")) + shutdown_function = kwargs.pop("shutdown_function") + if callable(shutdown_function): + self.shutdown_functions.append(shutdown_function) + else: + raise PluginValidationError( + f"Provided un-callable shutdown function {shutdown_function}" + ) + + self.startup_functions = [] + + for startup_function in kwargs.pop("startup_functions", []): + if callable(startup_function): + self.startup_functions.append(startup_function) + else: + raise PluginValidationError( + f"Provided un-callable startup function {shutdown_function}" + ) + + if "startup_function" in kwargs: + startup_function = kwargs.pop("startup_function") + if callable(startup_function): + self.startup_functions.append(startup_function) + else: + raise PluginValidationError( + f"Provided un-callable startup function {shutdown_function}" + ) # Now that logging is configured we can load the real config self._config = load_config(**kwargs) @@ -275,6 +325,18 @@ def run(self): try: self._startup() + + # Run provided startup functions + self._logger.debug("About to run annotated startup functions") + startup_functions = _parse_startup_functions(self._client) + startup_functions.extend(self.startup_functions) + startup_functions.extend(self._config.client_startup_functions) + + if getattr(self._config, "client_startup_function"): + startup_functions.append(self._config.client_startup_function) + + self._run_configured_functions(startup_functions) + self._logger.info("Plugin %s has started", self.unique_name) try: @@ -309,38 +371,35 @@ def client(self, new_client): self._set_client(new_client) - def _run_shutdown_functions( - self, shutdown_functions, executed_shutdown_functions=None - ): - if executed_shutdown_functions is None: - executed_shutdown_functions = [] + def _run_configured_functions(self, functions): + executed_functions = [] - for shutdown_function in shutdown_functions: - if callable(shutdown_function): - if shutdown_function not in executed_shutdown_functions: - shutdown_function() - executed_shutdown_functions.append(shutdown_function) + for function in functions: + if callable(function): + if function not in executed_functions: + function() + executed_functions.append(function) - elif self._client and hasattr(self._client, shutdown_function): - client_function = getattr(self._client, shutdown_function) + elif self._client and hasattr(self._client, function): + client_function = getattr(self._client, function) if callable(client_function): - if client_function not in executed_shutdown_functions: + if client_function not in executed_functions: client_function() - executed_shutdown_functions.append(client_function) + executed_functions.append(client_function) else: self._logger.error( - f"Provided non callable function for shutdown function: {shutdown_function}" + f"Provided non callable function for function: {function}" ) elif self._client: self._logger.error( ( "Provided function not existing on client " - f"for shutdown function: {shutdown_function}" + f"for function: {function}" ) ) else: self._logger.error( - f"No client provided to check for shutdown function: {shutdown_function}" + f"No client provided to check for function: {function}" ) def _set_client(self, new_client): @@ -539,20 +598,15 @@ def _shutdown(self, status="STOPPED"): # Run shutdown functions prior to setting shutdown event to allow for # any functions that might generate Requests - self._logger.debug("About to run annotated shutdown functions") - executed_shutdown_functions = self._run_shutdown_functions( - _parse_shutdown_functions(self._client) - ) + self._logger.debug("About to run shutdown functions") + shutdown_functions = _parse_shutdown_functions(self._client) + shutdown_functions.extend(self.shutdown_functions) + shutdown_functions.extend(self._config.client_shutdown_functions) - self._logger.debug("About to run plugin shutdown functions") - executed_shutdown_functions = self._run_shutdown_functions( - self._arg_shutdown_functions, executed_shutdown_functions - ) + if getattr(self._config, "client_shutdown_function"): + shutdown_functions.append(self._config.client_shutdown_function) - self._logger.debug("About to run config shutdown functions") - executed_shutdown_functions = self._run_shutdown_functions( - self._config.shutdown_functions, executed_shutdown_functions - ) + self._run_configured_functions(shutdown_functions) self._shutdown_event.set() diff --git a/brewtils/specification.py b/brewtils/specification.py index a2d5cc77..2cbf112b 100644 --- a/brewtils/specification.py +++ b/brewtils/specification.py @@ -188,18 +188,30 @@ def _is_json_dict(s): "description": "The dependency timeout to use", "default": 300, }, - "shutdown_function": { + "client_shutdown_function": { "type": "str", "description": "The function in client to be executed at shutdown", "required": False, }, - "shutdown_functions": { + "client_shutdown_functions": { "type": "list", "description": "The functions in client to be executed at shutdown", "items": {"name": {"type": "str"}}, "required": False, "default": [], }, + "client_startup_function": { + "type": "str", + "description": "The function in client to be executed at run", + "required": False, + }, + "client_startup_functions": { + "type": "list", + "description": "The functions in client to be executed at run", + "items": {"name": {"type": "str"}}, + "required": False, + "default": [], + }, } _PLUGIN_SPEC = { diff --git a/test/decorators_test.py b/test/decorators_test.py index 85753689..33c4a7b3 100644 --- a/test/decorators_test.py +++ b/test/decorators_test.py @@ -29,6 +29,7 @@ plugin_param, register, shutdown, + startup, system, ) from brewtils.errors import PluginParamError @@ -1459,3 +1460,23 @@ def cmd(): assert not hasattr(cmd, "_shutdown") assert not getattr(cmd, "_shutdown", False) + + +class TestStartup(object): + """Test shutdown decorator""" + + def test_startup(self): + @startup + def cmd(): + return True + + assert hasattr(cmd, "_startup") + assert cmd._startup + + def test_missing_startup(self): + @command + def cmd(): + return True + + assert not hasattr(cmd, "_startup") + assert not getattr(cmd, "_startup", False) diff --git a/test/plugin_test.py b/test/plugin_test.py index 4851893f..d71a0f90 100644 --- a/test/plugin_test.py +++ b/test/plugin_test.py @@ -434,7 +434,7 @@ def test_shutdown_plugin_shutdown_function_executed(self, plugin): plugin._admin_processor = Mock() mock_shutdown_function = Mock() - plugin._arg_shutdown_functions = [mock_shutdown_function] + plugin.shutdown_functions = [mock_shutdown_function] plugin._shutdown() assert mock_shutdown_function.called is True @@ -444,7 +444,7 @@ def test_shutdown_config_shutdown_function_executed(self, plugin): plugin._admin_processor = Mock() mock_shutdown_function = Mock() - plugin._config.shutdown_functions = [mock_shutdown_function] + plugin._config.client_shutdown_functions = [mock_shutdown_function] plugin._shutdown() assert mock_shutdown_function.called is True From 09e4a600a4001c0018c53b4864b4d810fbf0595b Mon Sep 17 00:00:00 2001 From: TheBurchLog <5104941+TheBurchLog@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:28:24 -0400 Subject: [PATCH 11/11] Update Changelog --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d325b1d0..cd2c48b6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Brewtils Changelog ================== -TBD +3.29.0 ------ TBD