Merge "Add MessagesBi.php"
[lhc/web/wiklou.git] / maintenance / populateArchiveRevId.php
index b8b9e68..60f5e8a 100644 (file)
@@ -21,6 +21,7 @@
  * @ingroup Maintenance
  */
 
+use Wikimedia\Rdbms\DBQueryError;
 use Wikimedia\Rdbms\IDatabase;
 
 require_once __DIR__ . '/Maintenance.php';
@@ -32,6 +33,10 @@ require_once __DIR__ . '/Maintenance.php';
  * @since 1.31
  */
 class PopulateArchiveRevId extends LoggedUpdateMaintenance {
+
+       /** @var array|null Dummy revision row */
+       private static $dummyRev = null;
+
        public function __construct() {
                parent::__construct();
                $this->addDescription( 'Populate ar_rev_id in pre-1.5 rows' );
@@ -45,6 +50,7 @@ class PopulateArchiveRevId extends LoggedUpdateMaintenance {
        protected function doDBUpdates() {
                $this->output( "Populating ar_rev_id...\n" );
                $dbw = $this->getDB( DB_MASTER );
+               self::checkMysqlAutoIncrementBug( $dbw );
 
                // Quick exit if there are no rows needing updates.
                $any = $dbw->selectField(
@@ -58,7 +64,6 @@ class PopulateArchiveRevId extends LoggedUpdateMaintenance {
                        return true;
                }
 
-               $rev = $this->makeDummyRevisionRow( $dbw );
                $count = 0;
                while ( true ) {
                        wfWaitForSlaves();
@@ -75,47 +80,107 @@ class PopulateArchiveRevId extends LoggedUpdateMaintenance {
                                return true;
                        }
 
+                       $count += self::reassignArRevIds( $dbw, $arIds, [ 'ar_rev_id' => null ] );
+
+                       $min = min( $arIds );
+                       $max = max( $arIds );
+                       $this->output( " ... $min-$max\n" );
+               }
+       }
+
+       /**
+        * Check for (and work around) a MySQL auto-increment bug
+        *
+        * (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.
+        *
+        * @param IDatabase $dbw
+        */
+       public static function checkMysqlAutoIncrementBug( IDatabase $dbw ) {
+               if ( $dbw->getType() !== 'mysql' ) {
+                       return;
+               }
+
+               if ( !self::$dummyRev ) {
+                       self::$dummyRev = self::makeDummyRevisionRow( $dbw );
+               }
+
+               $ok = false;
+               while ( !$ok ) {
                        try {
-                               $updates = $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $arIds, $rev ) {
-                                       // Create new rev_ids by inserting dummy rows into revision and then deleting them.
-                                       $dbw->insert( 'revision', array_fill( 0, count( $arIds ), $rev ), $fname );
-                                       $revIds = $dbw->selectFieldValues(
-                                               'revision',
-                                               'rev_id',
-                                               [ 'rev_timestamp' => $rev['rev_timestamp'] ],
-                                               $fname
+                               $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) {
+                                       $dbw->insert( 'revision', self::$dummyRev, $fname );
+                                       $id = $dbw->insertId();
+                                       $toDelete[] = $id;
+
+                                       $maxId = max(
+                                               (int)$dbw->selectField( 'archive', 'MAX(ar_rev_id)', [], __METHOD__ ),
+                                               (int)$dbw->selectField( 'slots', 'MAX(slot_revision_id)', [], __METHOD__ )
                                        );
-                                       if ( !is_array( $revIds ) ) {
-                                               throw new UnexpectedValueException( 'Failed to insert dummy revisions' );
+                                       if ( $id <= $maxId ) {
+                                               $dbw->insert( 'revision', [ 'rev_id' => $maxId + 1 ] + self::$dummyRev, $fname );
+                                               $toDelete[] = $maxId + 1;
                                        }
-                                       if ( count( $revIds ) !== count( $arIds ) ) {
-                                               throw new UnexpectedValueException(
-                                                       'Tried to insert ' . count( $arIds ) . ' dummy revisions, but found '
-                                                       . count( $revIds ) . ' matching rows.'
-                                               );
-                                       }
-                                       $dbw->delete( 'revision', [ 'rev_id' => $revIds ], $fname );
 
-                                       return array_combine( $arIds, $revIds );
+                                       $dbw->delete( 'revision', [ 'rev_id' => $toDelete ], $fname );
                                } );
-                       } catch ( UnexpectedValueException $ex ) {
-                               $this->fatalError( $ex->getMessage() );
+                               $ok = true;
+                       } catch ( DBQueryError $e ) {
+                               if ( $e->errno != 1062 ) { // 1062 is "duplicate entry", ignore it and retry
+                                       throw $e;
+                               }
                        }
+               }
+       }
 
-                       foreach ( $updates as $arId => $revId ) {
-                               $dbw->update(
-                                       'archive',
-                                       [ 'ar_rev_id' => $revId ],
-                                       [ 'ar_id' => $arId, 'ar_rev_id' => null ],
-                                       __METHOD__
+       /**
+        * Assign new ar_rev_ids to a set of ar_ids.
+        * @param IDatabase $dbw
+        * @param int[] $arIds
+        * @param array $conds Extra conditions for the update
+        * @return int Number of updated rows
+        */
+       public static function reassignArRevIds( IDatabase $dbw, array $arIds, array $conds = [] ) {
+               if ( !self::$dummyRev ) {
+                       self::$dummyRev = self::makeDummyRevisionRow( $dbw );
+               }
+
+               $updates = $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $arIds ) {
+                       // Create new rev_ids by inserting dummy rows into revision and then deleting them.
+                       $dbw->insert( 'revision', array_fill( 0, count( $arIds ), self::$dummyRev ), $fname );
+                       $revIds = $dbw->selectFieldValues(
+                               'revision',
+                               'rev_id',
+                               [ 'rev_timestamp' => self::$dummyRev['rev_timestamp'] ],
+                               $fname
+                       );
+                       if ( !is_array( $revIds ) ) {
+                               throw new UnexpectedValueException( 'Failed to insert dummy revisions' );
+                       }
+                       if ( count( $revIds ) !== count( $arIds ) ) {
+                               throw new UnexpectedValueException(
+                                       'Tried to insert ' . count( $arIds ) . ' dummy revisions, but found '
+                                       . count( $revIds ) . ' matching rows.'
                                );
-                               $count += $dbw->affectedRows();
                        }
+                       $dbw->delete( 'revision', [ 'rev_id' => $revIds ], $fname );
 
-                       $min = min( array_keys( $updates ) );
-                       $max = max( array_keys( $updates ) );
-                       $this->output( " ... $min-$max\n" );
+                       return array_combine( $arIds, $revIds );
+               } );
+
+               $count = 0;
+               foreach ( $updates as $arId => $revId ) {
+                       $dbw->update(
+                               'archive',
+                               [ 'ar_rev_id' => $revId ],
+                               [ 'ar_id' => $arId ] + $conds,
+                               __METHOD__
+                       );
+                       $count += $dbw->affectedRows();
                }
+               return $count;
        }
 
        /**
@@ -123,31 +188,41 @@ class PopulateArchiveRevId extends LoggedUpdateMaintenance {
         *
         * The row will have a wildly unlikely timestamp, and possibly a generic
         * user and comment, but will otherwise be derived from a revision on the
-        * wiki's main page.
+        * wiki's main page or some other revision in the database.
         *
         * @param IDatabase $dbw
         * @return array
         */
-       private function makeDummyRevisionRow( IDatabase $dbw ) {
+       private static function makeDummyRevisionRow( IDatabase $dbw ) {
                $ts = $dbw->timestamp( '11111111111111' );
+               $rev = null;
+
                $mainPage = Title::newMainPage();
-               if ( !$mainPage ) {
-                       $this->fatalError( 'Main page does not exist' );
+               $pageId = $mainPage ? $mainPage->getArticleId() : null;
+               if ( $pageId ) {
+                       $rev = $dbw->selectRow(
+                               'revision',
+                               '*',
+                               [ 'rev_page' => $pageId ],
+                               __METHOD__,
+                               [ 'ORDER BY' => 'rev_timestamp ASC' ]
+                       );
                }
-               $pageId = $mainPage->getArticleId();
-               if ( !$pageId ) {
-                       $this->fatalError( $mainPage->getPrefixedText() . ' has no ID' );
+
+               if ( !$rev ) {
+                       // No main page? Let's see if there are any revisions at all
+                       $rev = $dbw->selectRow(
+                               'revision',
+                               '*',
+                               [],
+                               __METHOD__,
+                               [ 'ORDER BY' => 'rev_timestamp ASC' ]
+                       );
                }
-               $rev = $dbw->selectRow(
-                       'revision',
-                       '*',
-                       [ 'rev_page' => $pageId ],
-                       __METHOD__,
-                       [ 'ORDER BY' => 'rev_timestamp ASC' ]
-               );
                if ( !$rev ) {
-                       $this->fatalError( $mainPage->getPrefixedText() . ' has no revisions' );
+                       throw new UnexpectedValueException( 'No revisions are available to copy' );
                }
+
                unset( $rev->rev_id );
                $rev = (array)$rev;
                $rev['rev_timestamp'] = $ts;
@@ -166,7 +241,7 @@ class PopulateArchiveRevId extends LoggedUpdateMaintenance {
                        __METHOD__
                );
                if ( $any ) {
-                       $this->fatalError( "... Why does your database contain a revision dated $ts?" );
+                       throw new UnexpectedValueException( "... Why does your database contain a revision dated $ts?" );
                }
 
                return $rev;