/**
* Poll the backend for changes.
*
* @param integer $heartbeat The heartbeat lifetime to wait for changes.
* @param integer $interval The wait interval between poll iterations.
* @param array $options An options array containing any of:
* - pingable: (boolean) Only poll collections with the pingable flag set.
* DEFAULT: false
*
* @return boolean|integer True if changes were detected in any of the
* collections, false if no changes detected
* or a status code if failed.
*/
public function pollForChanges($heartbeat, $interval, array $options = array())
{
$dataavailable = false;
$started = time();
$until = $started + $heartbeat;
$this->_logger->info(sprintf('Waiting for changes for %s seconds', $heartbeat));
// If pinging, make sure we have pingable collections. Note we can't
// filter on them here because the collections might change during the
// loop below.
if (!empty($options['pingable']) && !$this->havePingableCollections()) {
$this->_logger->err('No pingable collections.');
return self::COLLECTION_ERR_SERVER;
}
// Need to update AND SAVE the timestamp for race conditions to be
// detected.
$this->lasthbsyncstarted = $started;
$this->save();
// We only check for remote wipe request once every 5 iterations to
// save on DB load since we must reload the device's state each time.
$rw_check_countdown = 5;
while (($now = time()) < $until) {
// Try not to go over the heartbeat interval.
if ($until - $now < $interval) {
$interval = $until - $now;
}
// See if another process has altered the sync_cache.
if ($this->checkStaleRequest()) {
return self::COLLECTION_ERR_STALE;
}
// Make sure the collections are still there (there might have been
// an error in refreshing them from the cache). Ideally this should
// NEVER happen.
if (!count($this->_collections)) {
$this->_logger->err('NO COLLECTIONS! This should not happen!');
return self::COLLECTION_ERR_SERVER;
}
// Check for WIPE request once every 5 iterations to balance between
// performance and speed of catching a remote wipe request.
if ($rw_check_countdown-- == 0) {
$rw_check_countdown = 5;
if ($this->_as->provisioning != Horde_ActiveSync::PROVISIONING_NONE) {
$rwstatus = $this->_as->state->getDeviceRWStatus($this->_as->device->id, true);
if ($rwstatus == Horde_ActiveSync::RWSTATUS_PENDING || $rwstatus == Horde_ActiveSync::RWSTATUS_WIPED) {
return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED;
}
}
}
// Check each collection we are interested in.
foreach ($this->_collections as $id => $collection) {
// Initialize the collection's state data in the state handler.
try {
$this->initCollectionState($collection, true);
} catch (Horde_ActiveSync_Exception_StateGone $e) {
$this->_logger->notice(sprintf('[%s] State not found for %s. Continuing.', $this->_procid, $id));
if (!empty($options['pingable'])) {
return self::COLLECTION_ERR_PING_NEED_FULL;
}
$dataavailable = true;
$this->setGetChangesFlag($id);
continue;
} catch (Horde_ActiveSync_Exception_InvalidRequest $e) {
// Thrown when state is unable to be initialized because the
// collection has not yet been synched, but was requested to
// be pinged.
$this->_logger->err(sprintf('[%s] Unable to initialize state for %s. Ignoring during pollForChanges: %s.', $this->_procid, $id, $e->getMessage()));
continue;
} catch (Horde_ActiveSync_Exception_FolderGone $e) {
$this->_logger->warn('Folder gone for collection ' . $collection['id']);
return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED;
} catch (Horde_ActiveSync_Exception $e) {
$this->_logger->err('Error loading state: ' . $e->getMessage());
$this->_as->state->loadState(array(), null, Horde_ActiveSync::REQUEST_TYPE_SYNC, $id);
$this->setGetChangesFlag($id);
$dataavailable = true;
continue;
}
if (!empty($options['pingable']) && !$this->_cache->collectionIsPingable($id)) {
$this->_logger->notice(sprintf('[%s] Skipping %s because it is not PINGable.', $this->_procid, $id));
continue;
}
try {
if ($cnt = $this->getCollectionChangeCount(true)) {
$dataavailable = true;
$this->setGetChangesFlag($id);
if (!empty($options['pingable'])) {
$this->_cache->setPingChangeFlag($id);
}
} else {
try {
$this->_as->state->updateSyncStamp();
} catch (Horde_ActiveSync_Exception $e) {
$this->_logger->err($e->getMessage());
}
}
} catch (Horde_ActiveSync_Exception_StaleState $e) {
$this->_logger->notice(sprintf('[%s] SYNC terminating and force-clearing device state: %s', $this->_procid, $e->getMessage()));
$this->_as->state->loadState(array(), null, Horde_ActiveSync::REQUEST_TYPE_SYNC, $id);
$this->setGetChangesFlag($id);
$dataavailable = true;
} catch (Horde_ActiveSync_Exception_FolderGone $e) {
$this->_logger->notice(sprintf('[%s] SYNC terminating: %s', $this->_procid, $e->getMessage()));
// If we are missing a folder, we should clear the PING
// cache also, to be sure it picks up any hierarchy changes
// since most clients don't seem smart enough to figure this
// out on their own.
$this->resetPingCache();
return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED;
} catch (Horde_Exception_AuthenticationFailure $e) {
// We lost authentication for some reason.
$this->_logger->err(sprintf('[%s] Authentication lost during PING!!', $this->_procid));
return self::COLLECTION_ERR_AUTHENTICATION;
} catch (Horde_ActiveSync_Exception $e) {
$this->_logger->err(sprintf('[%s] Sync object cannot be configured, throttling: %s', $this->_procid, $e->getMessage()));
$this->_sleep(30);
continue;
}
}
if (!empty($dataavailable)) {
$this->_logger->info(sprintf('[%s] Found changes!', $this->_procid));
break;
}
// Wait a bit...
$this->_sleep($interval);
// Refresh the collections.
$this->updateCollectionsFromCache();
}
// Check that no other Sync process already started
// If so, we exit here and let the other process do the export.
if ($this->checkStaleRequest()) {
$this->_logger->info('Changes in cache determined during Sync Wait/Heartbeat, exiting here.');
return self::COLLECTION_ERR_STALE;
}
$this->_logger->info(sprintf('[%s] Looping Sync complete: DataAvailable: %s, DataImported: %s', $this->_procid, $dataavailable, $this->importedChanges));
return $dataavailable;
}