66 private $userEditTracker;
70 private $wikiPageFactory;
84 private const MAX_CACHE_RECENT = 2;
103 LoggerInterface $logger,
104 StatsdDataFactoryInterface $stats,
111 $this->cache = $cache;
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 ) {
135 $pageUpdater = $pageUpdater->newPageUpdater( $user );
138 $page = $pageUpdater->getPage();
139 $key = $this->getStashKey( $page, $this->getContentHash(
$content ), $user );
148 $dbw = $this->lb->getConnectionRef(
DB_PRIMARY );
149 if ( !$dbw->lock( $key, $fname, 0 ) ) {
154 $unlocker =
new ScopedCallback(
static function () use ( $dbw, $key, $fname ) {
155 $dbw->unlock( $key, $fname );
161 $editInfo = $this->getStashValue( $key );
162 if ( $editInfo && (
int)
wfTimestamp( TS_UNIX, $editInfo->timestamp ) >= $cutoffTime ) {
163 $alreadyCached =
true;
165 $pageUpdater->setContent( SlotRecord::MAIN,
$content );
168 $output = $update->getCanonicalParserOutput();
169 $output->setCacheTime( $update->getRevision()->getTimestamp() );
172 $editInfo = (object)[
173 'pstContent' => $update->getRawContent( SlotRecord::MAIN ),
175 'timestamp' => $output->getCacheTime()
178 $alreadyCached =
false;
181 $logContext = [
'cachekey' => $key,
'title' => (string)$page ];
183 if ( $editInfo->output ) {
185 $legacyUser = $this->userFactory->newFromUserIdentity( $user );
186 $legacyPage = $this->wikiPageFactory->newFromTitle( $page );
187 $this->hookRunner->onParserOutputStashForEdit(
188 $legacyPage,
$content, $editInfo->output, $summary, $legacyUser );
190 if ( $alreadyCached ) {
191 $logger->debug(
"Parser output for key '{cachekey}' already cached.", $logContext );
196 $code = $this->storeStashValue(
198 $editInfo->pstContent,
200 $editInfo->timestamp,
204 if ( $code ===
true ) {
205 $logger->debug(
"Cached parser output for key '{cachekey}'.", $logContext );
208 } elseif ( $code ===
'uncacheable' ) {
210 "Uncacheable parser output for key '{cachekey}' [{code}].",
211 $logContext + [
'code' => $code ]
217 "Failed to cache parser output for key '{cachekey}'.",
218 $logContext + [
'code' => $code ]
249 $legacyUser = $this->userFactory->newFromUserIdentity( $user );
252 !$legacyUser->getRequest()->wasPosted() ||
254 $this->initiator !== self::INITIATOR_USER ||
262 $logger = $this->logger;
264 $key = $this->getStashKey( $page, $this->getContentHash(
$content ), $user );
267 'title' => (string)$page,
271 $editInfo = $this->getAndWaitForStashValue( $key );
272 if ( !is_object( $editInfo ) || !$editInfo->output ) {
273 $this->incrStatsByContent(
'cache_misses.no_stash',
$content );
274 if ( $this->recentStashEntryCount( $user ) > 0 ) {
275 $logger->info(
"Empty cache for key '{key}' but not for user.", $logContext );
277 $logger->debug(
"Empty cache for key '{key}'.", $logContext );
283 $age = time() - (int)
wfTimestamp( TS_UNIX, $editInfo->output->getCacheTime() );
284 $logContext[
'age'] = $age;
286 $isCacheUsable =
true;
287 if ( $age <= self::PRESUME_FRESH_TTL_SEC ) {
289 $this->incrStatsByContent(
'cache_hits.presumed_fresh',
$content );
290 $logger->debug(
"Timestamp-based cache hit for key '{key}'.", $logContext );
292 $lastEdit = $this->lastEditTime( $user );
293 $cacheTime = $editInfo->output->getCacheTime();
294 if ( $lastEdit < $cacheTime ) {
296 $this->incrStatsByContent(
'cache_hits.presumed_fresh',
$content );
297 $logger->debug(
"Edit check based cache hit for key '{key}'.", $logContext );
299 $isCacheUsable =
false;
300 $this->incrStatsByContent(
'cache_misses.proven_stale',
$content );
301 $logger->info(
"Stale cache for key '{key}' due to outside edits.", $logContext );
304 if ( $editInfo->edits === $this->userEditTracker->getUserEditCount( $user ) ) {
306 $this->incrStatsByContent(
'cache_hits.presumed_fresh',
$content );
307 $logger->debug(
"Edit count based cache hit for key '{key}'.", $logContext );
309 $isCacheUsable =
false;
310 $this->incrStatsByContent(
'cache_misses.proven_stale',
$content );
311 $logger->info(
"Stale cache for key '{key}'due to outside edits.", $logContext );
315 if ( !$isCacheUsable ) {
319 if ( $editInfo->output->getOutputFlag( ParserOutputFlags::VARY_REVISION ) ) {
323 "Cache for key '{key}' has vary-revision; post-insertion parse inevitable.",
327 static $flagsMaybeReparse = [
329 ParserOutputFlags::VARY_REVISION_ID,
331 ParserOutputFlags::VARY_REVISION_TIMESTAMP,
333 ParserOutputFlags::VARY_REVISION_SHA1,
335 ParserOutputFlags::VARY_PAGE_ID,
337 foreach ( $flagsMaybeReparse as $flag ) {
338 if ( $editInfo->output->getOutputFlag( $flag ) ) {
340 "Cache for key '{key}' has $flag; post-insertion parse possible.",
355 $this->stats->increment(
'editstash.' . $subkey );
356 $this->stats->increment(
'editstash_by_model.' .
$content->getModel() .
'.' . $subkey );
363 private function getAndWaitForStashValue( $key ) {
364 $editInfo = $this->getStashValue( $key );
367 $start = microtime(
true );
370 $dbw = $this->lb->getConnection(
DB_PRIMARY );
371 if ( $dbw->lock( $key, __METHOD__, 30 ) ) {
372 $editInfo = $this->getStashValue( $key );
373 $dbw->unlock( $key, __METHOD__ );
376 $timeMs = 1000 * max( 0, microtime(
true ) - $start );
377 $this->stats->timing(
'editstash.lock_wait_time', $timeMs );
388 $textKey = $this->cache->makeKey(
'stashedit',
'text', $textHash );
390 return $this->cache->get( $textKey );
399 $textKey = $this->cache->makeKey(
'stashedit',
'text', $textHash );
401 return $this->cache->set(
405 BagOStuff::WRITE_ALLOW_SEGMENTS
414 $db = $this->lb->getConnectionRef(
DB_REPLICA );
416 $time = $db->newSelectQueryBuilder()
417 ->select(
'MAX(rc_timestamp)' )
418 ->from(
'recentchanges' )
419 ->join(
'actor',
null,
'actor_id=rc_actor' )
420 ->where( [
'actor_name' => $user->
getName() ] )
421 ->caller( __METHOD__ )
434 return sha1( implode(
"\n", [
453 private function getStashKey( PageIdentity $page, $contentHash, UserIdentity $user ) {
454 return $this->cache->makeKey(
456 md5(
"{$page->getNamespace()}\n{$page->getDBkey()}" ),
460 md5(
"{$user->getId()}\n{$user->getName()}" )
468 private function getStashValue( $key ) {
469 $serial = $this->cache->get( $key );
471 return $this->unserializeStashInfo( $serial );
486 private function storeStashValue(
496 $ttl = min( $parserOutput->
getCacheExpiry() - $age, self::MAX_CACHE_TTL );
498 if ( $parserOutput->
getOutputFlag( ParserOutputFlags::USER_SIGNATURE ) ) {
499 $ttl = min( $ttl, self::MAX_SIGNATURE_TTL );
503 return 'uncacheable';
507 $stashInfo = (object)[
508 'pstContent' => $pstContent,
509 'output' => $parserOutput,
510 'timestamp' => $timestamp,
511 'edits' => $user->isRegistered()
512 ? $this->userEditTracker->getUserEditCount( $user )
515 $serial = $this->serializeStashInfo( $stashInfo );
516 if ( $serial ===
false ) {
517 return 'store_error';
520 $ok = $this->cache->set( $key, $serial, $ttl, BagOStuff::WRITE_ALLOW_SEGMENTS );
523 $this->pruneExcessStashedEntries( $user, $key );
526 return $ok ?
true :
'store_error';
533 private function pruneExcessStashedEntries( UserIdentity $user, $newKey ) {
534 $key = $this->cache->makeKey(
'stash-edit-recent', sha1( $user->getName() ) );
536 $keyList = $this->cache->get( $key ) ?: [];
537 if ( count( $keyList ) >= self::MAX_CACHE_RECENT ) {
538 $oldestKey = array_shift( $keyList );
539 $this->cache->delete( $oldestKey, BagOStuff::WRITE_PRUNE_SEGMENTS );
542 $keyList[] = $newKey;
543 $this->cache->set( $key, $keyList, 2 * self::MAX_CACHE_TTL );
550 private function recentStashEntryCount( UserIdentity $user ) {
551 $key = $this->cache->makeKey(
'stash-edit-recent', sha1( $user->getName() ) );
553 return count( $this->cache->get( $key ) ?: [] );
556 private function serializeStashInfo( stdClass $stashInfo ) {
558 return serialize( $stashInfo );
561 private function unserializeStashInfo( $serial ) {
562 if ( is_string( $serial ) ) {
564 $stashInfo = unserialize( $serial );
565 if ( is_object( $stashInfo ) && $stashInfo->output instanceof
ParserOutput ) {
__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 representing page content.