User: Bypass repeatable-read when creating an actor_id
authorBrad Jorsch <bjorsch@wikimedia.org>
Thu, 29 Nov 2018 14:03:20 +0000 (09:03 -0500)
committerPaladox <thomasmulhall410@yahoo.com>
Thu, 27 Dec 2018 21:05:38 +0000 (21:05 +0000)
When MySQL is using repeatable-read transaction isolation (which is the
default), the following sequence of events can occur:

1. Request A: Begin a transaction.
2. Request A: Try to select the actor ID for a user. Find no rows.
3. Request B: Insert an actor ID for that user.
4. Request A: Try to insert an actor ID for the user. Fails because one
   exists.
5. Request A: Try to select the actor ID that must exist. Fail because of
   the snapshot created at step 2.

In MySQL we can avoid this issue at step #5 by using a locking select
(FOR UPDATE or LOCK IN SHARE MODE), so let's do that.

Bug: T210621
Change-Id: I6c1d255fdd14c6f49d2ea9790e7bd7d101e98ee4
(cherry picked from commit 37f48fdb25a78ba7623c57b50cdfd842292d3ccb)

RELEASE-NOTES-1.31
includes/user/User.php

index c5a2a97..5569367 100644 (file)
@@ -48,6 +48,7 @@ THIS IS NOT A RELEASE YET
   if --lang is used with the command-line installer (install.php).
 * Fix addition of ug_expiry column to user_groups table on MSSQL.
 * (T204767) Add join conditions to ActiveUsersPager
+* (T210621) User: Bypass repeatable-read when creating an actor_id.
 
 == MediaWiki 1.31.1 ==
 
index aa21184..76691ea 100644 (file)
@@ -2465,8 +2465,14 @@ class User implements IDBAccessObject, UserIdentity {
                                        $this->mActorId = (int)$dbw->insertId();
                                } else {
                                        // Outdated cache?
-                                       list( , $options ) = DBAccessObjectUtils::getDBOptions( $this->queryFlagsUsed );
-                                       $this->mActorId = (int)$dbw->selectField( 'actor', 'actor_id', $q, __METHOD__, $options );
+                                       // Use LOCK IN SHARE MODE to bypass any MySQL REPEATABLE-READ snapshot.
+                                       $this->mActorId = (int)$dbw->selectField(
+                                               'actor',
+                                               'actor_id',
+                                               $q,
+                                               __METHOD__,
+                                               [ 'LOCK IN SHARE MODE' ]
+                                       );
                                        if ( !$this->mActorId ) {
                                                throw new CannotCreateActorException(
                                                        "Cannot create actor ID for user_id={$this->getId()} user_name={$this->getName()}"