27use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
36use Psr\Log\LoggerInterface;
40use Wikimedia\ScopedCallback;
95 StatsdDataFactoryInterface
$stats,
107 $this->hookRunner =
new HookRunner( $hookContainer );
130 $dbw = $this->lb->getConnectionRef(
DB_PRIMARY );
131 if ( !$dbw->lock( $key, $fname, 0 ) ) {
136 $unlocker =
new ScopedCallback(
static function () use ( $dbw, $key, $fname ) {
137 $dbw->unlock( $key, $fname );
144 if ( $editInfo &&
wfTimestamp( TS_UNIX, $editInfo->timestamp ) >= $cutoffTime ) {
145 $alreadyCached =
true;
147 $format =
$content->getDefaultFormat();
149 $editInfo->output->setCacheTime( $editInfo->timestamp );
150 $alreadyCached =
false;
153 $logContext = [
'cachekey' => $key,
'title' => (string)$page ];
155 if ( $editInfo && $editInfo->output ) {
157 $legacyUser = $this->userFactory->newFromUserIdentity( $user );
158 $this->hookRunner->onParserOutputStashForEdit(
159 $page,
$content, $editInfo->output, $summary, $legacyUser );
161 if ( $alreadyCached ) {
162 $logger->debug(
"Parser output for key '{cachekey}' already cached.", $logContext );
169 $editInfo->pstContent,
171 $editInfo->timestamp,
175 if ( $code ===
true ) {
176 $logger->debug(
"Cached parser output for key '{cachekey}'.", $logContext );
179 } elseif ( $code ===
'uncacheable' ) {
181 "Uncacheable parser output for key '{cachekey}' [{code}].",
182 $logContext + [
'code' => $code ]
188 "Failed to cache parser output for key '{cachekey}'.",
189 $logContext + [
'code' => $code ]
224 $this->initiator !== self::INITIATOR_USER ||
237 'title' => (string)$page,
242 if ( !is_object( $editInfo ) || !$editInfo->output ) {
245 $logger->info(
"Empty cache for key '{key}' but not for user.", $logContext );
247 $logger->debug(
"Empty cache for key '{key}'.", $logContext );
253 $age = time() - (int)
wfTimestamp( TS_UNIX, $editInfo->output->getCacheTime() );
254 $logContext[
'age'] = $age;
256 $isCacheUsable =
true;
257 if ( $age <= self::PRESUME_FRESH_TTL_SEC ) {
260 $logger->debug(
"Timestamp-based cache hit for key '{key}'.", $logContext );
261 } elseif ( $user->
isAnon() ) {
263 $cacheTime = $editInfo->output->getCacheTime();
264 if ( $lastEdit < $cacheTime ) {
267 $logger->debug(
"Edit check based cache hit for key '{key}'.", $logContext );
269 $isCacheUsable =
false;
271 $logger->info(
"Stale cache for key '{key}' due to outside edits.", $logContext );
274 if ( $editInfo->edits === $this->userEditTracker->getUserEditCount( $user ) ) {
277 $logger->debug(
"Edit count based cache hit for key '{key}'.", $logContext );
279 $isCacheUsable =
false;
281 $logger->info(
"Stale cache for key '{key}'due to outside edits.", $logContext );
285 if ( !$isCacheUsable ) {
289 if ( $editInfo->output->getFlag(
'vary-revision' ) ) {
293 "Cache for key '{key}' has vary-revision; post-insertion parse inevitable.",
297 static $flagsMaybeReparse = [
301 'vary-revision-timestamp',
303 'vary-revision-sha1',
307 foreach ( $flagsMaybeReparse as $flag ) {
308 if ( $editInfo->output->getFlag( $flag ) ) {
310 "Cache for key '{key}' has $flag; post-insertion parse possible.",
325 $this->stats->increment(
'editstash.' . $subkey );
326 $this->stats->increment(
'editstash_by_model.' .
$content->getModel() .
'.' . $subkey );
337 $start = microtime(
true );
342 $dbw = $this->lb->getAnyOpenConnection( $this->lb->getWriterIndex() );
343 if ( $dbw && $dbw->lock( $key, __METHOD__, 30 ) ) {
345 $dbw->unlock( $key, __METHOD__ );
348 $timeMs = 1000 * max( 0, microtime(
true ) - $start );
349 $this->stats->timing(
'editstash.lock_wait_time', $timeMs );
360 $textKey = $this->cache->makeKey(
'stashedit',
'text', $textHash );
362 return $this->cache->get( $textKey );
371 $textKey = $this->cache->makeKey(
'stashedit',
'text', $textHash );
373 return $this->cache->set(
377 BagOStuff::WRITE_ALLOW_SEGMENTS
386 $db = $this->lb->getConnectionRef(
DB_REPLICA );
388 $time = $db->newSelectQueryBuilder()
389 ->select(
'MAX(rc_timestamp)' )
390 ->from(
'recentchanges' )
391 ->join(
'actor',
null,
'actor_id=rc_actor' )
392 ->where( [
'actor_name' => $user->
getName() ] )
393 ->caller( __METHOD__ )
406 return sha1( implode(
"\n", [
426 return $this->cache->makeKey(
428 md5(
"{$page->getNamespace()}\n{$page->getDBkey()}" ),
432 md5(
"{$user->getId()}\n{$user->getName()}" )
441 $stashInfo = $this->cache->get( $key );
442 if ( is_object( $stashInfo ) && $stashInfo->output instanceof
ParserOutput ) {
471 $ttl = min( $parserOutput->
getCacheExpiry() - $age, self::MAX_CACHE_TTL );
473 if ( $parserOutput->
getFlag(
'user-signature' ) ) {
474 $ttl = min( $ttl, self::MAX_SIGNATURE_TTL );
478 return 'uncacheable';
482 $stashInfo = (object)[
483 'pstContent' => $pstContent,
484 'output' => $parserOutput,
485 'timestamp' => $timestamp,
486 'edits' => $user->
isRegistered() ? $this->userEditTracker->getUserEditCount( $user ) :
null,
489 $ok = $this->cache->set( $key, $stashInfo, $ttl, BagOStuff::WRITE_ALLOW_SEGMENTS );
495 return $ok ?
true :
'store_error';
503 $key = $this->cache->makeKey(
'stash-edit-recent', sha1( $user->
getName() ) );
505 $keyList = $this->cache->get( $key ) ?: [];
506 if ( count( $keyList ) >= self::MAX_CACHE_RECENT ) {
507 $oldestKey = array_shift( $keyList );
508 $this->cache->delete( $oldestKey, BagOStuff::WRITE_PRUNE_SEGMENTS );
511 $keyList[] = $newKey;
512 $this->cache->set( $key, $keyList, 2 * self::MAX_CACHE_TTL );
520 $key = $this->cache->makeKey(
'stash-edit-recent', sha1( $user->
getName() ) );
522 return count( $this->cache->get( $key ) ?: [] );
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.
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Class for managing stashed edits used by the page updater classes.
storeStashValue( $key, Content $pstContent, ParserOutput $parserOutput, $timestamp, UserIdentity $user)
Build a value to store in memcached based on the PST content and parser output.
lastEditTime(UserIdentity $user)
__construct(BagOStuff $cache, ILoadBalancer $lb, LoggerInterface $logger, StatsdDataFactoryInterface $stats, UserEditTracker $userEditTracker, UserFactory $userFactory, HookContainer $hookContainer, $initiator)
pruneExcessStashedEntries(UserIdentity $user, $newKey)
parseAndCache(WikiPage $page, Content $content, UserIdentity $user, string $summary)
incrStatsByContent( $subkey, Content $content)
StatsdDataFactoryInterface $stats
ParserOutputStashForEditHook $hookRunner
UserEditTracker $userEditTracker
getContentHash(Content $content)
Get hash of the content, factoring in model/format.
getStashKey(PageIdentity $page, $contentHash, UserIdentity $user)
Get the temporary prepared edit stash key for a user.
const PRESUME_FRESH_TTL_SEC
recentStashEntryCount(UserIdentity $user)
stashInputText( $text, $textHash)
const INITIATOR_JOB_OR_CLI
getAndWaitForStashValue( $key)
fetchInputText( $textHash)
checkCache(PageIdentity $page, Content $content, User $user)
Check that a prepared edit is in cache and still up-to-date.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
getRequest()
Get the WebRequest object to use with this object.
getName()
Get the user name, or the IP of an anonymous user.
isAnon()
Get whether the user is anonymous.
Class representing a MediaWiki article and history.
prepareContentForEdit(Content $content, RevisionRecord $revision=null, UserIdentity $user=null, $serialFormat=null, $useCache=true)
Prepare content which is about to be saved.
Base interface for content objects.
Interface for objects (potentially) representing an editable wiki page.