Avoid parser cache miss that often occurs post-save
[lhc/web/wiklou.git] / includes / page / WikiPage.php
index cc182a4..5e72151 100644 (file)
@@ -1086,10 +1086,7 @@ class WikiPage implements Page, IDBAccessObject {
         * @return bool
         */
        public function isParserCacheUsed( ParserOptions $parserOptions, $oldid ) {
-               global $wgEnableParserCache;
-
-               return $wgEnableParserCache
-                       && $parserOptions->getStubThreshold() == 0
+               return $parserOptions->getStubThreshold() == 0
                        && $this->exists()
                        && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() )
                        && $this->getContentHandler()->isParserCacheSupported();
@@ -1111,7 +1108,7 @@ class WikiPage implements Page, IDBAccessObject {
                $useParserCache = $this->isParserCacheUsed( $parserOptions, $oldid );
                wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
                if ( $parserOptions->getStubThreshold() ) {
-                       wfIncrStats( 'pcache_miss_stub' );
+                       wfIncrStats( 'pcache.miss.stub' );
                }
 
                if ( $useParserCache ) {
@@ -1267,10 +1264,9 @@ class WikiPage implements Page, IDBAccessObject {
                        $conditions['page_latest'] = $lastRevision;
                }
 
-               $now = wfTimestampNow();
                $row = array( /* SET */
                        'page_latest'      => $revision->getId(),
-                       'page_touched'     => $dbw->timestamp( $now ),
+                       'page_touched'     => $dbw->timestamp( $revision->getTimestamp() ),
                        'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
                        'page_is_redirect' => $rt !== null ? 1 : 0,
                        'page_len'         => $len,
@@ -1692,6 +1688,7 @@ class WikiPage implements Page, IDBAccessObject {
         *     revision: The revision object for the inserted revision, or null.
         *
         * @since 1.21
+        * @throws MWException
         */
        public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false,
                User $user = null, $serialFormat = null
@@ -1803,56 +1800,48 @@ class WikiPage implements Page, IDBAccessObject {
 
                        if ( $changed ) {
                                $dbw->begin( __METHOD__ );
-                               try {
 
-                                       $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
-                                       $status->merge( $prepStatus );
+                               $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
+                               $status->merge( $prepStatus );
 
-                                       if ( !$status->isOK() ) {
-                                               $dbw->rollback( __METHOD__ );
+                               if ( !$status->isOK() ) {
+                                       $dbw->rollback( __METHOD__ );
 
-                                               return $status;
-                                       }
-                                       $revisionId = $revision->insertOn( $dbw );
+                                       return $status;
+                               }
+                               $revisionId = $revision->insertOn( $dbw );
 
-                                       // Update page
-                                       //
-                                       // We check for conflicts by comparing $oldid with the current latest revision ID.
-                                       $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect );
+                               // Update page
+                               //
+                               // We check for conflicts by comparing $oldid with the current latest revision ID.
+                               $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect );
 
-                                       if ( !$ok ) {
-                                               // Belated edit conflict! Run away!!
-                                               $status->fatal( 'edit-conflict' );
+                               if ( !$ok ) {
+                                       // Belated edit conflict! Run away!!
+                                       $status->fatal( 'edit-conflict' );
 
-                                               $dbw->rollback( __METHOD__ );
+                                       $dbw->rollback( __METHOD__ );
 
-                                               return $status;
-                                       }
+                                       return $status;
+                               }
 
-                                       Hooks::run( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) );
-                                       // Update recentchanges
-                                       if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
-                                               // Mark as patrolled if the user can do so
-                                               $patrolled = $wgUseRCPatrol && !count(
-                                               $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
-                                               // Add RC row to the DB
-                                               $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary,
-                                                       $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize,
-                                                       $revisionId, $patrolled
-                                               );
+                               Hooks::run( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) );
 
-                                               // Log auto-patrolled edits
-                                               if ( $patrolled ) {
-                                                       PatrolLog::record( $rc, true, $user );
-                                               }
-                                       }
-                                       $user->incEditCount();
-                               } catch ( Exception $e ) {
-                                       $dbw->rollback( __METHOD__ );
-                                       // Question: Would it perhaps be better if this method turned all
-                                       // exceptions into $status's?
-                                       throw $e;
+                               // Update recentchanges
+                               if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
+                                       // Mark as patrolled if the user can do so
+                                       $patrolled = $wgUseRCPatrol && !count(
+                                               $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
+                                       // Add RC row to the DB
+                                       RecentChange::notifyEdit(
+                                               $now, $this->mTitle, $isminor, $user, $summary,
+                                               $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize,
+                                               $revisionId, $patrolled
+                                       );
                                }
+
+                               $user->incEditCount();
+
                                $dbw->commit( __METHOD__ );
                        } else {
                                // Bug 32948: revision ID must be set to page {{REVISIONID}} and
@@ -1875,85 +1864,78 @@ class WikiPage implements Page, IDBAccessObject {
                                $revision = null;
                                // Update page_touched, this is usually implicit in the page update
                                // Other cache updates are done in onArticleEdit()
-                               $this->mTitle->invalidateCache();
+                               $this->mTitle->invalidateCache( $now );
                        }
                } else {
                        // Create new article
                        $status->value['new'] = true;
 
                        $dbw->begin( __METHOD__ );
-                       try {
 
-                               $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
-                               $status->merge( $prepStatus );
+                       $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
+                       $status->merge( $prepStatus );
 
-                               if ( !$status->isOK() ) {
-                                       $dbw->rollback( __METHOD__ );
+                       if ( !$status->isOK() ) {
+                               $dbw->rollback( __METHOD__ );
 
-                                       return $status;
-                               }
+                               return $status;
+                       }
 
-                               $status->merge( $prepStatus );
+                       $status->merge( $prepStatus );
 
-                               // Add the page record; stake our claim on this title!
-                               // This will return false if the article already exists
-                               $newid = $this->insertOn( $dbw );
+                       // Add the page record; stake our claim on this title!
+                       // This will return false if the article already exists
+                       $newid = $this->insertOn( $dbw );
 
-                               if ( $newid === false ) {
-                                       $dbw->rollback( __METHOD__ );
-                                       $status->fatal( 'edit-already-exists' );
+                       if ( $newid === false ) {
+                               $dbw->rollback( __METHOD__ );
+                               $status->fatal( 'edit-already-exists' );
 
-                                       return $status;
-                               }
+                               return $status;
+                       }
 
-                               // Save the revision text...
-                               $revision = new Revision( array(
-                                       'page'       => $newid,
-                                       'title'      => $this->getTitle(), // for determining the default content model
-                                       'comment'    => $summary,
-                                       'minor_edit' => $isminor,
-                                       'text'       => $serialized,
-                                       'len'        => $newsize,
-                                       'user'       => $user->getId(),
-                                       'user_text'  => $user->getName(),
-                                       'timestamp'  => $now,
-                                       'content_model' => $content->getModel(),
-                                       'content_format' => $serialFormat,
-                               ) );
-                               $revisionId = $revision->insertOn( $dbw );
+                       // Save the revision text...
+                       $revision = new Revision( array(
+                               'page'       => $newid,
+                               'title'      => $this->getTitle(), // for determining the default content model
+                               'comment'    => $summary,
+                               'minor_edit' => $isminor,
+                               'text'       => $serialized,
+                               'len'        => $newsize,
+                               'user'       => $user->getId(),
+                               'user_text'  => $user->getName(),
+                               'timestamp'  => $now,
+                               'content_model' => $content->getModel(),
+                               'content_format' => $serialFormat,
+                       ) );
+                       $revisionId = $revision->insertOn( $dbw );
 
-                               // Bug 37225: use accessor to get the text as Revision may trim it
-                               $content = $revision->getContent(); // sanity; get normalized version
+                       // Bug 37225: use accessor to get the text as Revision may trim it
+                       $content = $revision->getContent(); // sanity; get normalized version
 
-                               if ( $content ) {
-                                       $newsize = $content->getSize();
-                               }
+                       if ( $content ) {
+                               $newsize = $content->getSize();
+                       }
 
-                               // Update the page record with revision data
-                               $this->updateRevisionOn( $dbw, $revision, 0 );
+                       // Update the page record with revision data
+                       $this->updateRevisionOn( $dbw, $revision, 0 );
 
-                               Hooks::run( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
+                       Hooks::run( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
 
-                               // Update recentchanges
-                               if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
-                                       // Mark as patrolled if the user can do so
-                                       $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count(
-                                               $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
-                                       // Add RC row to the DB
-                                       $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot,
-                                               '', $newsize, $revisionId, $patrolled );
+                       // Update recentchanges
+                       if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
+                               // Mark as patrolled if the user can do so
+                               $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count(
+                                       $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
+                               // Add RC row to the DB
+                               RecentChange::notifyNew(
+                                       $now, $this->mTitle, $isminor, $user, $summary, $bot,
+                                       '', $newsize, $revisionId, $patrolled
+                               );
+                       }
 
-                                       // Log auto-patrolled edits
-                                       if ( $patrolled ) {
-                                               PatrolLog::record( $rc, true, $user );
-                                       }
-                               }
-                               $user->incEditCount();
+                       $user->incEditCount();
 
-                       } catch ( Exception $e ) {
-                               $dbw->rollback( __METHOD__ );
-                               throw $e;
-                       }
                        $dbw->commit( __METHOD__ );
 
                        // Update links, etc.
@@ -1975,13 +1957,13 @@ class WikiPage implements Page, IDBAccessObject {
                $status->value['revision'] = $revision;
 
                $hook_args = array( &$this, &$user, $content, $summary,
-                                                       $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId );
+                       $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId );
 
                ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args );
                Hooks::run( 'PageContentSaveComplete', $hook_args );
 
                // Promote user to any groups they meet the criteria for
-               $dbw->onTransactionIdle( function () use ( $user ) {
+               DeferredUpdates::addCallableUpdate( function () use ( $user ) {
                        $user->addAutopromoteOnceGroups( 'onEdit' );
                        $user->addAutopromoteOnceGroups( 'onView' ); // b/c
                } );
@@ -2166,8 +2148,6 @@ class WikiPage implements Page, IDBAccessObject {
         *   - 'no-change': don't update the article count, ever
         */
        public function doEditUpdates( Revision $revision, User $user, array $options = array() ) {
-               global $wgEnableParserCache;
-
                $options += array(
                        'changed' => true,
                        'created' => false,
@@ -2187,31 +2167,30 @@ class WikiPage implements Page, IDBAccessObject {
                        $editInfo = $this->mPreparedEdit;
                }
 
-               // Save it to the parser cache
-               if ( $wgEnableParserCache ) {
-                       $parserCache = ParserCache::singleton();
-                       $parserCache->save(
-                               $editInfo->output, $this, $editInfo->popts, $editInfo->timestamp, $editInfo->revid
-                       );
-               }
+               // Save it to the parser cache.
+               // Make sure the cache time matches page_touched to avoid double parsing.
+               ParserCache::singleton()->save(
+                       $editInfo->output, $this, $editInfo->popts,
+                       $revision->getTimestamp(), $editInfo->revid
+               );
 
                // Update the links tables and other secondary data
                if ( $content ) {
                        $recursive = $options['changed']; // bug 50785
                        $updates = $content->getSecondaryDataUpdates(
                                $this->getTitle(), null, $recursive, $editInfo->output );
-                       DataUpdate::runUpdates( $updates );
+                       foreach ( $updates as $update ) {
+                               DeferredUpdates::addUpdate( $update );
+                       }
                }
 
                Hooks::run( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
 
                if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) {
-                       JobQueueGroup::singleton()->push( array(
-                               // Flush old entries from the `recentchanges` table
-                               RecentChangesUpdateJob::newPurgeJob(),
-                               // Update the cached list of active users
-                               RecentChangesUpdateJob::newCacheUpdateJob()
-                       ) );
+                       // Flush old entries from the `recentchanges` table
+                       if ( mt_rand( 0, 9 ) == 0 ) {
+                               JobQueueGroup::singleton()->lazyPush( RecentChangesUpdateJob::newPurgeJob() );
+                       }
                }
 
                if ( !$this->exists() ) {
@@ -2280,7 +2259,6 @@ class WikiPage implements Page, IDBAccessObject {
                } elseif ( $options['changed'] ) { // bug 50785
                        self::onArticleEdit( $this->mTitle );
                }
-
        }
 
        /**
@@ -2784,9 +2762,16 @@ class WikiPage implements Page, IDBAccessObject {
                $dbw->begin( __METHOD__ );
 
                if ( $id == 0 ) {
-                       $this->loadPageData( 'forupdate' );
+                       // T98706: lock the page from various other updates but avoid using
+                       // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
+                       // the revisions queries (which also JOIN on user). Only lock the page
+                       // row and CAS check on page_latest to see if the trx snapshot matches.
+                       $latest = $this->lock();
+
+                       $this->loadPageData( WikiPage::READ_LATEST );
                        $id = $this->getID();
-                       if ( $id == 0 ) {
+                       if ( $id == 0 || $this->getLatest() != $latest ) {
+                               // Page not there or trx snapshot is stale
                                $dbw->rollback( __METHOD__ );
                                $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
                                return $status;
@@ -2893,6 +2878,24 @@ class WikiPage implements Page, IDBAccessObject {
                return $status;
        }
 
+       /**
+        * Lock the page row for this title and return page_latest (or 0)
+        *
+        * @return integer
+        */
+       protected function lock() {
+               return (int)wfGetDB( DB_MASTER )->selectField(
+                       'page',
+                       'page_latest',
+                       array(
+                               'page_namespace' => $this->getTitle()->getNamespace(),
+                               'page_title' => $this->getTitle()->getDBkey()
+                       ),
+                       __METHOD__,
+                       array( 'FOR UPDATE' )
+               );
+       }
+
        /**
         * Do some database updates after deletion
         *
@@ -3157,7 +3160,6 @@ class WikiPage implements Page, IDBAccessObject {
                // Update existence markers on article/talk tabs...
                $other = $title->getOtherPage();
 
-               $other->invalidateCache();
                $other->purgeSquid();
 
                $title->touchLinks();
@@ -3174,7 +3176,6 @@ class WikiPage implements Page, IDBAccessObject {
                // Update existence markers on article/talk tabs...
                $other = $title->getOtherPage();
 
-               $other->invalidateCache();
                $other->purgeSquid();
 
                $title->touchLinks();
@@ -3411,14 +3412,14 @@ class WikiPage implements Page, IDBAccessObject {
 
                // Check if the last link refresh was before page_touched
                if ( $this->getLinksTimestamp() < $this->getTouched() ) {
-                       JobQueueGroup::singleton()->push( EnqueueJob::newFromLocalJobs(
+                       $params['isOpportunistic'] = true;
+                       $params['rootJobTimestamp'] = $parserOutput->getCacheTime();
+
+                       JobQueueGroup::singleton()->lazyPush( EnqueueJob::newFromLocalJobs(
                                new JobSpecification( 'refreshLinks', $params,
                                        array( 'removeDuplicates' => true ), $this->mTitle )
                        ) );
-                       return;
                }
-
-               return;
        }
 
        /**