Switching to multiple state machines
authorEd Page <eopage@byu.net>
Fri, 8 Jan 2010 02:58:20 +0000 (20:58 -0600)
committerEd Page <eopage@byu.net>
Fri, 8 Jan 2010 02:58:20 +0000 (20:58 -0600)
hand_tests/sm.py
src/channel/debug_prompt.py
src/channel/text.py
src/gvoice/session.py
src/gvoice/state_machine.py
src/simple_presence.py

index 0fac9da..2d3b614 100755 (executable)
@@ -42,12 +42,26 @@ def main():
        try:
                state_machine.StateMachine._IS_DAEMON = False
 
-               initial = _I(startTime)
-               print "Initial:", initial
                regular = _I(startTime)
                print "Regular:", regular
 
-               sm = state_machine.StateMachine([initial], [regular])
+               sm = state_machine.UpdateStateMachine([regular])
+               sm.set_state_strategy(
+                       state_machine.StateMachine.STATE_DND,
+                       state_machine.NopStateStrategy(),
+               )
+               sm.set_state_strategy(
+                       state_machine.StateMachine.STATE_IDLE,
+                       state_machine.ConstantStateStrategy(state_machine.to_milliseconds(seconds=30)),
+               )
+               sm.set_state_strategy(
+                       state_machine.StateMachine.STATE_ACTIVE,
+                       state_machine.GeometricStateStrategy(
+                               state_machine.to_milliseconds(seconds=3),
+                               state_machine.to_milliseconds(seconds=3),
+                               state_machine.to_milliseconds(seconds=20),
+                       ),
+               )
                print "Starting", datetime.datetime.now() - startTime
                sm.start()
                time.sleep(60.0) # seconds
@@ -64,7 +78,4 @@ def main():
 
 
 if __name__ == "__main__":
-       print state_machine.StateMachine._INITIAL_ACTIVE_PERIOD
-       print state_machine.StateMachine._FINAL_ACTIVE_PERIOD
-       print state_machine.StateMachine._IDLE_PERIOD
        main()
index bc0ea38..a491212 100644 (file)
@@ -78,7 +78,8 @@ class DebugPromptChannel(telepathy.server.ChannelTypeText, cmd.Cmd):
                        return
 
                try:
-                       self._conn.session.stateMachine.reset_timers()
+                       for machine in self._conn.session.stateMachine._machines:
+                               machine.reset_timers()
                except Exception, e:
                        self._report_new_message(str(e))
 
@@ -88,7 +89,7 @@ class DebugPromptChannel(telepathy.server.ChannelTypeText, cmd.Cmd):
                        return
 
                try:
-                       state = self._conn.session.stateMachine.get_state()
+                       state = self._conn.session.stateMachine.state
                        self._report_new_message(str(state))
                except Exception, e:
                        self._report_new_message(str(e))
index b1a4af3..4eb601d 100644 (file)
@@ -56,7 +56,7 @@ class TextChannel(telepathy.server.ChannelTypeText):
                        raise telepathy.errors.NotImplemented("Unhandled message type: %r" % messageType)
 
                self._conn.session.backend.send_sms(self._otherHandle.phoneNumber, text)
-               self._conn.session.stateMachine.reset_timers()
+               self._conn.session.conversationsStateMachine.reset_timers()
 
                self.Sent(int(time.time()), messageType, text)
 
index a890ac9..adb551e 100644 (file)
@@ -18,19 +18,54 @@ class Session(object):
                self._password = None
 
                self._backend = backend.GVoiceBackend(cookiePath)
+
                self._addressbook = addressbook.Addressbook(self._backend)
