Commit 7cdb51e4 authored by Sebastian Kummer's avatar Sebastian Kummer

Merge pull request #148 in ZP/z-push from feature/ZP-832-detect-folder-changes-faster to develop

* commit 'bc067ce2':
  ZP-832 Run SyncCollections->CheckForChanges() stat comparing only on first run, improved comment.
  ZP-832 Add check if stats table can be loaded for shared users - log to INFO if that does not work, sync folders where no stat can be found only once a hour, include PR_CONTENT_COUNT, PR_CONTENT_UNREAD and PR_DELETED_MSG_COUNT into stat to reflect all kind of changes, SyncCollection->CheckForChanges() compares folderstats for changes when initiating, in case of error remove the folderstat to ensure folder is synchronized next Sync request, remove 'savestate' functionality as it is not used.
  ZP-832 Request folder statistics from the backend and setup the exporter only if the stat indicate that the folder contains changes. Per default the statistics are disabled (backend has to enable them). If disabled, an exporter will be setup for each folder.
  ZP-832 Log GlobalWindow size override only once.
parents 593dc404 bc067ce2
...@@ -1252,3 +1252,6 @@ define('PR_ZC_CONTACT_FOLDER_NAMES' ,mapi_prop_tag(PT_MV_TSTRI ...@@ -1252,3 +1252,6 @@ define('PR_ZC_CONTACT_FOLDER_NAMES' ,mapi_prop_tag(PT_MV_TSTRI
//Properties defined for Z-Push //Properties defined for Z-Push
define('PR_TODO_ITEM_FLAGS' ,mapi_prop_tag(PT_LONG, 0x0E2B)); define('PR_TODO_ITEM_FLAGS' ,mapi_prop_tag(PT_LONG, 0x0E2B));
define('PR_LOCAL_COMMIT_TIME_MAX' ,mapi_prop_tag(PT_SYSTIME, 0x670A));
define('PR_DELETED_MSG_COUNT' ,mapi_prop_tag(PT_LONG, 0x6640));
...@@ -89,6 +89,7 @@ class BackendZarafa implements IBackend, ISearchProvider { ...@@ -89,6 +89,7 @@ class BackendZarafa implements IBackend, ISearchProvider {
private $changesSinkStores; private $changesSinkStores;
private $wastebasket; private $wastebasket;
private $addressbook; private $addressbook;
private $folderStatCache;
// ZCP config parameter for PR_EC_ENABLED_FEATURES / PR_EC_DISABLED_FEATURES // ZCP config parameter for PR_EC_ENABLED_FEATURES / PR_EC_DISABLED_FEATURES
const ZPUSH_ENABLED = 'mobile'; const ZPUSH_ENABLED = 'mobile';
...@@ -115,6 +116,7 @@ class BackendZarafa implements IBackend, ISearchProvider { ...@@ -115,6 +116,7 @@ class BackendZarafa implements IBackend, ISearchProvider {
$this->changesSinkStores = array(); $this->changesSinkStores = array();
$this->wastebasket = false; $this->wastebasket = false;
$this->session = false; $this->session = false;
$this->folderStatCache = array();
ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa using PHP-MAPI version: %s", phpversion("mapi"))); ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendZarafa using PHP-MAPI version: %s", phpversion("mapi")));
} }
...@@ -1291,6 +1293,80 @@ class BackendZarafa implements IBackend, ISearchProvider { ...@@ -1291,6 +1293,80 @@ class BackendZarafa implements IBackend, ISearchProvider {
return $this->storeName; return $this->storeName;
} }
/**
* Indicates if the Backend supports folder statistics.
*
* @access public
* @return boolean
*/
public function HasFolderStats() {
return true;
}
/**
* Returns a status indication of the folder.
* If there are changes in the folder, the returned value must change.
* The returned values are compared with '===' to determine if a folder needs synchronization or not.
*
* @param string $store the store where the folder resides
* @param string $folderid the folder id
*
* @access public
* @return string
*/
public function GetFolderStat($store, $folderid) {
list($user, $domain) = Utils::SplitDomainUser($store);
if ($user === false) {
$user = $this->mainUser;
}
if (!isset($this->folderStatCache[$user])) {
$this->folderStatCache[$user] = array();
}
// TODO remove nameCache
if (!isset($this->nameCache))
$this->nameCache = array();
// if there is nothing in the cache for a store, load the data for all folders of it
if (empty($this->folderStatCache[$user])) {
// get the store
$userstore = $this->openMessageStore($user);
$rootfolder = mapi_msgstore_openentry($userstore);
$hierarchy = mapi_folder_gethierarchytable($rootfolder, CONVENIENT_DEPTH);
$rows = mapi_table_queryallrows($hierarchy, array(PR_SOURCE_KEY, PR_LOCAL_COMMIT_TIME_MAX, PR_CONTENT_COUNT, PR_CONTENT_UNREAD, PR_DELETED_MSG_COUNT, PR_DISPLAY_NAME));
if (count($rows) == 0) {
ZLog::Write(LOGLEVEL_INFO, sprintf("ZarafaBackend->GetFolderStat(): could not access folder statistics for user '%s'. Probably missing 'read' permissions on the root folder! Folders of this store will be synchronized ONCE per hour only!", $user));
}
foreach($rows as $folder) {
$commit_time = isset($folder[PR_LOCAL_COMMIT_TIME_MAX])? $folder[PR_LOCAL_COMMIT_TIME_MAX] : "0000000000";
$content_count = isset($folder[PR_CONTENT_COUNT])? $folder[PR_CONTENT_COUNT] : -1;
$content_unread = isset($folder[PR_CONTENT_UNREAD])? $folder[PR_CONTENT_UNREAD] : -1;
$content_deleted = isset($folder[PR_DELETED_MSG_COUNT])? $folder[PR_DELETED_MSG_COUNT] : -1;
$this->folderStatCache[$user][bin2hex($folder[PR_SOURCE_KEY])] = $commit_time ."/". $content_count ."/". $content_unread ."/". $content_deleted;
$this->nameCache[bin2hex($folder[PR_SOURCE_KEY])] = $folder[PR_DISPLAY_NAME];
}
ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->GetFolderStat() fetched status information of %d folders for store '%s'", count($this->folderStatCache[$user]), $user));
// TODO remove logging
foreach($this->folderStatCache[$user] as $fid => $stat) {
ZLog::Write(LOGLEVEL_INFO, sprintf("FolderStat: %s %s %s\t%s", $user, $fid, $stat, $this->nameCache[$fid]));
}
}
if (isset($this->folderStatCache[$user][$folderid])) {
// TODO remove nameCache output
ZLog::Write(LOGLEVEL_DEBUG, sprintf("ZarafaBackend->GetFolderStat() found stat for '%s': %s", $this->nameCache[$folderid], $this->folderStatCache[$user][$folderid]));
return $this->folderStatCache[$user][$folderid];
}
else {
// a timestamp that changes once per hour is returned in case there is no data found for this folder. It will be synchronized only once per hour.
return gmdate("Y-m-d-H");
}
}
/**---------------------------------------------------------------------------------------------------------- /**----------------------------------------------------------------------------------------------------------
* Private methods * Private methods
*/ */
......
...@@ -70,6 +70,7 @@ class SyncCollections implements Iterator { ...@@ -70,6 +70,7 @@ class SyncCollections implements Iterator {
private $lastSyncTime; private $lastSyncTime;
private $waitingTime = 0; private $waitingTime = 0;
private $loggedGlobalWindowSizeOverwrite = false;
/** /**
...@@ -368,7 +369,10 @@ class SyncCollections implements Iterator { ...@@ -368,7 +369,10 @@ class SyncCollections implements Iterator {
} }
if (defined("SYNC_MAX_ITEMS") && SYNC_MAX_ITEMS < $globalWindowSize) { if (defined("SYNC_MAX_ITEMS") && SYNC_MAX_ITEMS < $globalWindowSize) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->GetGlobalWindowSize() overwriting requested global window size of %d by %d forced in configuration.", $globalWindowSize, SYNC_MAX_ITEMS)); if (!$this->loggedGlobalWindowSizeOverwrite) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->GetGlobalWindowSize() overwriting requested global window size of %d by %d forced in configuration.", $globalWindowSize, SYNC_MAX_ITEMS));
$this->loggedGlobalWindowSizeOverwrite = true;
}
$globalWindowSize = SYNC_MAX_ITEMS; $globalWindowSize = SYNC_MAX_ITEMS;
} }
...@@ -454,31 +458,43 @@ class SyncCollections implements Iterator { ...@@ -454,31 +458,43 @@ class SyncCollections implements Iterator {
$pingTracking = new PingTracking(); $pingTracking = new PingTracking();
$this->changes = array(); $this->changes = array();
$changesAvailable = false;
ZPush::GetDeviceManager()->AnnounceProcessAsPush(); ZPush::GetDeviceManager()->AnnounceProcessAsPush();
ZPush::GetTopCollector()->AnnounceInformation(sprintf("lifetime %ds", $lifetime), true); ZPush::GetTopCollector()->AnnounceInformation(sprintf("lifetime %ds", $lifetime), true);
ZLog::Write(LOGLEVEL_INFO, sprintf("SyncCollections->CheckForChanges(): Waiting for %s changes... (lifetime %d seconds)", (empty($classes))?'policy':'store', $lifetime)); ZLog::Write(LOGLEVEL_INFO, sprintf("SyncCollections->CheckForChanges(): Waiting for %s changes... (lifetime %d seconds)", (empty($classes))?'policy':'store', $lifetime));
// use changes sink where available // use changes sink where available
$changesSink = false; $changesSink = ZPush::GetBackend()->HasChangesSink();
// do not create changessink if there are no folders
if (!empty($classes) && ZPush::GetBackend()->HasChangesSink()) {
$changesSink = true;
// create changessink and check folder stats if there are folders to Ping
if (!empty($classes)) {
// initialize all possible folders // initialize all possible folders
foreach ($this->collections as $folderid => $spa) { foreach ($this->collections as $folderid => $spa) {
if ($onlyPingable && $spa->GetPingableFlag() !== true) if ($onlyPingable && $spa->GetPingableFlag() !== true)
continue; continue;
// switch user store if this is a additional folder and initialize sink // get the user store if this is a additional folder
ZPush::GetBackend()->Setup(ZPush::GetAdditionalSyncFolderStore($folderid)); $store = ZPush::GetAdditionalSyncFolderStore($folderid);
if (! ZPush::GetBackend()->ChangesSinkInitialize($folderid))
throw new StatusException(sprintf("Error initializing ChangesSink for folder id '%s'", $folderid), self::ERROR_WRONG_HIERARCHY); // initialize sink if no immediate changes were found so far
if ($changesSink && empty($this->changes)) {
ZPush::GetBackend()->Setup($store);
if (! ZPush::GetBackend()->ChangesSinkInitialize($folderid))
throw new StatusException(sprintf("Error initializing ChangesSink for folder id '%s'", $folderid), self::ERROR_WRONG_HIERARCHY);
}
// check if the folder stat changed since the last sync, if so generate a change for it (only on first run)
if ($this->waitingTime == 0 && ZPush::GetBackend()->HasFolderStats() && $spa->HasFolderStat() && ZPush::GetBackend()->GetFolderStat($store, $spa->GetFolderId()) !== $spa->GetFolderStat()) {
$this->changes[$spa->GetFolderId()] = 1;
}
} }
} }
if (!empty($this->changes)) {
ZLog::Write(LOGLEVEL_DEBUG, "SyncCollections->CheckForChanges(): Using ChangesSink but found changes verifying the folder stats");
return true;
}
// wait for changes // wait for changes
$started = time(); $started = time();
$endat = time() + $lifetime; $endat = time() + $lifetime;
...@@ -622,28 +638,23 @@ class SyncCollections implements Iterator { ...@@ -622,28 +638,23 @@ class SyncCollections implements Iterator {
if ($ste->getCode() == SYNC_STATUS_FOLDERHIERARCHYCHANGED) { if ($ste->getCode() == SYNC_STATUS_FOLDERHIERARCHYCHANGED) {
ZLog::Write(LOGLEVEL_WARN, "SyncCollections->CountChange(): exporter can not be re-configured due to state error, emulating change in folder to force Sync."); ZLog::Write(LOGLEVEL_WARN, "SyncCollections->CountChange(): exporter can not be re-configured due to state error, emulating change in folder to force Sync.");
$this->changes[$folderid] = 1; $this->changes[$folderid] = 1;
// make sure this folder is fully synched on next Sync request
if($spa->HasFolderStat()) {
ZLog::Write(LOGLEVEL_DEBUG, sprintf("SyncCollections->CountChange(): removing folder stat '%s' for folderid '%s'", $spa->GetFolderStat(), $spa->GetFolderId()));
$spa->DelFolderStat();
$this->SaveCollection($spa);
}
return true; return true;
} }
throw new StatusException("SyncCollections->CountChange(): exporter can not be re-configured.", self::ERROR_WRONG_HIERARCHY, null, LOGLEVEL_WARN); throw new StatusException("SyncCollections->CountChange(): exporter can not be re-configured.", self::ERROR_WRONG_HIERARCHY, null, LOGLEVEL_WARN);
} }
// start over if exporter can not be configured atm // start over if exporter can not be configured atm
if ($changecount === false ) if ($changecount === false)
ZLog::Write(LOGLEVEL_WARN, "SyncCollections->CountChange(): no changes received from Exporter."); ZLog::Write(LOGLEVEL_WARN, "SyncCollections->CountChange(): no changes received from Exporter.");
$this->changes[$folderid] = $changecount; $this->changes[$folderid] = $changecount;
if(isset($this->addparms[$folderid]['savestate'])) {
try {
// Discard any data
while(is_array($exporter->Synchronize()));
$this->addparms[$folderid]['savestate'] = $exporter->GetState();
}
catch (StatusException $ste) {
throw new StatusException("SyncCollections->CountChange(): could not get new state from exporter", self::ERROR_WRONG_HIERARCHY, null, LOGLEVEL_WARN);
}
}
return ($changecount > 0); return ($changecount > 0);
} }
......
...@@ -71,6 +71,7 @@ class SyncParameters extends StateObject { ...@@ -71,6 +71,7 @@ class SyncParameters extends StateObject {
'contentparameters' => array(), 'contentparameters' => array(),
'foldersynctotal' => false, 'foldersynctotal' => false,
'foldersyncremaining' => false, 'foldersyncremaining' => false,
'folderstat' => false,
); );
/** /**
......
...@@ -238,6 +238,34 @@ abstract class Backend implements IBackend { ...@@ -238,6 +238,34 @@ abstract class Backend implements IBackend {
return $this->GetUserDetails(Request::GetAuthUser()); return $this->GetUserDetails(Request::GetAuthUser());
} }
/**
* Indicates if the Backend supports folder statistics.
*
* @access public
* @return boolean
*/
public function HasFolderStats() {
return false;
}
/**
* Returns a status indication of the folder.
* If there are changes in the folder, the returned value must change.
* The returned values are compared with '===' to determine if a folder needs synchronization or not.
*
* @param string $store the store where the folder resides
* @param string $folderid the folder id
*
* @access public
* @return string
*/
public function GetFolderStat($store, $folderid) {
// As this is not implemented, the value returned will change every hour.
// This will only be called if HasFolderStats() returns true.
return "not implemented-".gmdate("Y-m-d-H");
}
/**---------------------------------------------------------------------------------------------------------- /**----------------------------------------------------------------------------------------------------------
* Protected methods for BackendStorage * Protected methods for BackendStorage
* *
......
...@@ -307,4 +307,25 @@ interface IBackend { ...@@ -307,4 +307,25 @@ interface IBackend {
* @return Array * @return Array
*/ */
public function GetCurrentUsername(); public function GetCurrentUsername();
/**
* Indicates if the Backend supports folder statistics.
*
* @access public
* @return boolean
*/
public function HasFolderStats();
/**
* Returns a status indication of the folder.
* If there are changes in the folder, the returned value must change.
* The returned values are compared with '===' to determine if a folder needs synchronization or not.
*
* @param string $store the store where the folder resides
* @param string $folderid the folder id
*
* @access public
* @return string
*/
public function GetFolderStat($store, $folderid);
} }
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment