Merge "objectcache: optimize MemcachedPeclBagOStuff::*Multi() write methods"
[lhc/web/wiklou.git] / includes / libs / rdbms / database / Database.php
index 92b9471..60062fb 100644 (file)
@@ -30,7 +30,7 @@ use Psr\Log\LoggerInterface;
 use Psr\Log\NullLogger;
 use Wikimedia\ScopedCallback;
 use Wikimedia\Timestamp\ConvertibleTimestamp;
-use Wikimedia;
+use Wikimedia\AtEase\AtEase;
 use BagOStuff;
 use HashBagOStuff;
 use LogicException;
@@ -198,19 +198,23 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /** @var int Writes to this temporary table effect lastDoneWrites() */
        private static $TEMP_PSEUDO_PERMANENT = 2;
 
-       /** Number of times to re-try an operation in case of deadlock */
+       /** @var int Number of times to re-try an operation in case of deadlock */
        private static $DEADLOCK_TRIES = 4;
-       /** Minimum time to wait before retry, in microseconds */
+       /** @var int Minimum time to wait before retry, in microseconds */
        private static $DEADLOCK_DELAY_MIN = 500000;
-       /** Maximum time to wait before retry */
+       /** @var int Maximum time to wait before retry */
        private static $DEADLOCK_DELAY_MAX = 1500000;
 
-       /** How long before it is worth doing a dummy query to test the connection */
+       /** @var int How long before it is worth doing a dummy query to test the connection */
        private static $PING_TTL = 1.0;
+       /** @var string Dummy SQL query */
        private static $PING_QUERY = 'SELECT 1 AS ping';
 
+       /** @var float Guess of how many seconds it takes to replicate a small insert */
        private static $TINY_WRITE_SEC = 0.010;
+       /** @var float Consider a write slow if it took more than this many seconds */
        private static $SLOW_WRITE_SEC = 0.500;
+       /** @var float Assume an insert of this many rows or less should be fast to replicate */
        private static $SMALL_WRITE_ROWS = 100;
 
        /**
@@ -270,7 +274,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         */
        final public function initConnection() {
                if ( $this->isOpen() ) {
-                       throw new LogicException( __METHOD__ . ': already connected.' );
+                       throw new LogicException( __METHOD__ . ': already connected' );
                }
                // Establish the connection
                $this->doInitConnection();
@@ -294,17 +298,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->connectionParams['tablePrefix']
                        );
                } else {
-                       throw new InvalidArgumentException( "No database user provided." );
+                       throw new InvalidArgumentException( "No database user provided" );
                }
        }
 
        /**
         * Open a new connection to the database (closing any existing one)
         *
-        * @param string $server Database server host
-        * @param string $user Database user name
-        * @param string $password Database user password
-        * @param string $dbName Database name
+        * @param string|null $server Database server host
+        * @param string|null $user Database user name
+        * @param string|null $password Database user password
+        * @param string|null $dbName Database name
         * @param string|null $schema Database schema name
         * @param string $tablePrefix Table prefix
         * @throws DBConnectionError
@@ -316,8 +320,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         *
         * This also connects to the database immediately upon object construction
         *
-        * @param string $dbType A possible DB type (sqlite, mysql, postgres,...)
-        * @param array $p Parameter map with keys:
+        * @param string $type A possible DB type (sqlite, mysql, postgres,...)
+        * @param array $params Parameter map with keys:
         *   - host : The hostname of the DB server
         *   - user : The name of the database user the client operates under
         *   - password : The password for the database user
@@ -356,45 +360,51 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         * @throws InvalidArgumentException If the database driver or extension cannot be found
         * @since 1.18
         */
