Aaron Schulz has uploaded a new change for review.
https://gerrit.wikimedia.org/r/304688
Change subject: Fix premature transactions commit problems
......................................................................
Fix premature transactions commit problems
* Use startAtomic/endAtomic() instead of begin()/commit()
* Now that jobs are pushed pre-commit, add a locking SELECT
to them so they block until the transaction that spawned
them commits. This has the side-effect of making these
operations more atomic since job insertion failure causes
full rollback and rollback after job insertion causes jobs
to abort.
* Avoid using rollback() and use either a dumb insert (mysql)
with a try/catch or a locking SELECT to precheck for key
conflicts.
* Renamed confusing setStatus() to updateStatus().
Bug: T142231
Change-Id: I8677fb5484869edb4bdd9aec77541669feacada9
---
M includes/GlobalRename/GlobalRenameUser.php
M includes/GlobalRename/GlobalRenameUserDatabaseUpdates.php
M includes/GlobalRename/GlobalRenameUserStatus.php
M includes/GlobalRename/GlobalUserMerge.php
M includes/GlobalRename/GlobalUserMergeDatabaseUpdates.php
M includes/LocalRenameJob/LocalRenameJob.php
6 files changed, 57 insertions(+), 24 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/CentralAuth
refs/changes/88/304688/1
diff --git a/includes/GlobalRename/GlobalRenameUser.php
b/includes/GlobalRename/GlobalRenameUser.php
index bc2cb06..7bc33f4 100644
--- a/includes/GlobalRename/GlobalRenameUser.php
+++ b/includes/GlobalRename/GlobalRenameUser.php
@@ -138,6 +138,10 @@
$this->oldCAUser->quickInvalidateCache();
$this->newCAUser->quickInvalidateCache();
+ // If job insertion fails, an exception will cause rollback of
all DBs.
+ // The job will block on reading renameuser_status until this
commits due to it using
+ // a locking read and the pending update from
setRenameStatuses() above. If we end up
+ // rolling back, then the job will abort because the status
will not be 'queued'.
$this->injectLocalRenameUserJobs( $wikisAttached, $options );
$this->logger->log(
diff --git a/includes/GlobalRename/GlobalRenameUserDatabaseUpdates.php
b/includes/GlobalRename/GlobalRenameUserDatabaseUpdates.php
index a68186a..7a21912 100644
--- a/includes/GlobalRename/GlobalRenameUserDatabaseUpdates.php
+++ b/includes/GlobalRename/GlobalRenameUserDatabaseUpdates.php
@@ -22,7 +22,7 @@
public function update( $oldname, $newname ) {
$dbw = $this->getDB();
- $dbw->begin( __METHOD__ );
+ $dbw->startAtomic( __METHOD__ );
$dbw->update(
'globaluser',
array( 'gu_name' => $newname ),
@@ -36,6 +36,6 @@
__METHOD__
);
- $dbw->commit( __METHOD__ );
+ $dbw->endAtomic( __METHOD__ );
}
}
diff --git a/includes/GlobalRename/GlobalRenameUserStatus.php
b/includes/GlobalRename/GlobalRenameUserStatus.php
index ecb6101..fc8ec42 100644
--- a/includes/GlobalRename/GlobalRenameUserStatus.php
+++ b/includes/GlobalRename/GlobalRenameUserStatus.php
@@ -139,7 +139,7 @@
* @param string $wiki
* @param string $status
*/
- public function setStatus( $wiki, $status ) {
+ public function updateStatus( $wiki, $status ) {
$dbw = $this->getDB( DB_MASTER );
$nameWhere = $this->getNameWhereClause( $dbw ); // Can be
inlined easily once we require more than 5.3
$fname = __METHOD__;
@@ -164,21 +164,40 @@
public function setStatuses( array $rows ) {
$dbw = $this->getDB( DB_MASTER );
- $dbw->begin( __METHOD__ );
- $dbw->insert(
- 'renameuser_status',
- $rows,
- __METHOD__,
- array( 'IGNORE' )
- );
- if ( $dbw->affectedRows() !== count( $rows ) ) {
- // Race condition, the rename was already started
- $dbw->rollback( __METHOD__ );
- return false;
+ $dbw->startAtomic( __METHOD__ );
+ if ( $dbw->getType() === 'mysql' ) {
+ // If there is duplicate key error, the RDBMs will
rollback the INSERT statement.
+ //
http://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
+ try {
+ $dbw->insert( 'renameuser_status', $rows,
__METHOD__ );
+ $ok = true;
+ } catch ( DBQueryError $e ) {
+ $ok = false;
+ }
+ } else {
+ // At least Postgres does not like continuing after
errors. Only options are
+ // ROLLBACK or COMMIT as is. We could use SAVEPOINT
here, but it's not worth it.
+ $keyConds = [];
+ foreach ( $rows as $row ) {
+ $key = [ 'ru_wiki' => $row->ru_wiki,
'ru_oldname' => $row->ru_oldname ];
+ $keyConds[] = $dbw->makeList( $key, LIST_AND );
+ }
+ // (a) Do a locking check for conflicting rows on the
unique key
+ $ok = !$dbw->selectField(
+ 'renameuser_status',
+ '1',
+ $dbw->makeList( $keyConds, LIST_OR ),
+ __METHOD__,
+ [ 'FOR UPDATE' ]
+ );
+ // (b) Insert the new rows if no conflicts were found
+ if ( $ok ) {
+ $dbw->insert( 'renameuser_status', $rows,
__METHOD__ );
+ }
}
- $dbw->commit( __METHOD__ );
+ $dbw->endAtomic( __METHOD__ );
- return true;
+ return $ok;
}
/**
diff --git a/includes/GlobalRename/GlobalUserMerge.php
b/includes/GlobalRename/GlobalUserMerge.php
index 60066fd..49a99c4 100644
--- a/includes/GlobalRename/GlobalUserMerge.php
+++ b/includes/GlobalRename/GlobalUserMerge.php
@@ -98,7 +98,7 @@
public function merge( $reason ) {
$wikis = array();
- foreach( $this->oldCAUsers as $oldCAUser ) {
+ foreach ( $this->oldCAUsers as $oldCAUser ) {
$oldWikis = $oldCAUser->listAttached();
foreach ( $oldWikis as $wiki ) {
$wikis[$wiki][] = $oldCAUser->getName();
@@ -136,7 +136,10 @@
}
$this->clearCaches();
-
+ // If job insertion fails, an exception will cause rollback of
all DBs.
+ // The job will block on reading renameuser_status until this
commits due to it using
+ // a locking read and the pending update from
setRenameStatuses() above. If we end up
+ // rolling back, then the job will abort because the status
will not be 'queued'.
$this->injectJobs( $wikis );
$this->addLogEntry( $reason );
diff --git a/includes/GlobalRename/GlobalUserMergeDatabaseUpdates.php
b/includes/GlobalRename/GlobalUserMergeDatabaseUpdates.php
index dc477cf..c592233 100644
--- a/includes/GlobalRename/GlobalUserMergeDatabaseUpdates.php
+++ b/includes/GlobalRename/GlobalUserMergeDatabaseUpdates.php
@@ -25,7 +25,7 @@
public function merge( $oldname, $newname ) {
$dbw = $this->getDB();
- $dbw->begin( __METHOD__ );
+ $dbw->startAtomic( __METHOD__ );
// Delete the old user's globaluser row
$dbw->delete(
'globaluser',
@@ -53,7 +53,7 @@
array( 'lu_name' => $oldname )
);
- $dbw->commit( __METHOD__ );
+ $dbw->endAtomic( __METHOD__ );
}
/**
@@ -82,6 +82,7 @@
*/
public function mergeGlobalUserGroups( $fromId, $toId ) {
$dbw = $this->getDB();
+ $dbw->startAtomic( __METHOD__ );
$dbw->update(
'global_user_groups',
array( 'gug_user' => $toId ),
@@ -95,5 +96,6 @@
array( 'gug_user' => $fromId ),
__METHOD__
);
+ $dbw->endAtomic( __METHOD__ );
}
}
diff --git a/includes/LocalRenameJob/LocalRenameJob.php
b/includes/LocalRenameJob/LocalRenameJob.php
index 0eb0c88..e5d7266 100644
--- a/includes/LocalRenameJob/LocalRenameJob.php
+++ b/includes/LocalRenameJob/LocalRenameJob.php
@@ -17,8 +17,10 @@
public function run() {
$this->setRenameUserStatus( new GlobalRenameUserStatus(
$this->params['to'] ) );
- // bail if it's already done or in progress
- $status = $this->renameuserStatus->getStatus(
GlobalRenameUserStatus::READ_LATEST );
+ // Bail if it's already done or in progress. Use a locking read
to block until the
+ // transaction adding this job is done, so we can see its
changes. This is similar to
+ // the trick the the RenameUser extension does.
+ $status = $this->renameuserStatus->getStatus(
GlobalRenameUserStatus::READ_LOCKING );
if ( $status !== 'queued' && $status !== 'failed' ) {
LoggerFactory::getInstance( 'rename' )->info( 'skipping
duplicate rename from {user}', [
'user' => $this->params['from'],
@@ -27,6 +29,10 @@
] );
return true;
}
+ // Clear any REPEATABLE-READ snapshot in case the READ_LOCKING
blocked above. We want
+ // regular non-locking SELECTs to see all the changes from that
transaction we waited on.
+ $factory =
MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $factory->commitMasterChanges( __METHOD__ );
if ( isset( $this->params['session'] ) ) {
// Don't carry over users or sessions because it's
going to be wrong
@@ -43,7 +49,6 @@
$this->addTeardownCallback( [ $this, 'scheduleNextWiki'
] );
} catch ( Exception $e ) {
// This will lock the user out of their account until a
sysadmin intervenes
- $factory =
MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$factory->rollbackMasterChanges( __METHOD__ );
$this->updateStatus( 'failed' );
$factory->commitMasterChanges( __METHOD__ );
@@ -103,7 +108,7 @@
}
protected function updateStatus( $status ) {
- $this->renameuserStatus->setStatus( wfWikiID(), $status );
+ $this->renameuserStatus->updateStatus( wfWikiID(), $status );
}
protected function scheduleNextWiki() {
--
To view, visit https://gerrit.wikimedia.org/r/304688
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I8677fb5484869edb4bdd9aec77541669feacada9
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/CentralAuth
Gerrit-Branch: master
Gerrit-Owner: Aaron Schulz <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits