python: Honor zero probe interval in reconnect.py
[sliver-openvswitch.git] / python / ovs / reconnect.py
index 9048579..f1da930 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2010 Nicira Networks
+# Copyright (c) 2010, 2011, 2012 Nicira Networks
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import logging
 import os
 
+import ovs.vlog
+import ovs.util
+
 # Values returned by Reconnect.run()
 CONNECT = 'connect'
 DISCONNECT = 'disconnect'
 PROBE = 'probe'
 
-EOF = -1
+EOF = ovs.util.EOF
+vlog = ovs.vlog.Vlog("reconnect")
+
 
 class Reconnect(object):
     """A finite-state machine for connecting and reconnecting to a network
@@ -72,7 +76,7 @@ class Reconnect(object):
             return CONNECT
 
     class ConnectInProgress(object):
-        name = "CONNECT_IN_PROGRESS"
+        name = "CONNECTING"
         is_connected = False
 
         @staticmethod
@@ -96,9 +100,9 @@ class Reconnect(object):
 
         @staticmethod
         def run(fsm, now):
-            logging.debug("%s: idle %d ms, sending inactivity probe"
-                          % (fsm.name,
-                             now - max(fsm.last_received, fsm.state_entered)))
+            vlog.dbg("%s: idle %d ms, sending inactivity probe"
+                     % (fsm.name,
+                        now - max(fsm.last_received, fsm.state_entered)))
             fsm._transition(now, Reconnect.Idle)
             return PROBE
 
@@ -108,16 +112,18 @@ class Reconnect(object):
 
         @staticmethod
         def deadline(fsm):
-            return fsm.state_entered + fsm.probe_interval
+            if fsm.probe_interval:
+                return fsm.state_entered + fsm.probe_interval
+            return None
 
         @staticmethod
         def run(fsm, now):
-            logging.error("%s: no response to inactivity probe after %.3g "
-                          "seconds, disconnecting"
-                          % (fsm.name, (now - fsm.state_entered) / 1000.0))
+            vlog.err("%s: no response to inactivity probe after %.3g "
+                     "seconds, disconnecting"
+                      % (fsm.name, (now - fsm.state_entered) / 1000.0))
             return DISCONNECT
 
-    class Reconnect:
+    class Reconnect(object):
         name = "RECONNECT"
         is_connected = False
 
@@ -139,13 +145,14 @@ class Reconnect(object):
         self.max_backoff = 8000
         self.probe_interval = 5000
         self.passive = False
-        self.info_level = logging.info
+        self.info_level = vlog.info
 
         self.state = Reconnect.Void
         self.state_entered = now
         self.backoff = 0
         self.last_received = now
-        self.last_connected = now
+        self.last_connected = None
+        self.last_disconnected = None
         self.max_tries = None
 
         self.creation_time = now
@@ -159,16 +166,16 @@ class Reconnect(object):
         debug level, by default keeping them out of log files.  This is
         appropriate if the connection is one that is expected to be
         short-lived, so that the log messages are merely distracting.
-        
+
         If 'quiet' is false, this object logs informational messages at info
         level.  This is the default.
-        
+
         This setting has no effect on the log level of debugging, warning, or
         error messages."""
         if quiet:
-            self.info_level = logging.debug
+            self.info_level = vlog.dbg
         else:
-            self.info_level = logging.info
+            self.info_level = vlog.info
 
     def get_name(self):
         return self.name
@@ -176,7 +183,7 @@ class Reconnect(object):
     def set_name(self, name):
         """Sets this object's name to 'name'.  If 'name' is None, then "void"
         is used instead.
-        
+
         The name is used in log messages."""
         if name is None:
             self.name = "void"
@@ -234,7 +241,7 @@ class Reconnect(object):
         if (self.state == Reconnect.Backoff and
             self.backoff > self.max_backoff):
                 self.backoff = self.max_backoff
