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

@@ -37,7 +37,6 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.AccountsUpdate; import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.account.externalids.ExternalId;
@@ -71,8 +70,6 @@ import org.junit.Test;
/** Unit tests for {@link GerritPublicKeyChecker}. */ /** Unit tests for {@link GerritPublicKeyChecker}. */
public class GerritPublicKeyCheckerTest { public class GerritPublicKeyCheckerTest {
@Inject private Accounts accounts;
@Inject private AccountsUpdate.Server accountsUpdate; @Inject private AccountsUpdate.Server accountsUpdate;
@Inject private AccountManager accountManager; @Inject private AccountManager accountManager;
@@ -117,10 +114,8 @@ public class GerritPublicKeyCheckerTest {
db = schemaFactory.open(); db = schemaFactory.open();
schemaCreator.create(db); schemaCreator.create(db);
userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId(); userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
Account userAccount = accounts.get(db, userId);
// Note: does not match any key in TestKeys. // Note: does not match any key in TestKeys.
userAccount.setPreferredEmail("user@example.com"); accountsUpdate.create().atomicUpdate(db, userId, a -> a.setPreferredEmail("user@example.com"));
accountsUpdate.create().update(db, userAccount);
user = reloadUser(); user = reloadUser();
requestContext.setContext( requestContext.setContext(

View File

@@ -38,10 +38,13 @@ import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -149,7 +152,7 @@ public class AccountManager {
private void update(ReviewDb db, AuthRequest who, ExternalId extId) private void update(ReviewDb db, AuthRequest who, ExternalId extId)
throws OrmException, IOException, ConfigInvalidException { throws OrmException, IOException, ConfigInvalidException {
IdentifiedUser user = userFactory.create(extId.accountId()); 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, // If the email address was modified by the authentication provider,
// update our records to match the changed email. // update our records to match the changed email.
@@ -158,8 +161,7 @@ public class AccountManager {
String oldEmail = extId.email(); String oldEmail = extId.email();
if (newEmail != null && !newEmail.equals(oldEmail)) { if (newEmail != null && !newEmail.equals(oldEmail)) {
if (oldEmail != null && oldEmail.equals(user.getAccount().getPreferredEmail())) { if (oldEmail != null && oldEmail.equals(user.getAccount().getPreferredEmail())) {
toUpdate = load(toUpdate, user.getAccountId(), db); accountUpdates.add(a -> a.setPreferredEmail(newEmail));
toUpdate.setPreferredEmail(newEmail);
} }
externalIdsUpdateFactory externalIdsUpdateFactory
@@ -171,8 +173,7 @@ public class AccountManager {
if (!realm.allowsEdit(AccountFieldName.FULL_NAME) if (!realm.allowsEdit(AccountFieldName.FULL_NAME)
&& !Strings.isNullOrEmpty(who.getDisplayName()) && !Strings.isNullOrEmpty(who.getDisplayName())
&& !eq(user.getAccount().getFullName(), who.getDisplayName())) { && !eq(user.getAccount().getFullName(), who.getDisplayName())) {
toUpdate = load(toUpdate, user.getAccountId(), db); accountUpdates.add(a -> a.setFullName(who.getDisplayName()));
toUpdate.setFullName(who.getDisplayName());
} }
if (!realm.allowsEdit(AccountFieldName.USER_NAME) 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())); "Not changing already set username %s to %s", user.getUserName(), who.getUserName()));
} }
if (toUpdate != null) { if (!accountUpdates.isEmpty()) {
accountsUpdateFactory.create().update(db, toUpdate); 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)) { 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) { private static boolean eq(String a, String b) {
return (a == null && b == null) || (a != null && a.equals(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())); .insert(ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress()));
if (who.getEmailAddress() != null) { if (who.getEmailAddress() != null) {
Account a = accounts.get(db, to); accountsUpdateFactory
if (a.getPreferredEmail() == null) { .create()
a.setPreferredEmail(who.getEmailAddress()); .atomicUpdate(
accountsUpdateFactory.create().update(db, a); db,
} to,
a -> {
if (a.getPreferredEmail() == null) {
a.setPreferredEmail(who.getEmailAddress());
}
});
byEmailCache.evict(who.getEmailAddress()); byEmailCache.evict(who.getEmailAddress());
} }
} }
@@ -433,12 +432,17 @@ public class AccountManager {
externalIdsUpdateFactory.create().delete(extId); externalIdsUpdateFactory.create().delete(extId);
if (who.getEmailAddress() != null) { if (who.getEmailAddress() != null) {
Account a = accounts.get(db, from); accountsUpdateFactory
if (a.getPreferredEmail() != null .create()
&& a.getPreferredEmail().equals(who.getEmailAddress())) { .atomicUpdate(
a.setPreferredEmail(null); db,
accountsUpdateFactory.create().update(db, a); from,
} a -> {
if (a.getPreferredEmail() != null
&& a.getPreferredEmail().equals(who.getEmailAddress())) {
a.setPreferredEmail(null);
}
});
byEmailCache.evict(who.getEmailAddress()); 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 static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable; import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
@@ -37,6 +38,7 @@ import com.google.inject.Provider;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.CommitBuilder;
@@ -251,20 +253,41 @@ public class AccountsUpdate {
*/ */
public Account atomicUpdate(ReviewDb db, Account.Id accountId, Consumer<Account> consumer) public Account atomicUpdate(ReviewDb db, Account.Id accountId, Consumer<Account> consumer)
throws OrmException, IOException, ConfigInvalidException { 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 // Update in ReviewDb
db.accounts() db.accounts()
.atomicUpdate( .atomicUpdate(
accountId, accountId,
a -> { a -> {
consumer.accept(a); consumers.stream().forEach(c -> c.accept(a));
return a; return a;
}); });
// Update in NoteDb // Update in NoteDb
AccountConfig accountConfig = read(accountId); AccountConfig accountConfig = read(accountId);
Account account = accountConfig.getAccount(); Account account = accountConfig.getAccount();
consumer.accept(account); consumers.stream().forEach(c -> c.accept(account));
commit(accountConfig); commit(accountConfig);
return account; return account;
} }

View File

@@ -84,7 +84,6 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.Sequences; import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.AccountsUpdate; import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.change.ChangeInserter; import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.SetHashtagsOp; import com.google.gerrit.server.change.SetHashtagsOp;
@@ -302,7 +301,6 @@ public class ReceiveCommits {
private final Sequences seq; private final Sequences seq;
private final Provider<InternalChangeQuery> queryProvider; private final Provider<InternalChangeQuery> queryProvider;
private final ChangeNotes.Factory notesFactory; private final ChangeNotes.Factory notesFactory;
private final Accounts accounts;
private final AccountsUpdate.Server accountsUpdate; private final AccountsUpdate.Server accountsUpdate;
private final AccountResolver accountResolver; private final AccountResolver accountResolver;
private final PermissionBackend permissionBackend; private final PermissionBackend permissionBackend;
@@ -377,7 +375,6 @@ public class ReceiveCommits {
Sequences seq, Sequences seq,
Provider<InternalChangeQuery> queryProvider, Provider<InternalChangeQuery> queryProvider,
ChangeNotes.Factory notesFactory, ChangeNotes.Factory notesFactory,
Accounts accounts,
AccountsUpdate.Server accountsUpdate, AccountsUpdate.Server accountsUpdate,
AccountResolver accountResolver, AccountResolver accountResolver,
PermissionBackend permissionBackend, PermissionBackend permissionBackend,
@@ -417,7 +414,6 @@ public class ReceiveCommits {
this.seq = seq; this.seq = seq;
this.queryProvider = queryProvider; this.queryProvider = queryProvider;
this.notesFactory = notesFactory; this.notesFactory = notesFactory;
this.accounts = accounts;
this.accountsUpdate = accountsUpdate; this.accountsUpdate = accountsUpdate;
this.accountResolver = accountResolver; this.accountResolver = accountResolver;
this.permissionBackend = permissionBackend; this.permissionBackend = permissionBackend;
@@ -2788,11 +2784,20 @@ public class ReceiveCommits {
if (defaultName && user.hasEmailAddress(c.getCommitterIdent().getEmailAddress())) { if (defaultName && user.hasEmailAddress(c.getCommitterIdent().getEmailAddress())) {
try { try {
Account a = accounts.get(db, user.getAccountId()); String committerName = c.getCommitterIdent().getName();
if (a != null && Strings.isNullOrEmpty(a.getFullName())) { Account account =
a.setFullName(c.getCommitterIdent().getName()); accountsUpdate
accountsUpdate.create().update(db, a); .create()
user.getAccount().setFullName(a.getFullName()); .atomicUpdate(
db,
user.getAccountId(),
a -> {
if (Strings.isNullOrEmpty(a.getFullName())) {
a.setFullName(committerName);
}
});
if (account != null && Strings.isNullOrEmpty(account.getFullName())) {
user.getAccount().setFullName(account.getFullName());
} }
} catch (OrmException | IOException | ConfigInvalidException e) { } catch (OrmException | IOException | ConfigInvalidException e) {
logWarn("Cannot default full_name", e); logWarn("Cannot default full_name", e);

View File

@@ -496,11 +496,16 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
if (email != null) { if (email != null) {
accountManager.link(id, AuthRequest.forEmail(email)); accountManager.link(id, AuthRequest.forEmail(email));
} }
Account a = accounts.get(db, id); accountsUpdate
a.setFullName(fullName); .create()
a.setPreferredEmail(email); .atomicUpdate(
a.setActive(active); db,
accountsUpdate.create().update(db, a); id,
a -> {
a.setFullName(fullName);
a.setPreferredEmail(email);
a.setActive(active);
});
return id; return id;
} }
} }

View File

@@ -208,13 +208,11 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
db = schemaFactory.open(); db = schemaFactory.open();
userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId(); userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
Account userAccount = accounts.get(db, userId);
String email = "user@example.com"; String email = "user@example.com";
externalIdsUpdate.create().insert(ExternalId.createEmail(userId, email)); externalIdsUpdate.create().insert(ExternalId.createEmail(userId, email));
userAccount.setPreferredEmail(email); accountsUpdate.create().atomicUpdate(db, userId, a -> a.setPreferredEmail(email));
accountsUpdate.create().update(db, userAccount);
user = userFactory.create(userId); user = userFactory.create(userId);
requestContext.setContext(newRequestContext(userAccount.getId())); requestContext.setContext(newRequestContext(userId));
} }
protected RequestContext newRequestContext(Account.Id requestUserId) { protected RequestContext newRequestContext(Account.Id requestUserId) {
@@ -2340,11 +2338,16 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
if (email != null) { if (email != null) {
accountManager.link(id, AuthRequest.forEmail(email)); accountManager.link(id, AuthRequest.forEmail(email));
} }
Account a = accounts.get(db, id); accountsUpdate
a.setFullName(fullName); .create()
a.setPreferredEmail(email); .atomicUpdate(
a.setActive(active); db,
accountsUpdate.create().update(db, a); id,
a -> {
a.setFullName(fullName);
a.setPreferredEmail(email);
a.setActive(active);
});
return id; return id;
} }
} }

View File

@@ -319,11 +319,16 @@ public abstract class AbstractQueryGroupsTest extends GerritServerTests {
if (email != null) { if (email != null) {
accountManager.link(id, AuthRequest.forEmail(email)); accountManager.link(id, AuthRequest.forEmail(email));
} }
Account a = accounts.get(db, id); accountsUpdate
a.setFullName(fullName); .create()
a.setPreferredEmail(email); .atomicUpdate(
a.setActive(active); db,
accountsUpdate.create().update(db, a); id,
a -> {
a.setFullName(fullName);
a.setPreferredEmail(email);
a.setActive(active);
});
return id; return id;
} }
} }