示例#1
0
 public function testAutoReconnect()
 {
     Zotero_DB::query("SET wait_timeout = 1");
     Zotero_DB::query("SET @foo='bar'");
     $this->assertEquals("bar", Zotero_DB::valueQuery("SELECT @foo"));
     sleep(3);
     try {
         Zotero_DB::valueQuery("SELECT @foo");
         $fail = true;
     } catch (Exception $e) {
         $this->assertContains("MySQL server has gone away", $e->getMessage());
     }
     if (isset($fail)) {
         $this->fail("Reconnect should not be automatic");
     }
     Zotero_DB::close();
     $this->assertEquals(false, Zotero_DB::valueQuery("SELECT @foo"));
 }
示例#2
0
 private static function processUploadInternal($userID, SimpleXMLElement $xml, $syncQueueID = null, $syncProcessID = null)
 {
     $userLibraryID = Zotero_Users::getLibraryIDFromUserID($userID);
     $affectedLibraries = self::parseAffectedLibraries($xml->asXML());
     // Relations-only uploads don't have affected libraries
     if (!$affectedLibraries) {
         $affectedLibraries = array(Zotero_Users::getLibraryIDFromUserID($userID));
     }
     $processID = self::addUploadProcess($userID, $affectedLibraries, $syncQueueID, $syncProcessID);
     set_time_limit(5400);
     $profile = false;
     if ($profile) {
         $shardID = Zotero_Shards::getByUserID($userID);
         Zotero_DB::profileStart($shardID);
     }
     try {
         Zotero_DB::beginTransaction();
         // Mark libraries as updated
         foreach ($affectedLibraries as $libraryID) {
             Zotero_Libraries::updateVersion($libraryID);
         }
         $timestamp = Zotero_Libraries::updateTimestamps($affectedLibraries);
         Zotero_DB::registerTransactionTimestamp($timestamp);
         // Make sure no other upload sessions use this same timestamp
         // for any of these libraries, since we return >= 1 as the next
         // last sync time
         if (!Zotero_Libraries::setTimestampLock($affectedLibraries, $timestamp)) {
             throw new Exception("Library timestamp already used", Z_ERROR_LIBRARY_TIMESTAMP_ALREADY_USED);
         }
         $modifiedItems = array();
         // Add/update creators
         if ($xml->creators) {
             // DOM
             $keys = array();
             $xmlElements = dom_import_simplexml($xml->creators);
             $xmlElements = $xmlElements->getElementsByTagName('creator');
             Zotero_DB::query("SET foreign_key_checks = 0");
             try {
                 $addedLibraryIDs = array();
                 $addedCreatorDataHashes = array();
                 foreach ($xmlElements as $xmlElement) {
                     $key = $xmlElement->getAttribute('key');
                     if (isset($keys[$key])) {
                         throw new Exception("Creator {$key} already processed");
                     }
                     $keys[$key] = true;
                     $creatorObj = Zotero_Creators::convertXMLToCreator($xmlElement);
                     if (Zotero_Utilities::unicodeTrim($creatorObj->firstName) === '' && Zotero_Utilities::unicodeTrim($creatorObj->lastName) === '') {
                         continue;
                     }
                     $addedLibraryIDs[] = $creatorObj->libraryID;
                     $changed = $creatorObj->save($userID);
                     // If the creator changed, we need to update all linked items
                     if ($changed) {
                         $modifiedItems = array_merge($modifiedItems, $creatorObj->getLinkedItems());
                     }
                 }
             } catch (Exception $e) {
                 Zotero_DB::query("SET foreign_key_checks = 1");
                 throw $e;
             }
             Zotero_DB::query("SET foreign_key_checks = 1");
             unset($keys);
             unset($xml->creators);
             //
             // Manual foreign key checks
             //
             // libraryID
             foreach (array_unique($addedLibraryIDs) as $addedLibraryID) {
                 $shardID = Zotero_Shards::getByLibraryID($addedLibraryID);
                 $sql = "SELECT COUNT(*) FROM shardLibraries WHERE libraryID=?";
                 if (!Zotero_DB::valueQuery($sql, $addedLibraryID, $shardID)) {
                     throw new Exception("libraryID inserted into `creators` not found in `shardLibraries` ({$addedLibraryID}, {$shardID})");
                 }
             }
         }
         // Add/update items
         $savedItems = array();
         if ($xml->items) {
             $childItems = array();
             // DOM
             $xmlElements = dom_import_simplexml($xml->items);
             $xmlElements = $xmlElements->getElementsByTagName('item');
             foreach ($xmlElements as $xmlElement) {
                 $libraryID = (int) $xmlElement->getAttribute('libraryID');
                 $key = $xmlElement->getAttribute('key');
                 if (isset($savedItems[$libraryID . "/" . $key])) {
                     throw new Exception("Item {$libraryID}/{$key} already processed");
                 }
                 $itemObj = Zotero_Items::convertXMLToItem($xmlElement);
                 if (!$itemObj->getSourceKey()) {
                     try {
                         $modified = $itemObj->save($userID);
                         if ($modified) {
                             $savedItems[$libraryID . "/" . $key] = true;
                         }
                     } catch (Exception $e) {
                         if (strpos($e->getMessage(), 'libraryIDs_do_not_match') !== false) {
                             throw new Exception($e->getMessage() . " ({$key})");
                         }
                         throw $e;
                     }
                 } else {
                     $childItems[] = $itemObj;
                 }
             }
             unset($xml->items);
             while ($childItem = array_shift($childItems)) {
                 $libraryID = $childItem->libraryID;
                 $key = $childItem->key;
                 if (isset($savedItems[$libraryID . "/" . $key])) {
                     throw new Exception("Item {$libraryID}/{$key} already processed");
                 }
                 $modified = $childItem->save($userID);
                 if ($modified) {
                     $savedItems[$libraryID . "/" . $key] = true;
                 }
             }
         }
         // Add/update collections
         if ($xml->collections) {
             $collections = array();
             $collectionSets = array();
             // DOM
             // Build an array of unsaved collection objects and the keys of child items
             $keys = array();
             $xmlElements = dom_import_simplexml($xml->collections);
             $xmlElements = $xmlElements->getElementsByTagName('collection');
             foreach ($xmlElements as $xmlElement) {
                 $key = $xmlElement->getAttribute('key');
                 if (isset($keys[$key])) {
                     throw new Exception("Collection {$key} already processed");
                 }
                 $keys[$key] = true;
                 $collectionObj = Zotero_Collections::convertXMLToCollection($xmlElement);
                 $xmlItems = $xmlElement->getElementsByTagName('items')->item(0);
                 // Fix an error if there's leading or trailing whitespace,
                 // which was possible in 2.0.3
                 if ($xmlItems) {
                     $xmlItems = trim($xmlItems->nodeValue);
                 }
                 $arr = array('obj' => $collectionObj, 'items' => $xmlItems ? explode(' ', $xmlItems) : array());
                 $collections[] = $collectionObj;
                 $collectionSets[] = $arr;
             }
             unset($keys);
             unset($xml->collections);
             self::saveCollections($collections, $userID);
             unset($collections);
             // Set child items
             foreach ($collectionSets as $collection) {
                 // Child items
                 if (isset($collection['items'])) {
                     $ids = array();
                     foreach ($collection['items'] as $key) {
                         $item = Zotero_Items::getByLibraryAndKey($collection['obj']->libraryID, $key);
                         if (!$item) {
                             throw new Exception("Child item '{$key}' of collection {$collection['obj']->id} not found", Z_ERROR_ITEM_NOT_FOUND);
                         }
                         $ids[] = $item->id;
                     }
                     $collection['obj']->setItems($ids);
                 }
             }
             unset($collectionSets);
         }
         // Add/update saved searches
         if ($xml->searches) {
             $searches = array();
             $keys = array();
             foreach ($xml->searches->search as $xmlElement) {
                 $key = (string) $xmlElement['key'];
                 if (isset($keys[$key])) {
                     throw new Exception("Search {$key} already processed");
                 }
                 $keys[$key] = true;
                 $searchObj = Zotero_Searches::convertXMLToSearch($xmlElement);
                 $searchObj->save($userID);
             }
             unset($xml->searches);
         }
         // Add/update tags
         if ($xml->tags) {
             $keys = array();
             // DOM
             $xmlElements = dom_import_simplexml($xml->tags);
             $xmlElements = $xmlElements->getElementsByTagName('tag');
             foreach ($xmlElements as $xmlElement) {
                 // TEMP
                 $tagItems = $xmlElement->getElementsByTagName('items');
                 if ($tagItems->length && $tagItems->item(0)->nodeValue == "") {
                     error_log("Skipping tag with no linked items");
                     continue;
                 }
                 $libraryID = (int) $xmlElement->getAttribute('libraryID');
                 $key = $xmlElement->getAttribute('key');
                 $lk = $libraryID . "/" . $key;
                 if (isset($keys[$lk])) {
                     throw new Exception("Tag {$lk} already processed");
                 }
                 $keys[$lk] = true;
                 $itemKeysToUpdate = array();
                 $tagObj = Zotero_Tags::convertXMLToTag($xmlElement, $itemKeysToUpdate);
                 // We need to update removed items, added items, and,
                 // if the tag itself has changed, existing items
                 $modifiedItems = array_merge($modifiedItems, array_map(function ($key) use($libraryID) {
                     return $libraryID . "/" . $key;
                 }, $itemKeysToUpdate));
                 $tagObj->save($userID, true);
             }
             unset($keys);
             unset($xml->tags);
         }
         // Add/update relations
         if ($xml->relations) {
             // DOM
             $xmlElements = dom_import_simplexml($xml->relations);
             $xmlElements = $xmlElements->getElementsByTagName('relation');
             foreach ($xmlElements as $xmlElement) {
                 $relationObj = Zotero_Relations::convertXMLToRelation($xmlElement, $userLibraryID);
                 if ($relationObj->exists()) {
                     continue;
                 }
                 $relationObj->save($userID);
             }
             unset($keys);
             unset($xml->relations);
         }
         // Add/update settings
         if ($xml->settings) {
             // DOM
             $xmlElements = dom_import_simplexml($xml->settings);
             $xmlElements = $xmlElements->getElementsByTagName('setting');
             foreach ($xmlElements as $xmlElement) {
                 $settingObj = Zotero_Settings::convertXMLToSetting($xmlElement);
                 $settingObj->save($userID);
             }
             unset($xml->settings);
         }
         if ($xml->fulltexts) {
             // DOM
             $xmlElements = dom_import_simplexml($xml->fulltexts);
             $xmlElements = $xmlElements->getElementsByTagName('fulltext');
             foreach ($xmlElements as $xmlElement) {
                 Zotero_FullText::indexFromXML($xmlElement, $userID);
             }
             unset($xml->fulltexts);
         }
         // TODO: loop
         if ($xml->deleted) {
             // Delete collections
             if ($xml->deleted->collections) {
                 Zotero_Collections::deleteFromXML($xml->deleted->collections, $userID);
             }
             // Delete items
             if ($xml->deleted->items) {
                 Zotero_Items::deleteFromXML($xml->deleted->items, $userID);
             }
             // Delete creators
             if ($xml->deleted->creators) {
                 Zotero_Creators::deleteFromXML($xml->deleted->creators, $userID);
             }
             // Delete saved searches
             if ($xml->deleted->searches) {
                 Zotero_Searches::deleteFromXML($xml->deleted->searches, $userID);
             }
             // Delete tags
             if ($xml->deleted->tags) {
                 $xmlElements = dom_import_simplexml($xml->deleted->tags);
                 $xmlElements = $xmlElements->getElementsByTagName('tag');
                 foreach ($xmlElements as $xmlElement) {
                     $libraryID = (int) $xmlElement->getAttribute('libraryID');
                     $key = $xmlElement->getAttribute('key');
                     $tagObj = Zotero_Tags::getByLibraryAndKey($libraryID, $key);
                     if (!$tagObj) {
                         continue;
                     }
                     // We need to update all items on the deleted tag
                     $modifiedItems = array_merge($modifiedItems, array_map(function ($key) use($libraryID) {
                         return $libraryID . "/" . $key;
                     }, $tagObj->getLinkedItems(true)));
                 }
                 Zotero_Tags::deleteFromXML($xml->deleted->tags, $userID);
             }
             // Delete relations
             if ($xml->deleted->relations) {
                 Zotero_Relations::deleteFromXML($xml->deleted->relations, $userID);
             }
             // Delete relations
             if ($xml->deleted->settings) {
                 Zotero_Settings::deleteFromXML($xml->deleted->settings, $userID);
             }
         }
         $toUpdate = array();
         foreach ($modifiedItems as $item) {
             // libraryID/key string
             if (is_string($item)) {
                 if (isset($savedItems[$item])) {
                     continue;
                 }
                 $savedItems[$item] = true;
                 list($libraryID, $key) = explode("/", $item);
                 $item = Zotero_Items::getByLibraryAndKey($libraryID, $key);
                 if (!$item) {
                     // Item was deleted
                     continue;
                 }
             } else {
                 $lk = $item->libraryID . "/" . $item->key;
                 if (isset($savedItems[$lk])) {
                     continue;
                 }
                 $savedItems[$lk] = true;
             }
             $toUpdate[] = $item;
         }
         Zotero_Items::updateVersions($toUpdate, $userID);
         unset($savedItems);
         unset($modifiedItems);
         try {
             self::removeUploadProcess($processID);
         } catch (Exception $e) {
             if (strpos($e->getMessage(), 'MySQL server has gone away') !== false) {
                 // Reconnect
                 error_log("Reconnecting to MySQL master");
                 Zotero_DB::close();
                 self::removeUploadProcess($processID);
             } else {
                 throw $e;
             }
         }
         // Send notifications for changed libraries
         foreach ($affectedLibraries as $libraryID) {
             Zotero_Notifier::trigger('modify', 'library', $libraryID);
         }
         Zotero_DB::commit();
         if ($profile) {
             $shardID = Zotero_Shards::getByUserID($userID);
             Zotero_DB::profileEnd($shardID);
         }
         // Return timestamp + 1, to keep the next /updated call
         // (using >= timestamp) from returning this data
         return $timestamp + 1;
     } catch (Exception $e) {
         Zotero_DB::rollback(true);
         self::removeUploadProcess($processID);
         throw $e;
     }
 }
