Daniel Kinzler has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/399445 )

Change subject: [WIP] Prevent writes on connections obtained with DB_REPLICA.
......................................................................

[WIP] Prevent writes on connections obtained with DB_REPLICA.

This introduces DBNoWriteWrapper and refactors DBConnRef for
code sharing.

Bug: T183242
Change-Id: Ida135ec93acf4dc13bef1c5a1d03d2646e984bcb
---
M autoload.php
M includes/libs/rdbms/database/DBConnRef.php
A includes/libs/rdbms/database/DBConnectionProxy.php
A includes/libs/rdbms/database/DBNoWriteWrapper.php
M includes/libs/rdbms/loadbalancer/LoadBalancer.php
M tests/phpunit/includes/db/LoadBalancerTest.php
A tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php
A tests/phpunit/includes/libs/rdbms/database/DBNoWriteWrapperTest.php
A tests/phpunit/tests/MediaWikiTestCaseDbTest.php
9 files changed, 1,238 insertions(+), 576 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/45/399445/1

diff --git a/autoload.php b/autoload.php
index 6b8387b..99c6581 100644
--- a/autoload.php
+++ b/autoload.php
@@ -1663,9 +1663,11 @@
        'Wikimedia\\Rdbms\\DBAccessError' => __DIR__ . 
'/includes/libs/rdbms/exception/DBAccessError.php',
        'Wikimedia\\Rdbms\\DBConnRef' => __DIR__ . 
'/includes/libs/rdbms/database/DBConnRef.php',
        'Wikimedia\\Rdbms\\DBConnectionError' => __DIR__ . 
'/includes/libs/rdbms/exception/DBConnectionError.php',
+       'Wikimedia\\Rdbms\\DBConnectionProxy' => __DIR__ . 
'/includes/libs/rdbms/database/DBConnectionProxy.php',
        'Wikimedia\\Rdbms\\DBError' => __DIR__ . 
'/includes/libs/rdbms/exception/DBError.php',
        'Wikimedia\\Rdbms\\DBExpectedError' => __DIR__ . 
'/includes/libs/rdbms/exception/DBExpectedError.php',
        'Wikimedia\\Rdbms\\DBMasterPos' => __DIR__ . 
'/includes/libs/rdbms/database/position/DBMasterPos.php',
+       'Wikimedia\\Rdbms\\DBNoWriteWrapper' => __DIR__ . 
'/includes/libs/rdbms/database/DBNoWriteWrapper.php',
        'Wikimedia\\Rdbms\\DBQueryError' => __DIR__ . 
'/includes/libs/rdbms/exception/DBQueryError.php',
        'Wikimedia\\Rdbms\\DBQueryTimeoutError' => __DIR__ . 
'/includes/libs/rdbms/exception/DBQueryTimeoutError.php',
        'Wikimedia\\Rdbms\\DBReadOnlyError' => __DIR__ . 
'/includes/libs/rdbms/exception/DBReadOnlyError.php',
diff --git a/includes/libs/rdbms/database/DBConnRef.php 
b/includes/libs/rdbms/database/DBConnRef.php
index ef2953e..666d246 100644
--- a/includes/libs/rdbms/database/DBConnRef.php
+++ b/includes/libs/rdbms/database/DBConnRef.php
@@ -12,13 +12,12 @@
  * @ingroup Database
  * @since 1.22
  */
