# Avoid fatals if close() was called
$this->assertOpen();
- # Send the query to the server
+ # Send the query to the server and fetch any corresponding errors
$ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
+ $lastError = $this->lastError();
+ $lastErrno = $this->lastErrno();
# Try reconnecting if the connection was lost
- if ( false === $ret && $this->wasConnectionLoss() ) {
+ if ( $ret === false && $this->wasConnectionLoss() ) {
+ # Check if any meaningful session state was lost
$recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
- # Stash the last error values before anything might clear them
- $lastError = $this->lastError();
- $lastErrno = $this->lastErrno();
- # Update state tracking to reflect transaction loss due to disconnection
- $this->handleSessionLoss();
- if ( $this->reconnect() ) {
- $msg = __METHOD__ . ': lost connection to {dbserver}; reconnected';
- $params = [ 'dbserver' => $this->getServer() ];
- $this->connLogger->warning( $msg, $params );
- $this->queryLogger->warning( $msg, $params +
- [ 'trace' => ( new RuntimeException() )->getTraceAsString() ] );
-
- if ( $recoverable ) {
- # Should be safe to silently retry the query
- $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
- } else {
- # Callers may catch the exception and continue to use the DB
- $this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
+ # Update session state tracking and try to restore the connection
+ $reconnected = $this->replaceLostConnection( __METHOD__ );
+ # Silently resend the query to the server if it is safe and possible
+ if ( $reconnected && $recoverable ) {
+ $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
+ $lastError = $this->lastError();
+ $lastErrno = $this->lastErrno();
+
+ if ( $ret === false && $this->wasConnectionLoss() ) {
+ # Query probably causes disconnects; reconnect and do not re-run it
+ $this->replaceLostConnection( __METHOD__ );
}
- } else {
- $msg = __METHOD__ . ': lost connection to {dbserver} permanently';
- $this->connLogger->error( $msg, [ 'dbserver' => $this->getServer() ] );
}
}
- if ( false === $ret ) {
+ if ( $ret === false ) {
# Deadlocks cause the entire transaction to abort, not just the statement.
# https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
# https://www.postgresql.org/docs/9.1/static/explicit-locking.html
if ( $this->explicitTrxActive() || $priorWritesPending ) {
$tempIgnore = false; // not recoverable
}
+ # Usually the transaction is rolled back to BEGIN, leaving an empty transaction.
+ # Destroy any such transaction so the rollback callbacks run in AUTO-COMMIT mode
+ # as normal. Also, if DBO_TRX is set and an explicit transaction rolled back here,
+ # further queries should be back in AUTO-COMMIT mode, not stuck in a transaction.
+ $this->doRollback( __METHOD__ );
# Update state tracking to reflect transaction loss
- $this->handleSessionLoss();
+ $this->handleTransactionLoss();
}
- $this->reportQueryError(
- $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
+ $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore );
}
- $res = $this->resultObject( $ret );
-
- return $res;
+ return $this->resultObject( $ret );
}
/**
# didn't matter anyway (aside from DBO_TRX snapshot loss).
if ( $this->namedLocksHeld ) {
return false; // possible critical section violation
+ } elseif ( $this->sessionTempTables ) {
+ return false; // tables might be queried latter
} elseif ( $sql === 'COMMIT' ) {
return !$priorWritesPending; // nothing written anyway? (T127428)
} elseif ( $sql === 'ROLLBACK' ) {
}
/**
- * Clean things up after transaction loss due to disconnection
- *
- * @return null|Exception
+ * Clean things up after session (and thus transaction) loss
*/
private function handleSessionLoss() {
+ // Clean up tracking of session-level things...
+ // https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
+ // https://www.postgresql.org/docs/9.1/static/sql-createtable.html (ignoring ON COMMIT)
+ $this->sessionTempTables = [];
+ // https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
+ // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
+ $this->namedLocksHeld = [];
+ // Session loss implies transaction loss
+ $this->handleTransactionLoss();
+ }
+
+ /**
+ * Clean things up after transaction loss
+ */
+ private function handleTransactionLoss() {
$this->trxLevel = 0;
$this->trxAtomicCounter = 0;
$this->trxIdleCallbacks = []; // T67263; transaction already lost
$this->trxPreCommitCallbacks = []; // T67263; transaction already lost
- $this->sessionTempTables = [];
- $this->namedLocksHeld = [];
-
- // Note: if callback suppression is set then some *Callbacks arrays are not cleared here
- $e = null;
try {
- // Handle callbacks in trxEndCallbacks
+ // Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
+ // If callback suppression is set then the array will remain unhandled.
$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
} catch ( Exception $ex ) {
// Already logged; move on...
- $e = $e ?: $ex;
}
try {
- // Handle callbacks in trxRecurringCallbacks
+ // Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener()
$this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
} catch ( Exception $ex ) {
// Already logged; move on...
- $e = $e ?: $ex;
}
-
- return $e;
}
/**
}
/**
- * Close existing database connection and open a new connection
+ * Close any existing (dead) database connection and open a new connection
*
+ * @param string $fname
* @return bool True if new connection is opened successfully, false if error
*/
- protected function reconnect() {
+ protected function replaceLostConnection( $fname ) {
$this->closeConnection();
$this->opened = false;
$this->conn = false;
$this->open( $this->server, $this->user, $this->password, $this->dbName );
$this->lastPing = microtime( true );
$ok = true;
+
+ $this->connLogger->warning(
+ $fname . ': lost connection to {dbserver}; reconnected',
+ [
+ 'dbserver' => $this->getServer(),
+ 'trace' => ( new RuntimeException() )->getTraceAsString()
+ ]
+ );
} catch ( DBConnectionError $e ) {
$ok = false;
+
+ $this->connLogger->error(
+ $fname . ': lost connection to {dbserver} permanently',
+ [ 'dbserver' => $this->getServer() ]
+ );
}
+ $this->handleSessionLoss();
+
return $ok;
}
/*!
- * OOjs v2.1.0 optimised for jQuery
+ * OOjs v2.2.0 optimised for jQuery
* https://www.mediawiki.org/wiki/OOjs
*
- * Copyright 2011-2017 OOjs Team and other contributors.
+ * Copyright 2011-2018 OOjs Team and other contributors.
* Released under the MIT license
* https://oojs.mit-license.org
*
- * Date: 2017-05-30T22:56:52Z
+ * Date: 2018-04-03T19:45:13Z
*/
( function ( global ) {
'use strict';
-/* exported toString */
var
/**
* Namespace for all classes, static methods and static properties.
oo = {},
// Optimisation: Local reference to Object.prototype.hasOwnProperty
hasOwn = oo.hasOwnProperty,
+ // Marking this as "exported" doesn't work when parserOptions.sourceType is module
+ // eslint-disable-next-line no-unused-vars
toString = oo.toString;
/* Class Methods */
targetConstructor = targetFn.prototype.constructor;
- // Using ['super'] instead of .super because 'super' is not supported
- // by IE 8 and below (bug 63303).
- // Provide .parent as alias for code supporting older browsers which
+ // [DEPRECATED] Provide .parent as alias for code supporting older browsers which
// allows people to comply with their style guide.
- // eslint-disable-next-line dot-notation
- targetFn[ 'super' ] = targetFn.parent = originFn;
+ targetFn.super = targetFn.parent = originFn;
targetFn.prototype = Object.create( originFn.prototype, {
// Restore constructor property of targetFn
}
delete prop[ arguments[ i ] ];
// Walk back through props removing any plain empty objects
- while ( ( prop = props.pop() ) && oo.isPlainObject( prop ) && !Object.keys( prop ).length ) {
+ while ( props.length > 1 && ( prop = props.pop() ) && oo.isPlainObject( prop ) && !Object.keys( prop ).length ) {
delete props[ props.length - 1 ][ arguments[ props.length ] ];
}
};
for ( k in a ) {
if ( !hasOwn.call( a, k ) || a[ k ] === undefined || a[ k ] === b[ k ] ) {
- // Support es3-shim: Without the hasOwn filter, comparing [] to {} will be false in ES3
- // because the shimmed "forEach" is enumerable and shows up in Array but not Object.
- // Also ignore undefined values, because there is no conceptual difference between
+ // Ignore undefined values, because there is no conceptual difference between
// a key that is absent and a key that is present but whose value is undefined.
continue;
}
/**
* @private
- * @param {OO.EventEmitter} ee
- * @param {Function|string} method Function or method name
+ * @param {OO.EventEmitter} eventEmitter Event emitter
+ * @param {string} event Event name
* @param {Object} binding
*/
- function addBinding( ee, event, binding ) {
+ function addBinding( eventEmitter, event, binding ) {
var bindings;
// Auto-initialize bindings list
- if ( hasOwn.call( ee.bindings, event ) ) {
- bindings = ee.bindings[ event ];
+ if ( hasOwn.call( eventEmitter.bindings, event ) ) {
+ bindings = eventEmitter.bindings[ event ];
} else {
- bindings = ee.bindings[ event ] = [];
+ bindings = eventEmitter.bindings[ event ] = [];
}
// Add binding
bindings.push( binding );
* @param {Function|string} method Function or method name to call when event occurs
* @param {Array} [args] Arguments to pass to listener, will be prepended to emitted arguments
* @param {Object} [context=null] Context object for function or method call
- * @throws {Error} Listener argument is not a function or a valid method name
* @chainable
+ * @throws {Error} Listener argument is not a function or a valid method name
*/
oo.EventEmitter.prototype.on = function ( event, method, args, context ) {
validateMethod( method, context );
/**
* Add items to the sorted list.
*
- * @chainable
* @param {OO.EventEmitter|OO.EventEmitter[]} items Item to add or
* an array of items to add
+ * @chainable
*/
oo.SortedEmitterList.prototype.addItems = function ( items ) {
var index, i, insertionIndex;
/* global hasOwn */
/**
+ * A map interface for associating arbitrary data with a symbolic name. Used in
+ * place of a plain object to provide additional {@link #method-register registration}
+ * or {@link #method-lookup lookup} functionality.
+ *
+ * See <https://www.mediawiki.org/wiki/OOjs/Registries_and_factories>.
+ *
* @class OO.Registry
* @mixins OO.EventEmitter
*