Working around a bug for some and providing more helpful error message for others
[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(__name__)
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 __str__(self):
111                 return "NTimesStateStrategy(timeout=%r)" % (
112                         self.timeout,
113                 )
114
115         def __repr__(self):
116                 return "NTimesStateStrategy(timeouts=%r, postTimeout=%r)" % (
117                         self._timeouts,
118                         self._postTimeout,
119                 )
120
121
122 class GeometricStateStrategy(object):
123
124         def __init__(self, init, min, max):
125                 assert 0 < init and init < max or init == UpdateStateMachine.INFINITE_PERIOD
126                 assert 0 < min or min == UpdateStateMachine.INFINITE_PERIOD
127                 assert min < max or max == UpdateStateMachine.INFINITE_PERIOD
128                 self._min = min
129                 self._max = max
130                 self._init = init
131                 self._current = 0
132
133         def initialize_state(self):
134                 self._current = self._max
135
136         def reinitialize_state(self):
137                 self._current = self._min
138
139         def increment_state(self):
140                 if self._current == UpdateStateMachine.INFINITE_PERIOD:
141                         pass
142                 if self._init == UpdateStateMachine.INFINITE_PERIOD:
143                         self._current = UpdateStateMachine.INFINITE_PERIOD
144                 elif self._max == UpdateStateMachine.INFINITE_PERIOD:
145                         self._current *= 2
146                 else:
147                         self._current = min(2 * self._current, self._max - self._init)
148
149         @property
150         def timeout(self):
151                 if UpdateStateMachine.INFINITE_PERIOD in (self._init, self._current):
152                         timeout = UpdateStateMachine.INFINITE_PERIOD
153                 else:
154                         timeout = self._init + self._current
155                 return timeout
156
157         def __str__(self):
158                 return "GeometricStateStrategy(timeout=%r)" % (
159                         self.timeout
160                 )
161
162         def __repr__(self):
163                 return "GeometricStateStrategy(init=%r, min=%r, max=%r)" % (
164                         self._init, self._min, self._max
165                 )
166
167
168 class StateMachine(object):
169
170         STATE_ACTIVE = 0, "active"
171         STATE_IDLE = 1, "idle"
172         STATE_DND = 2, "dnd"
173
174         def start(self):
175                 raise NotImplementedError("Abstract")
176
177         def stop(self):
178                 raise NotImplementedError("Abstract")
179
180         def close(self):
181                 raise NotImplementedError("Abstract")
182
183         def set_state(self, state):
184                 raise NotImplementedError("Abstract")
185
186         @property
187         def state(self):
188                 raise NotImplementedError("Abstract")
189
190
191 class MasterStateMachine(StateMachine):
192
193         def __init__(self):
194                 self._machines = []
195                 self._state = self.STATE_ACTIVE
196
197         def append_machine(self, machine):
198                 self._machines.append(machine)
199
200         def start(self):
201                 # Confirm we are all on the same page
202                 for machine in self._machines:
203                         machine.set_state(self._state)
204                 for machine in self._machines:
205                         machine.start()
206
207         def stop(self):
208                 for machine in self._machines:
209                         machine.stop()
210
211         def close(self):
212                 for machine in self._machines:
213                         machine.close()
214
215         def set_state(self, state):
216                 self._state = state
217                 for machine in self._machines:
218                         machine.set_state(state)
219
220         @property
221         def state(self):
222                 return self._state
223
224
225 class UpdateStateMachine(StateMachine):
226         # Making sure the it is initialized is finicky, be careful
227
228         INFINITE_PERIOD = -1
229         DEFAULT_MAX_TIMEOUT = to_seconds(hours=24)
230
231         _IS_DAEMON = True
232
233         def __init__(self, updateItems, name="", maxTime = DEFAULT_MAX_TIMEOUT):
234                 self._name = name
235                 self._updateItems = updateItems
236                 self._maxTime = maxTime
237                 self._isActive = False
238
239                 self._state = self.STATE_ACTIVE
240                 self._onTimeout = gobject_utils.Timeout(self._on_timeout)
241
242                 self._strategies = {}
243                 self._callback = coroutines.func_sink(
244                         coroutines.expand_positional(
245                                 self._request_reset_timers
246                         )
247                 )
248
249         def __str__(self):
250                 return """UpdateStateMachine(
251         name=%r,
252         strategie=%s,
253         isActive=%r,
254         isPolling=%r,
255 )""" % (self._name, self._strategy, self._isActive, self._onTimeout.is_running())
256
257         def __repr__(self):
258                 return """UpdateStateMachine(
259         name=%r,
260         strategie=%r,
261 )""" % (self._name, self._strategies)
262
263         def set_state_strategy(self, state, strategy):
264                 self._strategies[state] = strategy
265
266         def start(self):
267                 for strategy in self._strategies.itervalues():
268                         strategy.initialize_state()
269                 if self._strategy.timeout != self.INFINITE_PERIOD:
270                         self._onTimeout.start(seconds=0)
271                 self._isActive = True
272                 _moduleLogger.info("%s Starting State Machine" % (self._name, ))
273
274         def stop(self):
275                 _moduleLogger.info("%s Stopping State Machine" % (self._name, ))
276                 self._isActive = False
277                 self._onTimeout.cancel()
278
279         def close(self):
280                 self._onTimeout.cancel()
281                 self._callback = None
282
283         def set_state(self, newState):
284                 if self._state == newState:
285                         return
286                 oldState = self._state
287                 _moduleLogger.info("%s Transitioning from %s to %s" % (self._name, oldState, newState))
288
289                 self._state = newState
290                 self._reset_timers(initialize=True)
291
292         @property
293         def state(self):
294                 return self._state
295
296         def reset_timers(self, initialize=False):
297                 self._reset_timers(initialize)
298
299         def update_now(self):
300                 if not self._isActive:
301                         return # not started yet
302                 _moduleLogger.info("%s Forcing immediate update of state machine" % (self._name, ))
303                 self._onTimeout.cancel()
304                 self._strategy.reinitialize_state()
305                 self._strategy.increment_state()
306                 nextTimeout = self._strategy.timeout
307                 if nextTimeout != self.INFINITE_PERIOD and nextTimeout < self._maxTime:
308                         nextTimeout = 0
309                         self._onTimeout.start(seconds=nextTimeout)
310                         _moduleLogger.info("%s Marked for update" % (self._name, ))
311                 else:
312                         _moduleLogger.info("%s Disabled, skipping update (timeout is %s seconds)" % (self._name, nextTimeout, ))
313
314         @property
315         def request_reset_timers(self):
316                 return self._callback
317
318         @property
319         def _strategy(self):
320                 return self._strategies[self._state]
321
322         @property
323         def maxTime(self):
324                 return self._maxTime
325
326         @misc_utils.log_exception(_moduleLogger)
327         def _request_reset_timers(self, *args):
328                 self._reset_timers()
329
330         def _reset_timers(self, initialize=False):
331                 if not self._isActive:
332                         return # not started yet
333                 _moduleLogger.info("%s Resetting State Machine" % (self._name, ))
334                 self._onTimeout.cancel()
335                 if initialize:
336                         self._strategy.initialize_state()
337                 else:
338                         self._strategy.reinitialize_state()
339                 self._schedule_update()
340
341         def _schedule_update(self):
342                 self._strategy.increment_state()
343                 nextTimeout = self._strategy.timeout
344                 if nextTimeout != self.INFINITE_PERIOD and nextTimeout < self._maxTime:
345                         assert 0 < nextTimeout
346                         self._onTimeout.start(seconds=nextTimeout)
347                         _moduleLogger.info("%s Next update in %s seconds" % (self._name, nextTimeout, ))
348                 else:
349                         _moduleLogger.info("%s No further updates (timeout is %s seconds)" % (self._name, nextTimeout, ))
350
351         @misc_utils.log_exception(_moduleLogger)
352         def _on_timeout(self):
353                 self._schedule_update()
354                 for item in self._updateItems:
355                         try:
356                                 item.update(force=True)
357                         except Exception:
358                                 _moduleLogger.exception("Update failed for %r" % item)