Add configurabel heartbeat intervals
authorMichael J. Rubinsky <mrubinsk@horde.org>
Sun, 18 Apr 2010 16:37:57 +0000 (12:37 -0400)
committerMichael J. Rubinsky <mrubinsk@horde.org>
Sun, 18 Apr 2010 16:37:57 +0000 (12:37 -0400)
framework/ActiveSync/lib/Horde/ActiveSync/Driver/Base.php
framework/ActiveSync/lib/Horde/ActiveSync/Request/Ping.php
framework/ActiveSync/lib/Horde/ActiveSync/State/File.php
framework/ActiveSync/lib/Horde/ActiveSync/State/History.php
framework/ActiveSync/lib/Horde/ActiveSync/Wbxml.php
horde/config/conf.xml
horde/rpc.php

index 3e5cf06..7c2d451 100644 (file)
@@ -164,6 +164,16 @@ abstract class Horde_ActiveSync_Driver_Base
     }
 
     /**
+     * Obtain the ping heartbeat settings
+     *
+     * @return array
+     */
+    public function getHeartbeatConfig()
+    {
+        return $this->_params['ping'];
+    }
+
+    /**
      * Get folder stat
      *  "id" => The server ID that will be used to identify the folder.
      *          It must be unique, and not too long. How long exactly is not
index 38afc26..436477c 100644 (file)
@@ -31,7 +31,7 @@ class Horde_ActiveSync_Request_Ping extends Horde_ActiveSync_Request_Base
     // Ping
     const PING = 'Ping:Ping';
     const STATUS = 'Ping:Status';
-    const LIFETIME =  'Ping:LifeTime';
+    const HEARTBEATINTERVAL =  'Ping:HeartbeatInterval';
     const FOLDERS =  'Ping:Folders';
     const FOLDER =  'Ping:Folder';
     const SERVERENTRYID =  'Ping:ServerEntryId';
@@ -50,11 +50,15 @@ class Horde_ActiveSync_Request_Ping extends Horde_ActiveSync_Request_Base
      */
     public function handle(Horde_ActiveSync $activeSync, $devId)
     {
+        $now = time();
         parent::handle($activeSync, $devId);
 
-        // FIXME
-        $timeout = 3;
-        $this->_logger->info('[' . $devId . '] Ping received.');
+        /* Get the settings for the server */
+        $ping_settings = $this->_driver->getHeartbeatConfig();
+        $timeout = $ping_settings['waitinterval'];
+
+        /* Notify */
+        $this->_logger->info('[' . $devId . '] Ping received at timestamp: ' . $now . '.');
 
         /* Glass half full kinda guy... */
         $this->_statusCode = self::STATUS_NOCHANGES;
@@ -66,13 +70,39 @@ class Horde_ActiveSync_Request_Ping extends Horde_ActiveSync_Request_Base
          * we read in the PING request since the PING request is allowed to omit
          * sections if they have been sent previously */
         $collections = array_values($state->initPingState($this->_devId));
-        $lifetime = $state->getPingLifetime();
+        $lifetime = $state->getHeartbeatInterval();
+        if ($lifetime !== 0 && $lifetime < $ping_settings['heartbeatmin']) {
+            $this->_statusCode = self::STATUS_HBOUTOFBOUNDS;
+            $lifetime = $ping_settings['heartbeatmin'];
+        } elseif ($lifetime > $ping_settings['heartbeatmax']) {
+            $this->_statusCode = self::STATUS_HBOUTOFBOUNDS;
+            $lifetime = $ping_settings['heartbeatmax'];
+        }
+        if ($this->_statusCode == self::STATUS_HBOUTOFBOUNDS) {
+            $state->resetPingState();
+            $this->_handleHBError($lifetime);
+            $state->savePingState();
+            return false;
+        }
 
         /* Build the $collections array if we receive request from PIM */
         if ($this->_decoder->getElementStartTag(self::PING)) {
-            if ($this->_decoder->getElementStartTag(self::LIFETIME)) {
+            if ($this->_decoder->getElementStartTag(self::HEARTBEATINTERVAL)) {
                 $lifetime = $this->_decoder->getElementContent();
-                $state->setPingLifetime($lifetime);
+                if ($lifetime !== 0 && $lifetime < $ping_settings['heartbeatmin']) {
+                    $this->_statusCode = self::STATUS_HBOUTOFBOUNDS;
+                    $lifetime = $ping_settings['heartbeatmin'];
+                } elseif ($lifetime > $ping_settings['heartbeatmax']) {
+                    $this->_statusCode = self::STATUS_HBOUTOFBOUNDS;
+                    $lifetime = $ping_settings['heartbeatmax'];
+                }
+                if ($this->_statusCode == self::STATUS_HBOUTOFBOUNDS) {
+                    $state->resetPingState();
+                    $this->_handleHBError($lifetime);
+                    $state->savePingState();
+                    return false;
+                }
+                $state->setHeartbeatInterval($lifetime);
                 $this->_decoder->getElementEndTag();
             }
 
@@ -110,11 +140,11 @@ class Horde_ActiveSync_Request_Ping extends Horde_ActiveSync_Request_Base
 
         /* Start waiting for changes, but only if we don't have any errors */
         if ($this->_statusCode == self::STATUS_NOCHANGES) {
-            $this->_logger->info(sprintf('[%s] Waiting for changes (lifetime: %d)', $this->_devId, $lifetime));
-            // FIXME
-            //for ($n = 0; $n < $lifetime / $timeout; $n++) {
-            for ($n = 0; $n < 10; $n++) {
-                //check the remote wipe status
+            $this->_logger->info(sprintf('[%s] Waiting for changes (heartbeat interval: %d)', $this->_devId, $lifetime));
+            $expire = $now + $lifetime;
+            while (time() <= $expire) {
+                /* Check the remote wipe status and request a foldersync if
+                 * we want the device wiped. */
                 if ($this->_provisioning === true) {
                     $rwstatus = $state->getDeviceRWStatus($this->_devId);
                     if ($rwstatus == Horde_ActiveSync::PROVISION_RWSTATUS_PENDING || $rwstatus == Horde_ActiveSync::PROVISION_RWSTATUS_WIPED) {
@@ -131,8 +161,6 @@ class Horde_ActiveSync_Request_Ping extends Horde_ActiveSync_Request_Base
 
                 for ($i = 0; $i < count($collections); $i++) {
                     $collection = $collections[$i];
-                    // Make sure we have the synckey (which is the devid for
-                    // PING requests.
                     $collection['synckey'] = $this->_devId;
                     $sync = $this->_driver->getSyncObject();
                     $state->loadPingCollectionState($collection);
@@ -141,7 +169,6 @@ class Horde_ActiveSync_Request_Ping extends Horde_ActiveSync_Request_Base
                     } catch (Horde_ActiveSync_Exception $e) {
                         /* Stop ping if exporter cannot be configured */
                         $this->_logger->err('Ping error: Exporter can not be configured. Waiting 30 seconds before ping is retried.');
-                        $n = $lifetime/ $timeout;
                         sleep(30);
                         break;
                     }
@@ -153,8 +180,8 @@ class Horde_ActiveSync_Request_Ping extends Horde_ActiveSync_Request_Base
                         $this->_statusCode = self::STATUS_NEEDSYNC;
                     }
 
-                    // Update the state, but don't bother with the backend since we
-                    // are not updating any data.
+                    /* Update the state, but don't bother with the backend since we
+                     * are not updating any data.*/
                     while (is_array($sync->syncronize(Horde_ActiveSync::BACKEND_DISCARD_DATA)));
                 }
 
@@ -162,9 +189,12 @@ class Horde_ActiveSync_Request_Ping extends Horde_ActiveSync_Request_Base
                     $this->_logger->info('[' . $this->_devId . '] Changes available');
                     break;
                 }
+                /* Wait a bit before trying again */
                 sleep($timeout);
             }
         }
+
+        /* Prepare for response */
         $this->_logger->info('[' . $this->_devId . '] Sending response for PING.');
         $this->_encoder->StartWBXML();
 
@@ -190,4 +220,18 @@ class Horde_ActiveSync_Request_Ping extends Horde_ActiveSync_Request_Base
 
         return true;
     }
+
+    protected function _handleHBError($lifetime)
+    {
+        $this->_logger->info('[' . $this->_devId . '] Sending response for PING.');
+        $this->_encoder->StartWBXML();
+        $this->_encoder->startTag(self::PING);
+        $this->_encoder->startTag(self::STATUS);
+        $this->_encoder->content($this->_statusCode);
+        $this->_encoder->endTag();
+        $this->_encoder->startTag(self::HEARTBEATINTERVAL);
+        $this->_encoder->content($lifetime);
+        $this->_encoder->endTag();
+        $this->_encoder->endTag();
+    }
 }
index 5dca9d0..fd0f06f 100644 (file)
@@ -334,14 +334,19 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
         if (file_exists($file)) {
             $this->_pingState = unserialize(file_get_contents($file));
         } else {
-            $this->_pingState = array(
-                'lifetime' => 0,
-                'collections' => array());
+            $this->resetPingState();
         }
 
         return $this->_pingState['collections'];
     }
 
