25 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
37 use Psr\Log\LoggerInterface;
40 use Wikimedia\ScopedCallback;
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 ) {
135 $pageUpdater = $pageUpdater->newPageUpdater( $user );
138 $page = $pageUpdater->getPage();
139 $key = $this->getStashKey( $page, $this->getContentHash(
$content ), $user );
148 $dbw = $this->dbProvider->getPrimaryDatabase();
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;
168 $output = $update->getCanonicalParserOutput();
169 $output->setCacheTime( $update->getRevision()->getTimestamp() );
172 $editInfo = (object)[
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->dbProvider->getPrimaryDatabase();
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(
414 $time = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder()
415 ->select(
'MAX(rc_timestamp)' )
416 ->from(
'recentchanges' )
417 ->join(
'actor',
null,
'actor_id=rc_actor' )
418 ->where( [
'actor_name' => $user->
getName() ] )
419 ->caller( __METHOD__ )
432 return sha1( implode(
"\n", [
451 private function getStashKey( PageIdentity $page, $contentHash, UserIdentity $user ) {
452 return $this->cache->makeKey(
454 md5(
"{$page->getNamespace()}\n{$page->getDBkey()}" ),
458 md5(
"{$user->getId()}\n{$user->getName()}" )
466 private function getStashValue( $key ) {
467 $serial = $this->cache->get( $key );
469 return $this->unserializeStashInfo( $serial );
484 private function storeStashValue(
494 $ttl = min( $parserOutput->
getCacheExpiry() - $age, self::MAX_CACHE_TTL );
496 if ( $parserOutput->
getOutputFlag( ParserOutputFlags::USER_SIGNATURE ) ) {
497 $ttl = min( $ttl, self::MAX_SIGNATURE_TTL );
501 return 'uncacheable';
505 $stashInfo = (object)[
506 'pstContent' => $pstContent,
507 'output' => $parserOutput,
508 'timestamp' => $timestamp,
509 'edits' => $this->userEditTracker->getUserEditCount( $user ),
511 $serial = $this->serializeStashInfo( $stashInfo );
512 if ( $serial ===
false ) {
513 return 'store_error';
519 $this->pruneExcessStashedEntries( $user, $key );
522 return $ok ?
true :
'store_error';
529 private function pruneExcessStashedEntries( UserIdentity $user, $newKey ) {
530 $key = $this->cache->makeKey(
'stash-edit-recent', sha1( $user->getName() ) );
532 $keyList = $this->cache->get( $key ) ?: [];
533 if ( count( $keyList ) >= self::MAX_CACHE_RECENT ) {
534 $oldestKey = array_shift( $keyList );
538 $keyList[] = $newKey;
539 $this->cache->set( $key, $keyList, 2 * self::MAX_CACHE_TTL );
546 private function recentStashEntryCount( UserIdentity $user ) {
547 $key = $this->cache->makeKey(
'stash-edit-recent', sha1( $user->getName() ) );
549 return count( $this->cache->get( $key ) ?: [] );
552 private function serializeStashInfo( stdClass $stashInfo ) {
554 return serialize( $stashInfo );
557 private function unserializeStashInfo( $serial ) {
558 if ( is_string( $serial ) ) {
560 $stashInfo = unserialize( $serial );
561 if ( is_object( $stashInfo ) && $stashInfo->output instanceof
ParserOutput ) {
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Class representing a cache/ephemeral data store.
const WRITE_ALLOW_SEGMENTS
Allow partitioning of the value if it is a large string.
const WRITE_PRUNE_SEGMENTS
Delete all the segments if the value is partitioned.
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Service for creating WikiPage objects.
Manage the pre-emptive page parsing for edits to wiki pages.
checkCache(PageIdentity $page, Content $content, UserIdentity $user)
Check that a prepared edit is in cache and still up-to-date.
parseAndCache( $pageUpdater, Content $content, UserIdentity $user, string $summary)
__construct(BagOStuff $cache, IConnectionProvider $dbProvider, LoggerInterface $logger, StatsdDataFactoryInterface $stats, UserEditTracker $userEditTracker, UserFactory $userFactory, WikiPageFactory $wikiPageFactory, HookContainer $hookContainer, $initiator)
const PRESUME_FRESH_TTL_SEC
stashInputText( $text, $textHash)
const INITIATOR_JOB_OR_CLI
fetchInputText( $textHash)
getOutputFlag(string $name)
Provides a uniform interface to various boolean flags stored in the ParserOutput.
Base representation for an editable wiki page.
Base interface for representing page content.
Interface for objects (potentially) representing an editable wiki page.