/** @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)
* @deprecated since 1.27, use LBFactory::destroy()
*/
public static function destroyInstance() {
- self::singleton()->destroy();
+ MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->destroy();
}
/**
* @param bool|string $wiki Wiki ID, or false for the current wiki
* @return LoadBalancer
*/
- abstract public function &getExternalLB( $cluster, $wiki = false );
+ abstract public function getExternalLB( $cluster, $wiki = false );
/**
* Execute a function for each tracked load balancer
}
/**
- * Flush any master transaction snapshots and set DBO_TRX (if DBO_DEFAULT is set)
- *
- * The DBO_TRX setting will be reverted to the default in each of these methods:
- * - commitMasterChanges()
- * - rollbackMasterChanges()
- * - commitAll()
- * This allows for custom transaction rounds from any outer transaction scope.
+ * Commit all replica DB transactions so as to flush any REPEATABLE-READ or SSI snapshot
*
- * @param string $fname
+ * @param string $fname Caller name
* @since 1.28
*/
- public function beginMasterChanges( $fname = __METHOD__ ) {
- $this->forEachLBCallMethod( 'beginMasterChanges', [ $fname ] );
+ public function flushReplicaSnapshots( $fname = __METHOD__ ) {
+ $this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname ] );
}
/**
* Commit on all connections. Done for two reasons:
* 1. To commit changes to the masters.
- * 2. To release the snapshot on all connections, master and slave.
+ * 2. To release the snapshot on all connections, master and replica DB.
* @param string $fname Caller name
* @param array $options Options map:
* - maxWriteDuration: abort if more than this much time was spent in write queries
$this->forEachLBCallMethod( 'commitAll', [ $fname ] );
}
+ /**
+ * Flush any master transaction snapshots and set DBO_TRX (if DBO_DEFAULT is set)
+ *
+ * The DBO_TRX setting will be reverted to the default in each of these methods:
+ * - commitMasterChanges()
+ * - rollbackMasterChanges()
+ * - commitAll()
+ *
+ * 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,
+ "$fname: transaction round '{$this->trxRoundId}' already started."
+ );
+ }
+ $this->trxRoundId = $fname;
+ // Set DBO_TRX flags on all appropriate DBs
+ $this->forEachLBCallMethod( 'beginMasterChanges', [ $fname ] );
+ }
+
/**
* Commit changes on all master connections
* @param string $fname Caller name
* @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
+ if ( $this->trxRoundId !== false && $this->trxRoundId !== $fname ) {
+ throw new DBTransactionError(
+ null,
+ "$fname: transaction round '{$this->trxRoundId}' still running."
+ );
+ }
+ // 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 );
+ } );
}
/**
}
/**
- * Detemine if any lagged slave connection was used
- * @since 1.27
+ * Detemine if any lagged replica DB connection was used
* @return bool
+ * @since 1.28
*/
- public function laggedSlaveUsed() {
+ public function laggedReplicaUsed() {
$ret = false;
$this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
- $ret = $ret || $lb->laggedSlaveUsed();
+ $ret = $ret || $lb->laggedReplicaUsed();
} );
return $ret;
}
+ /**
+ * @return bool
+ * @since 1.27
+ * @deprecated Since 1.28; use laggedReplicaUsed()
+ */
+ public function laggedSlaveUsed() {
+ return $this->laggedReplicaUsed();
+ }
+
/**
* Determine if any master connection has pending/written changes from this request
* @return bool
}
/**
- * Waits for the slave DBs to catch up to the current master position
+ * Waits for the replica DBs to catch up to the current master position
*
* Use this when updating very large numbers of rows, as in maintenance scripts,
- * to avoid causing too much lag. Of course, this is a no-op if there are no slaves.
+ * to avoid causing too much lag. Of course, this is a no-op if there are no replica DBs.
*
* By default this waits on all DB clusters actually used in this request.
* This makes sense when lag being waiting on is caused by the code that does this check.
'ifWritesSince' => null
];
+ foreach ( $this->replicationWaitCallbacks as $callback ) {
+ $callback();
+ }
+
// Figure out which clusters need to be checked
/** @var LoadBalancer[] $lbs */
$lbs = [];
$masterPositions = array_fill( 0, count( $lbs ), false );
foreach ( $lbs as $i => $lb ) {
if ( $lb->getServerCount() <= 1 ) {
- // Bug 27975 - Don't try to wait for slaves if there are none
+ // Bug 27975 - Don't try to wait for replica DBs if there are none
// Prevents permission error when getting master position
continue;
} elseif ( $opts['ifWritesSince']
if ( $failed ) {
throw new DBReplicationWaitError(
- "Could not wait for slaves to catch up to " .
+ "Could not wait for replica DBs to catch up to " .
implode( ', ', $failed )
);
}
}
+ /**
+ * 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
*
* This will commit and wait unless $ticket indicates it is unsafe to do so
*
* @param string $fname Caller name (e.g. __METHOD__)
- * @param mixed $ticket Result of getOuterTransactionScopeTicket()
+ * @param mixed $ticket Result of getEmptyTransactionTicket()
* @param array $opts Options to waitForReplication()
* @throws DBReplicationWaitError
* @since 1.28
return;
}
- $this->commitMasterChanges( $fname );
+ // The transaction owner and any caller with the empty transaction ticket can commit
+ // so that getEmptyTransactionTicket() callers don't risk seeing DBTransactionError.
+ if ( $this->trxRoundId !== false && $fname !== $this->trxRoundId ) {
+ $this->trxLogger->info( "$fname: committing on behalf of {$this->trxRoundId}." );
+ $fnameEffective = $this->trxRoundId;
+ } else {
+ $fnameEffective = $fname;
+ }
+
+ $this->commitMasterChanges( $fnameEffective );
$this->waitForReplication( $opts );
+ // If a nested caller committed on behalf of $fname, start another empty $fname
+ // transaction, leaving the caller with the same empty transaction state as before.
+ if ( $fnameEffective !== $fname ) {
+ $this->beginMasterChanges( $fnameEffective );
+ }
+ }
+
+ /**
+ * @param string $dbName DB master name (e.g. "db1052")
+ * @return float|bool UNIX timestamp when client last touched the DB or false if not recent
+ * @since 1.28
+ */
+ public function getChronologyProtectorTouched( $dbName ) {
+ return $this->chronProt->getTouched( $dbName );
}
/**
// Write them to the stash
$unsavedPositions = $cp->shutdown();
// If the positions failed to write to the stash, at least wait on local datacenter
- // slaves to catch up before responding. Even if there are several DCs, this increases
+ // replica DBs to catch up before responding. Even if there are several DCs, this increases
// the chance that the user will see their own changes immediately afterwards. As long
// as the sticky DC cookie applies (same domain), this is not even an issue.
$this->forEachLB( function ( LoadBalancer $lb ) use ( $unsavedPositions ) {
} );
}
+ /**
+ * Base parameters to LoadBalancer::__construct()
+ */
+ final protected function baseLoadBalancerParams() {
+ return [
+ 'readOnlyReason' => $this->readOnlyReason,
+ 'trxProfiler' => $this->trxProfiler,
+ 'srvCache' => $this->srvCache,
+ 'wanCache' => $this->wanCache,
+ 'localDomain' => wfWikiID(),
+ 'errorLogger' => [ MWExceptionHandler::class, 'logException' ]
+ ];
+ }
+
+ /**
+ * @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