|
| 1 | +""" StateMachine |
| 2 | +
|
| 3 | + This module contains the basic blocks to build a state machine (State and StateMachine) |
| 4 | +""" |
| 5 | +from DIRACCommon.Core.Utilities.ReturnValues import S_OK, S_ERROR |
| 6 | + |
| 7 | + |
| 8 | +class State: |
| 9 | + """ |
| 10 | + State class that represents a single step on a StateMachine, with all the |
| 11 | + possible transitions, the default transition and an ordering level. |
| 12 | +
|
| 13 | +
|
| 14 | + examples: |
| 15 | + >>> s0 = State(100) |
| 16 | + >>> s1 = State(0, ['StateName1', 'StateName2'], defState='StateName1') |
| 17 | + >>> s2 = State(0, ['StateName1', 'StateName2']) |
| 18 | + # this example is tricky. The transition rule says that will go to |
| 19 | + # nextState, e.g. 'StateNext'. But, it is not on the stateMap, and there |
| 20 | + # is no default defined, so it will end up going to StateNext anyway. You |
| 21 | + # must be careful while defining states and their stateMaps and defaults. |
| 22 | + """ |
| 23 | + |
| 24 | + def __init__(self, level, stateMap=None, defState=None): |
| 25 | + """ |
| 26 | + :param int level: each state is mapped to an integer, which is used to sort the states according to that integer. |
| 27 | + :param list stateMap: it is a list (of strings) with the reachable states from this particular status. |
| 28 | + If not defined, we assume there are no restrictions. |
| 29 | + :param str defState: default state used in case the next state is not in stateMap (not defined or simply not there). |
| 30 | + """ |
| 31 | + |
| 32 | + self.level = level |
| 33 | + self.stateMap = stateMap if stateMap else [] |
| 34 | + self.default = defState |
| 35 | + |
| 36 | + def transitionRule(self, nextState): |
| 37 | + """ |
| 38 | + Method that selects next state, knowing the default and the transitions |
| 39 | + map, and the proposed next state. If <nextState> is in stateMap, goes there. |
| 40 | + If not, then goes to <self.default> if any. Otherwise, goes to <nextState> |
| 41 | + anyway. |
| 42 | +
|
| 43 | + examples: |
| 44 | + >>> s0.transitionRule('nextState') |
| 45 | + 'nextState' |
| 46 | + >>> s1.transitionRule('StateName2') |
| 47 | + 'StateName2' |
| 48 | + >>> s1.transitionRule('StateNameNotInMap') |
| 49 | + 'StateName1' |
| 50 | + >>> s2.transitionRule('StateNameNotInMap') |
| 51 | + 'StateNameNotInMap' |
| 52 | +
|
| 53 | + :param str nextState: name of the state in the stateMap |
| 54 | + :return: state name |
| 55 | + :rtype: str |
| 56 | + """ |
| 57 | + |
| 58 | + # If next state is on the list of next states, go ahead. |
| 59 | + if nextState in self.stateMap: |
| 60 | + return nextState |
| 61 | + |
| 62 | + # If not, calculate defaultState: |
| 63 | + # if there is a default, that one |
| 64 | + # otherwise is nextState (states with empty list have no movement restrictions) |
| 65 | + defaultNext = self.default if self.default else nextState |
| 66 | + return defaultNext |
| 67 | + |
| 68 | + |
| 69 | +class StateMachine: |
| 70 | + """ |
| 71 | + StateMachine class that represents the whole state machine with all transitions. |
| 72 | +
|
| 73 | + examples: |
| 74 | + >>> sm0 = StateMachine() |
| 75 | + >>> sm1 = StateMachine(state = 'Active') |
| 76 | +
|
| 77 | + :param state: current state of the StateMachine, could be None if we do not use the |
| 78 | + StateMachine to calculate transitions. Beware, it is not checked if the |
| 79 | + state is on the states map ! |
| 80 | + :type state: None or str |
| 81 | +
|
| 82 | + """ |
| 83 | + |
| 84 | + def __init__(self, state=None): |
| 85 | + """ |
| 86 | + Constructor. |
| 87 | + """ |
| 88 | + |
| 89 | + self.state = state |
| 90 | + # To be overwritten by child classes, unless you like Nirvana state that much. |
| 91 | + self.states = {"Nirvana": State(100)} |
| 92 | + |
| 93 | + def getLevelOfState(self, state): |
| 94 | + """ |
| 95 | + Given a state name, it returns its level (integer), which defines the hierarchy. |
| 96 | +
|
| 97 | + >>> sm0.getLevelOfState('Nirvana') |
| 98 | + 100 |
| 99 | + >>> sm0.getLevelOfState('AnotherState') |
| 100 | + -1 |
| 101 | +
|
| 102 | + :param str state: name of the state, it should be on <self.states> key set |
| 103 | + :return: `int` || -1 (if not in <self.states>) |
| 104 | + """ |
| 105 | + |
| 106 | + if state not in self.states: |
| 107 | + return -1 |
| 108 | + return self.states[state].level |
| 109 | + |
| 110 | + def setState(self, candidateState, noWarn=False, *, logger_warn=None): |
| 111 | + """Makes sure the state is either None or known to the machine, and that it is a valid state to move into. |
| 112 | + Final states are also checked. |
| 113 | +
|
| 114 | + examples: |
| 115 | + >>> sm0.setState(None)['OK'] |
| 116 | + True |
| 117 | + >>> sm0.setState('Nirvana')['OK'] |
| 118 | + True |
| 119 | + >>> sm0.setState('AnotherState')['OK'] |
| 120 | + False |
| 121 | +
|
| 122 | + :param state: state which will be set as current state of the StateMachine |
| 123 | + :type state: None or str |
| 124 | + :return: S_OK || S_ERROR |
| 125 | + """ |
| 126 | + if candidateState == self.state: |
| 127 | + return S_OK(candidateState) |
| 128 | + |
| 129 | + if not candidateState: |
| 130 | + self.state = candidateState |
| 131 | + elif candidateState in self.states: |
| 132 | + if not self.states[self.state].stateMap: |
| 133 | + if not noWarn and logger_warn: |
| 134 | + logger_warn("Final state, won't move", f"({self.state}, asked to move to {candidateState})") |
| 135 | + return S_OK(self.state) |
| 136 | + if candidateState not in self.states[self.state].stateMap and logger_warn: |
| 137 | + logger_warn(f"Can't move from {self.state} to {candidateState}, choosing a good one") |
| 138 | + result = self.getNextState(candidateState) |
| 139 | + if not result["OK"]: |
| 140 | + return result |
| 141 | + self.state = result["Value"] |
| 142 | + # If the StateMachine does not accept the candidate, return error message |
| 143 | + else: |
| 144 | + return S_ERROR(f"setState: {candidateState!r} is not a valid state") |
| 145 | + |
| 146 | + return S_OK(self.state) |
| 147 | + |
| 148 | + def getStates(self): |
| 149 | + """ |
| 150 | + Returns all possible states in the state map |
| 151 | +
|
| 152 | + examples: |
| 153 | + >>> sm0.getStates() |
| 154 | + [ 'Nirvana' ] |
| 155 | +
|
| 156 | + :return: list(stateNames) |
| 157 | + """ |
| 158 | + |
| 159 | + return list(self.states) |
| 160 | + |
| 161 | + def getNextState(self, candidateState): |
| 162 | + """ |
| 163 | + Method that gets the next state, given the proposed transition to candidateState. |
| 164 | + If candidateState is not on the state map <self.states>, it is rejected. If it is |
| 165 | + not the case, we have two options: if <self.state> is None, then the next state |
| 166 | + will be <candidateState>. Otherwise, the current state is using its own |
| 167 | + transition rule to decide. |
| 168 | +
|
| 169 | + examples: |
| 170 | + >>> sm0.getNextState(None) |
| 171 | + S_OK(None) |
| 172 | + >>> sm0.getNextState('NextState') |
| 173 | + S_OK('NextState') |
| 174 | +
|
| 175 | + :param str candidateState: name of the next state |
| 176 | + :return: S_OK(nextState) || S_ERROR |
| 177 | + """ |
| 178 | + if candidateState not in self.states: |
| 179 | + return S_ERROR(f"getNextState: {candidateState!r} is not a valid state") |
| 180 | + |
| 181 | + # FIXME: do we need this anymore ? |
| 182 | + if self.state is None: |
| 183 | + return S_OK(candidateState) |
| 184 | + |
| 185 | + return S_OK(self.states[self.state].transitionRule(candidateState)) |
0 commit comments