Skip to content

Commit 365c718

Browse files
chrisburrclaude
andcommitted
refactor: migrate TimeUtilities, JobStatus, and StateMachine to DIRACCommon
Migrate three core modules from DIRAC to DIRACCommon to enable shared usage: - Move TimeUtilities.py with gLogger references to DIRACCommon/Core/Utilities/ - Move StateMachine.py with State and StateMachine classes to DIRACCommon/Core/Utilities/ - Move JobStatus.py with constants and JobsStateMachine to DIRACCommon/WorkloadManagementSystem/Client/ DIRAC modules now re-export from DIRACCommon for backward compatibility. Update JobStatusUtility.py imports to use migrated DIRACCommon modules. Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent fdd1fc8 commit 365c718

File tree

8 files changed

+576
-528
lines changed

8 files changed

+576
-528
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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

Comments
 (0)