-       final public static function factory( $dbType, $p = [], $connect = self::NEW_CONNECTED ) {
-               $class = self::getClass( $dbType, $p['driver'] ?? null );
+       final public static function factory( $type, $params = [], $connect = self::NEW_CONNECTED ) {
+               $class = self::getClass( $type, $params['driver'] ?? null );
 
                if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
-                       // Resolve some defaults for b/c
-                       $p['host'] = $p['host'] ?? false;
-                       $p['user'] = $p['user'] ?? false;
-                       $p['password'] = $p['password'] ?? false;
-                       $p['dbname'] = $p['dbname'] ?? false;
-                       $p['flags'] = $p['flags'] ?? 0;
-                       $p['variables'] = $p['variables'] ?? [];
-                       $p['tablePrefix'] = $p['tablePrefix'] ?? '';
-                       $p['schema'] = $p['schema'] ?? null;
-                       $p['cliMode'] = $p['cliMode'] ?? ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' );
-                       $p['agent'] = $p['agent'] ?? '';
-                       if ( !isset( $p['connLogger'] ) ) {
-                               $p['connLogger'] = new NullLogger();
-                       }
-                       if ( !isset( $p['queryLogger'] ) ) {
-                               $p['queryLogger'] = new NullLogger();
-                       }
-                       $p['profiler'] = $p['profiler'] ?? null;
-                       if ( !isset( $p['trxProfiler'] ) ) {
-                               $p['trxProfiler'] = new TransactionProfiler();
-                       }
-                       if ( !isset( $p['errorLogger'] ) ) {
-                               $p['errorLogger'] = function ( Exception $e ) {
+                       $params += [
+                               'host' => null,
+                               'user' => null,
+                               'password' => null,
+                               'dbname' => null,
+                               'schema' => null,
+                               'tablePrefix' => '',
+                               'flags' => 0,
+                               'variables' => [],
+                               'cliMode' => ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ),
+                               'agent' => basename( $_SERVER['SCRIPT_NAME'] ) . '@' . gethostname()
+                       ];
+
+                       $normalizedParams = [
+                               // Configuration
+                               'host' => strlen( $params['host'] ) ? $params['host'] : null,
+                               'user' => strlen( $params['user'] ) ? $params['user'] : null,
+                               'password' => is_string( $params['password'] ) ? $params['password'] : null,
+                               'dbname' => strlen( $params['dbname'] ) ? $params['dbname'] : null,
+                               'schema' => strlen( $params['schema'] ) ? $params['schema'] : null,
+                               'tablePrefix' => (string)$params['tablePrefix'],
+                               'flags' => (int)$params['flags'],
+                               'variables' => $params['variables'],
+                               'cliMode' => (bool)$params['cliMode'],
+                               'agent' => (string)$params['agent'],
+                               // Objects and callbacks
+                               'profiler' => $params['profiler'] ?? null,
+                               'trxProfiler' => $params['trxProfiler'] ?? new TransactionProfiler(),
+                               'connLogger' => $params['connLogger'] ?? new NullLogger(),
+                               'queryLogger' => $params['queryLogger'] ?? new NullLogger(),
+                               'errorLogger' => $params['errorLogger'] ?? function ( Exception $e ) {
                                        trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
-                               };
-                       }
-                       if ( !isset( $p['deprecationLogger'] ) ) {
-                               $p['deprecationLogger'] = function ( $msg ) {
+                               },
+                               'deprecationLogger' => $params['deprecationLogger'] ?? function ( $msg ) {
                                        trigger_error( $msg, E_USER_DEPRECATED );
-                               };
-                       }
+                               }
+                       ] + $params;
 
                        /** @var Database $conn */
-                       $conn = new $class( $p );
-                       if ( $connect == self::NEW_CONNECTED ) {
+                       $conn = new $class( $normalizedParams );
+                       if ( $connect === self::NEW_CONNECTED ) {
                                $conn->initConnection();
                        }
                } else {
@@ -541,7 +551,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        public function dbSchema( $schema = null ) {
                if ( strlen( $schema ) && $this->getDBname() === null ) {
-                       throw new DBUnexpectedError( $this, "Cannot set schema to '$schema'; no database set." );
+                       throw new DBUnexpectedError( $this, "Cannot set schema to '$schema'; no database set" );
                }
 
                $old = $this->currentDomain->getSchema();
@@ -576,11 +586,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                return null;
        }
 
-       public function setLBInfo( $name, $value = null ) {
-               if ( is_null( $value ) ) {
-                       $this->lbInfo = $name;
+       public function setLBInfo( $nameOrArray, $value = null ) {
+               if ( is_array( $nameOrArray ) ) {
+                       $this->lbInfo = $nameOrArray;
+               } elseif ( is_string( $nameOrArray ) ) {
+                       if ( $value !== null ) {
+                               $this->lbInfo[$nameOrArray] = $value;
+                       } else {
+                               unset( $this->lbInfo[$nameOrArray] );
+                       }
                } else {
-                       $this->lbInfo[$name] = $value;
+                       throw new InvalidArgumentException( "Got non-string key" );
                }
        }
 
@@ -726,7 +742,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
                if ( ( $flag & self::DBO_IGNORE ) ) {
-                       throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
+                       throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed" );
                }
 
                if ( $remember === self::REMEMBER_PRIOR ) {
@@ -737,7 +753,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
                if ( ( $flag & self::DBO_IGNORE ) ) {
-                       throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
+                       throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed" );
                }
 
                if ( $remember === self::REMEMBER_PRIOR ) {
@@ -875,7 +891,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                        $levels = $this->flatAtomicSectionList();
                                        $exception = new DBUnexpectedError(
                                                $this,
-                                               __METHOD__ . ": atomic sections $levels are still open."
+                                               __METHOD__ . ": atomic sections $levels are still open"
                                        );
                                } elseif ( $this->trxAutomatic ) {
                                        // Only the connection manager can commit non-empty DBO_TRX transactions
@@ -884,7 +900,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                                $exception = new DBUnexpectedError(
                                                        $this,
                                                        __METHOD__ .
-                                                       ": mass commit/rollback of peer transaction required (DBO_TRX set)."
+                                                       ": mass commit/rollback of peer transaction required (DBO_TRX set)"
                                                );
                                        }
                                } else {
@@ -892,14 +908,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                        // back, even if empty.
                                        $exception = new DBUnexpectedError(
                                                $this,
-                                               __METHOD__ . ": transaction is still open (from {$this->trxFname})."
+                                               __METHOD__ . ": transaction is still open (from {$this->trxFname})"
                                        );
                                }
 
                                if ( $this->trxEndCallbacksSuppressed ) {
                                        $exception = $exception ?: new DBUnexpectedError(
                                                $this,
-                                               __METHOD__ . ': callbacks are suppressed; cannot properly commit.'
+                                               __METHOD__ . ': callbacks are suppressed; cannot properly commit'
                                        );
                                }
 
@@ -929,7 +945,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $fnames = $this->pendingWriteAndCallbackCallers();
                        if ( $fnames ) {
                                throw new RuntimeException(
-                                       "Transaction callbacks are still pending:\n" . implode( ', ', $fnames )
+                                       "Transaction callbacks are still pending: " . implode( ', ', $fnames )
                                );
                        }
                }
@@ -947,7 +963,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         */
        final protected function assertHasConnectionHandle() {
                if ( !$this->isOpen() ) {
-                       throw new DBUnexpectedError( $this, "DB connection was already closed." );
+                       throw new DBUnexpectedError( $this, "DB connection was already closed" );
                }
        }
 
@@ -961,7 +977,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                if ( $this->getLBInfo( 'replica' ) === true ) {
                        throw new DBReadOnlyRoleError(
                                $this,
-                               'Write operations are not allowed on replica database connections.'
+                               'Write operations are not allowed on replica database connections'
                        );
                }
                $reason = $this->getReadOnlyReason();
@@ -1401,7 +1417,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        private function assertQueryIsCurrentlyAllowed( $sql, $fname ) {
                $verb = $this->getQueryVerb( $sql );
                if ( $verb === 'USE' ) {
-                       throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead." );
+                       throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead" );
                }
 
                if ( $verb === 'ROLLBACK' ) { // transaction/savepoint
@@ -1411,7 +1427,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                if ( $this->trxStatus < self::STATUS_TRX_OK ) {
                        throw new DBTransactionStateError(
                                $this,
-                               "Cannot execute query from $fname while transaction status is ERROR.",
+                               "Cannot execute query from $fname while transaction status is ERROR",
                                [],
                                $this->trxStatusCause
                        );
@@ -1552,7 +1568,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         */
        public function reportQueryError( $error, $errno, $sql, $fname, $ignore = false ) {
                if ( $ignore ) {
-                       $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
+                       $this->queryLogger->debug( "SQL ERROR (ignored): $error" );
                } else {
                        $exception = $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
 
@@ -1580,7 +1596,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                'trace' => ( new RuntimeException() )->getTraceAsString()
                        ] )
                );
-               $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
+               $this->queryLogger->debug( "SQL ERROR: " . $error . "" );
                if ( $this->wasQueryTimeout( $error, $errno ) ) {
                        $e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
                } elseif ( $this->wasConnectionError( $errno ) ) {
@@ -1813,7 +1829,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        // functions. Discourage use of such queries to encourage compatibility.
                        call_user_func(
                                $this->deprecationLogger,
-                               __METHOD__ . ": aggregation used with a locking SELECT ($fname)."
+                               __METHOD__ . ": aggregation used with a locking SELECT ($fname)"
                        );
                }
 
@@ -2010,7 +2026,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        } elseif ( count( $var ) == 1 ) {
                                $column = $var[0] ?? reset( $var );
                        } else {
-                               throw new DBUnexpectedError( $this, __METHOD__ . ': got multiple columns.' );
+                               throw new DBUnexpectedError( $this, __METHOD__ . ': got multiple columns' );
                        }
                } else {
                        $column = $var;
@@ -2380,7 +2396,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                if ( $name instanceof Subquery ) {
                        throw new DBUnexpectedError(
                                $this,
-                               __METHOD__ . ': got Subquery instance when expecting a string.'
+                               __METHOD__ . ': got Subquery instance when expecting a string'
                        );
                }
 
@@ -2401,7 +2417,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                # surrounded by symbols which may be considered word breaks.
                if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
                        $this->queryLogger->warning(
-                               __METHOD__ . ": use of subqueries is not supported this way.",
+                               __METHOD__ . ": use of subqueries is not supported this way",
                                [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
                        );
 
@@ -2524,12 +2540,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                } elseif ( $table instanceof Subquery ) {
                        $quotedTable = (string)$table;
                } else {
-                       throw new InvalidArgumentException( "Table must be a string or Subquery." );
+                       throw new InvalidArgumentException( "Table must be a string or Subquery" );
                }
 
                if ( $alias === false || $alias === $table ) {
                        if ( $table instanceof Subquery ) {
-                               throw new InvalidArgumentException( "Subquery table missing alias." );
+                               throw new InvalidArgumentException( "Subquery table missing alias" );
                        }
 
                        return $quotedTable;
@@ -3162,8 +3178,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        public function limitResult( $sql, $limit, $offset = false ) {
                if ( !is_numeric( $limit ) ) {
-                       throw new DBUnexpectedError( $this,
-                               "Invalid non-numeric limit passed to limitResult()\n" );
+                       throw new DBUnexpectedError(
+                               $this,
+                               "Invalid non-numeric limit passed to " . __METHOD__
+                       );
                }
                // This version works in MySQL and SQLite. It will very likely need to be
                // overridden for most other RDBMS subclasses.
@@ -3370,7 +3388,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
                if ( !$this->trxLevel() ) {
-                       throw new DBUnexpectedError( $this, "No transaction is active." );
+                       throw new DBUnexpectedError( $this, "No transaction is active" );
                }
                $this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
        }
@@ -3416,7 +3434,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        final public function onAtomicSectionCancel( callable $callback, $fname = __METHOD__ ) {
                if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
-                       throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
+                       throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)" );
                }
                $this->trxSectionCancelCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
        }
@@ -3552,7 +3570,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         */
        public function runOnTransactionIdleCallbacks( $trigger ) {
                if ( $this->trxLevel() ) { // sanity
-                       throw new DBUnexpectedError( $this, __METHOD__ . ': a transaction is still open.' );
+                       throw new DBUnexpectedError( $this, __METHOD__ . ': a transaction is still open' );
                }
 
                if ( $this->trxEndCallbacksSuppressed ) {
@@ -3809,7 +3827,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        final public function endAtomic( $fname = __METHOD__ ) {
                if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
-                       throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
+                       throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)" );
                }
 
                // Check if the current section matches $fname
@@ -3820,7 +3838,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                if ( $savedFname !== $fname ) {
                        throw new DBUnexpectedError(
                                $this,
-                               "Invalid atomic section ended (got $fname but expected $savedFname)."
+                               "Invalid atomic section ended (got $fname but expected $savedFname)"
                        );
                }
 
@@ -3845,7 +3863,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null
        ) {
                if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
-                       throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
+                       throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)" );
                }
 
                $excisedIds = [];
@@ -3887,7 +3905,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        if ( $savedFname !== $fname ) {
                                throw new DBUnexpectedError(
                                        $this,
-                                       "Invalid atomic section ended (got $fname but expected $savedFname)."
+                                       "Invalid atomic section ended (got $fname but expected $savedFname)"
                                );
                        }
 
@@ -3914,7 +3932,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->trxStatus = self::STATUS_TRX_ERROR;
                                $this->trxStatusCause = new DBUnexpectedError(
                                        $this,
-                                       "Uncancelable atomic section canceled (got $fname)."
+                                       "Uncancelable atomic section canceled (got $fname)"
                                );
                        }
                } finally {
@@ -3945,24 +3963,24 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
                static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
                if ( !in_array( $mode, $modes, true ) ) {
-                       throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'." );
+                       throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'" );
                }
 
                // Protect against mismatched atomic section, transaction nesting, and snapshot loss
                if ( $this->trxLevel() ) {
                        if ( $this->trxAtomicLevels ) {
                                $levels = $this->flatAtomicSectionList();
-                               $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
+                               $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open";
                                throw new DBUnexpectedError( $this, $msg );
                        } elseif ( !$this->trxAutomatic ) {
-                               $msg = "$fname: Explicit transaction already active (from {$this->trxFname}).";
+                               $msg = "$fname: Explicit transaction already active (from {$this->trxFname})";
                                throw new DBUnexpectedError( $this, $msg );
                        } else {
-                               $msg = "$fname: Implicit transaction already active (from {$this->trxFname}).";
+                               $msg = "$fname: Implicit transaction already active (from {$this->trxFname})";
                                throw new DBUnexpectedError( $this, $msg );
                        }
                } elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
-                       $msg = "$fname: Implicit transaction expected (DBO_TRX set).";
+                       $msg = "$fname: Implicit transaction expected (DBO_TRX set)";
                        throw new DBUnexpectedError( $this, $msg );
                }
 