-        
+
     def set_probe_interval(self, probe_interval):
         """Sets the "probe interval" to 'probe_interval', in milliseconds.  If
         this is zero, it disables the connection keepalive feature.  If it is
@@ -259,7 +266,8 @@ class Reconnect(object):
     def set_passive(self, passive, now):
         """Configures this FSM for active or passive mode.  In active mode (the
         default), the FSM is attempting to connect to a remote host.  In
-        passive mode, the FSM is listening for connections from a remote host."""
+        passive mode, the FSM is listening for connections from a remote
+        host."""
         if self.passive != passive:
             self.passive = passive
 
@@ -315,8 +323,8 @@ class Reconnect(object):
             # Report what happened
             if self.state in (Reconnect.Active, Reconnect.Idle):
                 if error > 0:
-                    logging.warning("%s: connection dropped (%s)"
-                                    % (self.name, os.strerror(error)))
+                    vlog.warn("%s: connection dropped (%s)"
+                              % (self.name, os.strerror(error)))
                 elif error == EOF:
                     self.info_level("%s: connection closed by peer"
                                     % self.name)
@@ -324,22 +332,25 @@ class Reconnect(object):
                     self.info_level("%s: connection dropped" % self.name)
             elif self.state == Reconnect.Listening:
                 if error > 0:
-                    logging.warning("%s: error listening for connections (%s)"
-                                    % (self.name, os.strerror(error)))
+                    vlog.warn("%s: error listening for connections (%s)"
+                              % (self.name, os.strerror(error)))
                 else:
                     self.info_level("%s: error listening for connections"
                                     % self.name)
             else:
                 if self.passive:
-                    type = "listen"
+                    type_ = "listen"
                 else:
-                    type = "connection"
+                    type_ = "connection"
                 if error > 0:
