Merge "resourceloader: Compile documentElement.className server-side"
[lhc/web/wiklou.git] / includes / objectcache / SqlBagOStuff.php
index 13c5ee6..d713396 100644 (file)
@@ -22,6 +22,7 @@
  */
 
 use MediaWiki\MediaWikiServices;
+use Wikimedia\AtEase\AtEase;
 use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\DBError;
@@ -30,6 +31,7 @@ use Wikimedia\Rdbms\DBConnectionError;
 use Wikimedia\Rdbms\IMaintainableDatabase;
 use Wikimedia\Rdbms\LoadBalancer;
 use Wikimedia\ScopedCallback;
+use Wikimedia\Timestamp\ConvertibleTimestamp;
 use Wikimedia\WaitConditionLoop;
 
 /**
@@ -142,7 +144,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                        $this->numServerShards = count( $this->serverInfos );
                } else {
                        // Default to using the main wiki's database servers
-                       $this->serverInfos = false;
+                       $this->serverInfos = [];
                        $this->numServerShards = 1;
                        $this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_BE;
                }
@@ -169,7 +171,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         * @return IMaintainableDatabase
         * @throws MWException
         */
-       private function getDB( $shardIndex ) {
+       private function getConnection( $shardIndex ) {
                if ( $shardIndex >= $this->numServerShards ) {
                        throw new MWException( __METHOD__ . ": Invalid server index \"$shardIndex\"" );
                }
@@ -189,28 +191,30 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                $type = $info['type'] ?? 'mysql';
                                $host = $info['host'] ?? '[unknown]';
                                $this->logger->debug( __CLASS__ . ": connecting to $host" );
-                               $db = Database::factory( $type, $info );
-                               $db->clearFlag( DBO_TRX ); // auto-commit mode
-                               $this->conns[$shardIndex] = $db;
+                               $conn = Database::factory( $type, $info );
+                               $conn->clearFlag( DBO_TRX ); // auto-commit mode
+                               $this->conns[$shardIndex] = $conn;
                        }
-                       $db = $this->conns[$shardIndex];
+                       $conn = $this->conns[$shardIndex];
                } else {
                        // Use the main LB database
                        $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
                        $index = $this->replicaOnly ? DB_REPLICA : DB_MASTER;
-                       if ( $lb->getServerType( $lb->getWriterIndex() ) !== 'sqlite' ) {
-                               // Keep a separate connection to avoid contention and deadlocks
-                               $db = $lb->getConnectionRef( $index, [], false, $lb::CONN_TRX_AUTOCOMMIT );
-                       } else {
-                               // However, SQLite has the opposite behavior due to DB-level locking.
-                               // Stock sqlite MediaWiki installs use a separate sqlite cache DB instead.
-                               $db = $lb->getConnectionRef( $index );
+                       // If the RDBMS has row-level locking, use the autocommit connection to avoid
+                       // contention and deadlocks. Do not do this if it only has DB-level locking since
+                       // that would just cause deadlocks.
+                       $attribs = $lb->getServerAttributes( $lb->getWriterIndex() );
+                       $flags = $attribs[Database::ATTR_DB_LEVEL_LOCKING] ? 0 : $lb::CONN_TRX_AUTOCOMMIT;
+                       $conn = $lb->getMaintenanceConnectionRef( $index, [], false, $flags );
+                       // Automatically create the objectcache table for sqlite as needed
+                       if ( $conn->getType() === 'sqlite' ) {
+                               $this->initSqliteDatabase( $conn );
                        }
                }
 
-               $this->logger->debug( sprintf( "Connection %s will be used for SqlBagOStuff", $db ) );
+               $this->logger->debug( sprintf( "Connection %s will be used for SqlBagOStuff", $conn ) );
 
-               return $db;
+               return $conn;
        }
 
        /**
@@ -290,7 +294,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                $dataRows = [];
                foreach ( $keysByTable as $shardIndex => $serverKeys ) {
                        try {
-                               $db = $this->getDB( $shardIndex );
+                               $db = $this->getConnection( $shardIndex );
                                foreach ( $serverKeys as $tableName => $tableKeys ) {
                                        $res = $db->select( $tableName,
                                                [ 'keyname', 'value', 'exptime' ],
@@ -321,7 +325,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                $this->debug( "get: retrieved data; expiry time is " . $row->exptime );
                                $db = null; // in case of connection failure
                                try {
-                                       $db = $this->getDB( $row->shardIndex );
+                                       $db = $this->getConnection( $row->shardIndex );
                                        if ( $this->isExpired( $db, $row->exptime ) ) { // MISS
                                                $this->debug( "get: key has expired" );
                                        } else { // HIT
@@ -364,7 +368,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                foreach ( $keysByTable as $shardIndex => $serverKeys ) {
                        $db = null; // in case of connection failure
                        try {
-                               $db = $this->getDB( $shardIndex );
+                               $db = $this->getConnection( $shardIndex );
                                $this->occasionallyGarbageCollect( $db ); // expire old entries if any
                                $dbExpiry = $exptime ? $db->timestamp( $exptime ) : $this->getMaxDateTime( $db );
                        } catch ( DBError $e ) {
@@ -391,7 +395,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                        }
                }
 
-               if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) {
+               if ( $this->fieldHasFlags( $flags, self::WRITE_SYNC ) ) {
                        $result = $this->waitForReplication() && $result;
                }
 
@@ -479,7 +483,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                $silenceScope = $this->silenceTransactionProfiler();
                $db = null; // in case of connection failure
                try {
-                       $db = $this->getDB( $shardIndex );
+                       $db = $this->getConnection( $shardIndex );
                        // (T26425) use a replace if the db supports it instead of
                        // delete/insert to avoid clashes with conflicting keynames
                        $db->update(
@@ -504,7 +508,12 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                        return false;
                }
 
-               return (bool)$db->affectedRows();
+               $success = (bool)$db->affectedRows();
+               if ( $this->fieldHasFlags( $flags, self::WRITE_SYNC ) ) {
+                       $success = $this->waitForReplication() && $success;
+               }
+
+               return $success;
        }
 
        protected function doDeleteMulti( array $keys, $flags = 0 ) {
@@ -520,7 +529,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                return $this->modifyMulti( [ $key => null ], 0, $flags, self::$OP_DELETE );
        }
 
-       public function incr( $key, $step = 1 ) {
+       public function incr( $key, $step = 1, $flags = 0 ) {
                list( $shardIndex, $tableName ) = $this->getTableByKey( $key );
 
                $newCount = false;
@@ -528,7 +537,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                $silenceScope = $this->silenceTransactionProfiler();
                $db = null; // in case of connection failure
                try {
-                       $db = $this->getDB( $shardIndex );
+                       $db = $this->getConnection( $shardIndex );
                        $encTimestamp = $db->addQuotes( $db->timestamp() );
                        $db->update(
                                $tableName,
@@ -554,13 +563,8 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                return $newCount;
        }
 
-       public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
-               $ok = $this->mergeViaCas( $key, $callback, $exptime, $attempts, $flags );
-               if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) {
-                       $ok = $this->waitForReplication() && $ok;
-               }
-
-               return $ok;
+       public function decr( $key, $value = 1, $flags = 0 ) {
+               return $this->incr( $key, -$value, $flags );
        }
 
        public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
@@ -581,10 +585,10 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
         * @param string $exptime
         * @return bool
         */