+    public function resetPingState()
+    {
+        $this->_pingState = array(
+            'lifetime' => 0,
+            'collections' => array());
+    }
+
     /**
      * Obtain the device object.
      *
@@ -490,7 +495,7 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
      * @return integer  The hearbeat interval, or zero if not found.
      * @throws Horde_ActiveSync_Exception
      */
-    public function getPingLifetime()
+    public function getHeartbeatInterval()
     {
         if (empty($this->_pingState)) {
             throw new Horde_ActiveSync_Exception('PING state not initialized');
@@ -499,7 +504,7 @@ class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
         return (!$this->_pingState) ? 0 : $this->_pingState['lifetime'];
     }
 
-    public function setPingLifetime($lifetime)
+    public function setHeartbeatInterval($lifetime)
     {
         $this->_pingState['lifetime'] = $lifetime;
     }
index 41e9cde..e5e4d09 100644 (file)
@@ -395,7 +395,7 @@ class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base
      * @return integer  The hearbeat interval, or zero if not found.
      * @throws Horde_ActiveSync_Exception
      */
-    public function getPingLifetime()
+    public function getHeartbeatInterval()
     {
         if (empty($this->_pingState)) {
             throw new Horde_ActiveSync_Exception('PING state not initialized');
@@ -404,7 +404,7 @@ class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base
         return (!$this->_pingState) ? 0 : $this->_pingState['lifetime'];
     }
 
-    public function setPingLifetime($lifetime)
+    public function setHeartbeatInterval($lifetime)
     {
         $this->_pingState['lifetime'] = $lifetime;
     }
index 4235a6a..a3a4583 100644 (file)
@@ -407,7 +407,7 @@ class Horde_ActiveSync_Wbxml
                     0xd => array (
                         0x05 => 'Ping',
                         0x07 => 'Status',
-                        0x08 => 'LifeTime',
+                        0x08 => 'HeartbeatInterval',
                         0x09 => 'Folders',
                         0x0a => 'Folder',
                         0x0b => 'ServerEntryId',
index b9af17a..68cf709 100644 (file)
         </case>
       </configswitch>
      </configsection>
+     <configsection name="ping">
+      <configheader>Ping Settings</configheader>
+      <configdescription>The Heartbeat Interval specifies the length of time, in
+      seconds, that the server SHOULD wait before notifying the client of
+      changes in a folder on the server.  These settings allow you to indicate
+      the allowable range for this setting. Note that not all clients will
+      request this information.</configdescription>
+      <configinteger name="heartbeatmin" desc="The minimum number of seconds to
+      wait before sending results. This value must be greater then zero.">10
+      </configinteger>
+      <configinteger name="heartbeatmax" desc="The maximum number of seconds to
+      wait before sending results. Note this value must not be greater then 3540
+      (59 minutes).">60</configinteger>
+      <configinteger name="heartbeatdefault" desc="If the client does not
+      request a heartbeat interval, this interval will be used by default.">10
+      </configinteger>
+      <configinteger name="waitinterval" desc="How many seconds should elapse
+      between checks for changes? Increasing this value will cause the server
+      to wait longer between checks for changes during the heartbeat,
+      essentially reducing the number of checks done during each heartbeat.
+      Lowering this value will increase the number of checks that are down
+      during each heartbeat.">5</configinteger>
+     </configsection>
      <configsection name="securitypolicies">
      <configheader>Security Policies</configheader>
       <configswitch name="provisioning" quote="false" desc="How should device
index 7144512..8931a93 100644 (file)
@@ -48,7 +48,6 @@ if ((!empty($_SERVER['CONTENT_TYPE']) && strpos($_SERVER['CONTENT_TYPE'], 'appli
         } else {
             $params['logger'] = $GLOBALS['injector']->getInstance('Horde_Log_Logger');
         }
-
         $mailer = $GLOBALS['injector']->getInstance('Mail');
 
         /* TODO: Probably want to bind a factory to injector for this? */
@@ -57,7 +56,8 @@ if ((!empty($_SERVER['CONTENT_TYPE']) && strpos($_SERVER['CONTENT_TYPE'], 'appli
         $stateMachine = new Horde_ActiveSync_State_File(array('stateDir' => $GLOBALS['conf']['activesync']['state']['directory']));
         $driver_params = array('connector' => $connector,
                                'state_basic' => $stateMachine,
-                               'mail' => $mailer);
+                               'mail' => $mailer,
+                               'ping' => $GLOBALS['conf']['activesync']['ping']);
         if ($params['provisioning'] = $GLOBALS['conf']['activesync']['securitypolicies']['provisioning']) {
             $driver_params['policies'] = $GLOBALS['conf']['activesync']['securitypolicies'];
         }