Skip to content

Commit

Permalink
Closes #15
Browse files Browse the repository at this point in the history
  • Loading branch information
julien6387 committed Apr 7, 2024
1 parent 56aef34 commit 1860527
Show file tree
Hide file tree
Showing 15 changed files with 894 additions and 229 deletions.
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
application operational status.
`status_formula` is added to the result of the XML-RPC `get_application_rules`.

* Implement [Issue #15](https://github.com/julien6387/supvisors/issues/15).
A `StarterModel` has been added to **Supvisors** to give a prediction of the application distribution when started.
The command is available through the new XML-RPCs `test_start_application` and `test_start_process` and have been
added to `supervisorctl`.

* Add a new XML-RPC `lazy_update_numprocs`, whose behaviour differ from `update_numprocs` in a way that the obsolete
processes will be deleted from the Supervisor configuration when they are stopped (exit, crash or later user request)
instead of being forced to stop immediately when requesting the numprocs to decrease.
Expand Down
27 changes: 20 additions & 7 deletions docs/supervisorctl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ The additional commands provided by |Supvisors| are available by typing :command
disable_stats sreload stop_process
enable sshutdown strategies
enable_stats sstate sversion
end_sync sstatus update_numprocs
instance_status start_any_process
lazy_update_numprocs start_any_process_args
end_sync sstatus test_start_application
instance_status start_any_process test_start_process
lazy_update_numprocs start_any_process_args update_numprocs
.. _extended_status:

Expand Down Expand Up @@ -259,6 +259,11 @@ It can take values among { ``CONFIG``, ``LESS_LOADED``, ``MOST_LOADED``, ``LOCAL

Restart multiple named managed applications with a starting strategy.

``test_start_application strategy appli``

Return a prediction of the distribution of the managed application named appli with a starting strategy.



Process Control
---------------
Expand Down Expand Up @@ -313,7 +318,7 @@ Process Control

``stop_process proc``

Stop the process named appli.
Stop the process named proc.

``stop_process proc1 proc2``

Expand All @@ -323,14 +328,22 @@ Process Control

Restart all processes with a starting strategy.

``restart_process strategy appli``
``restart_process strategy proc``

Restart the process named appli with a starting strategy.
Restart the process named proc with a starting strategy.

``restart_process strategy appli1 appli2``
``restart_process strategy proc1 proc2``

Restart multiple named process with a starting strategy.

``test_start_process strategy proc``

Return a prediction of the distribution of the process named proc with a starting strategy.

``test_start_process strategy appli:*``

Return a prediction of the distribution of all appli application processes with a starting strategy.

``update_numprocs program_name numprocs``

Increase or decrease dynamically the program numprocs (including FastCGI programs and Event listeners),
Expand Down
4 changes: 4 additions & 0 deletions docs/xml_rpc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,8 @@ Application Control

.. automethod:: restart_application(strategy, application_name, wait=True)

.. automethod:: test_start_application(strategy, application_name)


.. _xml_rpc_process:

Expand All @@ -300,6 +302,8 @@ Process Control

.. automethod:: restart_process(strategy, namespec, extra_args='', wait=True)

.. automethod:: test_start_process(strategy, namespec)

.. automethod:: update_numprocs(program_name, numprocs, wait=True)

.. hint::
Expand Down
307 changes: 237 additions & 70 deletions supvisors/commander.py

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion supvisors/initializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from supervisor.supervisord import Supervisor

from supvisors.internal_com.mapper import SupvisorsMapper
from .commander import Starter, Stopper
from .commander import Starter, Stopper, StarterModel
from .context import Context
from .listener import SupervisorListener
from .options import SupvisorsOptions, SupvisorsServerOptions, Automatic, get_logger_configuration
Expand Down Expand Up @@ -114,6 +114,8 @@ def __init__(self, supervisor: Supervisor, **config) -> None:
# create application starter and stopper
self.starter = Starter(self)
self.stopper = Stopper(self)
# create application starter model
self.starter_model = StarterModel(self)
# create the failure handler of crashing processes
# WARN: must be created before the state machine
self.failure_handler = RunningFailureHandler(self)
Expand Down
57 changes: 57 additions & 0 deletions supvisors/rpcinterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,35 @@ def onwait() -> RPCInterface.OnWaitReturnType:
return onwait # deferred
return in_progress

def test_start_application(self, strategy: EnumParameterType, application_name: str) -> PayloadList:
""" Return a distribution prediction for a start of the *Managed* application named ``application_name``
iaw the strategy and the rules file.
:param StartingStrategies strategy: the strategy used to choose a **Supvisors** instance,
as a string or as a value.
:param str application_name: the name of the application.
:return: a list of structures with the predicted distribution of the application processes.
:rtype: list[dict[str, Any]]
:raises RPCError: with code:
``SupvisorsFaults.BAD_SUPVISORS_STATE`` if **Supvisors** is not in state ``OPERATION`` ;
``Faults.INCORRECT_PARAMETERS`` if ``strategy`` is unknown to **Supvisors** ;
``Faults.BAD_NAME`` if ``application_name`` is unknown to **Supvisors** ;
``SupvisorsFaults.NOT_MANAGED`` if the application is not *Managed* in **Supvisors** ;
``Faults.ALREADY_STARTED`` if the application is ``STARTING``, ``STOPPING`` or ``RUNNING``.
"""
self.logger.trace(f'RPCInterface.test_start_application: strategy={strategy} application={application_name}')
self._check_operating()
strategy_enum = self._get_starting_strategy(strategy)
# check application name
application = self._get_application(application_name)
# check application is managed
if application_name not in self.supvisors.context.get_managed_applications():
self._raise(SupvisorsFaults.NOT_MANAGED.value, 'test_start_application', application_name)
# check application is not already RUNNING
if application.state != ApplicationStates.STOPPED:
self._raise(Faults.ALREADY_STARTED, 'test_start_application', application_name)
return self.supvisors.starter_model.test_start_application(strategy_enum, application)

def stop_application(self, application_name: str, wait: bool = True) -> WaitReturnType:
""" Stop the *Managed* application named ``application_name``.
To stop *Unmanaged* applications, use ``supervisor.stop('group:*')``.
Expand Down Expand Up @@ -528,6 +557,34 @@ def onwait() -> RPCInterface.OnWaitReturnType:
return onwait # deferred
return True

def test_start_process(self, strategy: EnumParameterType, namespec: str) -> WaitReturnType:
""" Start a process named ``namespec`` iaw the strategy and the rules file.
WARN: the 'wait_exit' rule is not considered here.
:param StartingStrategies strategy: the strategy used to choose a **Supvisors** instance,
as a string or as a value.
:param str namespec: the process namespec (``name``,``group:name``, or ``group:*``).
:return: a list of structures with the predicted distribution of the processes.
:rtype: list[dict[str, Any]].
:raises RPCError: with code:
``SupvisorsFaults.BAD_SUPVISORS_STATE`` if **Supvisors** is not in state ``OPERATION`` ;
``Faults.INCORRECT_PARAMETERS`` if ``strategy`` is unknown to **Supvisors** ;
``Faults.BAD_NAME`` if ``namespec`` is unknown to **Supvisors** ;
``Faults.ALREADY_STARTED`` if process is in a running state.
"""
self.logger.trace(f'RPCInterface.test_start_process: namespec={namespec} strategy={strategy}')
self._check_operating()
strategy_enum = self._get_starting_strategy(strategy)
# check names
application, process = self._get_application_process(namespec)
processes = [process] if process else application.processes.values()
# check processes are not already running
for process in processes:
if process.running():
self._raise(Faults.ALREADY_STARTED, 'test_start_process', process.namespec)
# start all processes
return self.supvisors.starter_model.test_start_processes(strategy_enum, processes)

def start_any_process(self, strategy: EnumParameterType, regex: str, extra_args: str = '',
wait: bool = True) -> WaitStringReturnType:
""" Start one process among those matching the ``regex`` iaw the strategy and the rules file.
Expand Down
6 changes: 2 additions & 4 deletions supvisors/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ def create_strategy(supvisors: Any, strategy: StartingStrategies) -> AbstractSta


def get_supvisors_instance(supvisors: Any, strategy: StartingStrategies, identifiers: NameList,
expected_load: int) -> Optional[str]:
expected_load: int, load_request_map: LoadMap) -> Optional[str]:
""" Creates a strategy and let it find a Supvisors instance to start a process having a defined load.
:param supvisors: the global Supvisors structure
Expand All @@ -307,9 +307,7 @@ def get_supvisors_instance(supvisors: Any, strategy: StartingStrategies, identif
return None
# create the relevant strategy to choose a Supvisors instance among the candidates
instance = create_strategy(supvisors, strategy)
# consider all pending starting requests into global load
load_request_map = supvisors.starter.get_load_requests()
supvisors.logger.debug(f'get_supvisors_instance: load_request_map={load_request_map}')
# sort the load_request_map per node
node_load_request_map = get_node_load_request_map(supvisors.mapper, load_request_map)
supvisors.logger.debug(f'get_supvisors_instance: node_load_request_map={node_load_request_map}')
# get nodes load
Expand Down
113 changes: 113 additions & 0 deletions supvisors/supvisorsctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,71 @@ def help_start_application(self):
self.ctl.output('start_application <strategy>\t\t\t'
'Start all managed applications with strategy.')

def do_test_start_application(self, arg):
""" Command to test the starting of Supvisors applications using a strategy and rules. """
if self._upcheck():
args = as_string(arg).split()
# check number of arguments
if len(args) != 2:
self.ctl.output('ERROR: test_start_application requires one strategy and one application name')
self.ctl.exitstatus = LSBInitExitStatuses.INVALID_ARGS
self.help_test_start_application()
return
# check strategy format
try:
strategy = StartingStrategies[args[0]]
except KeyError:
self.ctl.output('ERROR: unknown strategy for test_start_application.'
f' use one of {[x.name for x in StartingStrategies]}')
self.ctl.exitstatus = LSBInitExitStatuses.INVALID_ARGS
self.help_test_start_application()
return
# get all application info
application_name = args[1]
try:
info = self.supvisors().get_application_info(args[1])
except xmlrpclib.Fault:
self.ctl.output(f'ERROR: unknown application "{application_name}"')
self.ctl.exitstatus = LSBInitExitStatuses.INVALID_ARGS
return
# match with parameters
if not info['managed']:
self.ctl.output(f'ERROR: {application_name} unmanaged')
self.help_test_start_application()
return
# request test start for matching applications
try:
results_list = self.supvisors().test_start_application(strategy.value, application_name)
except xmlrpclib.Fault as e:
self.ctl.output(f'{application_name}: ERROR ({e.faultString})')
self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
return
# print results summary
max_appli = ControllerPlugin.max_template(results_list, 'application_name', 'Application')
max_process = ControllerPlugin.max_template(results_list, 'process_name', 'Process')
max_identifiers = ControllerPlugin.max_template(results_list, 'running_identifiers', 'Supervisor')
# print title
template = (f'%(appli)-{max_appli}s%(proc)-{max_process}s%(state)-12s'
f'%(identifiers)-{max_identifiers}s%(reason)s')
title = {'appli': 'Application', 'proc': 'Process', 'state': 'State',
'identifiers': 'Supervisor', 'reason': 'Reason'}
self.ctl.output(template % title)
# print results
for results in results_list:
payload = {'appli': results['application_name'],
'proc': results['process_name'],
'identifiers': results['running_identifiers'],
'state': results['state'],
'reason': results['forced_reason']}
self.ctl.output(template % payload)

def help_test_start_application(self):
""" Print the help of the test_start_application command."""
self.ctl.output('test_start_application <strategy> <appli>\t\t'
'Test the starting of the managed application named appli with strategy.')
self.ctl.output('test_start_application <strategy>\t\t\t'
'Test the starting of all managed applications with strategy.')

def do_restart_application(self, arg):
""" Command to restart Supvisors applications using a strategy and rules. """
if self._upcheck():
Expand Down Expand Up @@ -790,6 +855,54 @@ def help_start_process(self):
self.ctl.output('start_process <strategy> <proc> <proc>\t\tStart multiple named processes with strategy.')
self.ctl.output('start_process <strategy>\t\t\tStart all processes with strategy.')

def do_test_start_process(self, arg):
""" Command to test the starting of Supvisors processes using a strategy and rules. """
if self._upcheck():
args = arg.split()
if len(args) != 2:
self.ctl.output('ERROR: test_start_process requires one strategy and one namespec')
self.ctl.exitstatus = LSBInitExitStatuses.INVALID_ARGS
self.help_test_start_process()
return
try:
strategy = StartingStrategies[args[0]]
except KeyError:
self.ctl.output('ERROR: unknown strategy for test_start_process.'
f' use one of {[x.name for x in StartingStrategies]}')
self.ctl.exitstatus = LSBInitExitStatuses.INVALID_ARGS
self.help_test_start_process()
return
namespec = args[1]
try:
results_list = self.supvisors().test_start_process(strategy.value, namespec)
except xmlrpclib.Fault as e:
self.ctl.output(f'{namespec}: ERROR ({e.faultString})')
self.ctl.exitstatus = LSBInitExitStatuses.GENERIC
return
# print results summary
max_appli = ControllerPlugin.max_template(results_list, 'application_name', 'Application')
max_process = ControllerPlugin.max_template(results_list, 'process_name', 'Process')
max_identifiers = ControllerPlugin.max_template(results_list, 'running_identifiers', 'Supervisor')
# print title
template = (f'%(appli)-{max_appli}s%(proc)-{max_process}s%(state)-12s'
f'%(identifiers)-{max_identifiers}s%(reason)s')
title = {'appli': 'Application', 'proc': 'Process', 'state': 'State',
'identifiers': 'Supervisor', 'reason': 'Reason'}
self.ctl.output(template % title)
# print results
for results in results_list:
payload = {'appli': results['application_name'],
'proc': results['process_name'],
'identifiers': results['running_identifiers'],
'state': results['state'],
'reason': results['forced_reason']}
self.ctl.output(template % payload)

def help_test_start_process(self):
""" Print the help of the test_start_process command."""
self.ctl.output('test_start_process <strategy> <proc>\t\t\tStart the process named proc with strategy.')
self.ctl.output('test_start_process <strategy>\t\t\tStart all processes with strategy.')

def do_start_any_process(self, arg):
""" Command to start processes using regular expressions, with a strategy and rules. """
if self._upcheck():
Expand Down
3 changes: 2 additions & 1 deletion supvisors/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,14 @@ def __init__(self, supervisord, config):
# build context from node mapper
self.context = Context(self)
# mock by spec
from supvisors.commander import Starter, Stopper
from supvisors.commander import Starter, Stopper, StarterModel
from supvisors.strategy import RunningFailureHandler
from supvisors.statemachine import FiniteStateMachine
from supvisors.listener import SupervisorListener
from supvisors.sparser import Parser
from supvisors.internal_com import SupvisorsInternalEmitter
self.starter = Mock(spec=Starter)
self.starter_model = Mock(spec=StarterModel)
self.stopper = Mock(spec=Stopper)
self.failure_handler = Mock(spec=RunningFailureHandler)
self.fsm = Mock(spec=FiniteStateMachine, redeploy_mark=False)
Expand Down
Loading

0 comments on commit 1860527

Please sign in to comment.