-                    logging.warning("%s: %s attempt failed (%s)"
-                                    % (self.name, type, os.strerror(error)))
+                    vlog.warn("%s: %s attempt failed (%s)"
+                              % (self.name, type_, os.strerror(error)))
                 else:
                     self.info_level("%s: %s attempt timed out"
-                                    % (self.name, type))
+                                    % (self.name, type_))
+
+            if (self.state in (Reconnect.Active, Reconnect.Idle)):
+                self.last_disconnected = now
 
             # Back off
             if (self.state in (Reconnect.Active, Reconnect.Idle) and
@@ -382,15 +393,15 @@ class Reconnect(object):
             else:
                 self.info_level("%s: connecting..." % self.name)
             self._transition(now, Reconnect.ConnectInProgress)
-            
+
     def listening(self, now):
         """Tell this FSM that the client is listening for connection attempts.
         This state last indefinitely until the client reports some change.
-        
+
         The natural progression from this state is for the client to report
         that a connection has been accepted or is in progress of being
         accepted, by calling self.connecting() or self.connected().
-        
+
         The client may also report that listening failed (e.g. accept()
         returned an unexpected error such as ENOMEM) by calling
         self.listen_error(), in which case the FSM will back off and eventually
@@ -403,7 +414,7 @@ class Reconnect(object):
     def listen_error(self, now, error):
         """Tell this FSM that the client's attempt to accept a connection
         failed (e.g. accept() returned an unexpected error such as ENOMEM).
-        
+
         If the FSM is currently listening (self.listening() was called), it
         will back off and eventually return ovs.reconnect.CONNECT from
         self.run() to tell the client to try listening again.  If there is an
@@ -452,54 +463,56 @@ class Reconnect(object):
             if connected_before:
                 self.total_connected_duration += now - self.last_connected
             self.seqno += 1
-            
-        logging.debug("%s: entering %s" % (self.name, state.name))
+
+        vlog.dbg("%s: entering %s" % (self.name, state.name))
         self.state = state
         self.state_entered = now
 
     def run(self, now):
         """Assesses whether any action should be taken on this FSM.  The return
         value is one of:
-        
+
             - None: The client need not take any action.
-        
+
             - Active client, ovs.reconnect.CONNECT: The client should start a
               connection attempt and indicate this by calling
               self.connecting().  If the connection attempt has definitely
               succeeded, it should call self.connected().  If the connection
               attempt has definitely failed, it should call
               self.connect_failed().
-        
+
               The FSM is smart enough to back off correctly after successful
               connections that quickly abort, so it is OK to call
               self.connected() after a low-level successful connection
               (e.g. connect()) even if the connection might soon abort due to a
               failure at a high-level (e.g. SSL negotiation failure).
-        
+
             - Passive client, ovs.reconnect.CONNECT: The client should try to
               listen for a connection, if it is not already listening.  It
               should call self.listening() if successful, otherwise
               self.connecting() or reconnected_connect_failed() if the attempt
               is in progress or definitely failed, respectively.
-        
+
               A listening passive client should constantly attempt to accept a
               new connection and report an accepted connection with
               self.connected().
-        
+
             - ovs.reconnect.DISCONNECT: The client should abort the current
               connection or connection attempt or listen attempt and call
               self.disconnected() or self.connect_failed() to indicate it.
-        
+
             - ovs.reconnect.PROBE: The client should send some kind of request
               to the peer that will elicit a response, to ensure that the
               connection is indeed in working order.  (This will only be
               returned if the "probe interval" is nonzero--see
               self.set_probe_interval())."""
-        if now >= self.state.deadline(self):
+
+        deadline = self.state.deadline(self)
+        if deadline is not None and now >= deadline:
             return self.state.run(self, now)
         else:
             return None
-        
+
     def wait(self, poller, now):
         """Causes the next call to poller.block() to wake up when self.run()
         should be called."""
@@ -509,8 +522,8 @@ class Reconnect(object):
 
     def timeout(self, now):
         """Returns the number of milliseconds after which self.run() should be
-        called if nothing else notable happens in the meantime, or a negative
-        number if this is currently unnecessary."""
+        called if nothing else notable happens in the meantime, or None if this
+        is currently unnecessary."""
         deadline = self.state.deadline(self)
         if deadline is not None:
             remaining = deadline - now
@@ -525,14 +538,21 @@ class Reconnect(object):
         False otherwise."""
         return self.state.is_connected
 
-    def get_connection_duration(self, now):
-        """Returns the number of milliseconds for which this FSM has been
-        continuously connected to its peer.  (If this FSM is not currently
-        connected, this is 0.)"""
-        if self.is_connected():
+    def get_last_connect_elapsed(self, now):
+        """Returns the number of milliseconds since 'fsm' was last connected
+        to its peer. Returns None if never connected."""
+        if self.last_connected:
             return now - self.last_connected
         else:
-            return 0
+            return None
+
+    def get_last_disconnect_elapsed(self, now):
+        """Returns the number of milliseconds since 'fsm' was last disconnected
+        from its peer. Returns None if never disconnected."""
+        if self.last_disconnected:
+            return now - self.last_disconnected
+        else:
+            return None
 
     def get_stats(self, now):
         class Stats(object):
@@ -540,13 +560,17 @@ class Reconnect(object):
         stats = Stats()
         stats.creation_time = self.creation_time
         stats.last_connected = self.last_connected
+        stats.last_disconnected = self.last_disconnected
         stats.last_received = self.last_received
         stats.backoff = self.backoff
         stats.seqno = self.seqno
         stats.is_connected = self.is_connected()
-        stats.current_connection_duration = self.get_connection_duration(now)
-        stats.total_connected_duration = (stats.current_connection_duration +
-                                          self.total_connected_duration)
+        stats.msec_since_connect = self.get_last_connect_elapsed(now)
+        stats.msec_since_disconnect = self.get_last_disconnect_elapsed(now)
+        stats.total_connected_duration = self.total_connected_duration
+        if self.is_connected():
+            stats.total_connected_duration += (
+                    self.get_last_connect_elapsed(now))
         stats.n_attempted_connections = self.n_attempted_connections
         stats.n_successful_connections = self.n_successful_connections
         stats.state = self.state.name