X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2FStorage%2FRevisionStore.php;h=88d520c0911ed2e021b47fe54b27230fb4af51fd;hb=109b8fb65d56655af73fd11d63d48338f26147a2;hp=602364cf5db7ea6a5120f2e262cc6b37a9c78929;hpb=1655c86faf49196f41aca75c8df80c0c4fa4fb90;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/Storage/RevisionStore.php b/includes/Storage/RevisionStore.php index 602364cf5d..88d520c091 100644 --- a/includes/Storage/RevisionStore.php +++ b/includes/Storage/RevisionStore.php @@ -56,7 +56,7 @@ use Wikimedia\Assert\Assert; use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\DBConnRef; use Wikimedia\Rdbms\IDatabase; -use Wikimedia\Rdbms\LoadBalancer; +use Wikimedia\Rdbms\ILoadBalancer; /** * Service for looking up page revisions. @@ -88,7 +88,7 @@ class RevisionStore private $contentHandlerUseDB = true; /** - * @var LoadBalancer + * @var ILoadBalancer */ private $loadBalancer; @@ -128,9 +128,14 @@ class RevisionStore /** * @todo $blobStore should be allowed to be any BlobStore! * - * @param LoadBalancer $loadBalancer + * @param ILoadBalancer $loadBalancer * @param SqlBlobStore $blobStore - * @param WANObjectCache $cache + * @param WANObjectCache $cache A cache for caching revision rows. This can be the local + * wiki's default instance even if $wikiId refers to a different wiki, since + * makeGlobalKey() is used to constructed a key that allows cached revision rows from + * the same database to be re-used between wikis. For example, enwiki and frwiki will + * use the same cache keys for revision rows from the wikidatawiki database, regardless + * of the cache's default key space. * @param CommentStore $commentStore * @param NameTableStore $contentModelStore * @param NameTableStore $slotRoleStore @@ -141,7 +146,7 @@ class RevisionStore * @throws MWException if $mcrMigrationStage or $wikiId is invalid. */ public function __construct( - LoadBalancer $loadBalancer, + ILoadBalancer $loadBalancer, SqlBlobStore $blobStore, WANObjectCache $cache, CommentStore $commentStore, @@ -202,6 +207,20 @@ class RevisionStore return ( $this->mcrMigrationStage & $flags ) === $flags; } + /** + * Throws a RevisionAccessException if this RevisionStore is configured for cross-wiki loading + * and still reading from the old DB schema. + * + * @throws RevisionAccessException + */ + private function assertCrossWikiContentLoadingIsSafe() { + if ( $this->wikiId !== false && $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) { + throw new RevisionAccessException( + "Cross-wiki content loading is not supported by the pre-MCR schema" + ); + } + } + public function setLogger( LoggerInterface $logger ) { $this->logger = $logger; } @@ -239,7 +258,7 @@ class RevisionStore } /** - * @return LoadBalancer + * @return ILoadBalancer */ private function getDBLoadBalancer() { return $this->loadBalancer; @@ -727,6 +746,76 @@ class RevisionStore if ( !isset( $revisionRow['rev_id'] ) ) { // only if auto-increment was used $revisionRow['rev_id'] = intval( $dbw->insertId() ); + + if ( $dbw->getType() === 'mysql' ) { + // (T202032) MySQL until 8.0 and MariaDB until some version after 10.1.34 don't save the + // auto-increment value to disk, so on server restart it might reuse IDs from deleted + // revisions. We can fix that with an insert with an explicit rev_id value, if necessary. + + $maxRevId = intval( $dbw->selectField( 'archive', 'MAX(ar_rev_id)', '', __METHOD__ ) ); + $table = 'archive'; + if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) { + $maxRevId2 = intval( $dbw->selectField( 'slots', 'MAX(slot_revision_id)', '', __METHOD__ ) ); + if ( $maxRevId2 >= $maxRevId ) { + $maxRevId = $maxRevId2; + $table = 'slots'; + } + } + + if ( $maxRevId >= $revisionRow['rev_id'] ) { + $this->logger->debug( + '__METHOD__: Inserted revision {revid} but {table} has revisions up to {maxrevid}.' + . ' Trying to fix it.', + [ + 'revid' => $revisionRow['rev_id'], + 'table' => $table, + 'maxrevid' => $maxRevId, + ] + ); + + if ( !$dbw->lock( 'fix-for-T202032', __METHOD__ ) ) { + throw new MWException( 'Failed to get database lock for T202032' ); + } + $fname = __METHOD__; + $dbw->onTransactionResolution( function ( $trigger, $dbw ) use ( $fname ) { + $dbw->unlock( 'fix-for-T202032', $fname ); + } ); + + $dbw->delete( 'revision', [ 'rev_id' => $revisionRow['rev_id'] ], __METHOD__ ); + + // The locking here is mostly to make MySQL bypass the REPEATABLE-READ transaction + // isolation (weird MySQL "feature"). It does seem to block concurrent auto-incrementing + // inserts too, though, at least on MariaDB 10.1.29. + // + // Don't try to lock `revision` in this way, it'll deadlock if there are concurrent + // transactions in this code path thanks to the row lock from the original ->insert() above. + // + // And we have to use raw SQL to bypass the "aggregation used with a locking SELECT" warning + // that's for non-MySQL DBs. + $row1 = $dbw->query( + $dbw->selectSqlText( 'archive', [ 'v' => "MAX(ar_rev_id)" ], '', __METHOD__ ) . ' FOR UPDATE' + )->fetchObject(); + if ( $this->hasMcrSchemaFlags( SCHEMA_COMPAT_WRITE_NEW ) ) { + $row2 = $dbw->query( + $dbw->selectSqlText( 'slots', [ 'v' => "MAX(slot_revision_id)" ], '', __METHOD__ ) + . ' FOR UPDATE' + )->fetchObject(); + } else { + $row2 = null; + } + $maxRevId = max( + $maxRevId, + $row1 ? intval( $row1->v ) : 0, + $row2 ? intval( $row2->v ) : 0 + ); + + // If we don't have SCHEMA_COMPAT_WRITE_NEW, all except the first of any concurrent + // transactions will throw a duplicate key error here. It doesn't seem worth trying + // to avoid that. + $revisionRow['rev_id'] = $maxRevId + 1; + $dbw->insert( 'revision', $revisionRow, __METHOD__ ); + } + } } $commentCallback( $revisionRow['rev_id'] ); @@ -775,6 +864,8 @@ class RevisionStore // MCR migration note: rev_content_model and rev_content_format will go away if ( $this->contentHandlerUseDB ) { + $this->assertCrossWikiContentLoadingIsSafe(); + $defaultModel = ContentHandler::getDefaultModelFor( $title ); $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat(); @@ -883,6 +974,8 @@ class RevisionStore // if $wgContentHandlerUseDB is not set, // all revisions must use the default content model and format. + $this->assertCrossWikiContentLoadingIsSafe(); + $defaultModel = ContentHandler::getDefaultModelFor( $title ); $defaultHandler = ContentHandler::getForModelID( $defaultModel ); $defaultFormat = $defaultHandler->getDefaultFormat(); @@ -917,7 +1010,7 @@ class RevisionStore * Such revisions can for instance identify page rename * operations and other such meta-modifications. * - * @note: This method grabs a FOR UPDATE lock on the relevant row of the page table, + * @note This method grabs a FOR UPDATE lock on the relevant row of the page table, * to prevent a new revision from being inserted before the null revision has been written * to the database. * @@ -1202,6 +1295,8 @@ class RevisionStore if ( $mainSlotRow->model_name === null ) { $mainSlotRow->model_name = function ( SlotRecord $slot ) use ( $title ) { + $this->assertCrossWikiContentLoadingIsSafe(); + // TODO: MCR: consider slot role in getDefaultModelFor()! Use LinkTarget! // TODO: MCR: deprecate $title->getModel(). return ContentHandler::getDefaultModelFor( $title ); @@ -2149,7 +2244,9 @@ class RevisionStore // NOTE: even when this class is set to not read from the old schema, callers // should still be able to join against the text table, as long as we are still // writing the old schema for compatibility. - wfDeprecated( __METHOD__ . ' with `text` option', '1.32' ); + // TODO: This should trigger a deprecation warning eventually (T200918), but not + // before all known usages are removed (see T198341 and T201164). + // wfDeprecated( __METHOD__ . ' with `text` option', '1.32' ); } $ret['tables'][] = 'text';