Lots of work to try and get duplex conversations going plus disabled cookies
[theonering] / src / gvoice / state_machine.py
1 #!/usr/bin/env python
2
3 """
4 @todo Look into switching from POLL_TIME = min(F * 2^n, MAX) to POLL_TIME = min(CONST + F * 2^n, MAX)
5 @todo Look into supporting more states that have a different F and MAX
6 """
7
8 import Queue
9 import threading
10 import logging
11
12 import gobject
13
14 import util.algorithms as algorithms
15 import util.coroutines as coroutines
16
17
18 _moduleLogger = logging.getLogger("gvoice.state_machine")
19
20
21 def _to_milliseconds(**kwd):
22         if "milliseconds" in kwd:
23                 return kwd["milliseconds"]
24         elif "seconds" in kwd:
25                 return kwd["seconds"] * 1000
26         elif "minutes" in kwd:
27                 return kwd["minutes"] * 1000 * 60
28         raise KeyError("Unknown arg: %r" % kwd)
29
30
31 class StateMachine(object):
32
33         STATE_ACTIVE = "active"
34         STATE_IDLE = "idle"
35         STATE_DND = "dnd"
36
37         _ACTION_UPDATE = "update"
38         _ACTION_RESET = "reset"
39         _ACTION_STOP = "stop"
40
41         _INITIAL_ACTIVE_PERIOD = int(_to_milliseconds(seconds=5))
42         _FINAL_ACTIVE_PERIOD = int(_to_milliseconds(minutes=2))
43         _IDLE_PERIOD = int(_to_milliseconds(minutes=10))
44         _INFINITE_PERIOD = -1
45
46         _IS_DAEMON = True
47
48         def __init__(self, initItems, updateItems):
49                 self._initItems = initItems
50                 self._updateItems = updateItems
51
52                 self._actions = Queue.Queue()
53                 self._state = self.STATE_ACTIVE
54                 self._timeoutId = None
55                 self._thread = None
56                 self._currentPeriod = self._INITIAL_ACTIVE_PERIOD
57                 self._set_initial_period()
58
59         def start(self):
60                 assert self._thread is None
61                 self._thread = threading.Thread(target=self._run)
62                 self._thread.setDaemon(self._IS_DAEMON)
63                 self._thread.start()
64
65         def stop(self):
66                 if self._thread is not None:
67                         self._actions.put(self._ACTION_STOP)
68                         self._thread = None
69                 else:
70                         _moduleLogger.info("Stopping an already stopped state machine")
71
72         def set_state(self, state):
73                 self._state = state
74                 self.reset_timers()
75
76         def get_state(self):
77                 return self._state
78
79         def reset_timers(self):
80                 self._actions.put(self._ACTION_RESET)
81
82         @coroutines.func_sink
83         def request_reset_timers(self, args):
84                 self.reset_timers()
85
86         def _run(self):
87                 logging.basicConfig(level=logging.DEBUG)
88                 _moduleLogger.info("Starting State Machine")
89                 for item in self._initItems:
90                         try:
91                                 item.update()
92                         except Exception:
93                                 _moduleLogger.exception("Initial update failed for %r" % item)
94
95                 # empty the task queue
96                 actions = list(algorithms.itr_available(self._actions, initiallyBlock = False))
97                 self._schedule_update()
98                 if len(self._updateItems) == 0:
99                         self.stop()
100
101                 while True:
102                         # block till we get a task, or get all the tasks if we were late 
103                         actions = list(algorithms.itr_available(self._actions, initiallyBlock = True))
104
105                         if self._ACTION_STOP in actions:
106                                 _moduleLogger.info("Requested to stop")
107                                 self._stop_update()
108                                 break
109                         elif self._ACTION_RESET in actions:
110                                 _moduleLogger.info("Reseting timers")
111                                 self._reset_timers()
112                         elif self._ACTION_UPDATE in actions:
113                                 _moduleLogger.info("Update")
114                                 for item in self._updateItems:
115                                         try:
116                                                 item.update(force=True)
117                                         except Exception:
118                                                 _moduleLogger.exception("Update failed for %r" % item)
119                                 self._schedule_update()
120
121         def _set_initial_period(self):
122                 self._currentPeriod = self._INITIAL_ACTIVE_PERIOD / 2 # We will double it later
123
124         def _reset_timers(self):
125                 self._stop_update()
126                 self._set_initial_period()
127                 self._schedule_update()
128
129         def _schedule_update(self):
130                 nextTimeout = self._calculate_step(self._state, self._currentPeriod)
131                 nextTimeout = int(nextTimeout)
132                 if nextTimeout != self._INFINITE_PERIOD:
133                         self._timeoutId = gobject.timeout_add(nextTimeout, self._on_timeout)
134                 self._currentPeriod = nextTimeout
135
136         def _stop_update(self):
137                 if self._timeoutId is None:
138                         return
139                 gobject.source_remove(self._timeoutId)
140                 self._timeoutId = None
141
142         def _on_timeout(self):
143                 self._actions.put(self._ACTION_UPDATE)
144                 return False # do not continue
145
146         @classmethod
147         def _calculate_step(cls, state, period):
148                 if state == cls.STATE_ACTIVE:
149                         return min(period * 2, cls._FINAL_ACTIVE_PERIOD)
150                 elif state == cls.STATE_IDLE:
151                         return cls._IDLE_PERIOD
152                 elif state == cls.STATE_DND:
153                         return cls._INFINITE_PERIOD
154                 else:
155                         raise RuntimeError("Unknown state: %r" % (state, ))