0a04e074a843f5ebd3ea719c029046812f7dd3ff
[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 __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._isRunning = 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         isRunning=%r,
254 )""" % (self._name, self._strategy, self._onTimeout.is_running())
255
256         def __repr__(self):
257                 return """UpdateStateMachine(
258         name=%r,
259         strategie=%r,
260 )""" % (self._name, self._strategies)
261
262         def set_state_strategy(self, state, strategy):
263                 self._strategies[state] = strategy
264
265         def start(self):
266                 for strategy in self._strategies.itervalues():
267                         strategy.initialize_state()
268                 if self._strategy.timeout != self.INFINITE_PERIOD:
269                         self._onTimeout.start(seconds=0)
270                 self._isRunning = True
271                 _moduleLogger.info("%s Starting State Machine" % (self._name, ))
272
273         def stop(self):
274                 _moduleLogger.info("%s Stopping State Machine" % (self._name, ))
275                 self._isRunning = False
276                 self._onTimeout.cancel()
277
278         def close(self):
279                 self._onTimeout.cancel()
280                 self._callback = None
281
282         def set_state(self, newState):
283                 if self._state == newState:
284                         return
285                 oldState = self._state
286                 _moduleLogger.info("%s Transitioning from %s to %s" % (self._name, oldState, newState))
287
288                 self._state = newState
289                 self._reset_timers(initialize=True)
290
291         @property
292         def state(self):
293                 return self._state
294
295         def reset_timers(self):
296                 self._reset_timers()
297
298         @property
299         def request_reset_timers(self):
300                 return self._callback
301
302         @property
303         def _strategy(self):
304                 return self._strategies[self._state]
305
306         @property
307         def maxTime(self):
308                 return self._maxTime
309
310         @misc_utils.log_exception(_moduleLogger)
311         def _request_reset_timers(self, *args):
312                 self._reset_timers()
313
314         def _reset_timers(self, initialize=False):
315                 if not self._isRunning:
316                         return # not started yet
317                 _moduleLogger.info("%s Resetting State Machine" % (self._name, ))
318                 self._onTimeout.cancel()
319                 if initialize:
320                         self._strategy.initialize_state()
321                 else:
322                         self._strategy.reinitialize_state()
323                 self._schedule_update()
324
325         def _schedule_update(self):
326                 self._strategy.increment_state()
327                 nextTimeout = self._strategy.timeout
328                 if nextTimeout != self.INFINITE_PERIOD and nextTimeout < self._maxTime:
329                         assert 0 < nextTimeout
330                         self._onTimeout.start(seconds=nextTimeout)
331                         _moduleLogger.info("%s Next update in %s seconds" % (self._name, nextTimeout, ))
332                 else:
333                         _moduleLogger.info("%s No further updates (timeout is %s seconds)" % (self._name, nextTimeout, ))
334
335         @misc_utils.log_exception(_moduleLogger)
336         def _on_timeout(self):
337                 self._schedule_update()
338                 for item in self._updateItems:
339                         try:
340                                 item.update(force=True)
341                         except Exception:
342                                 _moduleLogger.exception("Update failed for %r" % item)