+               self._addressbookStateMachine = state_machine.UpdateStateMachine([self.addressbook])
+               self._addressbookStateMachine.set_state_strategy(
+                       state_machine.StateMachine.STATE_DND,
+                       state_machine.NopStateStrategy()
+               )
+               self._addressbookStateMachine.set_state_strategy(
+                       state_machine.StateMachine.STATE_IDLE,
+                       state_machine.ConstantStateStrategy(state_machine.to_milliseconds(hours=6))
+               )
+               self._addressbookStateMachine.set_state_strategy(
+                       state_machine.StateMachine.STATE_ACTIVE,
+                       state_machine.ConstantStateStrategy(state_machine.to_milliseconds(hours=1))
+               )
+
                self._conversations = conversations.Conversations(self._backend)
-               self._stateMachine = state_machine.StateMachine([self.addressbook], [self.conversations])
+               self._conversationsStateMachine = state_machine.UpdateStateMachine([self.conversations])
+               self._conversationsStateMachine.set_state_strategy(
+                       state_machine.StateMachine.STATE_DND,
+                       state_machine.NopStateStrategy()
+               )
+               self._conversationsStateMachine.set_state_strategy(
+                       state_machine.StateMachine.STATE_IDLE,
+                       state_machine.ConstantStateStrategy(state_machine.to_milliseconds(minutes=30))
+               )
+               self._conversationsStateMachine.set_state_strategy(
+                       state_machine.StateMachine.STATE_ACTIVE,
+                       state_machine.GeometricStateStrategy(
+                               state_machine.to_milliseconds(seconds=10),
+                               state_machine.to_milliseconds(seconds=1),
+                               state_machine.to_milliseconds(minutes=10),
+                       )
+               )
+
+               self._masterStateMachine = state_machine.MasterStateMachine()
+               self._masterStateMachine.append_machine(self._addressbookStateMachine)
+               self._masterStateMachine.append_machine(self._conversationsStateMachine)
 
                self._conversations.updateSignalHandler.register_sink(
-                       self._stateMachine.request_reset_timers
+                       self._conversationsStateMachine.request_reset_timers
                )
 
        def close(self):
                self._conversations.updateSignalHandler.unregister_sink(
-                       self._stateMachine.request_reset_timers
+                       self._conversationsStateMachine.request_reset_timers
                )
-               self._stateMachine.close()
+               self._masterStateMachine.close()
 
        def login(self, username, password):
                self._username = username
@@ -38,10 +73,10 @@ class Session(object):
                if not self._backend.is_authed():
                        self._backend.login(self._username, self._password)
 
-               self._stateMachine.start()
+               self._masterStateMachine.start()
 
        def logout(self):
-               self._stateMachine.stop()
+               self._masterStateMachine.stop()
                self._backend.logout()
 
                self._username = None
@@ -90,4 +125,12 @@ class Session(object):
 
        @property
        def stateMachine(self):
-               return self._stateMachine
+               return self._masterStateMachine
+
+       @property
+       def addressbookStateMachine(self):
+               return self._addressbookStateMachine
+
+       @property
+       def conversationsStateMachine(self):
+               return self._conversationsStateMachine
index 4a4211a..d26a57c 100644 (file)
@@ -17,72 +17,180 @@ import gtk_toolbox
 _moduleLogger = logging.getLogger("gvoice.state_machine")
 
 
-def _to_milliseconds(**kwd):
+def to_milliseconds(**kwd):
        if "milliseconds" in kwd:
                return kwd["milliseconds"]
        elif "seconds" in kwd:
                return kwd["seconds"] * 1000
        elif "minutes" in kwd:
                return kwd["minutes"] * 1000 * 60
+       elif "hours" in kwd:
+               return kwd["hours"] * 1000 * 60 * 60
        raise KeyError("Unknown arg: %r" % kwd)
 
 