示例#3
0
 public static function processUploadFromQueue($syncProcessID)
 {
     if (Z_Core::probability(30)) {
         $sql = "DELETE FROM syncProcesses WHERE started < (NOW() - INTERVAL 180 MINUTE)";
         Zotero_DB::query($sql);
     }
     if (Z_Core::probability(30)) {
         $sql = "UPDATE syncUploadQueue SET started=NULL WHERE started IS NOT NULL AND errorCheck!=1 AND\n\t\t\t\t\t\tstarted < (NOW() - INTERVAL 12 MINUTE) AND finished IS NULL AND dataLength<250000";
         Zotero_DB::query($sql);
     }
     if (Z_Core::probability(30)) {
         $sql = "UPDATE syncUploadQueue SET tries=0 WHERE started IS NULL AND\n\t\t\t\t\ttries>=5 AND finished IS NULL";
         Zotero_DB::query($sql);
     }
     Zotero_DB::beginTransaction();
     // Get a queued process
     $smallestFirst = Z_CONFIG::$SYNC_UPLOAD_SMALLEST_FIRST;
     $sortByQuota = !empty(Z_CONFIG::$SYNC_UPLOAD_SORT_BY_QUOTA);
     $sql = "SELECT syncUploadQueue.* FROM syncUploadQueue ";
     if ($sortByQuota) {
         $sql .= "LEFT JOIN storageAccounts USING (userID) ";
     }
     $sql .= "WHERE started IS NULL ";
     if (self::$minErrorCheckRequiredSize) {
         $sql .= "AND (errorCheck=2 OR dataLength<" . self::$minErrorCheckRequiredSize . ") ";
     }
     if ($smallestFirst && self::$maxSmallestSize) {
         $sql .= "AND dataLength<" . self::$maxSmallestSize . " ";
     }
     $sql .= "ORDER BY tries > 4, ";
     if ($sortByQuota) {
         $sql .= "quota DESC, ";
     }
     if ($smallestFirst) {
         //$sql .= "ROUND(dataLength / 1024 / 10), ";
         $sql .= "dataLength, ";
     }
     $sql .= "added LIMIT 1 FOR UPDATE";
     $row = Zotero_DB::rowQuery($sql);
     // No pending processes
     if (!$row) {
         Zotero_DB::commit();
         return 0;
     }
     $host = gethostbyname(gethostname());
     $startedTimestamp = microtime(true);
     list($started, $startedMS) = self::getTimestampParts($startedTimestamp);
     $sql = "UPDATE syncUploadQueue SET started=FROM_UNIXTIME(?), processorHost=INET_ATON(?) WHERE syncUploadQueueID=?";
     Zotero_DB::query($sql, array($started, $host, $row['syncUploadQueueID']));
     Zotero_DB::commit();
     Zotero_DB::close();
     $processData = array("syncUploadQueueID" => $row['syncUploadQueueID'], "userID" => $row['userID'], "dataLength" => $row['dataLength']);
     Z_Core::$MC->set("syncUploadProcess_" . $syncProcessID, $processData, 86400);
     $error = false;
     $lockError = false;
     try {
         $xml = new SimpleXMLElement($row['xmldata']);
         $timestamp = self::processUploadInternal($row['userID'], $xml, $row['syncUploadQueueID'], $syncProcessID);
     } catch (Exception $e) {
         $error = true;
         $code = $e->getCode();
         $msg = $e->getMessage();
     }
     Zotero_DB::beginTransaction();
     // Mark upload as finished — NULL indicates success
     if (!$error) {
         $sql = "UPDATE syncUploadQueue SET finished=FROM_UNIXTIME(?) WHERE syncUploadQueueID=?";
         Zotero_DB::query($sql, array($timestamp, $row['syncUploadQueueID']));
         try {
             $sql = "INSERT INTO syncUploadProcessLog\n\t\t\t\t\t\t(userID, dataLength, processorHost, processDuration, totalDuration, error)\n\t\t\t\t\t\tVALUES (?,?,INET_ATON(?),?,?,?)";
             Zotero_DB::query($sql, array($row['userID'], $row['dataLength'], $host, round((double) microtime(true) - $startedTimestamp, 2), max(0, min(time() - strtotime($row['added']), 65535)), 0));
         } catch (Exception $e) {
             Z_Core::logError($e);
         }
         try {
             self::processPostWriteLog($row['syncUploadQueueID'], $row['userID'], $timestamp);
         } catch (Exception $e) {
             Z_Core::logError($e);
         }
         try {
             // Index new items
             Zotero_Processors::notifyProcessors('index');
         } catch (Exception $e) {
             Z_Core::logError($e);
         }
     } else {
         if (strpos($msg, "Lock wait timeout exceeded; try restarting transaction") !== false || strpos($msg, "Deadlock found when trying to get lock; try restarting transaction") !== false || strpos($msg, "Too many connections") !== false || strpos($msg, "Can't connect to MySQL server") !== false || $code == Z_ERROR_LIBRARY_TIMESTAMP_ALREADY_USED || $code == Z_ERROR_SHARD_READ_ONLY || $code == Z_ERROR_SHARD_UNAVAILABLE) {
             Z_Core::logError($e);
             $sql = "UPDATE syncUploadQueue SET started=NULL, tries=tries+1 WHERE syncUploadQueueID=?";
             Zotero_DB::query($sql, $row['syncUploadQueueID']);
             $lockError = true;
         } else {
             // As of PHP 5.3.2 we can't serialize objects containing SimpleXMLElements,
             // and since the stack trace includes one, we have to catch this and
             // manually reconstruct an exception
             try {
                 $serialized = serialize($e);
             } catch (Exception $e2) {
                 //$id = substr(md5(uniqid(rand(), true)), 0, 10);
                 //file_put_contents("/tmp/uploadError_$id", $e);
                 $serialized = serialize(new Exception($msg, $e->getCode()));
             }
             Z_Core::logError($e);
             $sql = "UPDATE syncUploadQueue SET finished=?, errorCode=?, errorMessage=? WHERE syncUploadQueueID=?";
             Zotero_DB::query($sql, array(Zotero_DB::getTransactionTimestamp(), $e->getCode(), $serialized, $row['syncUploadQueueID']));
             try {
                 $sql = "INSERT INTO syncUploadProcessLog\n\t\t\t\t\t\t(userID, dataLength, processorHost, processDuration, totalDuration, error)\n\t\t\t\t\t\tVALUES (?,?,INET_ATON(?),?,?,?)";
                 Zotero_DB::query($sql, array($row['userID'], $row['dataLength'], $host, round((double) microtime(true) - $startedTimestamp, 2), max(0, min(time() - strtotime($row['added']), 65535)), 1));
             } catch (Exception $e) {
                 Z_Core::logError($e);
             }
         }
     }
     // Clear read locks
     $sql = "DELETE FROM syncUploadQueueLocks WHERE syncUploadQueueID=?";
     Zotero_DB::query($sql, $row['syncUploadQueueID']);
     Zotero_DB::commit();
     if ($lockError) {
         return -1;
     } else {
         if ($error) {
             return -2;
         }
     }
     return 1;
 }