-       private function isExpired( $db, $exptime ) {
+       private function isExpired( IDatabase $db, $exptime ) {
                return (
                        $exptime != $this->getMaxDateTime( $db ) &&
-                       wfTimestamp( TS_UNIX, $exptime ) < $this->getCurrentTime()
+                       ConvertibleTimestamp::convert( TS_UNIX, $exptime ) < $this->getCurrentTime()
                );
        }
 
@@ -651,7 +655,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                foreach ( $shardIndexes as $numServersDone => $shardIndex ) {
                        $db = null; // in case of connection failure
                        try {
-                               $db = $this->getDB( $shardIndex );
+                               $db = $this->getConnection( $shardIndex );
                                $this->deleteServerObjectsExpiringBefore(
                                        $db,
                                        $timestamp,
@@ -686,7 +690,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                $serversDoneCount = 0,
                &$keysDeletedCount = 0
        ) {
-               $cutoffUnix = wfTimestamp( TS_UNIX, $timestamp );
+               $cutoffUnix = ConvertibleTimestamp::convert( TS_UNIX, $timestamp );
                $shardIndexes = range( 0, $this->numTableShards - 1 );
                shuffle( $shardIndexes );
 
@@ -708,7 +712,8 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                if ( $res->numRows() ) {
                                        $row = $res->current();
                                        if ( $lag === null ) {
-                                               $lag = max( $cutoffUnix - wfTimestamp( TS_UNIX, $row->exptime ), 1 );
+                                               $rowExpUnix = ConvertibleTimestamp::convert( TS_UNIX, $row->exptime );
+                                               $lag = max( $cutoffUnix - $rowExpUnix, 1 );
                                        }
 
                                        $keys = [];
@@ -730,7 +735,8 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
 
                                if ( is_callable( $progressCallback ) ) {
                                        if ( $lag ) {
-                                               $remainingLag = $cutoffUnix - wfTimestamp( TS_UNIX, $continue );
+                                               $continueUnix = ConvertibleTimestamp::convert( TS_UNIX, $continue );
+                                               $remainingLag = $cutoffUnix - $continueUnix;
                                                $processedLag = max( $lag - $remainingLag, 0 );
                                                $doneRatio = ( $numShardsDone + $processedLag / $lag ) / $this->numTableShards;
                                        } else {
@@ -756,7 +762,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                for ( $shardIndex = 0; $shardIndex < $this->numServerShards; $shardIndex++ ) {
                        $db = null; // in case of connection failure
                        try {
-                               $db = $this->getDB( $shardIndex );
+                               $db = $this->getConnection( $shardIndex );
                                for ( $i = 0; $i < $this->numTableShards; $i++ ) {
                                        $db->delete( $this->getTableNameByShard( $i ), '*', __METHOD__ );
                                }
@@ -783,7 +789,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
 
                $db = null; // in case of connection failure
                try {
-                       $db = $this->getDB( $shardIndex );
+                       $db = $this->getConnection( $shardIndex );
                        $ok = $db->lock( $key, __METHOD__, $timeout );
                        if ( $ok ) {
                                $this->locks[$key] = [ 'class' => $rclass, 'depth' => 1 ];
@@ -815,7 +821,7 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
 
                        $db = null; // in case of connection failure
                        try {
-                               $db = $this->getDB( $shardIndex );
+                               $db = $this->getConnection( $shardIndex );
                                $ok = $db->unlock( $key, __METHOD__ );
                                if ( !$ok ) {
                                        $this->logger->warning(
@@ -866,9 +872,9 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                }
 
                if ( function_exists( 'gzinflate' ) ) {
-                       Wikimedia\suppressWarnings();
+                       AtEase::suppressWarnings();
                        $decomp = gzinflate( $serial );
-                       Wikimedia\restoreWarnings();
+                       AtEase::restoreWarnings();
 
                        if ( $decomp !== false ) {
                                $serial = $decomp;
@@ -947,20 +953,47 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
        }
 
        /**
-        * Create shard tables. For use from eval.php.
+        * @param IMaintainableDatabase $db
+        * @throws DBError
+        */
+       private function initSqliteDatabase( IMaintainableDatabase $db ) {
+               if ( $db->tableExists( 'objectcache' ) ) {
+                       return;
+               }
+               // Use one table for SQLite; sharding does not seem to have much benefit
+               $db->query( "PRAGMA journal_mode=WAL" ); // this is permanent
+               $db->startAtomic( __METHOD__ ); // atomic DDL
+               try {
+                       $encTable = $db->tableName( 'objectcache' );
+                       $encExptimeIndex = $db->addIdentifierQuotes( $db->tablePrefix() . 'exptime' );
+                       $db->query(
+                               "CREATE TABLE $encTable (\n" .
+                               "       keyname BLOB NOT NULL default '' PRIMARY KEY,\n" .
+                               "       value BLOB,\n" .
+                               "       exptime TEXT\n" .
+                               ")",
+                               __METHOD__
+                       );
+                       $db->query( "CREATE INDEX $encExptimeIndex ON $encTable (exptime)" );
+                       $db->endAtomic( __METHOD__ );
+               } catch ( DBError $e ) {
+                       $db->rollback( __METHOD__ );
+                       throw $e;
+               }
+       }
+
+       /**
+        * Create the shard tables on all databases (e.g. via eval.php/shell.php)
         */
        public function createTables() {
                for ( $shardIndex = 0; $shardIndex < $this->numServerShards; $shardIndex++ ) {
-                       $db = $this->getDB( $shardIndex );
-                       if ( $db->getType() !== 'mysql' ) {
-                               throw new MWException( __METHOD__ . ' is not supported on this DB server' );
-                       }
-
-                       for ( $i = 0; $i < $this->numTableShards; $i++ ) {
-                               $db->query(
-                                       'CREATE TABLE ' . $db->tableName( $this->getTableNameByShard( $i ) ) .
-                                       ' LIKE ' . $db->tableName( 'objectcache' ),
-                                       __METHOD__ );
+                       $db = $this->getConnection( $shardIndex );
+                       if ( in_array( $db->getType(), [ 'mysql', 'postgres' ], true ) ) {
+                               for ( $i = 0; $i < $this->numTableShards; $i++ ) {
+                                       $encBaseTable = $db->tableName( 'objectcache' );
+                                       $encShardTable = $db->tableName( $this->getTableNameByShard( $i ) );
+                                       $db->query( "CREATE TABLE $encShardTable LIKE $encBaseTable" );
+                               }
                        }
                }
        }