66 private $userEditTracker;
70 private $wikiPageFactory;
84 private const MAX_CACHE_RECENT = 2;
103 LoggerInterface $logger,
111 $this->cache = $cache;
112 $this->dbProvider = $dbProvider;
113 $this->logger = $logger;
114 $this->stats = $stats;
115 $this->userEditTracker = $userEditTracker;
116 $this->userFactory = $userFactory;
117 $this->wikiPageFactory = $wikiPageFactory;
118 $this->hookRunner =
new HookRunner( $hookContainer );
119 $this->initiator = $initiator;
130 $logger = $this->logger;
132 if ( $pageUpdater instanceof
WikiPage ) {
133 wfDeprecated( __METHOD__ .
' with WikiPage instance',
'1.42' );
134 $pageUpdater = $pageUpdater->newPageUpdater( $user );
137 $page = $pageUpdater->getPage();
138 $key = $this->getStashKey( $page, $this->getContentHash( $content ), $user );
147 $dbw = $this->dbProvider->getPrimaryDatabase();
148 if ( !$dbw->lock( $key, $fname, 0 ) ) {
153 $unlocker =
new ScopedCallback(
static function () use ( $dbw, $key, $fname ) {
154 $dbw->unlock( $key, $fname );
160 $editInfo = $this->getStashValue( $key );
161 if ( $editInfo && (
int)
wfTimestamp( TS_UNIX, $editInfo->timestamp ) >= $cutoffTime ) {
162 $alreadyCached =
true;
164 $pageUpdater->setContent( SlotRecord::MAIN, $content );
167 $output = $update->getCanonicalParserOutput();
168 $output->setCacheTime( $update->getRevision()->getTimestamp() );
171 $editInfo = (object)[
172 'pstContent' => $update->getRawContent( SlotRecord::MAIN ),
174 'timestamp' => $output->getCacheTime()
177 $alreadyCached =
false;
180 $logContext = [
'cachekey' => $key,
'title' => (string)$page ];
182 if ( $editInfo->output ) {
184 $legacyUser = $this->userFactory->newFromUserIdentity( $user );
185 $legacyPage = $this->wikiPageFactory->newFromTitle( $page );
186 $this->hookRunner->onParserOutputStashForEdit(
187 $legacyPage, $content, $editInfo->output, $summary, $legacyUser );
189 if ( $alreadyCached ) {
190 $logger->debug(
"Parser output for key '{cachekey}' already cached.", $logContext );
195 $code = $this->storeStashValue(
197 $editInfo->pstContent,
199 $editInfo->timestamp,
203 if ( $code ===
true ) {
204 $logger->debug(
"Cached parser output for key '{cachekey}'.", $logContext );
207 } elseif ( $code ===
'uncacheable' ) {
209 "Uncacheable parser output for key '{cachekey}' [{code}].",
210 $logContext + [
'code' => $code ]
216 "Failed to cache parser output for key '{cachekey}'.",
217 $logContext + [
'code' => $code ]
248 $legacyUser = $this->userFactory->newFromUserIdentity( $user );
251 !$legacyUser->getRequest()->wasPosted() ||
253 $this->initiator !== self::INITIATOR_USER ||
261 $logger = $this->logger;
263 $key = $this->getStashKey( $page, $this->getContentHash( $content ), $user );
266 'title' => (string)$page,
270 $editInfo = $this->getAndWaitForStashValue( $key );
271 if ( !is_object( $editInfo ) || !$editInfo->output ) {
272 $this->incrCacheReadStats(
'miss',
'no_stash', $content );
273 if ( $this->recentStashEntryCount( $user ) > 0 ) {
274 $logger->info(
"Empty cache for key '{key}' but not for user.", $logContext );
276 $logger->debug(
"Empty cache for key '{key}'.", $logContext );
282 $age = time() - (int)
wfTimestamp( TS_UNIX, $editInfo->output->getCacheTime() );
283 $logContext[
'age'] = $age;
285 $isCacheUsable =
true;
286 if ( $age <= self::PRESUME_FRESH_TTL_SEC ) {
288 $this->incrCacheReadStats(
'hit',
'presumed_fresh', $content );
289 $logger->debug(
"Timestamp-based cache hit for key '{key}'.", $logContext );
291 $lastEdit = $this->lastEditTime( $user );
292 $cacheTime = $editInfo->output->getCacheTime();
293 if ( $lastEdit < $cacheTime ) {
295 $this->incrCacheReadStats(
'hit',
'presumed_fresh', $content );
296 $logger->debug(
"Edit check based cache hit for key '{key}'.", $logContext );
298 $isCacheUsable =
false;
299 $this->incrCacheReadStats(
'miss',
'proven_stale', $content );
300 $logger->info(
"Stale cache for key '{key}' due to outside edits.", $logContext );
303 if ( $editInfo->edits === $this->userEditTracker->getUserEditCount( $user ) ) {
305 $this->incrCacheReadStats(
'hit',
'presumed_fresh', $content );
306 $logger->debug(
"Edit count based cache hit for key '{key}'.", $logContext );
308 $isCacheUsable =
false;
309 $this->incrCacheReadStats(
'miss',
'proven_stale', $content );
310 $logger->info(
"Stale cache for key '{key}'due to outside edits.", $logContext );
314 if ( !$isCacheUsable ) {
318 if ( $editInfo->output->getOutputFlag( ParserOutputFlags::VARY_REVISION ) ) {
322 "Cache for key '{key}' has vary-revision; post-insertion parse inevitable.",
326 static $flagsMaybeReparse = [
328 ParserOutputFlags::VARY_REVISION_ID,
330 ParserOutputFlags::VARY_REVISION_TIMESTAMP,
332 ParserOutputFlags::VARY_REVISION_SHA1,
334 ParserOutputFlags::VARY_PAGE_ID,
336 foreach ( $flagsMaybeReparse as $flag ) {
337 if ( $editInfo->output->getOutputFlag( $flag ) ) {
339 "Cache for key '{key}' has $flag; post-insertion parse possible.",
354 private function incrCacheReadStats( $result, $reason,
Content $content ) {
355 static $subtypeByResult = [
'miss' =>
'cache_misses',
'hit' =>
'cache_hits' ];
356 $this->stats->getCounter(
"editstash_cache_checks_total" )
357 ->setLabel(
'reason', $reason )
358 ->setLabel(
'result', $result )
359 ->setLabel(
'model', $content->
getModel() )
361 'editstash.' . $subtypeByResult[ $result ] .
'.' . $reason,
362 'editstash_by_model.' . $content->
getModel() .
'.' . $subtypeByResult[ $result ] .
'.' . $reason ] )
370 private function getAndWaitForStashValue( $key ) {
371 $editInfo = $this->getStashValue( $key );
374 $start = microtime(
true );
377 $dbw = $this->dbProvider->getPrimaryDatabase();
378 if ( $dbw->lock( $key, __METHOD__, 30 ) ) {
379 $editInfo = $this->getStashValue( $key );
380 $dbw->unlock( $key, __METHOD__ );
383 $timeMs = 1000 * max( 0, microtime(
true ) - $start );
384 $this->stats->getTiming(
'editstash_lock_wait_seconds' )
385 ->copyToStatsdAt(
'editstash.lock_wait_time' )
386 ->observe( $timeMs );
397 $textKey = $this->cache->makeKey(
'stashedit',
'text', $textHash );
399 return $this->cache->get( $textKey );
408 $textKey = $this->cache->makeKey(
'stashedit',
'text', $textHash );
410 return $this->cache->set(
414 BagOStuff::WRITE_ALLOW_SEGMENTS
423 $time = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder()
424 ->select(
'MAX(rc_timestamp)' )
425 ->from(
'recentchanges' )
426 ->join(
'actor',
null,
'actor_id=rc_actor' )
427 ->where( [
'actor_name' => $user->
getName() ] )
428 ->caller( __METHOD__ )
440 private function getContentHash( Content $content ) {
441 return sha1( implode(
"\n", [
442 $content->getModel(),
443 $content->getDefaultFormat(),
444 $content->serialize( $content->getDefaultFormat() )
460 private function getStashKey( PageIdentity $page, $contentHash, UserIdentity $user ) {
461 return $this->cache->makeKey(
463 md5(
"{$page->getNamespace()}\n{$page->getDBkey()}" ),
467 md5(
"{$user->getId()}\n{$user->getName()}" )
475 private function getStashValue( $key ) {
476 $serial = $this->cache->get( $key );
478 return $this->unserializeStashInfo( $serial );
493 private function storeStashValue(
496 ParserOutput $parserOutput,
502 $age = time() - (int)
wfTimestamp( TS_UNIX, $parserOutput->getCacheTime() );
503 $ttl = min( $parserOutput->getCacheExpiry() - $age, self::MAX_CACHE_TTL );
505 if ( $parserOutput->getOutputFlag( ParserOutputFlags::USER_SIGNATURE ) ) {
506 $ttl = min( $ttl, self::MAX_SIGNATURE_TTL );
510 return 'uncacheable';
514 $stashInfo = (object)[
515 'pstContent' => $pstContent,
516 'output' => $parserOutput,
517 'timestamp' => $timestamp,
518 'edits' => $this->userEditTracker->getUserEditCount( $user ),
520 $serial = $this->serializeStashInfo( $stashInfo );
521 if ( $serial ===
false ) {
522 return 'store_error';
525 $ok = $this->cache->set( $key, $serial, $ttl, BagOStuff::WRITE_ALLOW_SEGMENTS );
528 $this->pruneExcessStashedEntries( $user, $key );
531 return $ok ? true :
'store_error';
538 private function pruneExcessStashedEntries( UserIdentity $user, $newKey ) {
539 $key = $this->cache->makeKey(
'stash-edit-recent', sha1( $user->getName() ) );
541 $keyList = $this->cache->get( $key ) ?: [];
542 if ( count( $keyList ) >= self::MAX_CACHE_RECENT ) {
543 $oldestKey = array_shift( $keyList );
544 $this->cache->delete( $oldestKey, BagOStuff::WRITE_ALLOW_SEGMENTS );
547 $keyList[] = $newKey;
548 $this->cache->set( $key, $keyList, 2 * self::MAX_CACHE_TTL );
555 private function recentStashEntryCount( UserIdentity $user ) {
556 $key = $this->cache->makeKey(
'stash-edit-recent', sha1( $user->getName() ) );
558 return count( $this->cache->get( $key ) ?: [] );
561 private function serializeStashInfo( stdClass $stashInfo ) {
563 return serialize( $stashInfo );
566 private function unserializeStashInfo( $serial ) {
567 if ( is_string( $serial ) ) {
569 $stashInfo = unserialize( $serial );
570 if ( is_object( $stashInfo ) && $stashInfo->output instanceof ParserOutput ) {
__construct(BagOStuff $cache, IConnectionProvider $dbProvider, LoggerInterface $logger, StatsFactory $stats, UserEditTracker $userEditTracker, UserFactory $userFactory, WikiPageFactory $wikiPageFactory, HookContainer $hookContainer, $initiator)
Base representation for an editable wiki page.