/** * Check that a prepared edit is in cache and still up-to-date * * This method blocks if the prepared edit is already being rendered, * waiting until rendering finishes before doing final validity checks. * * The cache is rejected if template or file changes are detected. * Note that foreign template or file transclusions are not checked. * * The result is a map (pstContent,output,timestamp) with fields * extracted directly from WikiPage::prepareContentForEdit(). * * @param Title $title * @param Content $content * @param User $user User to get parser options from * @return stdClass|bool Returns false on cache miss */ public static function checkCache(Title $title, Content $content, User $user) { if ($user->isBot()) { return false; // bots never stash - don't pollute stats } $cache = ObjectCache::getLocalClusterInstance(); $logger = LoggerFactory::getInstance('StashEdit'); $stats = MediaWikiServices::getInstance()->getStatsdDataFactory(); $key = self::getStashKey($title, self::getContentHash($content), $user); $editInfo = $cache->get($key); if (!is_object($editInfo)) { $start = microtime(true); // We ignore user aborts and keep parsing. Block on any prior parsing // so as to use its results and make use of the time spent parsing. // Skip this logic if there no master connection in case this method // is called on an HTTP GET request for some reason. $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); $dbw = $lb->getAnyOpenConnection($lb->getWriterIndex()); if ($dbw && $dbw->lock($key, __METHOD__, 30)) { $editInfo = $cache->get($key); $dbw->unlock($key, __METHOD__); } $timeMs = 1000 * max(0, microtime(true) - $start); $stats->timing('editstash.lock_wait_time', $timeMs); } if (!is_object($editInfo) || !$editInfo->output) { $stats->increment('editstash.cache_misses.no_stash'); $logger->debug("Empty cache for key '{$key}' ('{$title}'); user '{$user->getName()}'."); return false; } $age = time() - wfTimestamp(TS_UNIX, $editInfo->output->getCacheTime()); if ($age <= self::PRESUME_FRESH_TTL_SEC) { // Assume nothing changed in this time $stats->increment('editstash.cache_hits.presumed_fresh'); $logger->debug("Timestamp-based cache hit for key '{$key}' (age: {$age} sec)."); } elseif (isset($editInfo->edits) && $editInfo->edits === $user->getEditCount()) { // Logged-in user made no local upload/template edits in the meantime $stats->increment('editstash.cache_hits.presumed_fresh'); $logger->debug("Edit count based cache hit for key '{$key}' (age: {$age} sec)."); } elseif ($user->isAnon() && self::lastEditTime($user) < $editInfo->output->getCacheTime()) { // Logged-out user made no local upload/template edits in the meantime $stats->increment('editstash.cache_hits.presumed_fresh'); $logger->debug("Edit check based cache hit for key '{$key}' (age: {$age} sec)."); } else { // User may have changed included content $editInfo = false; } if (!$editInfo) { $stats->increment('editstash.cache_misses.proven_stale'); $logger->info("Stale cache for key '{$key}'; old key with outside edits. (age: {$age} sec)"); } elseif ($editInfo->output->getFlag('vary-revision')) { // This can be used for the initial parse, e.g. for filters or doEditContent(), // but a second parse will be triggered in doEditUpdates(). This is not optimal. $logger->info("Cache for key '{$key}' ('{$title}') has vary_revision."); } elseif ($editInfo->output->getFlag('vary-revision-id')) { // Similar to the above if we didn't guess the ID correctly. $logger->info("Cache for key '{$key}' ('{$title}') has vary_revision_id."); } return $editInfo; }
/** * Check that a prepared edit is in cache and still up-to-date * * This method blocks if the prepared edit is already being rendered, * waiting until rendering finishes before doing final validity checks. * * The cache is rejected if template or file changes are detected. * Note that foreign template or file transclusions are not checked. * * The result is a map (pstContent,output,timestamp) with fields * extracted directly from WikiPage::prepareContentForEdit(). * * @param Title $title * @param Content $content * @param User $user User to get parser options from * @return stdClass|bool Returns false on cache miss */ public static function checkCache(Title $title, Content $content, User $user) { if ($user->isBot()) { return false; // bots never stash - don't pollute stats } $cache = ObjectCache::getLocalClusterInstance(); $logger = LoggerFactory::getInstance('StashEdit'); $stats = RequestContext::getMain()->getStats(); $key = self::getStashKey($title, $content, $user); $editInfo = $cache->get($key); if (!is_object($editInfo)) { $start = microtime(true); // We ignore user aborts and keep parsing. Block on any prior parsing // so as to use its results and make use of the time spent parsing. // Skip this logic if there no master connection in case this method // is called on an HTTP GET request for some reason. $lb = wfGetLB(); $dbw = $lb->getAnyOpenConnection($lb->getWriterIndex()); if ($dbw && $dbw->lock($key, __METHOD__, 30)) { $editInfo = $cache->get($key); $dbw->unlock($key, __METHOD__); } $timeMs = 1000 * max(0, microtime(true) - $start); $stats->timing('editstash.lock_wait_time', $timeMs); } if (!is_object($editInfo) || !$editInfo->output) { $stats->increment('editstash.cache_misses.no_stash'); $logger->debug("No cache value for key '{$key}'."); return false; } $age = time() - wfTimestamp(TS_UNIX, $editInfo->output->getCacheTime()); if ($age <= self::PRESUME_FRESH_TTL_SEC) { $stats->increment('editstash.cache_hits.presumed_fresh'); $logger->debug("Timestamp-based cache hit for key '{$key}' (age: {$age} sec)."); return $editInfo; // assume nothing changed } elseif (isset($editInfo->edits) && $editInfo->edits === $user->getEditCount()) { // Logged-in user made no local upload/template edits in the meantime $stats->increment('editstash.cache_hits.presumed_fresh'); $logger->debug("Edit count based cache hit for key '{$key}' (age: {$age} sec)."); return $editInfo; } elseif ($user->isAnon() && self::lastEditTime($user) < $editInfo->output->getCacheTime()) { // Logged-out user made no local upload/template edits in the meantime $stats->increment('editstash.cache_hits.presumed_fresh'); $logger->debug("Edit check based cache hit for key '{$key}' (age: {$age} sec)."); return $editInfo; } $dbr = wfGetDB(DB_SLAVE); $templates = []; // conditions to find changes/creations $templateUses = 0; // expected existing templates foreach ($editInfo->output->getTemplateIds() as $ns => $stuff) { foreach ($stuff as $dbkey => $revId) { $templates[(string) $ns][$dbkey] = (int) $revId; ++$templateUses; } } // Check that no templates used in the output changed... if (count($templates)) { $res = $dbr->select('page', ['ns' => 'page_namespace', 'dbk' => 'page_title', 'page_latest'], $dbr->makeWhereFrom2d($templates, 'page_namespace', 'page_title'), __METHOD__); $changed = false; foreach ($res as $row) { $changed = $changed || $row->page_latest != $templates[$row->ns][$row->dbk]; } if ($changed || $res->numRows() != $templateUses) { $stats->increment('editstash.cache_misses.proven_stale'); $logger->info("Stale cache for key '{$key}'; template changed. (age: {$age} sec)"); return false; } } $files = []; // conditions to find changes/creations foreach ($editInfo->output->getFileSearchOptions() as $name => $options) { $files[$name] = (string) $options['sha1']; } // Check that no files used in the output changed... if (count($files)) { $res = $dbr->select('image', ['name' => 'img_name', 'img_sha1'], ['img_name' => array_keys($files)], __METHOD__); $changed = false; foreach ($res as $row) { $changed = $changed || $row->img_sha1 != $files[$row->name]; } if ($changed || $res->numRows() != count($files)) { $stats->increment('editstash.cache_misses.proven_stale'); $logger->info("Stale cache for key '{$key}'; file changed. (age: {$age} sec)"); return false; } } $stats->increment('editstash.cache_hits.proven_fresh'); $logger->debug("Verified cache hit for key '{$key}' (age: {$age} sec)."); return $editInfo; }