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