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:
Edwin Kempin
2017-06-20 09:21:15 +02:00
parent e7e9fbbf23
commit d8c24c67e3
7 changed files with 105 additions and 65 deletions

View File

@@ -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());
}

View File

@@ -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;
}