1d784ab52bf0e7f1cdcad32127da6460796eeadc
[theonering] / src / gvoice / state_machine.py
1 #!/usr/bin/env python
2
3 import logging
4
5 import util.go_utils as gobject_utils
6 import util.coroutines as coroutines
7 import util.misc as misc_utils
8
9
10 _moduleLogger = logging.getLogger("gvoice.state_machine")
11
12
13 def to_milliseconds(**kwd):
14         if "milliseconds" in kwd:
15                 return kwd["milliseconds"]
16         elif "seconds" in kwd:
17                 return kwd["seconds"] * 1000
18         elif "minutes" in kwd:
19                 return kwd["minutes"] * 1000 * 60
20         elif "hours" in kwd:
21                 return kwd["hours"] * 1000 * 60 * 60
22         raise KeyError("Unknown arg: %r" % kwd)
23
24
25 def to_seconds(**kwd):
26         if "milliseconds" in kwd:
27                 return kwd["milliseconds"] / 1000
28         elif "seconds" in kwd:
29                 return kwd["seconds"]
30         elif "minutes" in kwd:
31                 return kwd["minutes"] * 60
32         elif "hours" in kwd:
33                 return kwd["hours"] * 60 * 60
34         raise KeyError("Unknown arg: %r" % kwd)
35
36
37 class NopStateStrategy(object):
38
39         def __init__(self):
40                 pass
41
42         def initialize_state(self):
43                 pass
44
45         def reinitialize_state(self):
46                 pass
47
48         def increment_state(self):
49                 pass
50
51         @property
52         def timeout(self):
53                 return UpdateStateMachine.INFINITE_PERIOD
54
55         def __repr__(self):
56                 return "NopStateStrategy()"
57
58
59 class ConstantStateStrategy(object):
60
61         def __init__(self, timeout):
62                 assert 0 < timeout or timeout == UpdateStateMachine.INFINITE_PERIOD
63                 self._timeout = timeout
64
65         def initialize_state(self):
66                 pass
67
68         def reinitialize_state(self):
69                 pass
70
71         def increment_state(self):
72                 pass
73
74         @property
75         def timeout(self):
76                 return self._timeout
77
78         def __repr__(self):
79                 return "ConstantStateStrategy(timeout=%r)" % self._timeout
80
81
82 class NTimesStateStrategy(object):
83
84         def __init__(self, timeouts, postTimeout):
85                 assert 0 < len(timeouts)
86                 for timeout in timeouts:
87                         assert 0 < timeout or timeout == UpdateStateMachine.INFINITE_PERIOD
88                 assert 0 < postTimeout or postTimeout == UpdateStateMachine.INFINITE_PERIOD
89                 self._timeouts = timeouts
90                 self._postTimeout = postTimeout
91
92                 self._attemptCount = 0
93
94         def initialize_state(self):
95                 self._attemptCount = len(self._timeouts)
96
97         def reinitialize_state(self):
98                 self._attemptCount = 0
99
100         def increment_state(self):
101                 self._attemptCount += 1
102
103         @property
104         def timeout(self):
105                 try:
106                         return self._timeouts[self._attemptCount]
107                 except IndexError:
108                         return self._postTimeout
109
110         def __repr__(self):
111                 return "NTimesStateStrategy(timeouts=%r, postTimeout=%r)" % (
112                         self._timeouts,
113                         self._postTimeout,
114                 )
115
116
117 class GeometricStateStrategy(object):
118
119         def __init__(self, init, min, max):
120                 assert 0 < init and init < max or init == UpdateStateMachine.INFINITE_PERIOD
121                 assert 0 < min or min == UpdateStateMachine.INFINITE_PERIOD
122                 assert min < max or max == UpdateStateMachine.INFINITE_PERIOD
123                 self._min = min
124                 self._max = max
125                 self._init = init
126                 self._current = 0
127
128         def initialize_state(self):
129                 self._current = self._max
130
131         def reinitialize_state(self):
132                 self._current = self._min
133
134         def increment_state(self):
135                 if self._current == UpdateStateMachine.INFINITE_PERIOD:
136                         pass
137                 if self._init == UpdateStateMachine.INFINITE_PERIOD:
138                         self._current = UpdateStateMachine.INFINITE_PERIOD
139                 elif self._max == UpdateStateMachine.INFINITE_PERIOD:
140                         self._current *= 2
141                 else:
142                         self._current = min(2 * self._current, self._max - self._init)
143
144         @property
145         def timeout(self):
146                 if UpdateStateMachine.INFINITE_PERIOD in (self._init, self._current):
147                         timeout = UpdateStateMachine.INFINITE_PERIOD
148                 else:
149                         timeout = self._init + self._current
150                 return timeout
151
152         def __repr__(self):
153                 return "GeometricStateStrategy(init=%r, min=%r, max=%r)" % (
154                         self._init, self._min, self._max
155                 )
156
157
158 class StateMachine(object):
159
160         STATE_ACTIVE = 0, "active"
161         STATE_IDLE = 1, "idle"
162         STATE_DND = 2, "dnd"
163
164         def start(self):
165                 raise NotImplementedError("Abstract")
166
167         def stop(self):
168                 raise NotImplementedError("Abstract")
169
170         def close(self):
171                 raise NotImplementedError("Abstract")
172
173         def set_state(self, state):
174                 raise NotImplementedError("Abstract")
175
176         @property
177         def state(self):
178                 raise NotImplementedError("Abstract")
179
180
181 class MasterStateMachine(StateMachine):
182
183         def __init__(self):
184                 self._machines = []
185                 self._state = self.STATE_ACTIVE
186
187         def append_machine(self, machine):
188                 self._machines.append(machine)
189
190         def start(self):
191                 # Confirm we are all on the same page
192                 for machine in self._machines:
193                         machine.set_state(self._state)
194                 for machine in self._machines:
195                         machine.start()
196
197         def stop(self):
198                 for machine in self._machines:
199                         machine.stop()
200
201         def close(self):
202                 for machine in self._machines:
203                         machine.close()
204
205         def set_state(self, state):
206                 self._state = state
207                 for machine in self._machines:
208                         machine.set_state(state)
209
210         @property
211         def state(self):
212                 return self._state
213
214
215 class UpdateStateMachine(StateMachine):
216         # Making sure the it is initialized is finicky, be careful
217
218         INFINITE_PERIOD = -1
219         DEFAULT_MAX_TIMEOUT = to_seconds(hours=24)
220
221         _IS_DAEMON = True
222
223         def __init__(self, updateItems, name="", maxTime = DEFAULT_MAX_TIMEOUT):
224                 self._name = name
225                 self._updateItems = updateItems
226                 self._maxTime = maxTime
227
228                 self._state = self.STATE_ACTIVE
229                 self._onTimeout = gobject_utils.Timeout(self._on_timeout)
230
231                 self._strategies = {}
232                 self._callback = coroutines.func_sink(
233                         coroutines.expand_positional(
234                                 self._request_reset_timers
235                         )
236                 )
237
238         def __repr__(self):
239                 return """UpdateStateMachine(
240         name=%r,
241         strategie=%r,
242 )""" % (self._name, self._strategies)
243
244         def set_state_strategy(self, state, strategy):
245                 self._strategies[state] = strategy
246
247         def start(self):
248                 for strategy in self._strategies.itervalues():
249                         strategy.initialize_state()
250                 if self._strategy.timeout != self.INFINITE_PERIOD:
251                         self._onTimeout.start(seconds=0)
252                 _moduleLogger.info("%s Starting State Machine" % (self._name, ))
253
254         def stop(self):
255                 _moduleLogger.info("%s Stopping State Machine" % (self._name, ))
256                 self._onTimeout.cancel()
257
258         def close(self):
259                 self._onTimeout.cancel()
260                 self._callback = None
261
262         def set_state(self, newState):
263                 if self._state == newState:
264                         return
265                 oldState = self._state
266                 _moduleLogger.info("%s Transitioning from %s to %s" % (self._name, oldState, newState))
267
268                 self._state = newState
269                 self._reset_timers(initialize=True)
270
271         @property
272         def state(self):
273                 return self._state
274
275         def reset_timers(self):
276                 self._reset_timers()
277
278         @property
279         def request_reset_timers(self):
280                 return self._callback
281
282         @property
283         def _strategy(self):
284                 return self._strategies[self._state]
285
286         @property
287         def maxTime(self):
288                 return self._maxTime
289
290         @misc_utils.log_exception(_moduleLogger)
291         def _request_reset_timers(self, *args):
292                 self._reset_timers()
293
294         def _reset_timers(self, initialize=False):
295                 if self._onTimeout.is_running():
296                         return # not started yet
297                 _moduleLogger.info("%s Resetting State Machine" % (self._name, ))
298                 self._onTimeout.cancel()
299                 if initialize:
300                         self._strategy.initialize_state()
301                 else:
302                         self._strategy.reinitialize_state()
303                 self._schedule_update()
304
305         def _schedule_update(self):
306                 self._strategy.increment_state()
307                 nextTimeout = self._strategy.timeout
308                 if nextTimeout != self.INFINITE_PERIOD and nextTimeout < self._maxTime:
309                         assert 0 < nextTimeout
310                         self._onTimeout.start(seconds=nextTimeout)
311                         _moduleLogger.info("%s Next update in %s seconds" % (self._name, nextTimeout, ))
312                 else:
313                         _moduleLogger.info("%s No further updates (timeout is %s seconds)" % (self._name, nextTimeout, ))
314
315         @misc_utils.log_exception(_moduleLogger)
316         def _on_timeout(self):
317                 self._schedule_update()
318                 for item in self._updateItems:
319                         try:
320                                 item.update(force=True)
321                         except Exception:
322                                 _moduleLogger.exception("Update failed for %r" % item)