ApiPageSet may contain entries where the 'from' value is percent-encoded as
the raw value cannot be represented in a valid API response. These are
indicated by a 'fromencoded' boolean alongside the existing 'from' parameter.
+* (T28680) action=paraminfo can now return info about all submodules of a
+ module without listing them all explicitly.
=== Action API internal changes in 1.28 ===
* Added a new hook, 'ApiMakeParserOptions', to allow extensions to better
$this->context->setLanguage( $this->getMain()->getLanguage() );
if ( is_array( $params['modules'] ) ) {
- $modules = $params['modules'];
+ $modules = [];
+ foreach ( $params['modules'] as $path ) {
+ if ( $path === '*' || $path === '**' ) {
+ $path = "main+$path";
+ }
+ if ( substr( $path, -2 ) === '+*' || substr( $path, -2 ) === ' *' ) {
+ $submodules = true;
+ $path = substr( $path, 0, -2 );
+ $recursive = false;
+ } elseif ( substr( $path, -3 ) === '+**' || substr( $path, -3 ) === ' **' ) {
+ $submodules = true;
+ $path = substr( $path, 0, -3 );
+ $recursive = true;
+ } else {
+ $submodules = false;
+ }
+
+ if ( $submodules ) {
+ try {
+ $module = $this->getModuleFromPath( $path );
+ } catch ( UsageException $ex ) {
+ $this->setWarning( $ex->getMessage() );
+ }
+ $submodules = $this->listAllSubmodules( $module, $recursive );
+ if ( $submodules ) {
+ $modules = array_merge( $modules, $submodules );
+ } else {
+ $this->setWarning( "Module $path has no submodules" );
+ }
+ } else {
+ $modules[] = $path;
+ }
+ }
} else {
$modules = [];
}
$formatModules = [];
}
+ $modules = array_unique( $modules );
+
$res = [];
foreach ( $modules as $m ) {
$result->addValue( null, $this->getModuleName(), $res );
}
+ /**
+ * List all submodules of a module
+ * @param ApiBase $module
+ * @param boolean $recursive
+ * @return string[]
+ */
+ private function listAllSubmodules( ApiBase $module, $recursive ) {
+ $manager = $module->getModuleManager();
+ if ( $manager ) {
+ $paths = [];
+ $names = $manager->getNames();
+ sort( $names );
+ foreach ( $names as $name ) {
+ $submodule = $manager->getModule( $name );
+ $paths[] = $submodule->getModulePath();
+ if ( $recursive && $submodule->getModuleManager() ) {
+ $paths = array_merge( $paths, $this->listAllSubmodules( $submodule, $recursive ) );
+ }
+ }
+ }
+ return $paths;
+ }
+
/**
* @param array $res Result array
* @param string $key Result key
protected function getExamplesMessages() {
return [
- 'action=paraminfo&modules=parse|phpfm|query+allpages|query+siteinfo'
+ 'action=paraminfo&modules=parse|phpfm|query%2Ballpages|query%2Bsiteinfo'
=> 'apihelp-paraminfo-example-1',
+ 'action=paraminfo&modules=query%2B*'
+ => 'apihelp-paraminfo-example-2',
];
}
"apihelp-options-example-complex": "Reset all preferences, then set <kbd>skin</kbd> and <kbd>nickname</kbd>.",
"apihelp-paraminfo-description": "Obtain information about API modules.",
- "apihelp-paraminfo-param-modules": "List of module names (values of the <var>action</var> and <var>format</var> parameters, or <kbd>main</kbd>). Can specify submodules with a <kbd>+</kbd>.",
+ "apihelp-paraminfo-param-modules": "List of module names (values of the <var>action</var> and <var>format</var> parameters, or <kbd>main</kbd>). Can specify submodules with a <kbd>+</kbd>, or all submodules with <kbd>+*</kbd>, or all submodules recursively with <kbd>+**</kbd>.",
"apihelp-paraminfo-param-helpformat": "Format of help strings.",
"apihelp-paraminfo-param-querymodules": "List of query module names (value of <var>prop</var>, <var>meta</var> or <var>list</var> parameter). Use <kbd>$1modules=query+foo</kbd> instead of <kbd>$1querymodules=foo</kbd>.",
"apihelp-paraminfo-param-mainmodule": "Get information about the main (top-level) module as well. Use <kbd>$1modules=main</kbd> instead.",
"apihelp-paraminfo-param-pagesetmodule": "Get information about the pageset module (providing titles= and friends) as well.",
"apihelp-paraminfo-param-formatmodules": "List of format module names (value of <var>format</var> parameter). Use <var>$1modules</var> instead.",
"apihelp-paraminfo-example-1": "Show info for <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>, <kbd>[[Special:ApiHelp/jsonfm|format=jsonfm]]</kbd>, <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd>, and <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd>.",
+ "apihelp-paraminfo-example-2": "Show info for all submodules of <kbd>[[Special:ApiHelp/query|action=query]]</kbd>.",
"apihelp-parse-description": "Parses content and returns parser output.\n\nSee the various prop-modules of <kbd>[[Special:ApiHelp/query|action=query]]</kbd> to get information from the current version of a page.\n\nThere are several ways to specify the text to parse:\n# Specify a page or revision, using <var>$1page</var>, <var>$1pageid</var>, or <var>$1oldid</var>.\n# Specify content explicitly, using <var>$1text</var>, <var>$1title</var>, and <var>$1contentmodel</var>.\n# Specify only a summary to parse. <var>$1prop</var> should be given an empty value.",
"apihelp-parse-param-title": "Title of page the text belongs to. If omitted, <var>$1contentmodel</var> must be specified, and [[API]] will be used as the title.",
"apihelp-paraminfo-param-pagesetmodule": "{{doc-apihelp-param|paraminfo|pagesetmodule}}",
"apihelp-paraminfo-param-formatmodules": "{{doc-apihelp-param|paraminfo|formatmodules}}",
"apihelp-paraminfo-example-1": "{{doc-apihelp-example|paraminfo}}",
+ "apihelp-paraminfo-example-2": "{{doc-apihelp-example|paraminfo}}",
"apihelp-parse-description": "{{doc-apihelp-description|parse}}",
"apihelp-parse-param-title": "{{doc-apihelp-param|parse|title}}",
"apihelp-parse-param-text": "{{doc-apihelp-param|parse|text}}",
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() );
}
protected $mTrxPreCommitCallbacks = [];
/** @var array[] List of (callable, method name) */
protected $mTrxEndCallbacks = [];
- /** @var bool Whether to suppress triggering of post-commit callbacks */
- protected $suppressPostCommitCallbacks = false;
+ /** @var array[] Map of (name => (callable, method name)) */
+ protected $mTrxRecurringCallbacks = [];
+ /** @var bool Whether to suppress triggering of transaction end callbacks */
+ protected $mTrxEndCallbacksSuppressed = false;
/** @var string */
protected $mTablePrefix;
try {
// Handle callbacks in mTrxEndCallbacks
$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
+ $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
return null;
} catch ( Exception $e ) {
// Already logged; move on...
}
}
+ final public function setTransactionListener( $name, callable $callback = null ) {
+ if ( $callback ) {
+ $this->mTrxRecurringCallbacks[$name] = [ $callback, wfGetCaller() ];
+ } else {
+ unset( $this->mTrxRecurringCallbacks[$name] );
+ }
+ }
+
/**
- * Whether to disable running of post-commit callbacks
+ * Whether to disable running of post-COMMIT/ROLLBACK callbacks
*
* This method should not be used outside of Database/LoadBalancer
*
* @param bool $suppress
* @since 1.28
*/
- final public function setPostCommitCallbackSupression( $suppress ) {
- $this->suppressPostCommitCallbacks = $suppress;
+ final public function setTrxEndCallbackSuppression( $suppress ) {
+ $this->mTrxEndCallbacksSuppressed = $suppress;
}
/**
* @throws Exception
*/
public function runOnTransactionIdleCallbacks( $trigger ) {
- if ( $this->suppressPostCommitCallbacks ) {
+ if ( $this->mTrxEndCallbacksSuppressed ) {
return;
}
}
}
+ /**
+ * Actually run any "transaction listener" callbacks.
+ *
+ * This method should not be used outside of Database/LoadBalancer
+ *
+ * @param integer $trigger IDatabase::TRIGGER_* constant
+ * @throws Exception
+ * @since 1.20
+ */
+ public function runTransactionListenerCallbacks( $trigger ) {
+ if ( $this->mTrxEndCallbacksSuppressed ) {
+ return;
+ }
+
+ /** @var Exception $e */
+ $e = null; // first exception
+
+ foreach ( $this->mTrxRecurringCallbacks as $callback ) {
+ try {
+ list( $phpCallback ) = $callback;
+ $phpCallback( $trigger, $this );
+ } catch ( Exception $ex ) {
+ MWExceptionHandler::logException( $ex );
+ $e = $e ?: $ex;
+ }
+ }
+
+ if ( $e instanceof Exception ) {
+ throw $e; // re-throw any first exception
+ }
+ }
+
final public function startAtomic( $fname = __METHOD__ ) {
if ( !$this->mTrxLevel ) {
$this->begin( $fname, self::TRANSACTION_INTERNAL );
}
$this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
+ $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
}
/**
$this->mTrxIdleCallbacks = []; // clear
$this->mTrxPreCommitCallbacks = []; // clear
$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
+ $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
}
/**
}
}
+ public function clearSnapshot( $fname = __METHOD__ ) {
+ if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
+ // This only flushes transactions to clear snapshots, not to write data
+ throw new DBUnexpectedError(
+ $this,
+ "$fname: Cannot COMMIT to clear snapshot because writes are pending."
+ );
+ }
+
+ $this->commit( $fname, self::FLUSHING_INTERNAL );
+ }
+
public function explicitTrxActive() {
return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
}
interface IDatabase {
/** @var int Callback triggered immediately due to no active transaction */
const TRIGGER_IDLE = 1;
- /** @var int Callback triggered by commit */
+ /** @var int Callback triggered by COMMIT */
const TRIGGER_COMMIT = 2;
- /** @var int Callback triggered by rollback */
+ /** @var int Callback triggered by ROLLBACK */
const TRIGGER_ROLLBACK = 3;
/** @var string Transaction is requested by regular caller outside of the DB layer */
/**
* Returns true if there is a transaction open with possible write
* queries or transaction pre-commit/idle callbacks waiting on it to finish.
+ * This does *not* count recurring callbacks, e.g. from setTransactionListener().
*
* @return bool
*/
* This is useful for combining cooperative locks and DB transactions.
*
* The callback takes one argument:
- * How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_ROLLBACK)
+ * - How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_ROLLBACK)
*
* @param callable $callback
* @return mixed
* Updates will execute in the order they were enqueued.
*
* The callback takes one argument:
- * How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_IDLE)
+ * - How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_IDLE)
*
* @param callable $callback
* @since 1.20
*/
public function onTransactionPreCommitOrIdle( callable $callback );
+ /**
+ * Run a callback each time any transaction commits or rolls back
+ *
+ * The callback takes two arguments:
+ * - IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_ROLLBACK
+ * - This IDatabase object
+ * Callbacks must commit any transactions that they begin.
+ *
+ * Registering a callback here will not affect writesOrCallbacks() pending
+ *
+ * @param string $name Callback name
+ * @param callable|null $callback Use null to unset a listener
+ * @return mixed
+ * @since 1.28
+ */
+ public function setTransactionListener( $name, callable $callback = null );
+
/**
* Begin an atomic section of statements
*
/** @var mixed */
protected $ticket;
+ /** @var string|bool String if a requested DBO_TRX transaction round is active */
+ protected $trxRoundId = false;
/** @var string|bool Reason all LBs are read-only or false if not */
protected $readOnlyReason = false;
+ /** @var callable[] */
+ protected $replicationWaitCallbacks = [];
const SHUTDOWN_NO_CHRONPROT = 1; // don't save ChronologyProtector positions (for async code)
* This allows for custom transaction rounds from any outer transaction scope.
*
* @param string $fname
+ * @throws DBTransactionError
* @since 1.28
*/
public function beginMasterChanges( $fname = __METHOD__ ) {
+ if ( $this->trxRoundId !== false ) {
+ throw new DBTransactionError(
+ null,
+ "Transaction round '{$this->trxRoundId}' already started."
+ );
+ }
+ $this->trxRoundId = $fname;
+ // Set DBO_TRX flags on all appropriate DBs
$this->forEachLBCallMethod( 'beginMasterChanges', [ $fname ] );
}
* @throws Exception
*/
public function commitMasterChanges( $fname = __METHOD__, array $options = [] ) {
- // Perform all pre-commit callbacks, aborting on failure
- $this->forEachLBCallMethod( 'runMasterPreCommitCallbacks' );
- // Perform all pre-commit checks, aborting on failure
+ // Run pre-commit callbacks and suppress post-commit callbacks, aborting on failure
+ $this->forEachLBCallMethod( 'finalizeMasterChanges' );
+ $this->trxRoundId = false;
+ // Perform pre-commit checks, aborting on failure
$this->forEachLBCallMethod( 'approveMasterChanges', [ $options ] );
// Log the DBs and methods involved in multi-DB transactions
$this->logIfMultiDbTransaction();
- // Actually perform the commit on all master DB connections
+ // Actually perform the commit on all master DB connections and revert DBO_TRX
$this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
// Run all post-commit callbacks
/** @var Exception $e */
$e = null; // first callback exception
$this->forEachLB( function ( LoadBalancer $lb ) use ( &$e ) {
- $ex = $lb->runMasterPostCommitCallbacks();
+ $ex = $lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_COMMIT );
$e = $e ?: $ex;
} );
// Commit any dangling DBO_TRX transactions from callbacks on one DB to another DB
* @since 1.23
*/
public function rollbackMasterChanges( $fname = __METHOD__ ) {
+ $this->trxRoundId = false;
+ $this->forEachLBCallMethod( 'suppressTransactionEndCallbacks' );
$this->forEachLBCallMethod( 'rollbackMasterChanges', [ $fname ] );
+ // Run all post-rollback callbacks
+ $this->forEachLB( function ( LoadBalancer $lb ) {
+ $lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_ROLLBACK );
+ } );
}
/**
'ifWritesSince' => null
];
+ foreach ( $this->replicationWaitCallbacks as $callback ) {
+ $callback();
+ }
+
// Figure out which clusters need to be checked
/** @var LoadBalancer[] $lbs */
$lbs = [];
}
}
+ /**
+ * Add a callback to be run in every call to waitForReplication() before waiting
+ *
+ * Callbacks must clear any transactions that they start
+ *
+ * @param string $name Callback name
+ * @param callable|null $callback Use null to unset a callback
+ * @since 1.28
+ */
+ public function setWaitForReplicationListener( $name, callable $callback = null ) {
+ if ( $callback ) {
+ $this->replicationWaitCallbacks[$name] = $callback;
+ } else {
+ unset( $this->replicationWaitCallbacks[$name] );
+ }
+ }
+
/**
* Get a token asserting that no transaction writes are active
*
} );
}
+ /**
+ * @param LoadBalancer $lb
+ */
+ protected function initLoadBalancer( LoadBalancer $lb ) {
+ if ( $this->trxRoundId !== false ) {
+ $lb->beginMasterChanges( $this->trxRoundId ); // set DBO_TRX
+ }
+ }
+
/**
* Close all open database connections on all open load balancers.
* @since 1.28
* @return LoadBalancer
*/
private function newLoadBalancer( $template, $loads, $groupLoads, $readOnlyReason ) {
- return new LoadBalancer( [
+ $lb = new LoadBalancer( [
'servers' => $this->makeServerArray( $template, $loads, $groupLoads ),
'loadMonitor' => $this->loadMonitorClass,
'readOnlyReason' => $readOnlyReason,
'srvCache' => $this->srvCache,
'wanCache' => $this->wanCache
] );
+
+ $this->initLoadBalancer( $lb );
+
+ return $lb;
}
/**
}
private function newLoadBalancer( array $servers ) {
- return new LoadBalancer( [
+ $lb = new LoadBalancer( [
'servers' => $servers,
'loadMonitor' => $this->loadMonitorClass,
'readOnlyReason' => $this->readOnlyReason,
'srvCache' => $this->srvCache,
'wanCache' => $this->wanCache
] );
+
+ $this->initLoadBalancer( $lb );
+
+ return $lb;
}
/**
private $srvCache;
/** @var WANObjectCache */
private $wanCache;
+ /** @var TransactionProfiler */
+ protected $trxProfiler;
/** @var bool|DatabaseBase Database connection that caused a problem */
private $mErrorConnection;
private $readOnlyReason = false;
/** @var integer Total connections opened */
private $connsOpened = 0;
-
- /** @var TransactionProfiler */
- protected $trxProfiler;
+ /** @var string|bool String if a requested DBO_TRX transaction round is active */
+ private $trxRoundId = false;
/** @var integer Warn when this many connection are held */
const CONN_HELD_WARN_THRESHOLD = 10;
$this->getLazyConnectionRef( DB_MASTER, [], $db->getWikiID() )
);
$db->setTransactionProfiler( $this->trxProfiler );
+ if ( $this->trxRoundId !== false ) {
+ $this->applyTransactionRoundFlags( $db );
+ }
return $db;
}
/**
* Commit transactions on all open connections
* @param string $fname Caller name
+ * @throws DBExpectedError
*/
public function commitAll( $fname = __METHOD__ ) {
- $this->forEachOpenConnection( function ( DatabaseBase $conn ) use ( $fname ) {
- $conn->commit( $fname, $conn::FLUSHING_ALL_PEERS );
- } );
+ $failures = [];
+
+ $restore = ( $this->trxRoundId !== false );
+ $this->trxRoundId = false;
+ $this->forEachOpenConnection(
+ function ( DatabaseBase $conn ) use ( $fname, $restore, &$failures ) {
+ try {
+ $conn->commit( $fname, $conn::FLUSHING_ALL_PEERS );
+ } catch ( DBError $e ) {
+ MWExceptionHandler::logException( $e );
+ $failures[] = "{$conn->getServer()}: {$e->getMessage()}";
+ }
+ if ( $restore && $conn->getLBInfo( 'master' ) ) {
+ $this->undoTransactionRoundFlags( $conn );
+ }
+ }
+ );
+
+ if ( $failures ) {
+ throw new DBExpectedError(
+ null,
+ "Commit failed on server(s) " . implode( "\n", array_unique( $failures ) )
+ );
+ }
}
/**
* Perform all pre-commit callbacks that remain part of the atomic transactions
- * and disable any post-commit callbacks until runMasterPostCommitCallbacks()
+ * and disable any post-commit callbacks until runMasterPostTrxCallbacks()
* @since 1.28
*/
- public function runMasterPreCommitCallbacks() {
+ public function finalizeMasterChanges() {
$this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) {
- // Any error will cause all DB transactions to be rolled back together.
+ // Any error should cause all DB transactions to be rolled back together
+ $conn->setTrxEndCallbackSuppression( false );
$conn->runOnTransactionPreCommitCallbacks();
- // Defer post-commit callbacks until COMMIT finishes for all DBs.
- $conn->setPostCommitCallbackSupression( true );
+ // Defer post-commit callbacks until COMMIT finishes for all DBs
+ $conn->setTrxEndCallbackSuppression( true );
} );
}
* This allows for custom transaction rounds from any outer transaction scope.
*
* @param string $fname
+ * @throws DBExpectedError
* @since 1.28
*/
public function beginMasterChanges( $fname = __METHOD__ ) {
- $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $fname ) {
- if ( $conn->writesOrCallbacksPending() ) {
- throw new DBTransactionError(
- $conn,
- "Transaction with pending writes still active."
- );
- } elseif ( $conn->trxLevel() ) {
- $conn->commit( $fname, $conn::FLUSHING_ALL_PEERS );
- }
- if ( $conn->getFlag( DBO_DEFAULT ) ) {
- // DBO_TRX is controlled entirely by CLI mode presence with DBO_DEFAULT.
- // Force DBO_TRX even in CLI mode since a commit round is expected soon.
- $conn->setFlag( DBO_TRX, $conn::REMEMBER_PRIOR );
- $conn->onTransactionResolution( function () use ( $conn ) {
- $conn->restoreFlags( $conn::RESTORE_PRIOR );
- } );
- } else {
- // Config has explicitly requested DBO_TRX be either on or off; respect that.
- // This is useful for things like blob stores which use auto-commit mode.
+ if ( $this->trxRoundId !== false ) {
+ throw new DBTransactionError(
+ null,
+ "$fname: Transaction round '{$this->trxRoundId}' already started."
+ );
+ }
+ $this->trxRoundId = $fname;
+
+ $failures = [];
+ $this->forEachOpenMasterConnection(
+ function ( DatabaseBase $conn ) use ( $fname, &$failures ) {
+ $conn->setTrxEndCallbackSuppression( true );
+ try {
+ $conn->clearSnapshot( $fname );
+ } catch ( DBError $e ) {
+ MWExceptionHandler::logException( $e );
+ $failures[] = "{$conn->getServer()}: {$e->getMessage()}";
+ }
+ $conn->setTrxEndCallbackSuppression( false );
+ $this->applyTransactionRoundFlags( $conn );
}
- } );
+ );
+
+ if ( $failures ) {
+ throw new DBExpectedError(
+ null,
+ "$fname: Flush failed on server(s) " . implode( "\n", array_unique( $failures ) )
+ );
+ }
}
/**
* Issue COMMIT on all master connections where writes where done
* @param string $fname Caller name
+ * @throws DBExpectedError
*/
public function commitMasterChanges( $fname = __METHOD__ ) {
- $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $fname ) {
- if ( $conn->writesOrCallbacksPending() ) {
- $conn->commit( $fname, $conn::FLUSHING_ALL_PEERS );
+ $failures = [];
+
+ $restore = ( $this->trxRoundId !== false );
+ $this->trxRoundId = false;
+ $this->forEachOpenMasterConnection(
+ function ( DatabaseBase $conn ) use ( $fname, $restore, &$failures ) {
+ try {
+ if ( $conn->writesOrCallbacksPending() ) {
+ $conn->commit( $fname, $conn::FLUSHING_ALL_PEERS );
+ } elseif ( $restore ) {
+ $conn->clearSnapshot( $fname );
+ }
+ } catch ( DBError $e ) {
+ MWExceptionHandler::logException( $e );
+ $failures[] = "{$conn->getServer()}: {$e->getMessage()}";
+ }
+ if ( $restore ) {
+ $this->undoTransactionRoundFlags( $conn );
+ }
}
- } );
+ );
+
+ if ( $failures ) {
+ throw new DBExpectedError(
+ null,
+ "$fname: Commit failed on server(s) " . implode( "\n", array_unique( $failures ) )
+ );
+ }
}
/**
- * Issue all pending post-commit callbacks
+ * Issue all pending post-COMMIT/ROLLBACK callbacks
+ * @param integer $type IDatabase::TRIGGER_* constant
* @return Exception|null The first exception or null if there were none
* @since 1.28
*/
- public function runMasterPostCommitCallbacks() {
+ public function runMasterPostTrxCallbacks( $type ) {
$e = null; // first exception
- $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( &$e ) {
- $conn->setPostCommitCallbackSupression( false );
+ $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $type, &$e ) {
+ $conn->clearSnapshot( __METHOD__ ); // clear no-op transactions
+
+ $conn->setTrxEndCallbackSuppression( false );
+ try {
+ $conn->runOnTransactionIdleCallbacks( $type );
+ } catch ( Exception $ex ) {
+ $e = $e ?: $ex;
+ }
try {
- $conn->runOnTransactionIdleCallbacks( $conn::TRIGGER_COMMIT );
+ $conn->runTransactionListenerCallbacks( $type );
} catch ( Exception $ex ) {
$e = $e ?: $ex;
}
* @since 1.23
*/
public function rollbackMasterChanges( $fname = __METHOD__ ) {
- $failedServers = [];
-
- $masterIndex = $this->getWriterIndex();
- foreach ( $this->mConns as $conns2 ) {
- if ( empty( $conns2[$masterIndex] ) ) {
- continue;
- }
- /** @var DatabaseBase $conn */
- foreach ( $conns2[$masterIndex] as $conn ) {
- if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) {
- try {
- $conn->rollback( $fname, $conn::FLUSHING_ALL_PEERS );
- } catch ( DBError $e ) {
- MWExceptionHandler::logException( $e );
- $failedServers[] = $conn->getServer();
- }
+ $restore = ( $this->trxRoundId !== false );
+ $this->trxRoundId = false;
+ $this->forEachOpenMasterConnection(
+ function ( DatabaseBase $conn ) use ( $fname, $restore ) {
+ if ( $conn->writesOrCallbacksPending() ) {
+ $conn->rollback( $fname, $conn::FLUSHING_ALL_PEERS );
+ }
+ if ( $restore ) {
+ $this->undoTransactionRoundFlags( $conn );
}
}
+ );
+ }
+
+ /**
+ * Suppress all pending post-COMMIT/ROLLBACK callbacks
+ * @return Exception|null The first exception or null if there were none
+ * @since 1.28
+ */
+ public function suppressTransactionEndCallbacks() {
+ $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) {
+ $conn->setTrxEndCallbackSuppression( true );
+ } );
+ }
+
+ /**
+ * @param DatabaseBase $conn
+ */
+ private function applyTransactionRoundFlags( DatabaseBase $conn ) {
+ if ( $conn->getFlag( DBO_DEFAULT ) ) {
+ // DBO_TRX is controlled entirely by CLI mode presence with DBO_DEFAULT.
+ // Force DBO_TRX even in CLI mode since a commit round is expected soon.
+ $conn->setFlag( DBO_TRX, $conn::REMEMBER_PRIOR );
+ // If config has explicitly requested DBO_TRX be either on or off by not
+ // setting DBO_DEFAULT, then respect that. Forcing no transactions is useful
+ // for things like blob stores (ExternalStore) which want auto-commit mode.
}
+ }
- if ( $failedServers ) {
- throw new DBExpectedError( null, "Rollback failed on server(s) " .
- implode( ', ', array_unique( $failedServers ) ) );
+ /**
+ * @param DatabaseBase $conn
+ */
+ private function undoTransactionRoundFlags( DatabaseBase $conn ) {
+ if ( $conn->getFlag( DBO_DEFAULT ) ) {
+ $conn->restoreFlags( $conn::RESTORE_PRIOR );
}
}
*
* @file
*/
+use MediaWiki\MediaWikiServices;
+
/**
* Update object handling the cleanup of links tables after a page was deleted.
**/
-class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
+class LinksDeletionUpdate extends DataUpdate implements EnqueueableDataUpdate {
/** @var WikiPage */
protected $page;
/** @var integer */
/** @var string */
protected $timestamp;
+ /** @var IDatabase */
+ private $db;
+
/**
* @param WikiPage $page Page we are updating
* @param integer|null $pageId ID of the page we are updating [optional]
* @throws MWException
*/
function __construct( WikiPage $page, $pageId = null, $timestamp = null ) {
- parent::__construct( false ); // no implicit transaction
-
$this->page = $page;
if ( $pageId ) {
$this->pageId = $pageId; // page ID at time of deletion
}
public function doUpdate() {
- $config = RequestContext::getMain()->getConfig();
+ $services = MediaWikiServices::getInstance();
+ $config = $services->getMainConfig();
+ $lbFactory = $services->getDBLoadBalancerFactory();
$batchSize = $config->get( 'UpdateRowsPerQuery' );
- $factory = wfGetLBFactory();
// Page may already be deleted, so don't just getId()
$id = $this->pageId;
// Make sure all links update threads see the changes of each other.
// This handles the case when updates have to batched into several COMMITs.
- $scopedLock = LinksUpdate::acquirePageLock( $this->mDb, $id );
+ $scopedLock = LinksUpdate::acquirePageLock( $this->getDB(), $id );
$title = $this->page->getTitle();
+ $dbw = $this->getDB(); // convenience
// Delete restrictions for it
- $this->mDb->delete( 'page_restrictions', [ 'pr_page' => $id ], __METHOD__ );
+ $dbw->delete( 'page_restrictions', [ 'pr_page' => $id ], __METHOD__ );
// Fix category table counts
- $cats = $this->mDb->selectFieldValues(
+ $cats = $dbw->selectFieldValues(
'categorylinks',
'cl_to',
[ 'cl_from' => $id ],
foreach ( $catBatches as $catBatch ) {
$this->page->updateCategoryCounts( [], $catBatch, $id );
if ( count( $catBatches ) > 1 ) {
- $factory->commitAndWaitForReplication(
- __METHOD__, $this->ticket, [ 'wiki' => $this->mDb->getWikiID() ]
+ $lbFactory->commitAndWaitForReplication(
+ __METHOD__, $this->ticket, [ 'wiki' => $dbw->getWikiID() ]
);
}
}
// Refresh the category table entry if it seems to have no pages. Check
// master for the most up-to-date cat_pages count.
if ( $title->getNamespace() === NS_CATEGORY ) {
- $row = $this->mDb->selectRow(
+ $row = $dbw->selectRow(
'category',
[ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
[ 'cat_title' => $title->getDBkey(), 'cat_pages <= 0' ],
__METHOD__
);
if ( $row ) {
- $cat = Category::newFromRow( $row, $title )->refreshCounts();
+ Category::newFromRow( $row, $title )->refreshCounts();
}
}
// If using cascading deletes, we can skip some explicit deletes
- if ( !$this->mDb->cascadingDeletes() ) {
+ if ( !$dbw->cascadingDeletes() ) {
// Delete outgoing links
$this->batchDeleteByPK(
'pagelinks',
$batchSize
);
// Delete any redirect entry or page props entries
- $this->mDb->delete( 'redirect', [ 'rd_from' => $id ], __METHOD__ );
- $this->mDb->delete( 'page_props', [ 'pp_page' => $id ], __METHOD__ );
+ $dbw->delete( 'redirect', [ 'rd_from' => $id ], __METHOD__ );
+ $dbw->delete( 'page_props', [ 'pp_page' => $id ], __METHOD__ );
}
// If using cleanup triggers, we can skip some manual deletes
- if ( !$this->mDb->cleanupTriggers() ) {
+ if ( !$dbw->cleanupTriggers() ) {
// Find recentchanges entries to clean up...
- $rcIdsForTitle = $this->mDb->selectFieldValues(
+ $rcIdsForTitle = $dbw->selectFieldValues(
'recentchanges',
'rc_id',
[
'rc_namespace' => $title->getNamespace(),
'rc_title' => $title->getDBkey(),
'rc_timestamp < ' .
- $this->mDb->addQuotes( $this->mDb->timestamp( $this->timestamp ) )
+ $dbw->addQuotes( $dbw->timestamp( $this->timestamp ) )
],
__METHOD__
);
- $rcIdsForPage = $this->mDb->selectFieldValues(
+ $rcIdsForPage = $dbw->selectFieldValues(
'recentchanges',
'rc_id',
[ 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ],
// T98706: delete by PK to avoid lock contention with RC delete log insertions
$rcIdBatches = array_chunk( array_merge( $rcIdsForTitle, $rcIdsForPage ), $batchSize );
foreach ( $rcIdBatches as $rcIdBatch ) {
- $this->mDb->delete( 'recentchanges', [ 'rc_id' => $rcIdBatch ], __METHOD__ );
+ $dbw->delete( 'recentchanges', [ 'rc_id' => $rcIdBatch ], __METHOD__ );
if ( count( $rcIdBatches ) > 1 ) {
- $factory->commitAndWaitForReplication(
- __METHOD__, $this->ticket, [ 'wiki' => $this->mDb->getWikiID() ]
+ $lbFactory->commitAndWaitForReplication(
+ __METHOD__, $this->ticket, [ 'wiki' => $dbw->getWikiID() ]
);
}
}
}
private function batchDeleteByPK( $table, array $conds, array $pk, $bSize ) {
- $dbw = $this->mDb; // convenience
- $factory = wfGetLBFactory();
+ $services = MediaWikiServices::getInstance();
+ $lbFactory = $services->getDBLoadBalancerFactory();
+ $dbw = $this->getDB(); // convenience
+
$res = $dbw->select( $table, $pk, $conds, __METHOD__ );
$pkDeleteConds = [];
foreach ( $res as $row ) {
- $pkDeleteConds[] = $this->mDb->makeList( (array)$row, LIST_AND );
+ $pkDeleteConds[] = $dbw->makeList( (array)$row, LIST_AND );
if ( count( $pkDeleteConds ) >= $bSize ) {
$dbw->delete( $table, $dbw->makeList( $pkDeleteConds, LIST_OR ), __METHOD__ );
- $factory->commitAndWaitForReplication(
- __METHOD__, $this->ticket, [ 'wiki' => $this->mDb->getWikiID() ]
+ $lbFactory->commitAndWaitForReplication(
+ __METHOD__, $this->ticket, [ 'wiki' => $dbw->getWikiID() ]
);
$pkDeleteConds = [];
}
}
}
+ protected function getDB() {
+ if ( !$this->db ) {
+ $this->db = wfGetDB( DB_MASTER );
+ }
+
+ return $this->db;
+ }
+
public function getAsJobSpecification() {
return [
- 'wiki' => $this->mDb->getWikiID(),
+ 'wiki' => $this->getDB()->getWikiID(),
'job' => new JobSpecification(
'deleteLinks',
[ 'pageId' => $this->pageId, 'timestamp' => $this->timestamp ],
* @file
*/
+use MediaWiki\MediaWikiServices;
+
/**
* Class the manages updates of *_link tables as well as similar extension-managed tables
*
*
* See docs/deferred.txt
*/
-class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate {
+class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
// @todo make members protected, but make sure extensions don't break
/** @var int Page ID of the article linked from */
*/
private $user;
+ /** @var IDatabase */
+ private $db;
+
/**
* Constructor
*
* @throws MWException
*/
function __construct( Title $title, ParserOutput $parserOutput, $recursive = true ) {
- // Implicit transactions are disabled as they interfere with batching
- parent::__construct( false );
-
$this->mTitle = $title;
$this->mId = $title->getArticleID( Title::GAID_FOR_UPDATE );
public function doUpdate() {
// Make sure all links update threads see the changes of each other.
// This handles the case when updates have to batched into several COMMITs.
- $scopedLock = self::acquirePageLock( $this->mDb, $this->mId );
+ $scopedLock = self::acquirePageLock( $this->getDB(), $this->mId );
Hooks::run( 'LinksUpdate', [ &$this ] );
$this->doIncrementalUpdate();
// Commit and release the lock
ScopedCallback::consume( $scopedLock );
// Run post-commit hooks without DBO_TRX
- $this->mDb->onTransactionIdle( function() {
+ $this->getDB()->onTransactionIdle( function() {
Hooks::run( 'LinksUpdateComplete', [ &$this ] );
} );
}
* @param array $cats
*/
function invalidateCategories( $cats ) {
- PurgeJobUtils::invalidatePages( $this->mDb, NS_CATEGORY, array_keys( $cats ) );
+ PurgeJobUtils::invalidatePages( $this->getDB(), NS_CATEGORY, array_keys( $cats ) );
}
/**
* @param array $images
*/
function invalidateImageDescriptions( $images ) {
- PurgeJobUtils::invalidatePages( $this->mDb, NS_FILE, array_keys( $images ) );
+ PurgeJobUtils::invalidatePages( $this->getDB(), NS_FILE, array_keys( $images ) );
}
/**
* @param array $insertions Rows to insert
*/
private function incrTableUpdate( $table, $prefix, $deletions, $insertions ) {
- $bSize = RequestContext::getMain()->getConfig()->get( 'UpdateRowsPerQuery' );
- $factory = wfGetLBFactory();
+ $services = MediaWikiServices::getInstance();
+ $bSize = $services->getMainConfig()->get( 'UpdateRowsPerQuery' );
+ $factory = $services->getDBLoadBalancerFactory();
if ( $table === 'page_props' ) {
$fromField = 'pp_page';
foreach ( $deletionBatches as $deletionBatch ) {
$deleteWheres[] = [
$fromField => $this->mId,
- $this->mDb->makeWhereFrom2d( $deletionBatch, $baseKey, "{$prefix}_title" )
+ $this->getDB()->makeWhereFrom2d( $deletionBatch, $baseKey, "{$prefix}_title" )
];
}
} else {
}
foreach ( $deleteWheres as $deleteWhere ) {
- $this->mDb->delete( $table, $deleteWhere, __METHOD__ );
+ $this->getDB()->delete( $table, $deleteWhere, __METHOD__ );
$factory->commitAndWaitForReplication(
- __METHOD__, $this->ticket, [ 'wiki' => $this->mDb->getWikiID() ]
+ __METHOD__, $this->ticket, [ 'wiki' => $this->getDB()->getWikiID() ]
);
}
$insertBatches = array_chunk( $insertions, $bSize );
foreach ( $insertBatches as $insertBatch ) {
- $this->mDb->insert( $table, $insertBatch, __METHOD__, 'IGNORE' );
+ $this->getDB()->insert( $table, $insertBatch, __METHOD__, 'IGNORE' );
$factory->commitAndWaitForReplication(
- __METHOD__, $this->ticket, [ 'wiki' => $this->mDb->getWikiID() ]
+ __METHOD__, $this->ticket, [ 'wiki' => $this->getDB()->getWikiID() ]
);
}
foreach ( $diffs as $url => $dummy ) {
foreach ( wfMakeUrlIndexes( $url ) as $index ) {
$arr[] = [
- 'el_id' => $this->mDb->nextSequenceValue( 'externallinks_el_id_seq' ),
+ 'el_id' => $this->getDB()->nextSequenceValue( 'externallinks_el_id_seq' ),
'el_from' => $this->mId,
'el_to' => $url,
'el_index' => $index,
'cl_from' => $this->mId,
'cl_to' => $name,
'cl_sortkey' => $sortkey,
- 'cl_timestamp' => $this->mDb->timestamp(),
+ 'cl_timestamp' => $this->getDB()->timestamp(),
'cl_sortkey_prefix' => $prefix,
'cl_collation' => $wgCategoryCollation,
'cl_type' => $type,
* @return array
*/
private function getExistingLinks() {
- $res = $this->mDb->select( 'pagelinks', [ 'pl_namespace', 'pl_title' ],
- [ 'pl_from' => $this->mId ], __METHOD__, $this->mOptions );
+ $res = $this->getDB()->select( 'pagelinks', [ 'pl_namespace', 'pl_title' ],
+ [ 'pl_from' => $this->mId ], __METHOD__ );
$arr = [];
foreach ( $res as $row ) {
if ( !isset( $arr[$row->pl_namespace] ) ) {
* @return array
*/
private function getExistingTemplates() {
- $res = $this->mDb->select( 'templatelinks', [ 'tl_namespace', 'tl_title' ],
- [ 'tl_from' => $this->mId ], __METHOD__, $this->mOptions );
+ $res = $this->getDB()->select( 'templatelinks', [ 'tl_namespace', 'tl_title' ],
+ [ 'tl_from' => $this->mId ], __METHOD__ );
$arr = [];
foreach ( $res as $row ) {
if ( !isset( $arr[$row->tl_namespace] ) ) {
* @return array
*/
private function getExistingImages() {
- $res = $this->mDb->select( 'imagelinks', [ 'il_to' ],
- [ 'il_from' => $this->mId ], __METHOD__, $this->mOptions );
+ $res = $this->getDB()->select( 'imagelinks', [ 'il_to' ],
+ [ 'il_from' => $this->mId ], __METHOD__ );
$arr = [];
foreach ( $res as $row ) {
$arr[$row->il_to] = 1;
* @return array
*/
private function getExistingExternals() {
- $res = $this->mDb->select( 'externallinks', [ 'el_to' ],
- [ 'el_from' => $this->mId ], __METHOD__, $this->mOptions );
+ $res = $this->getDB()->select( 'externallinks', [ 'el_to' ],
+ [ 'el_from' => $this->mId ], __METHOD__ );
$arr = [];
foreach ( $res as $row ) {
$arr[$row->el_to] = 1;
* @return array
*/
private function getExistingCategories() {
- $res = $this->mDb->select( 'categorylinks', [ 'cl_to', 'cl_sortkey_prefix' ],
- [ 'cl_from' => $this->mId ], __METHOD__, $this->mOptions );
+ $res = $this->getDB()->select( 'categorylinks', [ 'cl_to', 'cl_sortkey_prefix' ],
+ [ 'cl_from' => $this->mId ], __METHOD__ );
$arr = [];
foreach ( $res as $row ) {
$arr[$row->cl_to] = $row->cl_sortkey_prefix;
* @return array
*/
private function getExistingInterlangs() {
- $res = $this->mDb->select( 'langlinks', [ 'll_lang', 'll_title' ],
- [ 'll_from' => $this->mId ], __METHOD__, $this->mOptions );
+ $res = $this->getDB()->select( 'langlinks', [ 'll_lang', 'll_title' ],
+ [ 'll_from' => $this->mId ], __METHOD__ );
$arr = [];
foreach ( $res as $row ) {
$arr[$row->ll_lang] = $row->ll_title;
* Get an array of existing inline interwiki links, as a 2-D array
* @return array (prefix => array(dbkey => 1))
*/
- protected function getExistingInterwikis() {
- $res = $this->mDb->select( 'iwlinks', [ 'iwl_prefix', 'iwl_title' ],
- [ 'iwl_from' => $this->mId ], __METHOD__, $this->mOptions );
+ private function getExistingInterwikis() {
+ $res = $this->getDB()->select( 'iwlinks', [ 'iwl_prefix', 'iwl_title' ],
+ [ 'iwl_from' => $this->mId ], __METHOD__ );
$arr = [];
foreach ( $res as $row ) {
if ( !isset( $arr[$row->iwl_prefix] ) ) {
* @return array Array of property names and values
*/
private function getExistingProperties() {
- $res = $this->mDb->select( 'page_props', [ 'pp_propname', 'pp_value' ],
- [ 'pp_page' => $this->mId ], __METHOD__, $this->mOptions );
+ $res = $this->getDB()->select( 'page_props', [ 'pp_propname', 'pp_value' ],
+ [ 'pp_page' => $this->mId ], __METHOD__ );
$arr = [];
foreach ( $res as $row ) {
$arr[$row->pp_propname] = $row->pp_value;
/**
* Update links table freshness
*/
- protected function updateLinksTimestamp() {
+ private function updateLinksTimestamp() {
if ( $this->mId ) {
// The link updates made here only reflect the freshness of the parser output
$timestamp = $this->mParserOutput->getCacheTime();
- $this->mDb->update( 'page',
- [ 'page_links_updated' => $this->mDb->timestamp( $timestamp ) ],
+ $this->getDB()->update( 'page',
+ [ 'page_links_updated' => $this->getDB()->timestamp( $timestamp ) ],
[ 'page_id' => $this->mId ],
__METHOD__
);
}
}
+ /**
+ * @return IDatabase
+ */
+ private function getDB() {
+ if ( !$this->db ) {
+ $this->db = wfGetDB( DB_MASTER );
+ }
+
+ return $this->db;
+ }
+
public function getAsJobSpecification() {
if ( $this->user ) {
$userInfo = [
}
return [
- 'wiki' => $this->mDb->getWikiID(),
+ 'wiki' => $this->getDB()->getWikiID(),
'job' => new JobSpecification(
'refreshLinksPrioritized',
[
* a transaction will automatically be wrapped around the update. Starting another
* one would break the outer transaction bracket. If need be, subclasses can override
* the beginTransaction() and commitTransaction() methods.
+ *
+ * @deprecated Since 1.28 Use DataUpdate directly, injecting the database
*/
abstract class SqlDataUpdate extends DataUpdate {
/** @var IDatabase Database connection reference */
$this->assertTrue( $called, 'Callback reached' );
}
+ /**
+ * @covers DatabaseBase::setTransactionListener()
+ */
+ public function testTransactionListener() {
+ $db = $this->db;
+
+ $db->setTransactionListener( 'ping', function() use ( $db, &$called ) {
+ $called = true;
+ } );
+
+ $called = false;
+ $db->begin( __METHOD__ );
+ $db->commit( __METHOD__ );
+ $this->assertTrue( $called, 'Callback reached' );
+
+ $called = false;
+ $db->begin( __METHOD__ );
+ $db->commit( __METHOD__ );
+ $this->assertTrue( $called, 'Callback still reached' );
+
+ $called = false;
+ $db->begin( __METHOD__ );
+ $db->rollback( __METHOD__ );
+ $this->assertTrue( $called, 'Callback reached' );
+
+ $db->setTransactionListener( 'ping', null );
+ $called = false;
+ $db->begin( __METHOD__ );
+ $db->commit( __METHOD__ );
+ $this->assertFalse( $called, 'Callback not reached' );
+ }
+
+ /**
+ * @covers DatabaseBase::clearSnapshot()
+ */
+ public function testClearSnapshot() {
+ $db = $this->db;
+
+ $db->clearSnapshot( __METHOD__ ); // ok
+ $db->clearSnapshot( __METHOD__ ); // ok
+
+ $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
+ $db->query( 'SELECT 1', __METHOD__ );
+ $this->assertTrue( (bool)$db->trxLevel(), "Transaction started." );
+ $db->clearSnapshot( __METHOD__ ); // ok
+ $db->restoreFlags( $db::RESTORE_PRIOR );
+
+ $this->assertFalse( (bool)$db->trxLevel(), "Transaction cleared." );
+ }
+
public function testGetScopedLock() {
$db = $this->db;