Always update accounts atomically
This prevents that we unintentionally overwrite concurrent updates, e.g. updates that were done by a racing request or updates we didn't see because we read from a stale cache. Change-Id: I4dfc7726c9324f06806919590d3ef83555bd44a4 Signed-off-by: Edwin Kempin <ekempin@google.com>
This commit is contained in:
@@ -38,10 +38,13 @@ import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
import org.slf4j.Logger;
|
||||
@@ -149,7 +152,7 @@ public class AccountManager {
|
||||
private void update(ReviewDb db, AuthRequest who, ExternalId extId)
|
||||
throws OrmException, IOException, ConfigInvalidException {
|
||||
IdentifiedUser user = userFactory.create(extId.accountId());
|
||||
Account toUpdate = null;
|
||||
List<Consumer<Account>> accountUpdates = new ArrayList<>();
|
||||
|
||||
// If the email address was modified by the authentication provider,
|
||||
// update our records to match the changed email.
|
||||
@@ -158,8 +161,7 @@ public class AccountManager {
|
||||
String oldEmail = extId.email();
|
||||
if (newEmail != null && !newEmail.equals(oldEmail)) {
|
||||
if (oldEmail != null && oldEmail.equals(user.getAccount().getPreferredEmail())) {
|
||||
toUpdate = load(toUpdate, user.getAccountId(), db);
|
||||
toUpdate.setPreferredEmail(newEmail);
|
||||
accountUpdates.add(a -> a.setPreferredEmail(newEmail));
|
||||
}
|
||||
|
||||
externalIdsUpdateFactory
|
||||
@@ -171,8 +173,7 @@ public class AccountManager {
|
||||
if (!realm.allowsEdit(AccountFieldName.FULL_NAME)
|
||||
&& !Strings.isNullOrEmpty(who.getDisplayName())
|
||||
&& !eq(user.getAccount().getFullName(), who.getDisplayName())) {
|
||||
toUpdate = load(toUpdate, user.getAccountId(), db);
|
||||
toUpdate.setFullName(who.getDisplayName());
|
||||
accountUpdates.add(a -> a.setFullName(who.getDisplayName()));
|
||||
}
|
||||
|
||||
if (!realm.allowsEdit(AccountFieldName.USER_NAME)
|
||||
@@ -183,8 +184,12 @@ public class AccountManager {
|
||||
"Not changing already set username %s to %s", user.getUserName(), who.getUserName()));
|
||||
}
|
||||
|
||||
if (toUpdate != null) {
|
||||
accountsUpdateFactory.create().update(db, toUpdate);
|
||||
if (!accountUpdates.isEmpty()) {
|
||||
Account account =
|
||||
accountsUpdateFactory.create().atomicUpdate(db, user.getAccountId(), accountUpdates);
|
||||
if (account == null) {
|
||||
throw new OrmException("Account " + user.getAccountId() + " has been deleted");
|
||||
}
|
||||
}
|
||||
|
||||
if (newEmail != null && !newEmail.equals(oldEmail)) {
|
||||
@@ -193,17 +198,6 @@ public class AccountManager {
|
||||
}
|
||||
}
|
||||
|
||||
private Account load(Account toUpdate, Account.Id accountId, ReviewDb db)
|
||||
throws OrmException, IOException, ConfigInvalidException {
|
||||
if (toUpdate == null) {
|
||||
toUpdate = accounts.get(db, accountId);
|
||||
if (toUpdate == null) {
|
||||
throw new OrmException("Account " + accountId + " has been deleted");
|
||||
}
|
||||
}
|
||||
return toUpdate;
|
||||
}
|
||||
|
||||
private static boolean eq(String a, String b) {
|
||||
return (a == null && b == null) || (a != null && a.equals(b));
|
||||
}
|
||||
@@ -369,11 +363,16 @@ public class AccountManager {
|
||||
.insert(ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress()));
|
||||
|
||||
if (who.getEmailAddress() != null) {
|
||||
Account a = accounts.get(db, to);
|
||||
if (a.getPreferredEmail() == null) {
|
||||
a.setPreferredEmail(who.getEmailAddress());
|
||||
accountsUpdateFactory.create().update(db, a);
|
||||
}
|
||||
accountsUpdateFactory
|
||||
.create()
|
||||
.atomicUpdate(
|
||||
db,
|
||||
to,
|
||||
a -> {
|
||||
if (a.getPreferredEmail() == null) {
|
||||
a.setPreferredEmail(who.getEmailAddress());
|
||||
}
|
||||
});
|
||||
byEmailCache.evict(who.getEmailAddress());
|
||||
}
|
||||
}
|
||||
@@ -433,12 +432,17 @@ public class AccountManager {
|
||||
externalIdsUpdateFactory.create().delete(extId);
|
||||
|
||||
if (who.getEmailAddress() != null) {
|
||||
Account a = accounts.get(db, from);
|
||||
if (a.getPreferredEmail() != null
|
||||
&& a.getPreferredEmail().equals(who.getEmailAddress())) {
|
||||
a.setPreferredEmail(null);
|
||||
accountsUpdateFactory.create().update(db, a);
|
||||
}
|
||||
accountsUpdateFactory
|
||||
.create()
|
||||
.atomicUpdate(
|
||||
db,
|
||||
from,
|
||||
a -> {
|
||||
if (a.getPreferredEmail() != null
|
||||
&& a.getPreferredEmail().equals(who.getEmailAddress())) {
|
||||
a.setPreferredEmail(null);
|
||||
}
|
||||
});
|
||||
byEmailCache.evict(who.getEmailAddress());
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,7 @@ package com.google.gerrit.server.account;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
@@ -37,6 +38,7 @@ import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||
import org.eclipse.jgit.lib.CommitBuilder;
|
||||
@@ -251,20 +253,41 @@ public class AccountsUpdate {
|
||||
*/
|
||||
public Account atomicUpdate(ReviewDb db, Account.Id accountId, Consumer<Account> consumer)
|
||||
throws OrmException, IOException, ConfigInvalidException {
|
||||
return atomicUpdate(db, accountId, ImmutableList.of(consumer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the account and updates it atomically.
|
||||
*
|
||||
* <p>Changing the registration date of an account is not supported.
|
||||
*
|
||||
* @param db ReviewDb
|
||||
* @param accountId ID of the account
|
||||
* @param consumers consumers to update the account, only invoked if the account exists
|
||||
* @return the updated account, {@code null} if the account doesn't exist
|
||||
* @throws OrmException if updating the account fails
|
||||
*/
|
||||
public Account atomicUpdate(ReviewDb db, Account.Id accountId, List<Consumer<Account>> consumers)
|
||||
throws OrmException, IOException, ConfigInvalidException {
|
||||
if (consumers.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update in ReviewDb
|
||||
db.accounts()
|
||||
.atomicUpdate(
|
||||
accountId,
|
||||
a -> {
|
||||
consumer.accept(a);
|
||||
consumers.stream().forEach(c -> c.accept(a));
|
||||
return a;
|
||||
});
|
||||
|
||||
// Update in NoteDb
|
||||
AccountConfig accountConfig = read(accountId);
|
||||
Account account = accountConfig.getAccount();
|
||||
consumer.accept(account);
|
||||
consumers.stream().forEach(c -> c.accept(account));
|
||||
commit(accountConfig);
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user