public function addAutopromoteOnceGroups( $event ) {
global $wgAutopromoteOnceLogInRC, $wgAuth;
- $toPromote = array();
- if ( !wfReadOnly() && $this->getId() ) {
- $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
- if ( count( $toPromote ) ) {
- $oldGroups = $this->getGroups(); // previous groups
-
- foreach ( $toPromote as $group ) {
- $this->addGroup( $group );
- }
- // update groups in external authentication database
- $wgAuth->updateExternalDBGroups( $this, $toPromote );
+ if ( wfReadOnly() || !$this->getId() ) {
+ return array();
+ }
+
+ $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
+ if ( !count( $toPromote ) ) {
+ return array();
+ }
- $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
+ if ( !$this->checkAndSetTouched() ) {
+ return array(); // raced out (bug T48834)
+ }
- $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
- $logEntry->setPerformer( $this );
- $logEntry->setTarget( $this->getUserPage() );
- $logEntry->setParameters( array(
- '4::oldgroups' => $oldGroups,
- '5::newgroups' => $newGroups,
- ) );
- $logid = $logEntry->insert();
- if ( $wgAutopromoteOnceLogInRC ) {
- $logEntry->publish( $logid );
- }
- }
+ $oldGroups = $this->getGroups(); // previous groups
+ foreach ( $toPromote as $group ) {
+ $this->addGroup( $group );
+ }
+
+ // update groups in external authentication database
+ $wgAuth->updateExternalDBGroups( $this, $toPromote );
+
+ $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
+
+ $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
+ $logEntry->setPerformer( $this );
+ $logEntry->setTarget( $this->getUserPage() );
+ $logEntry->setParameters( array(
+ '4::oldgroups' => $oldGroups,
+ '5::newgroups' => $newGroups,
+ ) );
+ $logid = $logEntry->insert();
+ if ( $wgAutopromoteOnceLogInRC ) {
+ $logEntry->publish( $logid );
}
return $toPromote;
}
+ /**
+ * Bump user_touched if it didn't change since this object was loaded
+ *
+ * On success, the mTouched field is updated.
+ * The user serialization cache is always cleared.
+ *
+ * @return bool Whether user_touched was actually updated
+ * @since 1.26
+ */
+ protected function checkAndSetTouched() {
+ $this->load();
+
+ if ( !$this->mId ) {
+ return false; // anon
+ }
+
+ // Get a new user_touched that is higher than the old one
+ $oldTouched = $this->mTouched;
+ $newTouched = $this->newTouchedTimestamp();
+
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->update( 'user',
+ array( 'user_touched' => $dbw->timestamp( $newTouched ) ),
+ array(
+ 'user_id' => $this->mId,
+ 'user_touched' => $dbw->timestamp( $oldTouched ) // CAS check
+ ),
+ __METHOD__
+ );
+ $success = ( $dbw->affectedRows() > 0 );
+
+ if ( $success ) {
+ $this->mTouched = $newTouched;
+ }
+
+ // Clears on failure too since that is desired if the cache is stale
+ $this->clearSharedCache();
+
+ return $success;
+ }
+
/**
* Clear various cached data stored in this object. The cache of the user table
* data (i.e. self::$mCacheVars) is not cleared unless $reloadFrom is given.
* page. Ignored if null or !$val.
*/
public function setNewtalk( $val, $curRev = null ) {
- global $wgMemc;
-
if ( wfReadOnly() ) {
return;
}
$changed = $this->deleteNewtalk( $field, $id );
}
- if ( $this->isAnon() ) {
- // Anons have a separate memcached space, since
- // user records aren't kept for them.
- $key = wfMemcKey( 'newtalk', 'ip', $id );
- $wgMemc->set( $key, $val ? 1 : 0, 1800 );
- }
if ( $changed ) {
$this->invalidateCache();
}
* @since 1.25
*/
public function touch() {
- global $wgMemc;
-
$this->load();
if ( $this->mId ) {
+ $this->mQuickTouched = $this->newTouchedTimestamp();
+
+ $cache = ObjectCache::getMainWANInstance();
$key = wfMemcKey( 'user-quicktouched', 'id', $this->mId );
- $timestamp = $this->newTouchedTimestamp();
- $wgMemc->set( $key, $timestamp );
- $this->mQuickTouched = $timestamp;
+ $cache->touchCheckKey( $key );
}
}
* @return string TS_MW Timestamp
*/
public function getTouched() {
- global $wgMemc;
-
$this->load();
if ( $this->mId ) {
if ( $this->mQuickTouched === null ) {
+ $cache = ObjectCache::getMainWANInstance();
$key = wfMemcKey( 'user-quicktouched', 'id', $this->mId );
- $timestamp = $wgMemc->get( $key );
+
+ $timestamp = $cache->getCheckKeyTime( $key );
if ( $timestamp ) {
- $this->mQuickTouched = $timestamp;
+ $this->mQuickTouched = wfTimestamp( TS_MW, $timestamp );
} else {
# Set the timestamp to get HTTP 304 cache hits
$this->touch();
return $this->mTouched;
}
+ /**
+ * Get the user_touched timestamp field (time of last DB updates)
+ * @return string TS_MW Timestamp
+ * @since 1.26
+ */
+ protected function getDBTouched() {
+ $this->load();
+
+ return $this->mTouched;
+ }
+
/**
* @return Password
* @since 1.24
$force = 'force';
}
- $this->getWatchedItem( $title )->resetNotificationTimestamp( $force, $oldid );
+ $this->getWatchedItem( $title )->resetNotificationTimestamp(
+ $force, $oldid, WatchedItem::DEFERRED
+ );
}
/**
// This will be used for a CAS check as a last-resort safety
// check against race conditions and slave lag.
$oldTouched = $this->mTouched;
- $this->mTouched = $this->newTouchedTimestamp();
+ $newTouched = $this->newTouchedTimestamp();
if ( !$wgAuth->allowSetLocalPassword() ) {
$this->mPassword = self::getPasswordFactory()->newFromCiphertext( null );
'user_real_name' => $this->mRealName,
'user_email' => $this->mEmail,
'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
- 'user_touched' => $dbw->timestamp( $this->mTouched ),
+ 'user_touched' => $dbw->timestamp( $newTouched ),
'user_token' => strval( $this->mToken ),
'user_email_token' => $this->mEmailToken,
'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
);
if ( !$dbw->affectedRows() ) {
+ // Maybe the problem was a missed cache update; clear it to be safe
+ $this->clearSharedCache();
// User was changed in the meantime or loaded with stale data
MWExceptionHandler::logException( new MWException(
"CAS update failed on user_touched for user ID '{$this->mId}';" .
"the version of the user to be saved is older than the current version."
) );
- // Maybe the problem was a missed cache update; clear it to be safe
- $this->clearSharedCache();
return;
}
+ $this->mTouched = $newTouched;
$this->saveOptions();
Hooks::run( 'UserSaveSettings', array( $this ) );
$this->clearSharedCache();
$this->getUserPage()->invalidateCache();
+
+ // T95839: clear the cache again post-commit to reduce race conditions
+ // where stale values are written back to the cache by other threads.
+ // Note: this *still* doesn't deal with REPEATABLE-READ snapshot lag...
+ $that = $this;
+ $dbw->onTransactionIdle( function() use ( $that ) {
+ $that->clearSharedCache();
+ } );
}
/**