+class NopStateStrategy(object):
+
+       def __init__(self):
+               pass
+
+       def initialize_state(self):
+               pass
+
+       def increment_state(self):
+               pass
+
+       @property
+       def timeout(self):
+               return UpdateStateMachine.INFINITE_PERIOD
+
+
+class ConstantStateStrategy(object):
+
+       def __init__(self, timeout):
+               assert 0 < timeout or timeout == UpdateStateMachine.INFINITE_PERIOD
+               self._timeout = timeout
+
+       def initialize_state(self):
+               pass
+
+       def increment_state(self):
+               pass
+
+       @property
+       def timeout(self):
+               return self._timeout
+
+
+class GeometricStateStrategy(object):
+
+       def __init__(self, init, min, max):
+               assert 0 < init or init == UpdateStateMachine.INFINITE_PERIOD
+               assert 0 < min or min == UpdateStateMachine.INFINITE_PERIOD
+               assert min < max or max == UpdateStateMachine.INFINITE_PERIOD
+               self._min = min
+               self._max = max
+               self._init = init
+               self._current = min / 2
+
+       def initialize_state(self):
+               self._current = self._min / 2
+
+       def increment_state(self):
+               if self._max == UpdateStateMachine.INFINITE_PERIOD:
+                       self._current *= 2
+               else:
+                       self._current = min(2 * self._current, self._max - self._init)
+
+       @property
+       def timeout(self):
+               return self._init + self._current
+
+
 class StateMachine(object):
 
        STATE_ACTIVE = 0, "active"
        STATE_IDLE = 1, "idle"
        STATE_DND = 2, "dnd"
 
-       _ACTION_UPDATE = "update"
-       _ACTION_RESET = "reset"
-       _ACTION_STOP = "stop"
+       def start(self):
+               raise NotImplementedError("Abstract")
+
+       def stop(self):
+               raise NotImplementedError("Abstract")
+
+       def close(self):
+               raise NotImplementedError("Abstract")
+
+       def set_state(self, state):
+               raise NotImplementedError("Abstract")
+
+       @property
+       def state(self):
+               raise NotImplementedError("Abstract")
+
 
-       _INITIAL_ACTIVE_PERIOD = int(_to_milliseconds(seconds=10))
-       _FINAL_ACTIVE_PERIOD = int(_to_milliseconds(minutes=10))
-       _IDLE_PERIOD = int(_to_milliseconds(minutes=30))
-       _INFINITE_PERIOD = -1
+class MasterStateMachine(StateMachine):
+
+       def __init__(self):
+               self._machines = []
+               self._state = self.STATE_ACTIVE
+
+       def append_machine(self, machine):
+               self._machines.append(machine)
+
+       def start(self):
+               # Confirm we are all on the same page
+               for machine in self._machines:
+                       machine.set_state(self._state)
+               for machine in self._machines:
+                       machine.start()
+
+       def stop(self):
+               for machine in self._machines:
+                       machine.stop()
+
+       def close(self):
+               for machine in self._machines:
+                       machine.close()
+
+       def set_state(self, state):
+               self._state = state
+               for machine in self._machines:
+                       machine.set_state(state)
+
+       @property
+       def state(self):
+               return self._state
+
+
+class UpdateStateMachine(StateMachine):
+       # Making sure the it is initialized is finicky, be careful
+
+       INFINITE_PERIOD = -1
 
        _IS_DAEMON = True
 
-       def __init__(self, initItems, updateItems):
-               self._initItems = initItems
+       def __init__(self, updateItems):
                self._updateItems = updateItems
 
                self._state = self.STATE_ACTIVE
-               self._startId = None
                self._timeoutId = None
-               self._currentPeriod = self._INITIAL_ACTIVE_PERIOD
-               self._set_initial_period()
 
+               self._strategies = {}
                self._callback = coroutines.func_sink(
                        coroutines.expand_positional(
                                self._request_reset_timers
                        )
                )
 
-       def close(self):
-               self._callback = None
+       def set_state_strategy(self, state, strategy):
+               self._strategies[state] = strategy
 
        def start(self):
-               assert self._startId is None
                assert self._timeoutId is None
-               self._startId = gobject.idle_add(self._start)
+               for strategy in self._strategies.itervalues():
+                       strategy.initialize_state()
+               self._timeoutId = gobject.idle_add(self._on_timeout)
+               _moduleLogger.info("%s Starting State Machine" % (self._name, ))
 
        def stop(self):