@@ -4008,7 +4026,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
                static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
                if ( !in_array( $flush, $modes, true ) ) {
-                       throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'." );
+                       throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'" );
                }
 
                if ( $this->trxLevel() && $this->trxAtomicLevels ) {
@@ -4016,7 +4034,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $levels = $this->flatAtomicSectionList();
                        throw new DBUnexpectedError(
                                $this,
-                               "$fname: Got COMMIT while atomic sections $levels are still open."
+                               "$fname: Got COMMIT while atomic sections $levels are still open"
                        );
                }
 
@@ -4026,17 +4044,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        } elseif ( !$this->trxAutomatic ) {
                                throw new DBUnexpectedError(
                                        $this,
-                                       "$fname: Flushing an explicit transaction, getting out of sync."
+                                       "$fname: Flushing an explicit transaction, getting out of sync"
                                );
                        }
                } elseif ( !$this->trxLevel() ) {
                        $this->queryLogger->error(
-                               "$fname: No transaction to commit, something got out of sync." );
+                               "$fname: No transaction to commit, something got out of sync" );
                        return; // nothing to do
                } elseif ( $this->trxAutomatic ) {
                        throw new DBUnexpectedError(
                                $this,
-                               "$fname: Expected mass commit of all peer transactions (DBO_TRX set)."
+                               "$fname: Expected mass commit of all peer transactions (DBO_TRX set)"
                        );
                }
 
