66 private $userEditTracker;
70 private $wikiPageFactory;
84 private const MAX_CACHE_RECENT = 2;
103 LoggerInterface $logger,
104 StatsdDataFactoryInterface $stats,
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->incrStatsByContent(
'cache_misses.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->incrStatsByContent(
'cache_hits.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->incrStatsByContent(
'cache_hits.presumed_fresh', $content );
296 $logger->debug(
"Edit check based cache hit for key '{key}'.", $logContext );
298 $isCacheUsable =
false;
299 $this->incrStatsByContent(
'cache_misses.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->incrStatsByContent(
'cache_hits.presumed_fresh', $content );
306 $logger->debug(
"Edit count based cache hit for key '{key}'.", $logContext );
308 $isCacheUsable =
false;
309 $this->incrStatsByContent(
'cache_misses.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.",
353 private function incrStatsByContent( $subkey,
Content $content ) {
354 $this->stats->increment(
'editstash.' . $subkey );
355 $this->stats->increment(
'editstash_by_model.' . $content->
getModel() .
'.' . $subkey );
362 private function getAndWaitForStashValue( $key ) {
363 $editInfo = $this->getStashValue( $key );
366 $start = microtime(
true );
369 $dbw = $this->dbProvider->getPrimaryDatabase();
370 if ( $dbw->lock( $key, __METHOD__, 30 ) ) {
371 $editInfo = $this->getStashValue( $key );
372 $dbw->unlock( $key, __METHOD__ );
375 $timeMs = 1000 * max( 0, microtime(
true ) - $start );
376 $this->stats->timing(
'editstash.lock_wait_time', $timeMs );
387 $textKey = $this->cache->makeKey(
'stashedit',
'text', $textHash );
389 return $this->cache->get( $textKey );
398 $textKey = $this->cache->makeKey(
'stashedit',
'text', $textHash );
400 return $this->cache->set(
404 BagOStuff::WRITE_ALLOW_SEGMENTS
413 $time = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder()
414 ->select(
'MAX(rc_timestamp)' )
415 ->from(
'recentchanges' )
416 ->join(
'actor',
null,
'actor_id=rc_actor' )
417 ->where( [
'actor_name' => $user->
getName() ] )
418 ->caller( __METHOD__ )
430 private function getContentHash(
Content $content ) {
431 return sha1( implode(
"\n", [
450 private function getStashKey( PageIdentity $page, $contentHash, UserIdentity $user ) {
451 return $this->cache->makeKey(
453 md5(
"{$page->getNamespace()}\n{$page->getDBkey()}" ),
457 md5(
"{$user->getId()}\n{$user->getName()}" )
465 private function getStashValue( $key ) {
466 $serial = $this->cache->get( $key );
468 return $this->unserializeStashInfo( $serial );
483 private function storeStashValue(
486 ParserOutput $parserOutput,
492 $age = time() - (int)
wfTimestamp( TS_UNIX, $parserOutput->getCacheTime() );
493 $ttl = min( $parserOutput->getCacheExpiry() - $age, self::MAX_CACHE_TTL );
495 if ( $parserOutput->getOutputFlag( ParserOutputFlags::USER_SIGNATURE ) ) {
496 $ttl = min( $ttl, self::MAX_SIGNATURE_TTL );
500 return 'uncacheable';
504 $stashInfo = (object)[
505 'pstContent' => $pstContent,
506 'output' => $parserOutput,
507 'timestamp' => $timestamp,
508 'edits' => $this->userEditTracker->getUserEditCount( $user ),
510 $serial = $this->serializeStashInfo( $stashInfo );
511 if ( $serial ===
false ) {
512 return 'store_error';
515 $ok = $this->cache->set( $key, $serial, $ttl, BagOStuff::WRITE_ALLOW_SEGMENTS );
518 $this->pruneExcessStashedEntries( $user, $key );
521 return $ok ? true :
'store_error';
528 private function pruneExcessStashedEntries( UserIdentity $user, $newKey ) {
529 $key = $this->cache->makeKey(
'stash-edit-recent', sha1( $user->getName() ) );
531 $keyList = $this->cache->get( $key ) ?: [];
532 if ( count( $keyList ) >= self::MAX_CACHE_RECENT ) {
533 $oldestKey = array_shift( $keyList );
534 $this->cache->delete( $oldestKey, BagOStuff::WRITE_PRUNE_SEGMENTS );
537 $keyList[] = $newKey;
538 $this->cache->set( $key, $keyList, 2 * self::MAX_CACHE_TTL );
545 private function recentStashEntryCount( UserIdentity $user ) {
546 $key = $this->cache->makeKey(
'stash-edit-recent', sha1( $user->getName() ) );
548 return count( $this->cache->get( $key ) ?: [] );
551 private function serializeStashInfo( stdClass $stashInfo ) {
553 return serialize( $stashInfo );
556 private function unserializeStashInfo( $serial ) {
557 if ( is_string( $serial ) ) {
559 $stashInfo = unserialize( $serial );
560 if ( is_object( $stashInfo ) && $stashInfo->output instanceof ParserOutput ) {
__construct(BagOStuff $cache, IConnectionProvider $dbProvider, LoggerInterface $logger, StatsdDataFactoryInterface $stats, UserEditTracker $userEditTracker, UserFactory $userFactory, WikiPageFactory $wikiPageFactory, HookContainer $hookContainer, $initiator)
Base representation for an editable wiki page.
Base interface for representing page content.
getModel()
Returns the ID of the content model used by this Content object.
serialize( $format=null)
Convenience method for serializing this Content object.
getDefaultFormat()
Convenience method that returns the default serialization format for the content model that this Cont...