-               if self._startId is not None:
-                       _moduleLogger.info("Stopping state machine before it even had a chance to start")
-                       gobject.source_remove(self._startId)
-                       self._startId = None
                self._stop_update()
 
+       def close(self):
+               self._callback = None
+
        def set_state(self, newState):
+               if self._state == newState:
+                       return
                oldState = self._state
-               _moduleLogger.info("Transitioning from %s to %s" % (oldState, newState))
+               _moduleLogger.info("%s Transitioning from %s to %s" % (self._name, oldState, newState))
 
                self._state = newState
                self._reset_timers()
 
-       def get_state(self):
+       @property
+       def state(self):
                return self._state
 
        def reset_timers(self):
@@ -92,6 +200,14 @@ class StateMachine(object):
        def request_reset_timers(self):
                return self._callback
 
+       @property
+       def _strategy(self):
+               return self._strategies[self._state]
+
+       @property
+       def _name(self):
+               return "/".join(type(s).__name__ for s in self._updateItems)
+
        @gobject_utils.async
        @gtk_toolbox.log_exception(_moduleLogger)
        def _request_reset_timers(self, *args):
@@ -102,27 +218,14 @@ class StateMachine(object):
 
        def _schedule_update(self):
                assert self._timeoutId is None
-               nextTimeout = self._calculate_step(self._state, self._currentPeriod)
-               nextTimeout = int(nextTimeout)
-               if nextTimeout != self._INFINITE_PERIOD:
+               self._strategy.increment_state()
+               nextTimeout = self._strategy.timeout
+               if nextTimeout != self.INFINITE_PERIOD:
                        self._timeoutId = gobject.timeout_add(nextTimeout, self._on_timeout)
-               _moduleLogger.info("Next update in %s ms" % (nextTimeout, ))
-               self._currentPeriod = nextTimeout
-
-       def _start(self):
-               _moduleLogger.info("Starting State Machine")
-               for item in self._initItems:
-                       try:
-                               item.update()
-                       except Exception:
-                               _moduleLogger.exception("Initial update failed for %r" % item)
-               self._schedule_update()
-               self._startId = None
-               return False # do not continue
+               _moduleLogger.info("%s Next update in %s ms" % (self._name, nextTimeout, ))
 
        def _stop_update(self):
                if self._timeoutId is None:
-                       _moduleLogger.info("Stopping an already stopped state machine")
                        return
                gobject.source_remove(self._timeoutId)
                self._timeoutId = None
@@ -131,11 +234,11 @@ class StateMachine(object):
                if self._timeoutId is None:
                        return # not started yet
                self._stop_update()
-               self._set_initial_period()
+               self._strategy.initialize_state()
                self._schedule_update()
 
        def _on_timeout(self):
-               _moduleLogger.info("Update")
+               _moduleLogger.info("%s Update" % (self._name))
                for item in self._updateItems:
                        try:
                                item.update(force=True)
@@ -144,14 +247,3 @@ class StateMachine(object):
                self._timeoutId = None
                self._schedule_update()
                return False # do not continue
-
-       @classmethod
-       def _calculate_step(cls, state, period):
-               if state == cls.STATE_ACTIVE:
-                       return min(period * 2, cls._FINAL_ACTIVE_PERIOD)
-               elif state == cls.STATE_IDLE:
-                       return cls._IDLE_PERIOD
-               elif state == cls.STATE_DND:
-                       return cls._INFINITE_PERIOD
-               else:
-                       raise RuntimeError("Unknown state: %r" % (state, ))
index 49941f8..7cad575 100644 (file)
@@ -56,7 +56,7 @@ class TheOneRingPresence(object):
                                if isDnd:
                                        presence = TheOneRingPresence.HIDDEN
                                else:
-                                       state = self.session.stateMachine.get_state()
+                                       state = self.session.stateMachine.state
                                        if state == state_machine.StateMachine.STATE_ACTIVE:
                                                presence = TheOneRingPresence.ONLINE
                                        elif state == state_machine.StateMachine.STATE_IDLE: