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;
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;
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->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(
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';
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 );
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 ) {
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, ILoadBalancer $lb, 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.