const ERROR_UNCACHEABLE = 'uncacheable';
public function execute() {
- $cache = ObjectCache::getLocalClusterInstance();
-
$user = $this->getUser();
$params = $this->extractRequestParams();
if ( !$content ) { // merge3() failed
$this->getResult()->addValue( null,
- $this->getModuleName(), array( 'status' => 'editconflict' ) );
+ $this->getModuleName(), [ 'status' => 'editconflict' ] );
return;
}
// The user will abort the AJAX request by pressing "save", so ignore that
ignore_user_abort( true );
+ // Use the master DB for fast blocking locks
+ $dbw = wfGetDB( DB_MASTER );
+
// Get a key based on the source text, format, and user preferences
$key = self::getStashKey( $title, $content, $user );
// De-duplicate requests on the same key
if ( $user->pingLimiter( 'stashedit' ) ) {
$status = 'ratelimited';
- } elseif ( $cache->lock( $key, 0, 30 ) ) {
- /** @noinspection PhpUnusedLocalVariableInspection */
- $unlocker = new ScopedCallback( function() use ( $cache, $key ) {
- $cache->unlock( $key );
- } );
+ } elseif ( $dbw->lock( $key, __METHOD__, 1 ) ) {
$status = self::parseAndStash( $page, $content, $user );
+ $dbw->unlock( $key, __METHOD__ );
} else {
$status = 'busy';
}
- $this->getResult()->addValue( null, $this->getModuleName(), array( 'status' => $status ) );
+ $this->getResult()->addValue( null, $this->getModuleName(), [ 'status' => $status ] );
}
/**
if ( $editInfo && $editInfo->output ) {
$key = self::getStashKey( $page->getTitle(), $content, $user );
+ // Let extensions add ParserOutput metadata or warm other caches
+ Hooks::run( 'ParserOutputStashForEdit', [ $page, $content, $editInfo->output ] );
+
list( $stashInfo, $ttl ) = self::buildStashValue(
$editInfo->pstContent, $editInfo->output, $editInfo->timestamp
);
if ( $stashInfo ) {
$ok = $cache->set( $key, $stashInfo, $ttl );
if ( $ok ) {
+
$logger->debug( "Cached parser output for key '$key'." );
return self::ERROR_NONE;
} else {
public static function checkCache( Title $title, Content $content, User $user ) {
$cache = ObjectCache::getLocalClusterInstance();
$logger = LoggerFactory::getInstance( 'StashEdit' );
+ $stats = RequestContext::getMain()->getStats();
$key = self::getStashKey( $title, $content, $user );
$editInfo = $cache->get( $key );
if ( !is_object( $editInfo ) ) {
$start = microtime( true );
// We ignore user aborts and keep parsing. Block on any prior parsing
- // so as to use it's results and make use of the time spent parsing.
- if ( $cache->lock( $key, 30, 30 ) ) {
+ // so as to use its results and make use of the time spent parsing.
+ // Skip this logic if there no master connection in case this method
+ // is called on an HTTP GET request for some reason.
+ $lb = wfGetLB();
+ $dbw = $lb->getAnyOpenConnection( $lb->getWriterIndex() );
+ if ( $dbw && $dbw->lock( $key, __METHOD__, 30 ) ) {
$editInfo = $cache->get( $key );
- $cache->unlock( $key );
- }
- $sec = microtime( true ) - $start;
- if ( $sec > .01 ) {
- $logger->warning( "Waited $sec seconds on '$key'." );
+ $dbw->unlock( $key, __METHOD__ );
}
+
+ $timeMs = 1000 * max( 0, microtime( true ) - $start );
+ $stats->timing( 'editstash.lock-wait-time', $timeMs );
}
if ( !is_object( $editInfo ) || !$editInfo->output ) {
+ $stats->increment( 'editstash.cache-misses' );
$logger->debug( "No cache value for key '$key'." );
return false;
}
$time = wfTimestamp( TS_UNIX, $editInfo->output->getTimestamp() );
if ( ( time() - $time ) <= 3 ) {
+ $stats->increment( 'editstash.cache-hits' );
$logger->debug( "Timestamp-based cache hit for key '$key'." );
return $editInfo; // assume nothing changed
}
$dbr = wfGetDB( DB_SLAVE );
- $templates = array(); // conditions to find changes/creations
+ $templates = []; // conditions to find changes/creations
$templateUses = 0; // expected existing templates
foreach ( $editInfo->output->getTemplateIds() as $ns => $stuff ) {
foreach ( $stuff as $dbkey => $revId ) {
if ( count( $templates ) ) {
$res = $dbr->select(
'page',
- array( 'ns' => 'page_namespace', 'dbk' => 'page_title', 'page_latest' ),
+ [ 'ns' => 'page_namespace', 'dbk' => 'page_title', 'page_latest' ],
$dbr->makeWhereFrom2d( $templates, 'page_namespace', 'page_title' ),
__METHOD__
);
}
if ( $changed || $res->numRows() != $templateUses ) {
+ $stats->increment( 'editstash.cache-misses' );
$logger->info( "Stale cache for key '$key'; template changed." );
return false;
}
}
- $files = array(); // conditions to find changes/creations
+ $files = []; // conditions to find changes/creations
foreach ( $editInfo->output->getFileSearchOptions() as $name => $options ) {
$files[$name] = (string)$options['sha1'];
}
if ( count( $files ) ) {
$res = $dbr->select(
'image',
- array( 'name' => 'img_name', 'img_sha1' ),
- array( 'img_name' => array_keys( $files ) ),
+ [ 'name' => 'img_name', 'img_sha1' ],
+ [ 'img_name' => array_keys( $files ) ],
__METHOD__
);
$changed = false;
}
if ( $changed || $res->numRows() != count( $files ) ) {
+ $stats->increment( 'editstash.cache-misses' );
$logger->info( "Stale cache for key '$key'; file changed." );
return false;
}
}
+ $stats->increment( 'editstash.cache-hits' );
$logger->debug( "Cache hit for key '$key'." );
return $editInfo;
* @return string
*/
protected static function getStashKey( Title $title, Content $content, User $user ) {
- $hash = sha1( implode( ':', array(
+ $hash = sha1( implode( ':', [
$content->getModel(),
$content->getDefaultFormat(),
sha1( $content->serialize( $content->getDefaultFormat() ) ),
$user->getId() ?: md5( $user->getName() ), // account for user parser options
$user->getId() ? $user->getDBTouched() : '-' // handle preference change races
- ) ) );
+ ] ) );
return wfMemcKey( 'prepared-edit', md5( $title->getPrefixedDBkey() ), $hash );
}
if ( $ttl > 0 && !$parserOutput->getFlag( 'vary-revision' ) ) {
// Only store what is actually needed
- $stashInfo = (object)array(
+ $stashInfo = (object)[
'pstContent' => $pstContent,
'output' => $parserOutput,
'timestamp' => $timestamp
- );
- return array( $stashInfo, $ttl );
+ ];
+ return [ $stashInfo, $ttl ];
}
- return array( null, 0 );
+ return [ null, 0 ];
}
public function getAllowedParams() {
- return array(
- 'title' => array(
+ return [
+ 'title' => [
ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_REQUIRED => true
- ),
- 'section' => array(
+ ],
+ 'section' => [
ApiBase::PARAM_TYPE => 'string',
- ),
- 'sectiontitle' => array(
+ ],
+ 'sectiontitle' => [
ApiBase::PARAM_TYPE => 'string'
- ),
- 'text' => array(
+ ],
+ 'text' => [
ApiBase::PARAM_TYPE => 'text',
ApiBase::PARAM_REQUIRED => true
- ),
- 'contentmodel' => array(
+ ],
+ 'contentmodel' => [
ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
ApiBase::PARAM_REQUIRED => true
- ),
- 'contentformat' => array(
+ ],
+ 'contentformat' => [
ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
ApiBase::PARAM_REQUIRED => true
- ),
- 'baserevid' => array(
+ ],
+ 'baserevid' => [
ApiBase::PARAM_TYPE => 'integer',
ApiBase::PARAM_REQUIRED => true
- )
- );
+ ]
+ ];
}
- function needsToken() {
+ public function needsToken() {
return 'csrf';
}
- function mustBePosted() {
+ public function mustBePosted() {
+ return true;
+ }
+
+ public function isWriteMode() {
return true;
}
- function isInternal() {
+ public function isInternal() {
return true;
}
}