-class DBConnRef implements IDatabase {
+class DBConnRef extends DBConnectionProxy {
        /** @var ILoadBalancer */
        private $lb;
-       /** @var Database|null Live connection handle */
-       private $conn;
+
        /** @var array|null N-tuple of (server index, group, 
DatabaseDomain|string) */
-       private $params;
+       private $params = null;
 
        const FLD_INDEX = 0;
        const FLD_GROUP = 1;
@@ -27,597 +26,50 @@
 
        /**
         * @param ILoadBalancer $lb Connection manager for $conn
-        * @param Database|array $conn Database handle or (server index, query 
groups, domain, flags)
+        * @param IDatabase|array $conn Database handle or (server index, query 
groups, domain, flags)
         */
        public function __construct( ILoadBalancer $lb, $conn ) {
                $this->lb = $lb;
-               if ( $conn instanceof Database ) {
-                       $this->conn = $conn; // live handle
+
+               if ( $conn instanceof IDatabase ) {
+                       parent::__construct( $conn );
                } elseif ( count( $conn ) >= 4 && $conn[self::FLD_DOMAIN] !== 
false ) {
                        $this->params = $conn;
+
+                       $instantiator = function() {
+                               list( $db, $groups, $wiki, $flags ) = 
$this->params;
+                               return $this->lb->getConnection( $db, $groups, 
$wiki, $flags );
+                       };
+
+                       parent::__construct( $instantiator );
                } else {
                        throw new InvalidArgumentException( "Missing lazy 
connection arguments." );
                }
        }
 
-       function __call( $name, array $arguments ) {
-               if ( $this->conn === null ) {
-                       list( $db, $groups, $wiki, $flags ) = $this->params;
-                       $this->conn = $this->lb->getConnection( $db, $groups, 
$wiki, $flags );
-               }
-
-               return call_user_func_array( [ $this->conn, $name ], $arguments 
);
-       }
-
-       public function getServerInfo() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function bufferResults( $buffer = null ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function trxLevel() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function trxTimestamp() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function explicitTrxActive() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function tablePrefix( $prefix = null ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function dbSchema( $schema = null ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getLBInfo( $name = null ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function setLBInfo( $name, $value = null ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function setLazyMasterHandle( IDatabase $conn ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function implicitGroupby() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function implicitOrderby() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function lastQuery() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function doneWrites() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function lastDoneWrites() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function writesPending() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function writesOrCallbacksPending() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL 
) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function pendingWriteCallers() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function pendingWriteRowsAffected() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function isOpen() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function restoreFlags( $state = self::RESTORE_PRIOR ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getFlag( $flag ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getProperty( $name ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
        public function getDomainID() {
-               if ( $this->conn === null ) {
+               // Avoid triggering a database connection
+               if ( $this->params ) {
                        $domain = $this->params[self::FLD_DOMAIN];
-                       // Avoid triggering a database connection
                        return $domain instanceof DatabaseDomain ? 
$domain->getId() : $domain;
                }
 
-               return $this->__call( __FUNCTION__, func_get_args() );
+               return parent::getDomainId();
        }
 
        public function getWikiID() {
                return $this->getDomainID();
        }
 
-       public function getType() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function open( $server, $user, $password, $dbName ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function fetchObject( $res ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function fetchRow( $res ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function numRows( $res ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function numFields( $res ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function fieldName( $res, $n ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function insertId() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function dataSeek( $res, $row ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function lastErrno() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function lastError() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function fieldInfo( $table, $field ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function affectedRows() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getSoftwareLink() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getServerVersion() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function close() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function reportConnectionError( $error = 'Unknown error' ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) 
{
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function reportQueryError( $error, $errno, $sql, $fname, 
$tempIgnore = false ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function freeResult( $res ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function selectField(
-               $table, $var, $cond = '', $fname = __METHOD__, $options = [], 
$join_conds = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function selectFieldValues(
-               $table, $var, $cond = '', $fname = __METHOD__, $options = [], 
$join_conds = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function select(
-               $table, $vars, $conds = '', $fname = __METHOD__,
-               $options = [], $join_conds = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function selectSQLText(
-               $table, $vars, $conds = '', $fname = __METHOD__,
-               $options = [], $join_conds = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function selectRow(
-               $table, $vars, $conds, $fname = __METHOD__,
-               $options = [], $join_conds = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function estimateRowCount(
-               $table, $vars = '*', $conds = '', $fname = __METHOD__, $options 
= []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function selectRowCount(
-               $tables, $vars = '*', $conds = '', $fname = __METHOD__, 
$options = [], $join_conds = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function fieldExists( $table, $field, $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function indexExists( $table, $index, $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function tableExists( $table, $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function indexUnique( $table, $index ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function insert( $table, $a, $fname = __METHOD__, $options = [] 
) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function update( $table, $values, $conds, $fname = __METHOD__, 
$options = [] ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function makeList( $a, $mode = self::LIST_COMMA ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function aggregateValue( $valuedata, $valuename = 'value' ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function bitNot( $field ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function bitAnd( $fieldLeft, $fieldRight ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function bitOr( $fieldLeft, $fieldRight ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function buildConcat( $stringList ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function buildGroupConcatField(
-               $delim, $table, $field, $conds = '', $join_conds = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function buildStringCast( $field ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function databasesAreIndependent() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function selectDB( $db ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getDBname() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getServer() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function addQuotes( $s ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function buildLike() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function anyChar() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function anyString() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function nextSequenceValue( $seqName ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function replace( $table, $uniqueIndexes, $rows, $fname = 
__METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function upsert(
-               $table, array $rows, array $uniqueIndexes, array $set, $fname = 
__METHOD__
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function deleteJoin(
-               $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 
__METHOD__
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function delete( $table, $conds, $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function insertSelect(
-               $destTable, $srcTable, $varMap, $conds,
-               $fname = __METHOD__, $insertOptions = [], $selectOptions = [], 
$selectJoinConds = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function unionSupportsOrderAndLimit() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function unionQueries( $sqls, $all ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function unionConditionPermutations(
-               $table, $vars, array $permute_conds, $extra_conds = '', $fname 
= __METHOD__,
-               $options = [], $join_conds = []
-       ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function conditional( $cond, $trueVal, $falseVal ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function strreplace( $orig, $old, $new ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getServerUptime() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function wasDeadlock() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function wasLockTimeout() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function wasErrorReissuable() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function wasReadOnlyError() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function masterPosWait( DBMasterPos $pos, $timeout ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getReplicaPos() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getMasterPos() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function serverIsReadOnly() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function onTransactionResolution( callable $callback, $fname = 
__METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function onTransactionIdle( callable $callback, $fname = 
__METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function onTransactionPreCommitOrIdle( callable $callback, 
$fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function setTransactionListener( $name, callable $callback = 
null ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function startAtomic( $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function endAtomic( $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function doAtomicSection( $fname, callable $callback ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function begin( $fname = __METHOD__, $mode = 
IDatabase::TRANSACTION_EXPLICIT ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function commit( $fname = __METHOD__, $flush = '' ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function rollback( $fname = __METHOD__, $flush = '' ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function flushSnapshot( $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function listTables( $prefix = null, $fname = __METHOD__ ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function timestamp( $ts = 0 ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function timestampOrNull( $ts = null ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function ping( &$rtt = null ) {
-               return func_num_args()
-                       ? $this->__call( __FUNCTION__, [ &$rtt ] )
-                       : $this->__call( __FUNCTION__, [] ); // method cares 
about null vs missing
-       }
-
-       public function getLag() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getSessionLagStatus() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function maxListLen() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function encodeBlob( $b ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function decodeBlob( $b ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function setSessionOptions( array $options ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function setSchemaVars( $vars ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function lockIsFree( $lockName, $method ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function lock( $lockName, $method, $timeout = 5 ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function unlock( $lockName, $method ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function namedLocksEnqueue() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function getInfinity() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function encodeExpiry( $expiry ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function decodeExpiry( $expiry, $format = TS_MW ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function setBigSelects( $value = true ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function isReadOnly() {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
-       public function setTableAliases( array $aliases ) {
-               return $this->__call( __FUNCTION__, func_get_args() );
-       }
-
        /**
         * Clean up the connection when out of scope
         */
        function __destruct() {
-               if ( $this->conn ) {
-                       $this->lb->reuseConnection( $this->conn );
+               if ( $this->connection ) {
+                       $this->lb->reuseConnection( $this->connection );
                }
        }
+
 }
 
 class_alias( DBConnRef::class, 'DBConnRef' );
diff --git a/includes/libs/rdbms/database/DBConnectionProxy.php 
b/includes/libs/rdbms/database/DBConnectionProxy.php
new file mode 100644
index 0000000..70ff54f
--- /dev/null
+++ b/includes/libs/rdbms/database/DBConnectionProxy.php
@@ -0,0 +1,616 @@
+<?php
+
+namespace Wikimedia\Rdbms;
+
+use InvalidArgumentException;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Abstract base class for implementations of IDatabase that proxy all (or 
most)
+ * calls to an underlying IDatabase instance.
+ *
+ * @note: proxy methods are defined explicitly to avoid interface errors
+ *
+ * @ingroup Database
+ * @since 1.31
+ */
+abstract class DBConnectionProxy implements IDatabase {
+
+       /**
+        * @var IDatabase|null A database connection handle.
+        * If this is null null, $connectionInstantiator must be set.
+        */
+       protected $connection;
+
+       /**
+        * @var callable|null An instantiator callback returning an IDatabase 
object
+        */
+       private $connectionInstantiator;
+
+       /**
+        * @param Database|callable $conn Database handle, or a callback 
returning a database handle.
+        */
+       public function __construct( $conn ) {
+               if ( $conn instanceof IDatabase ) {
+                       $this->connection = $conn; // live handle
+               } elseif ( is_callable( $conn ) ) {
+                       $this->connectionInstantiator = $conn; // instantiator 
callback
+               } else {
+                       throw new InvalidArgumentException(
+                               '$conn must be an IDatabase instance of a 
callable instantiator'
+                               . ' that will return an IDatabase instance.'
+                       );
+               }
+       }
+
+       function __call( $name, array $arguments ) {
+               if ( $this->connection === null ) {
+                       $connection = call_user_func( 
$this->connectionInstantiator );
+
+                       Assert::postcondition(
+                               $connection instanceof IDatabase,
+                               'Instantiator is expected to return an 
IDatabase instance'
+                       );
+
+                       $this->connection = $connection;
+               }
+
+               return call_user_func_array( [ $this->connection, $name ], 
$arguments );
+       }
+
+       public function getServerInfo() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function bufferResults( $buffer = null ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function trxLevel() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function trxTimestamp() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function explicitTrxActive() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function tablePrefix( $prefix = null ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function dbSchema( $schema = null ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getLBInfo( $name = null ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function setLBInfo( $name, $value = null ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function setLazyMasterHandle( IDatabase $conn ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function implicitGroupby() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function implicitOrderby() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function lastQuery() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function doneWrites() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function lastDoneWrites() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function writesPending() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function writesOrCallbacksPending() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL 
) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function pendingWriteCallers() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function pendingWriteRowsAffected() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function isOpen() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function restoreFlags( $state = self::RESTORE_PRIOR ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getFlag( $flag ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getProperty( $name ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getDomainID() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getWikiID() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getType() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function open( $server, $user, $password, $dbName ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function fetchObject( $res ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function fetchRow( $res ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function numRows( $res ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function numFields( $res ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function fieldName( $res, $n ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function insertId() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function dataSeek( $res, $row ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function lastErrno() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function lastError() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function fieldInfo( $table, $field ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function affectedRows() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getSoftwareLink() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getServerVersion() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function close() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function reportConnectionError( $error = 'Unknown error' ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) 
{
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function reportQueryError( $error, $errno, $sql, $fname, 
$tempIgnore = false ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function freeResult( $res ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function selectField(
+               $table, $var, $cond = '', $fname = __METHOD__, $options = [], 
$join_conds = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function selectFieldValues(
+               $table, $var, $cond = '', $fname = __METHOD__, $options = [], 
$join_conds = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function select(
+               $table, $vars, $conds = '', $fname = __METHOD__,
+               $options = [], $join_conds = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function selectSQLText(
+               $table, $vars, $conds = '', $fname = __METHOD__,
+               $options = [], $join_conds = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function selectRow(
+               $table, $vars, $conds, $fname = __METHOD__,
+               $options = [], $join_conds = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function estimateRowCount(
+               $table, $vars = '*', $conds = '', $fname = __METHOD__, $options 
= []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function selectRowCount(
+               $tables, $vars = '*', $conds = '', $fname = __METHOD__, 
$options = [], $join_conds = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function fieldExists( $table, $field, $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function indexExists( $table, $index, $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function tableExists( $table, $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function indexUnique( $table, $index ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function insert( $table, $a, $fname = __METHOD__, $options = [] 
) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function update( $table, $values, $conds, $fname = __METHOD__, 
$options = [] ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function makeList( $a, $mode = self::LIST_COMMA ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function aggregateValue( $valuedata, $valuename = 'value' ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function bitNot( $field ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function bitAnd( $fieldLeft, $fieldRight ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function bitOr( $fieldLeft, $fieldRight ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function buildConcat( $stringList ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function buildGroupConcatField(
+               $delim, $table, $field, $conds = '', $join_conds = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function buildStringCast( $field ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function databasesAreIndependent() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function selectDB( $db ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getDBname() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getServer() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function addQuotes( $s ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function buildLike() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function anyChar() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function anyString() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function nextSequenceValue( $seqName ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function replace( $table, $uniqueIndexes, $rows, $fname = 
__METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function upsert(
+               $table, array $rows, array $uniqueIndexes, array $set, $fname = 
__METHOD__
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function deleteJoin(
+               $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 
__METHOD__
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function delete( $table, $conds, $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function insertSelect(
+               $destTable, $srcTable, $varMap, $conds,
+               $fname = __METHOD__, $insertOptions = [], $selectOptions = [], 
$selectJoinConds = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function unionSupportsOrderAndLimit() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function unionQueries( $sqls, $all ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function unionConditionPermutations(
+               $table, $vars, array $permute_conds, $extra_conds = '', $fname 
= __METHOD__,
+               $options = [], $join_conds = []
+       ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function conditional( $cond, $trueVal, $falseVal ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function strreplace( $orig, $old, $new ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getServerUptime() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function wasDeadlock() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function wasLockTimeout() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function wasErrorReissuable() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function wasReadOnlyError() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function masterPosWait( DBMasterPos $pos, $timeout ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getReplicaPos() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getMasterPos() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function serverIsReadOnly() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function onTransactionResolution( callable $callback, $fname = 
__METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function onTransactionIdle( callable $callback, $fname = 
__METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function onTransactionPreCommitOrIdle( callable $callback, 
$fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function setTransactionListener( $name, callable $callback = 
null ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function startAtomic( $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function endAtomic( $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function doAtomicSection( $fname, callable $callback ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function begin( $fname = __METHOD__, $mode = 
IDatabase::TRANSACTION_EXPLICIT ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function commit( $fname = __METHOD__, $flush = '' ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function rollback( $fname = __METHOD__, $flush = '' ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function flushSnapshot( $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function listTables( $prefix = null, $fname = __METHOD__ ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function timestamp( $ts = 0 ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function timestampOrNull( $ts = null ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function ping( &$rtt = null ) {
+               return func_num_args()
+                       ? $this->__call( __FUNCTION__, [ &$rtt ] )
+                       : $this->__call( __FUNCTION__, [] ); // method cares 
about null vs missing
+       }
+
+       public function getLag() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getSessionLagStatus() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function maxListLen() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function encodeBlob( $b ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function decodeBlob( $b ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function setSessionOptions( array $options ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function setSchemaVars( $vars ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function lockIsFree( $lockName, $method ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function lock( $lockName, $method, $timeout = 5 ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function unlock( $lockName, $method ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function namedLocksEnqueue() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function getInfinity() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function encodeExpiry( $expiry ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function decodeExpiry( $expiry, $format = TS_MW ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function setBigSelects( $value = true ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function isReadOnly() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+       public function setTableAliases( array $aliases ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
+}
diff --git a/includes/libs/rdbms/database/DBNoWriteWrapper.php 
b/includes/libs/rdbms/database/DBNoWriteWrapper.php
new file mode 100644
index 0000000..180d566
--- /dev/null
+++ b/includes/libs/rdbms/database/DBNoWriteWrapper.php
@@ -0,0 +1,178 @@
+<?php
+
+namespace Wikimedia\Rdbms;
+
+use InvalidArgumentException;
+
+/**
+ * Database connection wrapper that prevents write operations.
+ *
+ * This should be used when returning a connection to the master database when 
a database
+ * connection was requested for the DB_REPLICA index, which is typically the 
case in a
+ * singe database setup. Failing on attempts to write to such a connection 
makes it
+ * more likely to discover accidental writes to a replica in a single database 
development
+ * or testing environment, and especially unit tests.
+ *
+ * @note The wrapper provides protection against writes on a "best effort" 
basis. It is not
+ * guaranteed to prevent all writes under all conditions.
+ *
+ * @ingroup Database
+ * @since 1.31
+ */
+class DBNoWriteWrapper extends DBConnectionProxy {
+
+       /**
+        * @param string $verb
+        * @param string $caller
+        * @throws DBError
+        */
+       private function throwNoWrite( $verb, $caller ) {
+               throw new DBError(
+                       $this,
+                       "Write operation ($verb) is not allowed on this 
database connection! Caused by $caller."
+               );
+       }
+
+       /**
+        * @todo move to a trait, so we don't have to duplicate the code from 
the Database here.
+        *
+        * @param string $sql
+        * @return bool
+        */
+       private function isWriteQuery( $sql ) {
+               return !preg_match(
+                       
'/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
+       }
+
+       /**
+        * @todo move to a trait, so we don't have to duplicate the code from 
the Database here.
+        *
+        * @param string $sql
+        * @return string|null
+        */
+       private function getQueryVerb( $sql ) {
+               return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( 
$m[1] ) : null;
+       }
+
+       /**
+        * @param string $sql
+        * @param string $fname
+        * @param bool $tempIgnore
+        * @throws DBError
+        * @return bool|mixed|IResultWrapper
+        */
+       public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) 
{
+               if ( $this->isWriteQuery( $sql ) ) {
+                       $this->throwNoWrite( $this->getQueryVerb( $sql ), 
$fname );
+               }
+
+               return parent::query( $sql, $fname, $tempIgnore );
+       }
+
+       /**
+        * @param string $table
+        * @param array|string $conds
+        * @param string $fname
+        * @throws DBError
+        */
+       public function delete( $table, $conds, $fname = __METHOD__ ) {
+               $this->throwNoWrite( __FUNCTION__, $fname );
+       }
+
+       /**
+        * @param string $table
+        * @param array $a
+        * @param string $fname
+        * @param array $options
+        * @throws DBError
+        */
+       public function insert( $table, $a, $fname = __METHOD__, $options = [ ] 
) {
+               $this->throwNoWrite( __FUNCTION__, $fname );
+       }
+
+       /**
+        * @param string $table
+        * @param array $values
+        * @param array $conds
+        * @param string $fname
+        * @param array $options
+        * @throws DBError
+        */
+       public function update( $table, $values, $conds, $fname = __METHOD__, 
$options = [ ] ) {
+               $this->throwNoWrite( __FUNCTION__, $fname );
+       }
+
+       /**
+        * @param string $table
+        * @param array $uniqueIndexes
+        * @param array $rows
+        * @param string $fname
+        * @throws DBError
+        */
+       public function replace( $table, $uniqueIndexes, $rows, $fname = 
__METHOD__ ) {
+               $this->throwNoWrite( __FUNCTION__, $fname );
+       }
+
+       /**
+        * @param string $table
+        * @param array $rows
+        * @param array $uniqueIndexes
+        * @param array $set
+        * @param string $fname
+        * @throws DBError
+        */
+       public function upsert(
+               $table,
+               array $rows,
+               array $uniqueIndexes,
+               array $set,
+               $fname = __METHOD__
+       ) {
+               $this->throwNoWrite( __FUNCTION__, $fname );
+       }
+
+       /**
+        * @param string $delTable
+        * @param string $joinTable
+        * @param string $delVar
+        * @param string $joinVar
+        * @param array $conds
+        * @param string $fname
+        * @throws DBError
+        */
+       public function deleteJoin(
+               $delTable,
+               $joinTable,
+               $delVar,
+               $joinVar,
+               $conds,
+               $fname = __METHOD__
+       ) {
+               $this->throwNoWrite( __FUNCTION__, $fname );
+       }
+
+       /**
+        * @param string $destTable
+        * @param array|string $srcTable
+        * @param array $varMap
+        * @param array $conds
+        * @param string $fname
+        * @param array $insertOptions
+        * @param array $selectOptions
+        * @param array $selectJoinConds
+        * @throws DBError
+        */
+       public function insertSelect(
+               $destTable,
+               $srcTable,
+               $varMap,
+               $conds,
+               $fname = __METHOD__,
+               $insertOptions = [ ],
+               $selectOptions = [ ],
+               $selectJoinConds = [ ]
+       ) {
+               $this->throwNoWrite( __FUNCTION__, $fname );
+       }
+
+}
diff --git a/includes/libs/rdbms/loadbalancer/LoadBalancer.php 
b/includes/libs/rdbms/loadbalancer/LoadBalancer.php
index eb288dd..1f0ee01 100644
--- a/includes/libs/rdbms/loadbalancer/LoadBalancer.php
+++ b/includes/libs/rdbms/loadbalancer/LoadBalancer.php
@@ -434,7 +434,7 @@
                        $serverName = $this->getServerName( $i );
                        $this->connLogger->debug( __METHOD__ . ": Using reader 
#$i: $serverName..." );
 
-                       $conn = $this->openConnection( $i, $domain );
+                       $conn = $this->openConnection( $i, $domain, 
self::CONN_NO_WRITE );
                        if ( !$conn ) {
                                $this->connLogger->warning( __METHOD__ . ": 
Failed connecting to $i/$domain" );
                                unset( $currentLoads[$i] ); // avoid this 
server next iteration
@@ -588,7 +588,7 @@
 
                                return false;
                        } else {
-                               $conn = $this->openConnection( $index, 
self::DOMAIN_ANY );
+                               $conn = $this->openConnection( $index, 
self::DOMAIN_ANY, self::CONN_NO_WRITE );
                                if ( !$conn ) {
                                        $this->replLogger->warning( __METHOD__ 
. ': failed to connect to {dbserver}',
                                                [ 'dbserver' => $server ] );
diff --git a/tests/phpunit/includes/db/LoadBalancerTest.php 
b/tests/phpunit/includes/db/LoadBalancerTest.php
index f8ab7f4..2fade4f 100644
--- a/tests/phpunit/includes/db/LoadBalancerTest.php
+++ b/tests/phpunit/includes/db/LoadBalancerTest.php
@@ -1,5 +1,7 @@
 <?php
 
+use Wikimedia\Rdbms\DBError;
+use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\LoadBalancer;
 
 /**
@@ -24,7 +26,7 @@
  * @file
  */
 class LoadBalancerTest extends MediaWikiTestCase {
-       public function testLBSimpleServer() {
+       public function testWithoutReplica() {
                global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, 
$wgDBtype, $wgSQLiteDataDir;
 
                $servers = [
@@ -48,10 +50,13 @@
                $dbw = $lb->getConnection( DB_MASTER );
                $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows 
as master' );
                $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set 
on master" );
+               $this->assertWriteAllowed( $dbw );
 
                $dbr = $lb->getConnection( DB_REPLICA );
+               $this->assertNotSame( $dbw, $dbr, 'Replica connection is not 
master connection' );
                $this->assertTrue( $dbr->getLBInfo( 'master' ), 'DB_REPLICA 
also gets the master' );
-               $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set 
on replica" );
+               $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set 
on replica" );
+               $this->assertWriteForbidden( $dbr );
 
                $dbwAuto = $lb->getConnection( DB_MASTER, [], false, 
$lb::CONN_TRX_AUTO );
                $this->assertFalse( $dbwAuto->getFlag( $dbw::DBO_TRX ), "No 
DBO_TRX with CONN_TRX_AUTO" );
@@ -69,7 +74,7 @@
                $lb->closeAll();
        }
 
-       public function testLBSimpleServers() {
+       public function testWithReplica() {
                global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, 
$wgDBtype, $wgSQLiteDataDir;
 
                $servers = [
@@ -83,7 +88,7 @@
                                'load'        => 0,
                                'flags'       => DBO_TRX // REPEATABLE-READ for 
consistency
                        ],
-                       [ // emulated slave
+                       [ // emulated replica
                                'host'        => $wgDBserver,
                                'dbname'      => $wgDBname,
                                'user'        => $wgDBuser,
@@ -108,14 +113,17 @@
                        $dbw->getLBInfo( 'clusterMasterHost' ),
                        'cluster master set' );
                $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set 
on master" );
+               $this->assertWriteAllowed( $dbw );
 
                $dbr = $lb->getConnection( DB_REPLICA );
-               $this->assertTrue( $dbr->getLBInfo( 'replica' ), 'slave shows 
as slave' );
+               $this->assertNotSame( $dbw, $dbr, 'Replica connection is not 
master connection' );
+               $this->assertTrue( $dbr->getLBInfo( 'replica' ), 'replica shows 
as replica' );
                $this->assertEquals(
                        ( $wgDBserver != '' ) ? $wgDBserver : 'localhost',
                        $dbr->getLBInfo( 'clusterMasterHost' ),
                        'cluster master set' );
-               $this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set 
on replica" );
+               $this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set 
on replica" );
+               $this->assertWriteForbidden( $dbr );
 
                $dbwAuto = $lb->getConnection( DB_MASTER, [], false, 
$lb::CONN_TRX_AUTO );
                $this->assertFalse( $dbwAuto->getFlag( $dbw::DBO_TRX ), "No 
DBO_TRX with CONN_TRX_AUTO" );
@@ -132,4 +140,30 @@
 
                $lb->closeAll();
        }
+
+       private function assertWriteForbidden( IDatabase $db ) {
+               try {
+                       $db->delete( 'user', [ 'user_id' => 57634126 ], 'TEST' 
);
+                       $this->fail( 'Write operation should have failed!' );
+               } catch ( DBError $ex ) {
+                       // check that the exception message contains "Write 
operation"
+                       $constriant = new 
PHPUnit_Framework_Constraint_StringContains( 'Write operation' );
+
+                       if ( !$constriant->evaluate( $ex->getMessage(), '', 
true ) ) {
+                               // re-throw original error, to preserve stack 
trace
+                               throw $ex;
+                       }
+               } finally {
+                       $db->rollback( __METHOD__, 'flush' );
+               }
+       }
+
+       private function assertWriteAllowed( IDatabase $db ) {
+               try {
+                       $this->assertNotSame( false, $db->delete( 'user', [ 
'user_id' => 57634126 ] ) );
+               } finally {
+                       $db->rollback( __METHOD__, 'flush' );
+               }
+       }
+
 }
diff --git a/tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php 
b/tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php
new file mode 100644
index 0000000..d37ce97
--- /dev/null
+++ b/tests/phpunit/includes/libs/rdbms/database/DBConnRefTest.php
@@ -0,0 +1,133 @@
+<?php
+
+use Wikimedia\Rdbms\DBConnRef;
+use Wikimedia\Rdbms\FakeResultWrapper;
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\ILoadBalancer;
+use Wikimedia\Rdbms\ResultWrapper;
+
+/**
+ * @covers Wikimedia\Rdbms\DBConnRef
+ */
+class DBConnRefTest extends PHPUnit_Framework_TestCase {
+
+       /**
+        * @return ILoadBalancer
+        */
+       private function getLoadBalancerMock() {
+               $lb = $this->getMock( ILoadBalancer::class );
+
+               $lb->method( 'getConnection' )->willReturnCallback(
+                       function () {
+                               return $this->getDatabaseMock();
+                       }
+               );
+
+               $lb->method( 'getConnectionRef' )->willReturnCallback(
+                       function() use ( $lb ) {
+                               return $this->getDBConnRef( $lb );
+                       }
+               );
+
+               return $lb;
+       }
+
+       /**
+        * @return IDatabase
+        */
+       private function getDatabaseMock() {
+               $db = $this->getMock( IDatabase::class );
+
+               $db->method( 'select' )->willReturn( new FakeResultWrapper( [] 
) );
+
+               return $db;
+       }
+
+       /**
+        * @return IDatabase
+        */
+       private function getDBConnRef( ILoadBalancer $lb = null ) {
+               $lb = $lb ?: $this->getLoadBalancerMock();
+               return new DBConnRef( $lb, $this->getDatabaseMock() );
+       }
+
+       public function testConstruct() {
+               $lb = $this->getLoadBalancerMock();
+               $ref = new DBConnRef( $lb, $this->getDatabaseMock() );
+
+               $this->assertInstanceOf( ResultWrapper::class, $ref->select( 
'whatever', '*' ) );
+       }
+
+       public function testConstruct_params() {
+               $lb = $this->getMock( ILoadBalancer::class );
+
+               $lb->expects( $this->once() )
+                       ->method( 'getConnection' )
+                       ->with( DB_MASTER, [ 'test' ], 'dummy', 
ILoadBalancer::CONN_TRX_AUTO )
+                       ->willReturnCallback(
+                               function () {
+                                       return $this->getDatabaseMock();
+                               }
+                       );
+
+               $ref = new DBConnRef(
+                       $lb,
+                       [ DB_MASTER, [ 'test' ], 'dummy', 
ILoadBalancer::CONN_TRX_AUTO ]
+               );
+
+               $this->assertInstanceOf( ResultWrapper::class, $ref->select( 
'whatever', '*' ) );
+       }
+
+       public function testDestruct() {
+               $lb = $this->getLoadBalancerMock();
+
+               $lb->expects( $this->once() )
+                       ->method( 'reuseConnection' );
+
+               $this->innerMethodForTestDestruct( $lb );
+       }
+
+       private function innerMethodForTestDestruct( ILoadBalancer $lb ) {
+               $ref = $lb->getConnectionRef( DB_REPLICA );
+
+               $this->assertInstanceOf( ResultWrapper::class, $ref->select( 
'whatever', '*' ) );
+       }
+
+       public function testConstruct_failure() {
+               $this->setExpectedException( InvalidArgumentException::class, 
'' );
+
+               $lb = $this->getLoadBalancerMock();
+               new DBConnRef( $lb, 17 ); // bad constructor argument
+       }
+
+       public function testGetWikiID() {
+               $lb = $this->getMock( ILoadBalancer::class );
+
+               // getWikiID is optimized to not create a connection
+               $lb->expects( $this->never() )
+                       ->method( 'getConnection' );
+
+               $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ] );
+
+               $this->assertSame( 'dummy', $ref->getWikiID() );
+       }
+
+       public function testGetDomainID() {
+               $lb = $this->getMock( ILoadBalancer::class );
+
+               // getDomainID is optimized to not create a connection
+               $lb->expects( $this->never() )
+                       ->method( 'getConnection' );
+
+               $ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ] );
+
+               $this->assertSame( 'dummy', $ref->getDomainID() );
+       }
+
+       public function testSelect() {
+               // select should get passed through normally
+               $ref = $this->getDBConnRef();
+               $this->assertInstanceOf( ResultWrapper::class, $ref->select( 
'whatever', '*' ) );
+       }
+
+}
diff --git 
a/tests/phpunit/includes/libs/rdbms/database/DBNoWriteWrapperTest.php 
b/tests/phpunit/includes/libs/rdbms/database/DBNoWriteWrapperTest.php
new file mode 100644
index 0000000..8d26269
--- /dev/null
+++ b/tests/phpunit/includes/libs/rdbms/database/DBNoWriteWrapperTest.php
@@ -0,0 +1,189 @@
+<?php
+
+use Wikimedia\Rdbms\DBError;
+use Wikimedia\Rdbms\DBNoWriteWrapper;
+use Wikimedia\Rdbms\FakeResultWrapper;
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\ResultWrapper;
+
+/**
+ * @covers Wikimedia\Rdbms\DBNoWriteWrapper
+ */
+class DBNoWriteWrapperTest extends PHPUnit_Framework_TestCase {
+
+       /**
+        * @return IDatabase
+        */
+       private function getDatabaseMock() {
+               $db = $this->getMock( IDatabase::class );
+
+               $db->method( 'select' )->willReturn( new FakeResultWrapper( [] 
) );
+               $db->method( 'query' )->willReturn( new FakeResultWrapper( [] ) 
);
+
+               return $db;
+       }
+
+       /**
+        * @return IDatabase
+        */
+       private function getDBNoWriteWrapper() {
+               return new DBNoWriteWrapper( $this->getDatabaseMock() );
+       }
+
+       public function testConstruct() {
+               $wrapper = new DBNoWriteWrapper( $this->getDatabaseMock() );
+
+               $this->assertInstanceOf( ResultWrapper::class, 
$wrapper->select( 'whatever', '*' ) );
+       }
+
+       public function testConstruct_callback() {
+               $wrapper = new DBNoWriteWrapper(
+                       function () {
+                               return $this->getDatabaseMock();
+                       }
+               );
+
+               $this->assertInstanceOf( ResultWrapper::class, 
$wrapper->select( 'whatever', '*' ) );
+       }
+
+       public function testConstruct_failure() {
+               $this->setExpectedException( InvalidArgumentException::class, 
'' );
+               new DBNoWriteWrapper( 17 ); // bad constructor argument
+       }
+
+       public function provideQuery_read() {
+               yield [ 'SELECT * FROM whatever' ];
+       }
+
+       /**
+        * @dataProvider provideQuery_read()
+        * @param string $sql
+        */
+       public function testQuery_read( $sql ) {
+               $wrapper = $this->getDBNoWriteWrapper();
+               $this->assertInstanceOf( ResultWrapper::class, $wrapper->query( 
$sql, 'TEST' ) );
+       }
+
+       public function provideQuery_write() {
+               yield [
+                       'DELETE FROM whatever',
+                       'Write operation (DELETE) is not allowed on this 
database connection! Caused by TEST.',
+               ];
+               yield [
+                       'UPDATE whatever SET foo = 0 WHERE bar = 1',
+                       'Write operation (UPDATE) is not allowed on this 
database connection! Caused by TEST.',
+               ];
+               yield [
+                       'INSERT INTO whatever ( foo, bar ) VALUES ( foo = 0, 
bar = 1 )',
+                       'Write operation (INSERT) is not allowed on this 
database connection! Caused by TEST.',
+               ];
+       }
+
+       /**
+        * @dataProvider provideQuery_write()
+        * @param string $sql
+        * @param string $message
+        */
+       public function testQuery_write( $sql, $message ) {
+               $this->setExpectedException( DBError::class, $message );
+
+               $wrapper = $this->getDBNoWriteWrapper();
+               $wrapper->query( $sql, 'TEST' );
+       }
+
+       public function testDelete() {
+               $this->setExpectedException(
+                       DBError::class,
+                       'Write operation (delete) is not allowed on this 
database connection! Caused by TEST.'
+               );
+
+               $wrapper = $this->getDBNoWriteWrapper();
+               $wrapper->delete( 'whatever', '*', 'TEST' );
+       }
+
+       public function testInsert() {
+               $this->setExpectedException(
+                       DBError::class,
+                       'Write operation (insert) is not allowed on this 
database connection! Caused by TEST.'
+               );
+
+               $wrapper = $this->getDBNoWriteWrapper();
+               $wrapper->insert( 'whatever', [], 'TEST' );
+       }
+
+       public function testUpdate() {
+               $this->setExpectedException(
+                       DBError::class,
+                       'Write operation (update) is not allowed on this 
database connection! Caused by TEST.'
+               );
+
+               $wrapper = $this->getDBNoWriteWrapper();
+               $wrapper->update( 'whatever', [], [], 'TEST' );
+       }
+
+       public function testReplace() {
+               $this->setExpectedException(
+                       DBError::class,
+                       'Write operation (replace) is not allowed on this 
database connection! Caused by TEST.'
+               );
+
+               $wrapper = $this->getDBNoWriteWrapper();
+               $wrapper->replace( 'whatever', [], [], 'TEST' );
+       }
+
+       public function testUpsert() {
+               $this->setExpectedException(
+                       DBError::class,
+                       'Write operation (upsert) is not allowed on this 
database connection! Caused by TEST.'
+               );
+
+               $wrapper = $this->getDBNoWriteWrapper();
+               $wrapper->upsert( 'whatever', [], [], [], 'TEST' );
+       }
+
+       public function testDeleteJoin() {
+               $this->setExpectedException(
+                       DBError::class,
+                       'Write operation (deleteJoin) is not allowed on this 
database connection! Caused by TEST.'
+               );
+
+               $wrapper = $this->getDBNoWriteWrapper();
+               $wrapper->deleteJoin( 'whatever', 'something', [], [], [], 
'TEST' );
+       }
+
+       public function testInsertSelect() {
+               $this->setExpectedException(
+                       DBError::class,
+                       'Write operation (insertSelect) is not allowed on this 
database connection! Caused by TEST.'
+               );
+
+               $wrapper = $this->getDBNoWriteWrapper();
+               $wrapper->insertSelect( 'whatever', 'something', [], [], 'TEST' 
);
+       }
+
+       public function testSelect() {
+               $wrapper = $this->getDBNoWriteWrapper();
+               $this->assertInstanceOf( ResultWrapper::class, 
$wrapper->select( 'whatever', '*' ) );
+       }
+
+       public function testIsOpen() {
+               $wrapper = $this->getDBNoWriteWrapper();
+               $this->assertNull( $wrapper->isOpen() );
+       }
+
+       public function testGetDomainID() {
+               $wrapper = $this->getDBNoWriteWrapper();
+               $this->assertNull( $wrapper->getDomainID() );
+       }
+
+       public function testGetReplicaPos() {
+               $wrapper = $this->getDBNoWriteWrapper();
+               $this->assertNull( $wrapper->getReplicaPos() );
+       }
+
+       public function testTableExists() {
+               $wrapper = $this->getDBNoWriteWrapper();
+               $this->assertNull( $wrapper->tableExists( 'whatever', 'TEST' ) 
);
+       }
+
+}
diff --git a/tests/phpunit/tests/MediaWikiTestCaseDbTest.php 
b/tests/phpunit/tests/MediaWikiTestCaseDbTest.php
new file mode 100644
index 0000000..fbe4d42
--- /dev/null
+++ b/tests/phpunit/tests/MediaWikiTestCaseDbTest.php
@@ -0,0 +1,58 @@
+<?php
+use Wikimedia\Rdbms\DBError;
+use Wikimedia\Rdbms\IDatabase;
+
+/**
+ * @covers MediaWikiTestCase
+ *
+ * @group Database
+ */
+class MediaWikiTestCaseDbTest extends MediaWikiTestCase {
+
+       public function testDbPrefix() {
+               global $wgDBprefix;
+
+               // $wgDBprefix is overwritten for unit tests!
+               $this->assertNotSame( '', $wgDBprefix, '$wgDBprefix' );
+               $this->assertSame( $wgDBprefix, $this->dbPrefix() );
+               $this->assertSame( $this->dbPrefix(), $this->db->tablePrefix() 
);
+       }
+
+       public function testMasterConnection() {
+               $dbw = wfGetDB( DB_MASTER );
+               $this->assertSame( $this->db, $dbw );
+               $this->assertWriteAllowed( $dbw );
+       }
+
+       public function testReplicaConnection() {
+               $dbr = wfGetDB( DB_REPLICA );
+               $this->assertNotSame( $this->db, $dbr );
+               $this->assertWriteForbidden( $dbr );
+       }
+
+       private function assertWriteForbidden( IDatabase $db ) {
+               try {
+                       $db->delete( 'user', [ 'user_id' => 57634126 ], 'TEST' 
);
+                       $this->fail( 'Write operation should have failed!' );
+               } catch ( DBError $ex ) {
+                       // check that the exception message contains "Write 
operation"
+                       $constriant = new 
PHPUnit_Framework_Constraint_StringContains( 'Write operation' );
+
+                       if ( !$constriant->evaluate( $ex->getMessage(), '', 
true ) ) {
+                               // re-throw original error, to preserve stack 
trace
+                               throw $ex;
+                       }
+               } finally {
+                       $db->rollback( __METHOD__, 'flush' );
+               }
+       }
+
+       private function assertWriteAllowed( IDatabase $db ) {
+               try {
+                       $this->assertNotSame( false, $db->delete( 'user', [ 
'user_id' => 57634126 ] ) );
+               } finally {
+                       $db->rollback( __METHOD__, 'flush' );
+               }
+       }
+
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/399445
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ida135ec93acf4dc13bef1c5a1d03d2646e984bcb
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Daniel Kinzler <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to