X-Git-Url: https://git.cyclocoop.org/%27.WWW_URL.%27admin/?a=blobdiff_plain;f=includes%2FStorage%2FDerivedPageDataUpdater.php;h=a7dfb4b7071f2510657b3a67c05c822d1ba003ab;hb=51602a436c723fe8ccf353d5fe5940aec857935f;hp=2df1670c886dfe81b041b4d9a23a91bfe9d1af65;hpb=0ecfe75502b5a7a7224cf79531b1fd90662048cb;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/Storage/DerivedPageDataUpdater.php b/includes/Storage/DerivedPageDataUpdater.php index 2df1670c88..a7dfb4b707 100644 --- a/includes/Storage/DerivedPageDataUpdater.php +++ b/includes/Storage/DerivedPageDataUpdater.php @@ -51,6 +51,7 @@ use SiteStatsUpdate; use Title; use User; use Wikimedia\Assert\Assert; +use Wikimedia\Rdbms\LBFactory; use WikiPage; /** @@ -121,6 +122,11 @@ class DerivedPageDataUpdater implements IDBAccessObject { */ private $messageCache; + /** + * @var LBFactory + */ + private $loadbalancerFactory; + /** * @var string see $wgArticleCountMethod */ @@ -132,15 +138,22 @@ class DerivedPageDataUpdater implements IDBAccessObject { private $rcWatchCategoryMembership = false; /** - * See $options on prepareUpdate. + * Stores (most of) the $options parameter of prepareUpdate(). + * @see prepareUpdate() */ private $options = [ 'changed' => true, 'created' => false, 'moved' => false, 'restored' => false, + 'oldrevision' => null, 'oldcountable' => null, 'oldredirect' => null, + 'triggeringUser' => null, + // causeAction/causeAgent default to 'unknown' but that's handled where it's read, + // to make the life of prepareUpdate() callers easier. + 'causeAction' => null, + 'causeAgent' => null, ]; /** @@ -235,6 +248,7 @@ class DerivedPageDataUpdater implements IDBAccessObject { * @param JobQueueGroup $jobQueueGroup * @param MessageCache $messageCache * @param Language $contLang + * @param LBFactory $loadbalancerFactory */ public function __construct( WikiPage $wikiPage, @@ -243,7 +257,8 @@ class DerivedPageDataUpdater implements IDBAccessObject { ParserCache $parserCache, JobQueueGroup $jobQueueGroup, MessageCache $messageCache, - Language $contLang + Language $contLang, + LBFactory $loadbalancerFactory ) { $this->wikiPage = $wikiPage; @@ -253,6 +268,9 @@ class DerivedPageDataUpdater implements IDBAccessObject { $this->jobQueueGroup = $jobQueueGroup; $this->messageCache = $messageCache; $this->contLang = $contLang; + // XXX only needed for waiting for slaves to catch up; there should be a narrower + // interface for that. + $this->loadbalancerFactory = $loadbalancerFactory; } /** @@ -868,6 +886,14 @@ class DerivedPageDataUpdater implements IDBAccessObject { } } + private function assertHasRevision( $method ) { + if ( !$this->revision->getId() ) { + throw new LogicException( + 'Must call prepareUpdate() before calling ' . $method + ); + } + } + /** * Whether the edit creates the page. * @@ -998,7 +1024,8 @@ class DerivedPageDataUpdater implements IDBAccessObject { * - moved: bool, whether the page was moved (default false) * - restored: bool, whether the page was undeleted (default false) * - oldrevision: Revision object for the pre-update revision (default null) - * - triggeringuser: The user triggering the update (UserIdentity, default null) + * - triggeringUser: The user triggering the update (UserIdentity, defaults to the + * user who created the revision) * - oldredirect: bool, null, or string 'no-change' (default null): * - bool: whether the page was counted as a redirect before that * revision, only used in changed is true and created is false @@ -1010,6 +1037,10 @@ class DerivedPageDataUpdater implements IDBAccessObject { * is true, do update the article count * - 'no-change': don't update the article count, ever * When set to null, pageState['oldCountable'] will be used instead if available. + * - causeAction: an arbitrary string identifying the reason for the update. + * See DataUpdate::getCauseAction(). (default 'unknown') + * - causeAgent: name of the user who caused the update. See DataUpdate::getCauseAgent(). + * (string, default 'unknown') */ public function prepareUpdate( RevisionRecord $revision, array $options = [] ) { Assert::parameter( @@ -1020,9 +1051,9 @@ class DerivedPageDataUpdater implements IDBAccessObject { 'must be a RevisionRecord (or Revision)' ); Assert::parameter( - !isset( $options['triggeringuser'] ) - || $options['triggeringuser'] instanceof UserIdentity, - '$options["triggeringuser"]', + !isset( $options['triggeringUser'] ) + || $options['triggeringUser'] instanceof UserIdentity, + '$options["triggeringUser"]', 'must be a UserIdentity' ); @@ -1260,40 +1291,16 @@ class DerivedPageDataUpdater implements IDBAccessObject { $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks! - // NOTE: this may trigger the first parsing of the new content after an edit (when not - // using pre-generated stashed output). - // XXX: we may want to use the PoolCounter here. This would perhaps allow the initial parse - // to be perform post-send. The client could already follow a HTTP redirect to the - // page view, but would then have to wait for a response until rendering is complete. - $output = $this->getCanonicalParserOutput(); - - // Save it to the parser cache. - // Make sure the cache time matches page_touched to avoid double parsing. - $this->parserCache->save( - $output, $wikiPage, $this->getCanonicalParserOptions(), - $this->revision->getTimestamp(), $this->revision->getId() - ); - $legacyUser = User::newFromIdentity( $this->user ); $legacyRevision = new Revision( $this->revision ); - // Update the links tables and other secondary data - $recursive = $this->options['changed']; // T52785 - $updates = $this->getSecondaryDataUpdates( $recursive ); + $this->doParserCacheUpdate(); - $triggeringUser = $this->options['triggeringuser'] ?? $this->user; - if ( !$triggeringUser instanceof User ) { - $triggeringUser = User::newFromIdentity( $triggeringUser ); - } - foreach ( $updates as $update ) { - // TODO: make an $option field for the cause - $update->setCause( 'edit-page', $triggeringUser->getName() ); - if ( $update instanceof LinksUpdate ) { - $update->setRevision( $legacyRevision ); - $update->setTriggeringUser( $triggeringUser ); - } - DeferredUpdates::addUpdate( $update ); - } + $this->doSecondaryDataUpdates( [ + // T52785 do not update any other pages on a null edit + 'recursive' => $this->options['changed'], + 'defer' => DeferredUpdates::POSTSEND, + ] ); // TODO: MCR: check if *any* changed slot supports categories! if ( $this->rcWatchCategoryMembership @@ -1421,4 +1428,91 @@ class DerivedPageDataUpdater implements IDBAccessObject { $this->doTransition( 'done' ); } + /** + * Do secondary data updates (such as updating link tables). + * + * MCR note: this method is temporarily exposed via WikiPage::doSecondaryDataUpdates. + * + * @param array $options + * - recursive: make the update recursive, i.e. also update pages which transclude the + * current page or otherwise depend on it (default: false) + * - defer: one of the DeferredUpdates constants, or false to run immediately after waiting + * for replication of the changes from the SecondaryDataUpdates hooks (default: false) + * - transactionTicket: a transaction ticket from LBFactory::getEmptyTransactionTicket(), + * only when defer is false (default: null) + * @since 1.32 + */ + public function doSecondaryDataUpdates( array $options = [] ) { + $this->assertHasRevision( __METHOD__ ); + $options += [ + 'recursive' => false, + 'defer' => false, + 'transactionTicket' => null, + ]; + $deferValues = [ false, DeferredUpdates::PRESEND, DeferredUpdates::POSTSEND ]; + if ( !in_array( $options['defer'], $deferValues, true ) ) { + throw new InvalidArgumentException( 'invalid value for defer: ' . $options['defer'] ); + } + Assert::parameterType( 'integer|null', $options['transactionTicket'], + '$options[\'transactionTicket\']' ); + + $updates = $this->getSecondaryDataUpdates( $options['recursive'] ); + + $triggeringUser = $this->options['triggeringUser'] ?? $this->user; + if ( !$triggeringUser instanceof User ) { + $triggeringUser = User::newFromIdentity( $triggeringUser ); + } + $causeAction = $this->options['causeAction'] ?? 'unknown'; + $causeAgent = $this->options['causeAgent'] ?? 'unknown'; + $legacyRevision = new Revision( $this->revision ); + + if ( $options['defer'] === false && $options['transactionTicket'] !== null ) { + // For legacy hook handlers doing updates via LinksUpdateConstructed, make sure + // any pending writes they made get flushed before the doUpdate() calls below. + // This avoids snapshot-clearing errors in LinksUpdate::acquirePageLock(). + $this->loadbalancerFactory->commitAndWaitForReplication( + __METHOD__, $options['transactionTicket'] + ); + } + + foreach ( $updates as $update ) { + $update->setCause( $causeAction, $causeAgent ); + if ( $update instanceof LinksUpdate ) { + $update->setRevision( $legacyRevision ); + $update->setTriggeringUser( $triggeringUser ); + } + if ( $options['defer'] === false ) { + if ( $options['transactionTicket'] !== null ) { + $update->setTransactionTicket( $options['transactionTicket'] ); + } + $update->doUpdate(); + } else { + DeferredUpdates::addUpdate( $update, $options['defer'] ); + } + } + } + + public function doParserCacheUpdate() { + $this->assertHasRevision( __METHOD__ ); + + $wikiPage = $this->getWikiPage(); // TODO: ParserCache should accept a RevisionRecord instead + + // NOTE: this may trigger the first parsing of the new content after an edit (when not + // using pre-generated stashed output). + // XXX: we may want to use the PoolCounter here. This would perhaps allow the initial parse + // to be performed post-send. The client could already follow a HTTP redirect to the + // page view, but would then have to wait for a response until rendering is complete. + $output = $this->getCanonicalParserOutput(); + + // Save it to the parser cache. Use the revision timestamp in the case of a + // freshly saved edit, as that matches page_touched and a mismatch would trigger an + // unnecessary reparse. + $timestamp = $this->options['changed'] ? $this->revision->getTimestamp() + : $output->getTimestamp(); + $this->parserCache->save( + $output, $wikiPage, $this->getCanonicalParserOptions(), + $timestamp, $this->revision->getId() + ); + } + }