state on dilsc as of 2022-10-03
vector field, but no new state machine yet
This commit is contained in:
@ -30,7 +30,7 @@ The created state object may hold variables needed for the state.
|
||||
A state function may return either:
|
||||
- a function for the next state to transition to
|
||||
- Retry(<delay>) to keep the state and call the
|
||||
- or `None` for finishing
|
||||
- or `Finish` for finishing
|
||||
|
||||
|
||||
Initialisation Code
|
||||
@ -44,33 +44,23 @@ def state_x(stateobj):
|
||||
... further code ...
|
||||
|
||||
|
||||
Cleanup Function
|
||||
----------------
|
||||
Error Handler
|
||||
-------------
|
||||
|
||||
cleanup=<cleanup function> as argument in StateMachine.__init__ or .start
|
||||
defines a cleanup function to be called whenever the machine is stopped or
|
||||
an error is raised in a state function. A cleanup function may return
|
||||
either None for finishing or a further state function for continuing.
|
||||
In case of stop or restart, this return value is ignored.
|
||||
|
||||
|
||||
State Specific Cleanup Code
|
||||
---------------------------
|
||||
|
||||
To execute state specific cleanup, the cleanup may examine the current state
|
||||
(stateobj.state) in order to decide what to be done.
|
||||
|
||||
If a need arises, a future extension to this library may support specific
|
||||
cleanup functions by means of a decorator adding the specific cleanup function
|
||||
as an attribute to the state function.
|
||||
handler=<handler object> as argument in StateMachine.__init__ or .start
|
||||
defines a handler object to be called whenever the machine is stopped,
|
||||
restarted. finished or an error is raised in a state function.
|
||||
<handler>.on_error may return either None for finishing or a further state
|
||||
function for continuing. The other handler methods always return None,
|
||||
as there is no useful follow up state.
|
||||
|
||||
|
||||
Threaded Use
|
||||
------------
|
||||
|
||||
On start, a thread is started, which is waiting for a trigger event when the
|
||||
machine is not active. For test purposes or special needs, the thread creation
|
||||
may be disabled. :meth:`cycle` must be called periodically in this case.
|
||||
machine is not active. In case the thread creation is disabled. :meth:`cycle`
|
||||
must be called periodically for running the state machine.
|
||||
"""
|
||||
|
||||
import time
|
||||
@ -80,6 +70,7 @@ from logging import getLogger
|
||||
from secop.lib import mkthread, UniqueObject
|
||||
|
||||
|
||||
Finish = UniqueObject('Finish')
|
||||
Stop = UniqueObject('Stop')
|
||||
Restart = UniqueObject('Restart')
|
||||
|
||||
@ -89,6 +80,52 @@ class Retry:
|
||||
self.delay = delay
|
||||
|
||||
|
||||
class StateHandler:
|
||||
"""default handlers
|
||||
|
||||
may be used as base class or mixin for implementing custom handlers
|
||||
"""
|
||||
def on_error(self, statemachine, exc):
|
||||
"""called on error
|
||||
|
||||
:param statemachine: the state machine object
|
||||
:param exc: the exception
|
||||
:return: None or a state function to be executed for handling the error state
|
||||
"""
|
||||
statemachine.log.warning('%r raised in state %r', exc, statemachine.status_string)
|
||||
|
||||
def on_transition(self, statemachine, newstate):
|
||||
"""called when state is changed
|
||||
|
||||
:param statemachine: the statemachine
|
||||
:param newstate: the new state function
|
||||
|
||||
this method will not be called when the state is changed to None,
|
||||
e.g. on finish, restart, stop or when None is returned from the error handler
|
||||
"""
|
||||
|
||||
def on_restart(self, statemachine):
|
||||
"""called on restart
|
||||
|
||||
:param statemachine: the state machine object
|
||||
"""
|
||||
|
||||
def on_stop(self, statemachine):
|
||||
"""called when stopped
|
||||
|
||||
:param statemachine: the state machine object
|
||||
"""
|
||||
|
||||
def on_finish(self, statemachine):
|
||||
"""called on finish
|
||||
|
||||
:param statemachine: the state machine object
|
||||
"""
|
||||
|
||||
|
||||
default_handler = StateHandler()
|
||||
|
||||
|
||||
class StateMachine:
|
||||
"""a simple, but powerful state machine"""
|
||||
# class attributes are not allowed to be overriden by kwds of __init__ or :meth:`start`
|
||||
@ -98,7 +135,7 @@ class StateMachine:
|
||||
now = None
|
||||
init = True
|
||||
stopped = False
|
||||
last_error = None # last exception raised or Stop or Restart
|
||||
restarted = False
|
||||
_last_time = 0
|
||||
|
||||
def __init__(self, state=None, logger=None, threaded=True, **kwds):
|
||||
@ -109,9 +146,9 @@ class StateMachine:
|
||||
:param threaded: whether a thread should be started (default: True)
|
||||
:param kwds: any attributes for the state object
|
||||
"""
|
||||
self.default_delay = 0.25 # default delay when returning None
|
||||
self.default_delay = 0.25 # default delay when returning Retry(None)
|
||||
self.now = time.time() # avoid calling time.time several times per state
|
||||
self.cleanup = self.default_cleanup # default cleanup: finish on error
|
||||
self.handler = default_handler
|
||||
self.log = logger or getLogger('dummy')
|
||||
self._update_attributes(kwds)
|
||||
self._lock = threading.RLock()
|
||||
@ -120,23 +157,9 @@ class StateMachine:
|
||||
self._thread_queue = queue.Queue()
|
||||
self._idle_event = threading.Event()
|
||||
self._thread = None
|
||||
self._restart = None
|
||||
if state:
|
||||
self.start(state)
|
||||
|
||||
@staticmethod
|
||||
def default_cleanup(state):
|
||||
"""default cleanup
|
||||
|
||||
:param self: the state object
|
||||
:return: None (for custom cleanup functions this might be a new state)
|
||||
"""
|
||||
if state.stopped: # stop or restart
|
||||
verb = 'stopped' if state.stopped is Stop else 'restarted'
|
||||
state.log.debug('%s in state %r', verb, state.status_string)
|
||||
else:
|
||||
state.log.warning('%r raised in state %r', state.last_error, state.status_string)
|
||||
|
||||
def _update_attributes(self, kwds):
|
||||
"""update allowed attributes"""
|
||||
cls = type(self)
|
||||
@ -168,10 +191,12 @@ class StateMachine:
|
||||
|
||||
def _new_state(self, state):
|
||||
self.state = state
|
||||
self.init = True
|
||||
self.now = time.time()
|
||||
self.transition_time = self.now
|
||||
self.log.debug('state: %s', self.status_string)
|
||||
if state:
|
||||
self.handler.on_transition(self, state)
|
||||
self.init = True
|
||||
self.now = time.time()
|
||||
self.transition_time = self.now
|
||||
|
||||
def cycle(self):
|
||||
"""do one cycle in the thread loop
|
||||
@ -187,18 +212,26 @@ class StateMachine:
|
||||
ret = self.state(self)
|
||||
self.init = False
|
||||
if self.stopped:
|
||||
self.last_error = self.stopped
|
||||
self.cleanup(self)
|
||||
self.log.debug('stopped in state %r', self.status_string)
|
||||
self.handler.on_stop(self)
|
||||
self.stopped = False
|
||||
ret = None
|
||||
elif self.restarted:
|
||||
self.log.debug('restarted in state %r', self.status_string)
|
||||
self.handler.on_restart(self)
|
||||
self.restarted = False
|
||||
ret = None
|
||||
except Exception as e:
|
||||
self.last_error = e
|
||||
ret = self.cleanup(self)
|
||||
self.log.debug('called %r %sexc=%r', self.cleanup,
|
||||
'ret=%r ' % ret if ret else '', e)
|
||||
if ret is None:
|
||||
self.log.debug('state: None after cleanup')
|
||||
self.state = None
|
||||
try:
|
||||
ret = self.handler.on_error(self, e)
|
||||
self.log.debug('called on_error with exc=%r%s', e,
|
||||
' ret=%r' % ret if ret else '')
|
||||
except Exception as ee:
|
||||
self.log.exception('%r raised in on_error(state, %r)', ee, e)
|
||||
if ret is Finish:
|
||||
self.log.debug('finish in state %r', self.status_string)
|
||||
self.handler.on_finish(self)
|
||||
self._new_state(None)
|
||||
self._idle_event.set()
|
||||
return None
|
||||
if callable(ret):
|
||||
@ -210,12 +243,13 @@ class StateMachine:
|
||||
if ret.delay is None:
|
||||
return self.default_delay
|
||||
return ret.delay
|
||||
self.last_error = RuntimeError('return value must be callable, Retry(...) or finish')
|
||||
self.handler.on_error(self, RuntimeError(
|
||||
'return value must be callable, Retry(...) or Finish, not %r' % ret))
|
||||
break
|
||||
else:
|
||||
self.last_error = RuntimeError('too many states chained - probably infinite loop')
|
||||
self.cleanup(self)
|
||||
self.state = None
|
||||
self.handler.on_error(self, RuntimeError(
|
||||
'too many states chained - probably infinite loop'))
|
||||
self._new_state(None)
|
||||
return None
|
||||
|
||||
def trigger(self, delay=0):
|
||||
@ -238,9 +272,8 @@ class StateMachine:
|
||||
delay = self.cycle()
|
||||
|
||||
def _start(self, state, **kwds):
|
||||
self._restart = None
|
||||
self._idle_event.clear()
|
||||
self.last_error = None
|
||||
self.restarted = False
|
||||
self.stopped = False
|
||||
self._update_attributes(kwds)
|
||||
self._new_state(state)
|
||||
@ -259,19 +292,18 @@ class StateMachine:
|
||||
"""start with a new state
|
||||
|
||||
and interrupt the current state
|
||||
the cleanup function will be called with state.stopped=Restart
|
||||
the cleanup function will be called with state.restarted = True
|
||||
|
||||
:param state: the first state
|
||||
:param kwds: items to put as attributes on the state machine
|
||||
"""
|
||||
self.log.debug('start %r', kwds)
|
||||
if self.state:
|
||||
self.stopped = Restart
|
||||
self.restarted = True
|
||||
with self._lock: # wait for running cycle finished
|
||||
if self.stopped: # cleanup is not yet done
|
||||
self.last_error = self.stopped
|
||||
self.cleanup(self) # ignore return state on restart
|
||||
self.stopped = False
|
||||
if self.restarted: # on_restart is not yet called
|
||||
self.handler.on_restart(self)
|
||||
self.restarted = False
|
||||
self._start(state, **kwds)
|
||||
else:
|
||||
self._start(state, **kwds)
|
||||
@ -279,16 +311,15 @@ class StateMachine:
|
||||
def stop(self):
|
||||
"""stop machine, go to idle state
|
||||
|
||||
the cleanup function will be called with state.stopped=Stop
|
||||
the cleanup function will be called with state.stopped = True
|
||||
"""
|
||||
self.log.debug('stop')
|
||||
self.stopped = Stop
|
||||
self.stopped = True
|
||||
with self._lock:
|
||||
if self.stopped: # cleanup is not yet done
|
||||
self.last_error = self.stopped
|
||||
self.cleanup(self) # ignore return state on restart
|
||||
if self.stopped: # on_stop is not yet called
|
||||
self.handler.on_stop(self)
|
||||
self.stopped = False
|
||||
self.state = None
|
||||
self._new_state(None)
|
||||
|
||||
def wait(self, timeout=None):
|
||||
"""wait for state machine being idle"""
|
||||
|
Reference in New Issue
Block a user