@@ -4089,7 +4107,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                ) {
                        throw new DBUnexpectedError(
                                $this,
-                               "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)."
+                               "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)"
                        );
                }
 
@@ -4151,13 +4169,32 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
        }
 
-       public function flushSnapshot( $fname = __METHOD__ ) {
-               if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
+       public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
+               if ( $this->explicitTrxActive() ) {
+                       // Committing this transaction would break callers that assume it is still open
+                       throw new DBUnexpectedError(
+                               $this,
+                               "$fname: Cannot flush snapshot; " .
+                               "explicit transaction '{$this->trxFname}' is still open"
+                       );
+               } elseif ( $this->writesOrCallbacksPending() ) {
                        // This only flushes transactions to clear snapshots, not to write data
                        $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
                        throw new DBUnexpectedError(
                                $this,
-                               "$fname: Cannot flush snapshot because writes are pending ($fnames)."
+                               "$fname: Cannot flush snapshot; " .
+                               "writes from transaction {$this->trxFname} are still pending ($fnames)"
+                       );
+               } elseif (
+                       $this->trxLevel() &&
+                       $this->getTransactionRoundId() &&
+                       $flush !== self::FLUSHING_INTERNAL &&
+                       $flush !== self::FLUSHING_ALL_PEERS
+               ) {
+                       $this->queryLogger->warning(
+                               "$fname: Expected mass snapshot flush of all peer transactions " .
+                               "in the explicit transactions round '{$this->getTransactionRoundId()}'",
+                               [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
                        );
                }
 
@@ -4269,8 +4306,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->server,
                                $this->user,
                                $this->password,
-                               $this->getDBname(),
-                               $this->dbSchema(),
+                               $this->currentDomain->getDatabase(),
+                               $this->currentDomain->getSchema(),
                                $this->tablePrefix()
                        );
                        $this->lastPing = microtime( true );
@@ -4408,12 +4445,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $fname = false,
                callable $inputCallback = null
        ) {
-               Wikimedia\suppressWarnings();
+               AtEase::suppressWarnings();
                $fp = fopen( $filename, 'r' );
-               Wikimedia\restoreWarnings();
+               AtEase::restoreWarnings();
 
                if ( $fp === false ) {
-                       throw new RuntimeException( "Could not open \"{$filename}\".\n" );
+                       throw new RuntimeException( "Could not open \"{$filename}\"" );
                }
 
                if ( !$fname ) {
@@ -4630,7 +4667,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        $fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
                        throw new DBUnexpectedError(
                                $this,
-                               "$fname: Cannot flush pre-lock snapshot because writes are pending ($fnames)."
+                               "$fname: Cannot flush pre-lock snapshot; " .
+                               "writes from transaction {$this->trxFname} are still pending ($fnames)"
                        );
                }
 
@@ -4669,7 +4707,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        final public function lockTables( array $read, array $write, $method ) {
                if ( $this->writesOrCallbacksPending() ) {
-                       throw new DBUnexpectedError( $this, "Transaction writes or callbacks still pending." );
+                       throw new DBUnexpectedError( $this, "Transaction writes or callbacks still pending" );
                }
 
                if ( $this->tableLocksHaveTransactionScope() ) {
@@ -4758,8 +4796,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         */
        protected function getReadOnlyReason() {
                $reason = $this->getLBInfo( 'readOnlyReason' );
+               if ( is_string( $reason ) ) {
+                       return $reason;
+               } elseif ( $this->getLBInfo( 'replica' ) ) {
+                       return "Server is configured in the role of a read-only replica database.";
+               }
 
-               return is_string( $reason ) ? $reason : false;
+               return false;
        }
 
        public function setTableAliases( array $aliases ) {
@@ -4794,7 +4837,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                if ( !$this->conn ) {
                        throw new DBUnexpectedError(
                                $this,
-                               'DB connection was already closed or the connection dropped.'
+                               'DB connection was already closed or the connection dropped'
                        );
                }
 
@@ -4827,8 +4870,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         */
        public function __clone() {
                $this->connLogger->warning(
-                       "Cloning " . static::class . " is not recommended; forking connection:\n" .
-                       ( new RuntimeException() )->getTraceAsString()
+                       "Cloning " . static::class . " is not recommended; forking connection",
+                       [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
                );
 
                if ( $this->isOpen() ) {
@@ -4841,8 +4884,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                $this->server,
                                $this->user,
                                $this->password,
-                               $this->getDBname(),
-                               $this->dbSchema(),
+                               $this->currentDomain->getDatabase(),
+                               $this->currentDomain->getSchema(),
                                $this->tablePrefix()
                        );
                        $this->lastPing = microtime( true );
@@ -4856,7 +4899,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         */
        public function __sleep() {
                throw new RuntimeException( 'Database serialization may cause problems, since ' .
-                       'the connection is not restored on wakeup.' );
+                       'the connection is not restored on wakeup' );
        }
 
        /**
@@ -4864,22 +4907,22 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
         */
        public function __destruct() {
                if ( $this->trxLevel() && $this->trxDoneWrites ) {
-                       trigger_error( "Uncommitted DB writes (transaction from {$this->trxFname})." );
+                       trigger_error( "Uncommitted DB writes (transaction from {$this->trxFname})" );
                }
 
                $danglingWriters = $this->pendingWriteAndCallbackCallers();
                if ( $danglingWriters ) {
                        $fnames = implode( ', ', $danglingWriters );
-                       trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
+                       trigger_error( "DB transaction writes or callbacks still pending ($fnames)" );
                }
 
                if ( $this->conn ) {
                        // Avoid connection leaks for sanity. Normally, resources close at script completion.
                        // The connection might already be closed in zend/hhvm by now, so suppress warnings.
-                       Wikimedia\suppressWarnings();
+                       AtEase::suppressWarnings();
                        $this->closeConnection();
-                       Wikimedia\restoreWarnings();
-                       $this->conn = false;
+                       AtEase::restoreWarnings();
+                       $this->conn = null;
                }
        }
 }