65 private $userEditTracker;
69 private $wikiPageFactory;
83 private const MAX_CACHE_RECENT = 2;
102 LoggerInterface $logger,
103 StatsdDataFactoryInterface $stats,
112 $this->logger = $logger;
113 $this->stats = $stats;
114 $this->userEditTracker = $userEditTracker;
115 $this->userFactory = $userFactory;
116 $this->wikiPageFactory = $wikiPageFactory;
117 $this->hookRunner =
new HookRunner( $hookContainer );
118 $this->initiator = $initiator;
129 $logger = $this->logger;
131 if ( $pageUpdater instanceof
WikiPage ) {
134 $pageUpdater = $pageUpdater->newPageUpdater( $user );
137 $page = $pageUpdater->getPage();
138 $key = $this->getStashKey( $page, $this->getContentHash(
$content ), $user );
147 $dbw = $this->lb->getConnectionRef(
DB_PRIMARY );
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.",
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 );
371 $dbw = $this->lb->getAnyOpenConnection( $this->lb->getWriterIndex() );
372 if ( $dbw && $dbw->lock( $key, __METHOD__, 30 ) ) {
373 $editInfo = $this->getStashValue( $key );
374 $dbw->unlock( $key, __METHOD__ );
377 $timeMs = 1000 * max( 0, microtime(
true ) - $start );
378 $this->stats->timing(
'editstash.lock_wait_time', $timeMs );
389 $textKey = $this->cache->makeKey(
'stashedit',
'text', $textHash );
391 return $this->cache->get( $textKey );
400 $textKey = $this->cache->makeKey(
'stashedit',
'text', $textHash );
402 return $this->cache->set(
406 BagOStuff::WRITE_ALLOW_SEGMENTS
415 $db = $this->lb->getConnectionRef(
DB_REPLICA );
417 $time = $db->newSelectQueryBuilder()
418 ->select(
'MAX(rc_timestamp)' )
419 ->from(
'recentchanges' )
420 ->join(
'actor',
null,
'actor_id=rc_actor' )
421 ->where( [
'actor_name' => $user->
getName() ] )
422 ->caller( __METHOD__ )
435 return sha1( implode(
"\n", [
454 private function getStashKey( PageIdentity $page, $contentHash, UserIdentity $user ) {
455 return $this->cache->makeKey(
457 md5(
"{$page->getNamespace()}\n{$page->getDBkey()}" ),
461 md5(
"{$user->getId()}\n{$user->getName()}" )
469 private function getStashValue( $key ) {
470 $stashInfo = $this->cache->get( $key );
471 if ( is_object( $stashInfo ) && $stashInfo->output instanceof
ParserOutput ) {
490 private function storeStashValue(
500 $ttl = min( $parserOutput->
getCacheExpiry() - $age, self::MAX_CACHE_TTL );
502 if ( $parserOutput->
getOutputFlag( ParserOutputFlags::USER_SIGNATURE ) ) {
503 $ttl = min( $ttl, self::MAX_SIGNATURE_TTL );
507 return 'uncacheable';
511 $stashInfo = (object)[
512 'pstContent' => $pstContent,
513 'output' => $parserOutput,
514 'timestamp' => $timestamp,
515 'edits' => $user->isRegistered() ? $this->userEditTracker->getUserEditCount( $user ) :
null,
518 $ok = $this->cache->set( $key, $stashInfo, $ttl, BagOStuff::WRITE_ALLOW_SEGMENTS );
521 $this->pruneExcessStashedEntries( $user, $key );
524 return $ok ?
true :
'store_error';
531 private function pruneExcessStashedEntries( UserIdentity $user, $newKey ) {
532 $key = $this->cache->makeKey(
'stash-edit-recent', sha1( $user->getName() ) );
534 $keyList = $this->cache->get( $key ) ?: [];
535 if ( count( $keyList ) >= self::MAX_CACHE_RECENT ) {
536 $oldestKey = array_shift( $keyList );
537 $this->cache->delete( $oldestKey, BagOStuff::WRITE_PRUNE_SEGMENTS );
540 $keyList[] = $newKey;
541 $this->cache->set( $key, $keyList, 2 * self::MAX_CACHE_TTL );
548 private function recentStashEntryCount( UserIdentity $user ) {
549 $key = $this->cache->makeKey(
'stash-edit-recent', sha1( $user->getName() ) );
551 return count( $this->cache->get( $key ) ?: [] );
__construct(BagOStuff $cache, ILoadBalancer $lb, LoggerInterface $logger, StatsdDataFactoryInterface $stats, UserEditTracker $userEditTracker, UserFactory $userFactory, WikiPageFactory $wikiPageFactory, HookContainer $hookContainer, $initiator)
Base representation for an editable wiki page.
Base interface for content objects.