Serialize AccountCache

Data on googlesource.com suggests that we spend a significant amount of
time loading accounts from NoteDb. This is true for all Gerrit
installations, but especially for distributed setups or setups that
restart often.

This commit serializes the AccountCache using established mechanisms.
To do that, we decompose AccountState - the entity that we currently
cache - into smaller chunks that can be cached individually:

1) External IDs + user name (cached in ExternalIdCache)
2) CachedAccountDetails (newly cached)
3) Gerrit's default settings (we start caching this in a follow-up
   change)

CachedAccountDetails - a new class representing all information stored
under the user's ref (refs/users/<sharded-id>) is now cached in the
'accounts' cache instead of AccountState. AccountState is contructed
when requested from the sources 1-3 and not cached itself as it's
just a plain wrapper around other state, that we already cache.

This has the following advantages:
1) CachedAccountDetails contains only details from
   refs/users/<sharded-id>.
   By that, we can use the SHA1 of that ref as cache key and start
   serializing the cache to eliminate cold start penalty as well as
   router assignment change penalty (for distributed setups).
   It also means that we don't have to do invalidation ourselves
   anymore.
2) When the server's default preferences change, we don't have to
   invalidate all accounts anymore. This is a shortcoming of the
   current approach.
3) The projected speed improvements that come from persisting the
   cache makes it so that we can remove the logic to load accounts
   in parallel.

The new aproach also means that:
1) We now need to get the SHA1 from refs/users/<sharded-id> for
   every account that we look up. Data suggests that this is not an
   issue for latency as ref lookups are cheap. We retain the method
   in AccountCacheImpl that allows the caller to load a
   Set<AccountState> so that in the cases where we want many many
   accounts (change queries, ...) we have to open All-Users only once.
   In case we discover that - against our assumptions - this is a
   bottleneck we can add a small in-memory cache for AccountState.

Related prework:
The new aproach shows that the way we handle user preferences is
suboptimal, because:
1) We pipe through API data types to the storage
2) We overlay defaults directly in the storage
3) Use reflection to get/set fields.

I considered and prototyped a rewrite of this and initially thought I
could get it done before serializing the account cache. However it turned
out to be significantly more work and the impact of that work (besides
being a much desired cleanup) is rather low. So I decided to get the
cache serialized independently.

Change-Id: I61ae57802f37c62ee9e3552e4a0f19fe3d8d762b
This commit is contained in:
Patrick Hiesel 2020-03-26 17:22:23 +01:00
parent 9ab6603065
commit e945da3880
30 changed files with 607 additions and 1006 deletions

View File

@ -380,8 +380,7 @@ public abstract class AbstractDaemonTest {
initSsh();
}
protected void evictAndReindexAccount(Account.Id accountId) {
accountCache.evict(accountId);
protected void reindexAccount(Account.Id accountId) {
accountIndexer.index(accountId);
}
@ -436,8 +435,8 @@ public abstract class AbstractDaemonTest {
user = accountCreator.user();
// Evict and reindex accounts in case tests modify them.
evictAndReindexAccount(admin.id());
evictAndReindexAccount(user.id());
reindexAccount(admin.id());
reindexAccount(user.id());
adminRestSession = new RestSession(server, admin);
userRestSession = new RestSession(server, user);

View File

@ -339,8 +339,8 @@ public abstract class AbstractNotificationTest extends AbstractDaemonTest {
return description.getClassName();
}
private TestAccount evictAndCopy(TestAccount account) {
evictAndReindexAccount(account.id());
private TestAccount reindexAndCopy(TestAccount account) {
reindexAccount(account.id());
return account;
}
@ -348,14 +348,14 @@ public abstract class AbstractNotificationTest extends AbstractDaemonTest {
synchronized (stagedUsers) {
if (stagedUsers.containsKey(usersCacheKey())) {
StagedUsers existing = stagedUsers.get(usersCacheKey());
owner = evictAndCopy(existing.owner);
author = evictAndCopy(existing.author);
uploader = evictAndCopy(existing.uploader);
reviewer = evictAndCopy(existing.reviewer);
ccer = evictAndCopy(existing.ccer);
starrer = evictAndCopy(existing.starrer);
assignee = evictAndCopy(existing.assignee);
watchingProjectOwner = evictAndCopy(existing.watchingProjectOwner);
owner = reindexAndCopy(existing.owner);
author = reindexAndCopy(existing.author);
uploader = reindexAndCopy(existing.uploader);
reviewer = reindexAndCopy(existing.reviewer);
ccer = reindexAndCopy(existing.ccer);
starrer = reindexAndCopy(existing.starrer);
assignee = reindexAndCopy(existing.assignee);
watchingProjectOwner = reindexAndCopy(existing.watchingProjectOwner);
watchers.putAll(existing.watchers);
return;
}

View File

@ -335,18 +335,18 @@ public class ProjectResetter implements AutoCloseable {
// Make sure all accounts are evicted and reindexed.
try (Repository repo = repoManager.openRepository(allUsersName)) {
for (Account.Id id : accountIds(repo)) {
evictAndReindexAccount(id);
reindexAccount(id);
}
}
// Remove deleted accounts from the cache and index.
for (Account.Id id : deletedAccounts) {
evictAndReindexAccount(id);
reindexAccount(id);
}
} else {
// Evict and reindex all modified and deleted accounts.
for (Account.Id id : Sets.union(modifiedAccounts, deletedAccounts)) {
evictAndReindexAccount(id);
reindexAccount(id);
}
}
}
@ -367,10 +367,7 @@ public class ProjectResetter implements AutoCloseable {
}
}
private void evictAndReindexAccount(Account.Id accountId) {
if (accountCache != null) {
accountCache.evict(accountId);
}
private void reindexAccount(Account.Id accountId) {
if (groupIncludeCache != null) {
groupIncludeCache.evictGroupsWithMember(accountId);
}

View File

@ -19,15 +19,10 @@ import static com.google.gerrit.entities.RefNames.REFS_STARRED_CHANGES;
import static com.google.gerrit.entities.RefNames.REFS_USERS;
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.proto.Protos;
import com.google.gerrit.server.cache.proto.Cache.AccountProto;
import com.google.gerrit.server.cache.serialize.CacheSerializer;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.Optional;
/**
@ -125,42 +120,6 @@ public abstract class Account {
}
}
enum Serializer implements CacheSerializer<Account> {
INSTANCE;
@Override
public byte[] serialize(Account account) {
// We don't care about the difference of empty strings and null in the Account entity.
AccountProto.Builder proto =
AccountProto.newBuilder()
.setId(account.id().get())
.setRegisteredOn(account.registeredOn().toInstant().toEpochMilli())
.setInactive(account.inactive())
.setFullName(Strings.nullToEmpty(account.fullName()))
.setDisplayName(Strings.nullToEmpty(account.displayName()))
.setPreferredEmail(Strings.nullToEmpty(account.preferredEmail()))
.setStatus(Strings.nullToEmpty(account.status()))
.setMetaId(Strings.nullToEmpty(account.metaId()));
return Protos.toByteArray(proto.build());
}
@Override
public Account deserialize(byte[] in) {
// We don't care about the difference of empty strings and null in the Account entity.
AccountProto proto = Protos.parseUnchecked(AccountProto.parser(), in);
return Account.builder(
Account.id(proto.getId()),
Timestamp.from(Instant.ofEpochMilli(proto.getRegisteredOn())))
.setFullName(Strings.emptyToNull(proto.getFullName()))
.setDisplayName(Strings.emptyToNull(proto.getDisplayName()))
.setPreferredEmail(Strings.emptyToNull(proto.getPreferredEmail()))
.setInactive(proto.getInactive())
.setStatus(Strings.emptyToNull(proto.getStatus()))
.setMetaId(Strings.emptyToNull(proto.getMetaId()))
.build();
}
}
public abstract Id id();
/** Date and time the user registered with the review server. */

View File

@ -14,7 +14,6 @@
package com.google.gerrit.server.account;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import java.util.Map;
import java.util.Optional;
@ -73,14 +72,4 @@ public interface AccountCache {
* exists or if loading the external ID fails {@link Optional#empty()} is returned
*/
Optional<AccountState> getByUsername(String username);
/**
* Evicts the account from the cache.
*
* @param accountId account ID of the account that should be evicted
*/
void evict(@Nullable Account.Id accountId);
/** Evict all accounts from the cache. */
void evictAll();
}

View File

@ -20,12 +20,15 @@ import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.server.FanOutExecutor;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.CachedPreferences;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
@ -33,34 +36,34 @@ import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
/** Caches important (but small) account state to avoid database hits. */
@Singleton
public class AccountCacheImpl implements AccountCache {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String BYID_NAME = "accounts";
private static final String BYID_AND_REV_NAME = "accounts";
public static Module module() {
return new CacheModule() {
@Override
protected void configure() {
cache(BYID_NAME, Account.Id.class, new TypeLiteral<AccountState>() {})
.loader(ByIdLoader.class);
persist(BYID_AND_REV_NAME, CachedAccountDetails.Key.class, CachedAccountDetails.class)
.version(1)
.keySerializer(CachedAccountDetails.Key.Serializer.INSTANCE)
.valueSerializer(CachedAccountDetails.Serializer.INSTANCE)
.loader(Loader.class);
bind(AccountCacheImpl.class);
bind(AccountCache.class).to(AccountCacheImpl.class);
@ -69,76 +72,60 @@ public class AccountCacheImpl implements AccountCache {
}
private final ExternalIds externalIds;
private final LoadingCache<Account.Id, AccountState> byId;
private final ExecutorService executor;
private final LoadingCache<CachedAccountDetails.Key, CachedAccountDetails> accountDetailsCache;
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
@Inject
AccountCacheImpl(
ExternalIds externalIds,
@Named(BYID_NAME) LoadingCache<Account.Id, AccountState> byId,
@FanOutExecutor ExecutorService executor) {
@Named(BYID_AND_REV_NAME)
LoadingCache<CachedAccountDetails.Key, CachedAccountDetails> accountDetailsCache,
GitRepositoryManager repoManager,
AllUsersName allUsersName) {
this.externalIds = externalIds;
this.byId = byId;
this.executor = executor;
this.accountDetailsCache = accountDetailsCache;
this.repoManager = repoManager;
this.allUsersName = allUsersName;
}
@Override
public AccountState getEvenIfMissing(Account.Id accountId) {
try {
return byId.get(accountId);
} catch (ExecutionException e) {
if (!(e.getCause() instanceof AccountNotFoundException)) {
logger.atWarning().withCause(e).log("Cannot load AccountState for %s", accountId);
}
return missing(accountId);
}
return get(accountId).orElse(missing(accountId));
}
@Override
public Optional<AccountState> get(Account.Id accountId) {
try {
return Optional.ofNullable(byId.get(accountId));
} catch (ExecutionException e) {
if (!(e.getCause() instanceof AccountNotFoundException)) {
logger.atWarning().withCause(e).log("Cannot load AccountState for %s", accountId);
}
return Optional.empty();
}
return Optional.ofNullable(get(Collections.singleton(accountId)).getOrDefault(accountId, null));
}
@Override
public Map<Account.Id, AccountState> get(Set<Account.Id> accountIds) {
Map<Account.Id, AccountState> accountStates = new HashMap<>(accountIds.size());
List<Callable<Optional<AccountState>>> callables = new ArrayList<>();
for (Account.Id accountId : accountIds) {
AccountState state = byId.getIfPresent(accountId);
if (state != null) {
// The value is in-memory, so we just get the state
accountStates.put(accountId, state);
} else {
// Queue up a callable so that we can load accounts in parallel
callables.add(() -> get(accountId));
}
}
if (callables.isEmpty()) {
return accountStates;
}
List<Future<Optional<AccountState>>> futures;
try {
futures = executor.invokeAll(callables);
} catch (InterruptedException e) {
logger.atSevere().withCause(e).log("Cannot load AccountStates");
return ImmutableMap.of();
}
for (Future<Optional<AccountState>> f : futures) {
try {
f.get().ifPresent(s -> accountStates.put(s.account().id(), s));
} catch (InterruptedException | ExecutionException e) {
logger.atSevere().withCause(e).log("Cannot load AccountState");
try (Repository allUsers = repoManager.openRepository(allUsersName)) {
// TODO(hiesel): Cache the server's default config
Config defaultConfig = StoredPreferences.readDefaultConfig(allUsersName, allUsers);
ImmutableMap.Builder<Account.Id, AccountState> result = ImmutableMap.builder();
for (Account.Id id : accountIds) {
Ref userRef = allUsers.exactRef(RefNames.refsUsers(id));
if (userRef == null) {
continue;
}
result.put(
id,
AccountState.forCachedAccount(
accountDetailsCache.get(
CachedAccountDetails.Key.create(id, userRef.getObjectId())),
CachedPreferences.fromConfig(defaultConfig),
externalIds));
}
return result.build();
}
} catch (IOException | ExecutionException | ConfigInvalidException e) {
throw new StorageException(e);
}
return accountStates;
}
@Override
@ -154,42 +141,37 @@ public class AccountCacheImpl implements AccountCache {
}
}
@Override
public void evict(@Nullable Account.Id accountId) {
if (accountId != null) {
logger.atFine().log("Evict account %d", accountId.get());
byId.invalidate(accountId);
}
}
@Override
public void evictAll() {
logger.atFine().log("Evict all accounts");
byId.invalidateAll();
}
private AccountState missing(Account.Id accountId) {
Account.Builder account = Account.builder(accountId, TimeUtil.nowTs());
account.setActive(false);
return AccountState.forAccount(account.build());
}
static class ByIdLoader extends CacheLoader<Account.Id, AccountState> {
private final Accounts accounts;
@Singleton
static class Loader extends CacheLoader<CachedAccountDetails.Key, CachedAccountDetails> {
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
@Inject
ByIdLoader(Accounts accounts) {
this.accounts = accounts;
Loader(GitRepositoryManager repoManager, AllUsersName allUsersName) {
this.repoManager = repoManager;
this.allUsersName = allUsersName;
}
@Override
public AccountState load(Account.Id who) throws Exception {
try (TraceTimer timer =
TraceContext.newTimer(
"Loading account", Metadata.builder().accountId(who.get()).build())) {
return accounts
.get(who)
.orElseThrow(() -> new AccountNotFoundException(who + " not found"));
public CachedAccountDetails load(CachedAccountDetails.Key key) throws Exception {
try (TraceTimer ignored =
TraceContext.newTimer(
"Loading account", Metadata.builder().accountId(key.accountId().get()).build());
Repository repo = repoManager.openRepository(allUsersName)) {
AccountConfig cfg = new AccountConfig(key.accountId(), allUsersName, repo).load(key.id());
Account account =
cfg.getLoadedAccount()
.orElseThrow(() -> new AccountNotFoundException(key.accountId() + " not found"));
return CachedAccountDetails.create(
account,
cfg.getProjectWatches(),
CachedPreferences.fromConfig(cfg.getRawPreferences()));
}
}
}

View File

@ -24,9 +24,6 @@ import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.DuplicateKeyException;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.EditPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.server.account.ProjectWatches.NotifyType;
import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
import com.google.gerrit.server.account.externalids.ExternalIds;
@ -106,6 +103,11 @@ public class AccountConfig extends VersionedMetaData implements ValidationError.
return this;
}
public AccountConfig load(ObjectId rev) throws IOException, ConfigInvalidException {
load(allUsersName, repo, rev);
return this;
}
/**
* Get the loaded account.
*
@ -142,36 +144,6 @@ public class AccountConfig extends VersionedMetaData implements ValidationError.
return projectWatches.getProjectWatches();
}
/**
* Get the general preferences of the loaded account.
*
* @return the general preferences of the loaded account
*/
public GeneralPreferencesInfo getGeneralPreferences() {
checkLoaded();
return preferences.getGeneralPreferences();
}
/**
* Get the diff preferences of the loaded account.
*
* @return the diff preferences of the loaded account
*/
public DiffPreferencesInfo getDiffPreferences() {
checkLoaded();
return preferences.getDiffPreferences();
}
/**
* Get the edit preferences of the loaded account.
*
* @return the edit preferences of the loaded account
*/
public EditPreferencesInfo getEditPreferences() {
checkLoaded();
return preferences.getEditPreferences();
}
/**
* Sets the account. This means the loaded account will be overwritten with the given account.
*
@ -228,6 +200,21 @@ public class AccountConfig extends VersionedMetaData implements ValidationError.
return this;
}
/** Returns the content of the {@code preferences.config} file. */
Config getRawPreferences() {
checkLoaded();
return preferences.getRaw();
}
/**
* Returns the content of the {@code preferences.config} file on {@code
* refs/preferences/defaults}.
*/
Config getRawDefaultPreferences() {
checkLoaded();
return preferences.getRawDefault();
}
@Override
protected void onLoad() throws IOException, ConfigInvalidException {
if (revision != null) {

View File

@ -28,6 +28,7 @@ import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.config.CachedPreferences;
import java.io.IOException;
import java.util.Collection;
import java.util.Optional;
@ -92,12 +93,6 @@ public abstract class AccountState {
// an open Repository instance.
ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches =
accountConfig.getProjectWatches();
Preferences.General generalPreferences =
Preferences.General.fromInfo(accountConfig.getGeneralPreferences());
Preferences.Diff diffPreferences =
Preferences.Diff.fromInfo(accountConfig.getDiffPreferences());
Preferences.Edit editPreferences =
Preferences.Edit.fromInfo(accountConfig.getEditPreferences());
return Optional.of(
new AutoValue_AccountState(
@ -105,9 +100,8 @@ public abstract class AccountState {
extIds,
ExternalId.getUserName(extIds),
projectWatches,
generalPreferences,
diffPreferences,
editPreferences));
Optional.of(CachedPreferences.fromConfig(accountConfig.getRawDefaultPreferences())),
Optional.of(CachedPreferences.fromConfig(accountConfig.getRawPreferences()))));
}
/**
@ -121,6 +115,26 @@ public abstract class AccountState {
return forAccount(account, ImmutableSet.of());
}
/**
* Creates an AccountState for a given account and external IDs.
*
* @param account the account
* @return the account state
*/
public static AccountState forCachedAccount(
CachedAccountDetails account, CachedPreferences defaultConfig, ExternalIds externalIds)
throws IOException {
ImmutableSet<ExternalId> extIds =
ImmutableSet.copyOf(externalIds.byAccount(account.account().id()));
return new AutoValue_AccountState(
account.account(),
extIds,
ExternalId.getUserName(extIds),
account.projectWatches(),
Optional.of(defaultConfig),
Optional.of(account.preferences()));
}
/**
* Creates an AccountState for a given account with no project watches and default preferences.
*
@ -134,9 +148,8 @@ public abstract class AccountState {
ImmutableSet.copyOf(extIds),
ExternalId.getUserName(extIds),
ImmutableMap.of(),
Preferences.General.fromInfo(GeneralPreferencesInfo.defaults()),
Preferences.Diff.fromInfo(DiffPreferencesInfo.defaults()),
Preferences.Edit.fromInfo(EditPreferencesInfo.defaults()));
Optional.empty(),
Optional.empty());
}
/** Get the cached account metadata. */
@ -158,17 +171,20 @@ public abstract class AccountState {
/** The general preferences of the account. */
public GeneralPreferencesInfo generalPreferences() {
return immutableGeneralPreferences().toInfo();
return CachedPreferences.general(
defaultPreferences(), userPreferences().orElse(CachedPreferences.EMPTY));
}
/** The diff preferences of the account. */
public DiffPreferencesInfo diffPreferences() {
return immutableDiffPreferences().toInfo();
return CachedPreferences.diff(
defaultPreferences(), userPreferences().orElse(CachedPreferences.EMPTY));
}
/** The edit preferences of the account. */
public EditPreferencesInfo editPreferences() {
return immutableEditPreferences().toInfo();
return CachedPreferences.edit(
defaultPreferences(), userPreferences().orElse(CachedPreferences.EMPTY));
}
@Override
@ -178,9 +194,9 @@ public abstract class AccountState {
return h.toString();
}
protected abstract Preferences.General immutableGeneralPreferences();
/** Gerrit's default preferences as stored in {@code preferences.config}. */
protected abstract Optional<CachedPreferences> defaultPreferences();
protected abstract Preferences.Diff immutableDiffPreferences();
protected abstract Preferences.Edit immutableEditPreferences();
/** User preferences as stored in {@code preferences.config}. */
protected abstract Optional<CachedPreferences> userPreferences();
}

View File

@ -0,0 +1,174 @@
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.account;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.auto.value.AutoValue;
import com.google.common.base.Enums;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
import com.google.gerrit.proto.Protos;
import com.google.gerrit.server.cache.proto.Cache;
import com.google.gerrit.server.cache.serialize.CacheSerializer;
import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
import com.google.gerrit.server.config.CachedPreferences;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.Map;
import org.eclipse.jgit.lib.ObjectId;
/** Details of an account that are cached persistently in {@link AccountCache}. */
@AutoValue
abstract class CachedAccountDetails {
@AutoValue
abstract static class Key {
static Key create(Account.Id accountId, ObjectId id) {
return new AutoValue_CachedAccountDetails_Key(accountId, id.copy());
}
/** Identifier of the account. */
abstract Account.Id accountId();
/**
* Git revision at which the account was loaded. Corresponds to a revision on the account ref
* ({@code refs/users/<sharded-id>}).
*/
abstract ObjectId id();
/** Serializer used to read this entity from and write it to a persistent storage. */
enum Serializer implements CacheSerializer<Key> {
INSTANCE;
@Override
public byte[] serialize(Key object) {
return Protos.toByteArray(
Cache.AccountKeyProto.newBuilder()
.setAccountId(object.accountId().get())
.setId(ObjectIdConverter.create().toByteString(object.id()))
.build());
}
@Override
public Key deserialize(byte[] in) {
Cache.AccountKeyProto proto = Protos.parseUnchecked(Cache.AccountKeyProto.parser(), in);
return Key.create(
Account.id(proto.getAccountId()),
ObjectIdConverter.create().fromByteString(proto.getId()));
}
}
}
/** Essential attributes of the account, such as name or registration time. */
abstract Account account();
/** Projects that the user has configured to watch. */
abstract ImmutableMap<ProjectWatches.ProjectWatchKey, ImmutableSet<ProjectWatches.NotifyType>>
projectWatches();
/** Preferences that this user has. Serialized as Git-config style string. */
abstract CachedPreferences preferences();
static CachedAccountDetails create(
Account account,
ImmutableMap<ProjectWatches.ProjectWatchKey, ImmutableSet<ProjectWatches.NotifyType>>
projectWatches,
CachedPreferences preferences) {
return new AutoValue_CachedAccountDetails(account, projectWatches, preferences);
}
/** Serializer used to read this entity from and write it to a persistent storage. */
enum Serializer implements CacheSerializer<CachedAccountDetails> {
INSTANCE;
@Override
public byte[] serialize(CachedAccountDetails cachedAccountDetails) {
Cache.AccountDetailsProto.Builder serialized = Cache.AccountDetailsProto.newBuilder();
// We don't care about the difference of empty strings and null in the Account entity.
Account account = cachedAccountDetails.account();
Cache.AccountProto.Builder accountProto =
Cache.AccountProto.newBuilder()
.setId(account.id().get())
.setRegisteredOn(account.registeredOn().toInstant().toEpochMilli())
.setInactive(account.inactive())
.setFullName(Strings.nullToEmpty(account.fullName()))
.setDisplayName(Strings.nullToEmpty(account.displayName()))
.setPreferredEmail(Strings.nullToEmpty(account.preferredEmail()))
.setStatus(Strings.nullToEmpty(account.status()))
.setMetaId(Strings.nullToEmpty(account.metaId()));
serialized.setAccount(accountProto);
for (Map.Entry<ProjectWatches.ProjectWatchKey, ImmutableSet<ProjectWatches.NotifyType>>
watch : cachedAccountDetails.projectWatches().entrySet()) {
Cache.ProjectWatchProto.Builder proto =
Cache.ProjectWatchProto.newBuilder().setProject(watch.getKey().project().get());
if (watch.getKey().filter() != null) {
proto.setFilter(watch.getKey().filter());
}
watch
.getValue()
.forEach(
n ->
proto.addNotifyType(
Enums.stringConverter(ProjectWatches.NotifyType.class)
.reverse()
.convert(n)));
serialized.addProjectWatchProto(proto);
}
serialized.setUserPreferences(cachedAccountDetails.preferences().config());
return Protos.toByteArray(serialized.build());
}
@Override
public CachedAccountDetails deserialize(byte[] in) {
Cache.AccountDetailsProto proto =
Protos.parseUnchecked(Cache.AccountDetailsProto.parser(), in);
Account account =
Account.builder(
Account.id(proto.getAccount().getId()),
Timestamp.from(Instant.ofEpochMilli(proto.getAccount().getRegisteredOn())))
.setFullName(Strings.emptyToNull(proto.getAccount().getFullName()))
.setDisplayName(Strings.emptyToNull(proto.getAccount().getDisplayName()))
.setPreferredEmail(Strings.emptyToNull(proto.getAccount().getPreferredEmail()))
.setInactive(proto.getAccount().getInactive())
.setStatus(Strings.emptyToNull(proto.getAccount().getStatus()))
.setMetaId(Strings.emptyToNull(proto.getAccount().getMetaId()))
.build();
ImmutableMap.Builder<ProjectWatches.ProjectWatchKey, ImmutableSet<ProjectWatches.NotifyType>>
projectWatches = ImmutableMap.builder();
proto.getProjectWatchProtoList().stream()
.forEach(
p ->
projectWatches.put(
ProjectWatches.ProjectWatchKey.create(
Project.nameKey(p.getProject()), p.getFilter()),
p.getNotifyTypeList().stream()
.map(
e ->
Enums.stringConverter(ProjectWatches.NotifyType.class).convert(e))
.collect(toImmutableSet())));
return CachedAccountDetails.create(
account,
projectWatches.build(),
CachedPreferences.fromString(proto.getUserPreferences()));
}
}
}

View File

@ -1,429 +0,0 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.account;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
import com.google.gerrit.extensions.client.EditPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DateFormat;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DefaultBase;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.TimeFormat;
import com.google.gerrit.extensions.client.MenuItem;
import java.util.Optional;
@AutoValue
public abstract class Preferences {
@AutoValue
public abstract static class General {
public abstract Optional<Integer> changesPerPage();
public abstract Optional<String> downloadScheme();
public abstract Optional<DateFormat> dateFormat();
public abstract Optional<TimeFormat> timeFormat();
public abstract Optional<Boolean> expandInlineDiffs();
public abstract Optional<Boolean> highlightAssigneeInChangeTable();
public abstract Optional<Boolean> relativeDateInChangeTable();
public abstract Optional<DiffView> diffView();
public abstract Optional<Boolean> sizeBarInChangeTable();
public abstract Optional<Boolean> legacycidInChangeTable();
public abstract Optional<Boolean> muteCommonPathPrefixes();
public abstract Optional<Boolean> signedOffBy();
public abstract Optional<EmailStrategy> emailStrategy();
public abstract Optional<EmailFormat> emailFormat();
public abstract Optional<DefaultBase> defaultBaseForMerges();
public abstract Optional<Boolean> publishCommentsOnPush();
public abstract Optional<Boolean> workInProgressByDefault();
public abstract Optional<ImmutableList<MenuItem>> my();
public abstract Optional<ImmutableList<String>> changeTable();
@AutoValue.Builder
public abstract static class Builder {
abstract Builder changesPerPage(@Nullable Integer val);
abstract Builder downloadScheme(@Nullable String val);
abstract Builder dateFormat(@Nullable DateFormat val);
abstract Builder timeFormat(@Nullable TimeFormat val);
abstract Builder expandInlineDiffs(@Nullable Boolean val);
abstract Builder highlightAssigneeInChangeTable(@Nullable Boolean val);
abstract Builder relativeDateInChangeTable(@Nullable Boolean val);
abstract Builder diffView(@Nullable DiffView val);
abstract Builder sizeBarInChangeTable(@Nullable Boolean val);
abstract Builder legacycidInChangeTable(@Nullable Boolean val);
abstract Builder muteCommonPathPrefixes(@Nullable Boolean val);
abstract Builder signedOffBy(@Nullable Boolean val);
abstract Builder emailStrategy(@Nullable EmailStrategy val);
abstract Builder emailFormat(@Nullable EmailFormat val);
abstract Builder defaultBaseForMerges(@Nullable DefaultBase val);
abstract Builder publishCommentsOnPush(@Nullable Boolean val);
abstract Builder workInProgressByDefault(@Nullable Boolean val);
abstract Builder my(@Nullable ImmutableList<MenuItem> val);
abstract Builder changeTable(@Nullable ImmutableList<String> val);
abstract General build();
}
public static General fromInfo(GeneralPreferencesInfo info) {
return (new AutoValue_Preferences_General.Builder())
.changesPerPage(info.changesPerPage)
.downloadScheme(info.downloadScheme)
.dateFormat(info.dateFormat)
.timeFormat(info.timeFormat)
.expandInlineDiffs(info.expandInlineDiffs)
.highlightAssigneeInChangeTable(info.highlightAssigneeInChangeTable)
.relativeDateInChangeTable(info.relativeDateInChangeTable)
.diffView(info.diffView)
.sizeBarInChangeTable(info.sizeBarInChangeTable)
.legacycidInChangeTable(info.legacycidInChangeTable)
.muteCommonPathPrefixes(info.muteCommonPathPrefixes)
.signedOffBy(info.signedOffBy)
.emailStrategy(info.emailStrategy)
.emailFormat(info.emailFormat)
.defaultBaseForMerges(info.defaultBaseForMerges)
.publishCommentsOnPush(info.publishCommentsOnPush)
.workInProgressByDefault(info.workInProgressByDefault)
.my(info.my == null ? null : ImmutableList.copyOf(info.my))
.changeTable(info.changeTable == null ? null : ImmutableList.copyOf(info.changeTable))
.build();
}
public GeneralPreferencesInfo toInfo() {
GeneralPreferencesInfo info = new GeneralPreferencesInfo();
info.changesPerPage = changesPerPage().orElse(null);
info.downloadScheme = downloadScheme().orElse(null);
info.dateFormat = dateFormat().orElse(null);
info.timeFormat = timeFormat().orElse(null);
info.expandInlineDiffs = expandInlineDiffs().orElse(null);
info.highlightAssigneeInChangeTable = highlightAssigneeInChangeTable().orElse(null);
info.relativeDateInChangeTable = relativeDateInChangeTable().orElse(null);
info.diffView = diffView().orElse(null);
info.sizeBarInChangeTable = sizeBarInChangeTable().orElse(null);
info.legacycidInChangeTable = legacycidInChangeTable().orElse(null);
info.muteCommonPathPrefixes = muteCommonPathPrefixes().orElse(null);
info.signedOffBy = signedOffBy().orElse(null);
info.emailStrategy = emailStrategy().orElse(null);
info.emailFormat = emailFormat().orElse(null);
info.defaultBaseForMerges = defaultBaseForMerges().orElse(null);
info.publishCommentsOnPush = publishCommentsOnPush().orElse(null);
info.workInProgressByDefault = workInProgressByDefault().orElse(null);
info.my = my().orElse(null);
info.changeTable = changeTable().orElse(null);
return info;
}
}
@AutoValue
public abstract static class Edit {
public abstract Optional<Integer> tabSize();
public abstract Optional<Integer> lineLength();
public abstract Optional<Integer> indentUnit();
public abstract Optional<Integer> cursorBlinkRate();
public abstract Optional<Boolean> hideTopMenu();
public abstract Optional<Boolean> showTabs();
public abstract Optional<Boolean> showWhitespaceErrors();
public abstract Optional<Boolean> syntaxHighlighting();
public abstract Optional<Boolean> hideLineNumbers();
public abstract Optional<Boolean> matchBrackets();
public abstract Optional<Boolean> lineWrapping();
public abstract Optional<Boolean> indentWithTabs();
public abstract Optional<Boolean> autoCloseBrackets();
public abstract Optional<Boolean> showBase();
@AutoValue.Builder
public abstract static class Builder {
abstract Builder tabSize(@Nullable Integer val);
abstract Builder lineLength(@Nullable Integer val);
abstract Builder indentUnit(@Nullable Integer val);
abstract Builder cursorBlinkRate(@Nullable Integer val);
abstract Builder hideTopMenu(@Nullable Boolean val);
abstract Builder showTabs(@Nullable Boolean val);
abstract Builder showWhitespaceErrors(@Nullable Boolean val);
abstract Builder syntaxHighlighting(@Nullable Boolean val);
abstract Builder hideLineNumbers(@Nullable Boolean val);
abstract Builder matchBrackets(@Nullable Boolean val);
abstract Builder lineWrapping(@Nullable Boolean val);
abstract Builder indentWithTabs(@Nullable Boolean val);
abstract Builder autoCloseBrackets(@Nullable Boolean val);
abstract Builder showBase(@Nullable Boolean val);
abstract Edit build();
}
public static Edit fromInfo(EditPreferencesInfo info) {
return (new AutoValue_Preferences_Edit.Builder())
.tabSize(info.tabSize)
.lineLength(info.lineLength)
.indentUnit(info.indentUnit)
.cursorBlinkRate(info.cursorBlinkRate)
.hideTopMenu(info.hideTopMenu)
.showTabs(info.showTabs)
.showWhitespaceErrors(info.showWhitespaceErrors)
.syntaxHighlighting(info.syntaxHighlighting)
.hideLineNumbers(info.hideLineNumbers)
.matchBrackets(info.matchBrackets)
.lineWrapping(info.lineWrapping)
.indentWithTabs(info.indentWithTabs)
.autoCloseBrackets(info.autoCloseBrackets)
.showBase(info.showBase)
.build();
}
public EditPreferencesInfo toInfo() {
EditPreferencesInfo info = new EditPreferencesInfo();
info.tabSize = tabSize().orElse(null);
info.lineLength = lineLength().orElse(null);
info.indentUnit = indentUnit().orElse(null);
info.cursorBlinkRate = cursorBlinkRate().orElse(null);
info.hideTopMenu = hideTopMenu().orElse(null);
info.showTabs = showTabs().orElse(null);
info.showWhitespaceErrors = showWhitespaceErrors().orElse(null);
info.syntaxHighlighting = syntaxHighlighting().orElse(null);
info.hideLineNumbers = hideLineNumbers().orElse(null);
info.matchBrackets = matchBrackets().orElse(null);
info.lineWrapping = lineWrapping().orElse(null);
info.indentWithTabs = indentWithTabs().orElse(null);
info.autoCloseBrackets = autoCloseBrackets().orElse(null);
info.showBase = showBase().orElse(null);
return info;
}
}
@AutoValue
public abstract static class Diff {
public abstract Optional<Integer> context();
public abstract Optional<Integer> tabSize();
public abstract Optional<Integer> fontSize();
public abstract Optional<Integer> lineLength();
public abstract Optional<Integer> cursorBlinkRate();
public abstract Optional<Boolean> expandAllComments();
public abstract Optional<Boolean> intralineDifference();
public abstract Optional<Boolean> manualReview();
public abstract Optional<Boolean> showLineEndings();
public abstract Optional<Boolean> showTabs();
public abstract Optional<Boolean> showWhitespaceErrors();
public abstract Optional<Boolean> syntaxHighlighting();
public abstract Optional<Boolean> hideTopMenu();
public abstract Optional<Boolean> autoHideDiffTableHeader();
public abstract Optional<Boolean> hideLineNumbers();
public abstract Optional<Boolean> renderEntireFile();
public abstract Optional<Boolean> hideEmptyPane();
public abstract Optional<Boolean> matchBrackets();
public abstract Optional<Boolean> lineWrapping();
public abstract Optional<Whitespace> ignoreWhitespace();
public abstract Optional<Boolean> retainHeader();
public abstract Optional<Boolean> skipDeleted();
public abstract Optional<Boolean> skipUnchanged();
public abstract Optional<Boolean> skipUncommented();
@AutoValue.Builder
public abstract static class Builder {
abstract Builder context(@Nullable Integer val);
abstract Builder tabSize(@Nullable Integer val);
abstract Builder fontSize(@Nullable Integer val);
abstract Builder lineLength(@Nullable Integer val);
abstract Builder cursorBlinkRate(@Nullable Integer val);
abstract Builder expandAllComments(@Nullable Boolean val);
abstract Builder intralineDifference(@Nullable Boolean val);
abstract Builder manualReview(@Nullable Boolean val);
abstract Builder showLineEndings(@Nullable Boolean val);
abstract Builder showTabs(@Nullable Boolean val);
abstract Builder showWhitespaceErrors(@Nullable Boolean val);
abstract Builder syntaxHighlighting(@Nullable Boolean val);
abstract Builder hideTopMenu(@Nullable Boolean val);
abstract Builder autoHideDiffTableHeader(@Nullable Boolean val);
abstract Builder hideLineNumbers(@Nullable Boolean val);
abstract Builder renderEntireFile(@Nullable Boolean val);
abstract Builder hideEmptyPane(@Nullable Boolean val);
abstract Builder matchBrackets(@Nullable Boolean val);
abstract Builder lineWrapping(@Nullable Boolean val);
abstract Builder ignoreWhitespace(@Nullable Whitespace val);
abstract Builder retainHeader(@Nullable Boolean val);
abstract Builder skipDeleted(@Nullable Boolean val);
abstract Builder skipUnchanged(@Nullable Boolean val);
abstract Builder skipUncommented(@Nullable Boolean val);
abstract Diff build();
}
public static Diff fromInfo(DiffPreferencesInfo info) {
return (new AutoValue_Preferences_Diff.Builder())
.context(info.context)
.tabSize(info.tabSize)
.fontSize(info.fontSize)
.lineLength(info.lineLength)
.cursorBlinkRate(info.cursorBlinkRate)
.expandAllComments(info.expandAllComments)
.intralineDifference(info.intralineDifference)
.manualReview(info.manualReview)
.showLineEndings(info.showLineEndings)
.showTabs(info.showTabs)
.showWhitespaceErrors(info.showWhitespaceErrors)
.syntaxHighlighting(info.syntaxHighlighting)
.hideTopMenu(info.hideTopMenu)
.autoHideDiffTableHeader(info.autoHideDiffTableHeader)
.hideLineNumbers(info.hideLineNumbers)
.renderEntireFile(info.renderEntireFile)
.hideEmptyPane(info.hideEmptyPane)
.matchBrackets(info.matchBrackets)
.lineWrapping(info.lineWrapping)
.ignoreWhitespace(info.ignoreWhitespace)
.retainHeader(info.retainHeader)
.skipDeleted(info.skipDeleted)
.skipUnchanged(info.skipUnchanged)
.skipUncommented(info.skipUncommented)
.build();
}
public DiffPreferencesInfo toInfo() {
DiffPreferencesInfo info = new DiffPreferencesInfo();
info.context = context().orElse(null);
info.tabSize = tabSize().orElse(null);
info.fontSize = fontSize().orElse(null);
info.lineLength = lineLength().orElse(null);
info.cursorBlinkRate = cursorBlinkRate().orElse(null);
info.expandAllComments = expandAllComments().orElse(null);
info.intralineDifference = intralineDifference().orElse(null);
info.manualReview = manualReview().orElse(null);
info.showLineEndings = showLineEndings().orElse(null);
info.showTabs = showTabs().orElse(null);
info.showWhitespaceErrors = showWhitespaceErrors().orElse(null);
info.syntaxHighlighting = syntaxHighlighting().orElse(null);
info.hideTopMenu = hideTopMenu().orElse(null);
info.autoHideDiffTableHeader = autoHideDiffTableHeader().orElse(null);
info.hideLineNumbers = hideLineNumbers().orElse(null);
info.renderEntireFile = renderEntireFile().orElse(null);
info.hideEmptyPane = hideEmptyPane().orElse(null);
info.matchBrackets = matchBrackets().orElse(null);
info.lineWrapping = lineWrapping().orElse(null);
info.ignoreWhitespace = ignoreWhitespace().orElse(null);
info.retainHeader = retainHeader().orElse(null);
info.skipDeleted = skipDeleted().orElse(null);
info.skipUnchanged = skipUnchanged().orElse(null);
info.skipUncommented = skipUncommented().orElse(null);
return info;
}
}
}

View File

@ -32,9 +32,6 @@ import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
import com.google.gerrit.proto.Protos;
import com.google.gerrit.server.cache.proto.Cache.ProjectWatchKeyProto;
import com.google.gerrit.server.cache.serialize.CacheSerializer;
import com.google.gerrit.server.git.ValidationError;
import java.util.ArrayList;
import java.util.Collection;
@ -90,26 +87,6 @@ public class ProjectWatches {
public abstract Project.NameKey project();
public abstract @Nullable String filter();
enum Serializer implements CacheSerializer<ProjectWatchKey> {
INSTANCE;
@Override
public byte[] serialize(ProjectWatchKey key) {
ProjectWatchKeyProto.Builder proto =
ProjectWatchKeyProto.newBuilder().setProject(key.project().get());
if (key.filter() != null) {
proto.setFilter(key.filter());
}
return Protos.toByteArray(proto.build());
}
@Override
public ProjectWatchKey deserialize(byte[] in) {
ProjectWatchKeyProto proto = Protos.parseUnchecked(ProjectWatchKeyProto.parser(), in);
return ProjectWatchKey.create(Project.nameKey(proto.getProject()), proto.getFilter());
}
}
}
public enum NotifyType {

View File

@ -184,6 +184,19 @@ public class StoredPreferences {
return cfg;
}
/** Returns the content of the {@code preferences.config} file as {@link Config}. */
Config getRaw() {
return cfg;
}
/**
* Returns the content of the {@code preferences.config} file on {@code
* refs/preferences/defaults}.
*/
Config getRawDefault() {
return defaultCfg;
}
private GeneralPreferencesInfo parseGeneralPreferences(@Nullable GeneralPreferencesInfo input) {
try {
return parseGeneralPreferences(cfg, defaultCfg, input);
@ -224,7 +237,12 @@ public class StoredPreferences {
}
}
private static GeneralPreferencesInfo parseGeneralPreferences(
/**
* Returns a {@link GeneralPreferencesInfo} that is the result of parsing {@code defaultCfg} for
* the server's default configs and {@code cfg} for the user's config. These configs are then
* overlaid to inherit values (default -> user -> input (if provided).
*/
public static GeneralPreferencesInfo parseGeneralPreferences(
Config cfg, @Nullable Config defaultCfg, @Nullable GeneralPreferencesInfo input)
throws ConfigInvalidException {
GeneralPreferencesInfo r =
@ -247,7 +265,12 @@ public class StoredPreferences {
return r;
}
private static DiffPreferencesInfo parseDiffPreferences(
/**
* Returns a {@link DiffPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
* server's default configs and {@code cfg} for the user's config. These configs are then overlaid
* to inherit values (default -> user -> input (if provided).
*/
public static DiffPreferencesInfo parseDiffPreferences(
Config cfg, @Nullable Config defaultCfg, @Nullable DiffPreferencesInfo input)
throws ConfigInvalidException {
return loadSection(
@ -261,7 +284,12 @@ public class StoredPreferences {
input);
}
private static EditPreferencesInfo parseEditPreferences(
/**
* Returns a {@link EditPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
* server's default configs and {@code cfg} for the user's config. These configs are then overlaid
* to inherit values (default -> user -> input (if provided).
*/
public static EditPreferencesInfo parseEditPreferences(
Config cfg, @Nullable Config defaultCfg, @Nullable EditPreferencesInfo input)
throws ConfigInvalidException {
return loadSection(

View File

@ -745,9 +745,6 @@ public class ExternalIdNotes extends VersionedMetaData {
.map(ExternalId::accountId)
.filter(i -> !accountsToSkip.contains(i))
.collect(toSet())) {
if (accountCache != null) {
accountCache.evict(id);
}
if (accountIndexer != null) {
accountIndexer.get().index(id);
}

View File

@ -0,0 +1,97 @@
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.config;
import com.google.auto.value.AutoValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.EditPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.server.account.StoredPreferences;
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
/**
* Container class for preferences serialized as Git-style config files. Keeps the values as {@link
* String}s as they are immutable and thread-safe.
*/
@AutoValue
public abstract class CachedPreferences {
public static CachedPreferences EMPTY = fromString("");
public abstract String config();
/** Returns a cache-able representation of the config. */
public static CachedPreferences fromConfig(Config cfg) {
return new AutoValue_CachedPreferences(cfg.toText());
}
/**
* Returns a cache-able representation of the config. To be used only when constructing a {@link
* CachedPreferences} from a serialized, cached value.
*/
public static CachedPreferences fromString(String cfg) {
return new AutoValue_CachedPreferences(cfg);
}
public static GeneralPreferencesInfo general(
Optional<CachedPreferences> defaultPreferences, CachedPreferences userPreferences) {
try {
return StoredPreferences.parseGeneralPreferences(
asConfig(userPreferences), configOrNull(defaultPreferences), null);
} catch (ConfigInvalidException e) {
return GeneralPreferencesInfo.defaults();
}
}
public static EditPreferencesInfo edit(
Optional<CachedPreferences> defaultPreferences, CachedPreferences userPreferences) {
try {
return StoredPreferences.parseEditPreferences(
asConfig(userPreferences), configOrNull(defaultPreferences), null);
} catch (ConfigInvalidException e) {
return EditPreferencesInfo.defaults();
}
}
public static DiffPreferencesInfo diff(
Optional<CachedPreferences> defaultPreferences, CachedPreferences userPreferences) {
try {
return StoredPreferences.parseDiffPreferences(
asConfig(userPreferences), configOrNull(defaultPreferences), null);
} catch (ConfigInvalidException e) {
return DiffPreferencesInfo.defaults();
}
}
@Nullable
private static Config configOrNull(Optional<CachedPreferences> cachedPreferences) {
return cachedPreferences.map(CachedPreferences::asConfig).orElse(null);
}
private static Config asConfig(CachedPreferences cachedPreferences) {
Config cfg = new Config();
try {
cfg.fromText(cachedPreferences.config());
} catch (ConfigInvalidException e) {
// Programmer error: We have parsed this config before and are unable to parse it now.
throw new StorageException(e);
}
return cfg;
}
}

View File

@ -83,7 +83,6 @@ public class AccountIndexerImpl implements AccountIndexer {
@Override
public void index(Account.Id id) {
byIdCache.evict(id);
Optional<AccountState> accountState = byIdCache.get(id);
if (accountState.isPresent()) {

View File

@ -90,7 +90,6 @@ public class AllAccountsIndexer extends SiteIndexer<Account.Id, AccountState, Ac
executor.submit(
() -> {
try {
accountCache.evict(id);
Optional<AccountState> a = accountCache.get(id);
if (a.isPresent()) {
index.replace(a.get());

View File

@ -94,7 +94,6 @@ public class ReindexAfterRefUpdate implements GitReferenceUpdatedListener {
if (allUsersName.get().equals(event.getProjectName())) {
Account.Id accountId = Account.Id.fromRef(event.getRefName());
if (accountId != null && !event.getRefName().startsWith(RefNames.REFS_STARRED_CHANGES)) {
accountCache.evict(accountId);
indexer.get().index(accountId);
}
}

View File

@ -67,7 +67,6 @@ public class SetDiffPreferences implements RestModifyView<ConfigResource, DiffPr
try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
DiffPreferencesInfo updatedPrefs = StoredPreferences.updateDefaultDiffPreferences(md, input);
accountCache.evictAll();
return Response.ok(updatedPrefs);
}
}

View File

@ -67,7 +67,6 @@ public class SetEditPreferences implements RestModifyView<ConfigResource, EditPr
try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
EditPreferencesInfo updatedPrefs = StoredPreferences.updateDefaultEditPreferences(md, input);
accountCache.evictAll();
return Response.ok(updatedPrefs);
}
}

View File

@ -64,7 +64,6 @@ public class SetPreferences implements RestModifyView<ConfigResource, GeneralPre
try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
GeneralPreferencesInfo updatedPrefs =
StoredPreferences.updateDefaultGeneralPreferences(md, input);
accountCache.evictAll();
return Response.ok(updatedPrefs);
}
}

View File

@ -16,7 +16,6 @@ package com.google.gerrit.testing;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
@ -61,18 +60,6 @@ public class FakeAccountCache implements AccountCache {
throw new UnsupportedOperationException();
}
@Override
public synchronized void evict(@Nullable Account.Id accountId) {
if (byId != null) {
byId.remove(accountId);
}
}
@Override
public synchronized void evictAll() {
byId.clear();
}
public synchronized void put(Account account) {
AccountState state = newState(account);
byId.put(account.id(), state);

View File

@ -20,9 +20,7 @@ import static org.mockito.Mockito.only;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
@ -253,130 +251,6 @@ public class ProjectResetterTest {
verify(projectCache, only()).evict(project2);
}
@Test
public void accountEvictionIfUserBranchIsReset() throws Exception {
Account.Id accountId = Account.id(1);
Project.NameKey allUsers = Project.nameKey(AllUsersNameProvider.DEFAULT);
Repository allUsersRepo = repoManager.createRepository(allUsers);
Ref userBranch = createRef(allUsersRepo, RefNames.refsUsers(accountId));
AccountCache accountCache = mock(AccountCache.class);
AccountIndexer accountIndexer = mock(AccountIndexer.class);
// Non-user branch because it's not in All-Users.
Ref nonUserBranch = createRef(RefNames.refsUsers(Account.id(2)));
try (ProjectResetter resetProject =
builder(null, accountCache, accountIndexer, null, null, null, null)
.build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
updateRef(nonUserBranch);
updateRef(allUsersRepo, userBranch);
}
verify(accountCache, only()).evict(accountId);
verify(accountIndexer, only()).index(accountId);
}
@Test
public void accountEvictionIfUserBranchIsDeleted() throws Exception {
Account.Id accountId = Account.id(1);
Project.NameKey allUsers = Project.nameKey(AllUsersNameProvider.DEFAULT);
Repository allUsersRepo = repoManager.createRepository(allUsers);
AccountCache accountCache = mock(AccountCache.class);
AccountIndexer accountIndexer = mock(AccountIndexer.class);
try (ProjectResetter resetProject =
builder(null, accountCache, accountIndexer, null, null, null, null)
.build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
// Non-user branch because it's not in All-Users.
createRef(RefNames.refsUsers(Account.id(2)));
createRef(allUsersRepo, RefNames.refsUsers(accountId));
}
verify(accountCache, only()).evict(accountId);
verify(accountIndexer, only()).index(accountId);
}
@Test
public void accountEvictionIfExternalIdsBranchIsReset() throws Exception {
Account.Id accountId = Account.id(1);
Project.NameKey allUsers = Project.nameKey(AllUsersNameProvider.DEFAULT);
Repository allUsersRepo = repoManager.createRepository(allUsers);
Ref externalIds = createRef(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
createRef(allUsersRepo, RefNames.refsUsers(accountId));
Account.Id accountId2 = Account.id(2);
AccountCache accountCache = mock(AccountCache.class);
AccountIndexer accountIndexer = mock(AccountIndexer.class);
// Non-user branch because it's not in All-Users.
Ref nonUserBranch = createRef(RefNames.refsUsers(Account.id(3)));
try (ProjectResetter resetProject =
builder(null, accountCache, accountIndexer, null, null, null, null)
.build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
updateRef(nonUserBranch);
updateRef(allUsersRepo, externalIds);
createRef(allUsersRepo, RefNames.refsUsers(accountId2));
}
verify(accountCache).evict(accountId);
verify(accountCache).evict(accountId2);
verify(accountIndexer).index(accountId);
verify(accountIndexer).index(accountId2);
verifyNoMoreInteractions(accountCache, accountIndexer);
}
@Test
public void accountEvictionIfExternalIdsBranchIsDeleted() throws Exception {
Account.Id accountId = Account.id(1);
Project.NameKey allUsers = Project.nameKey(AllUsersNameProvider.DEFAULT);
Repository allUsersRepo = repoManager.createRepository(allUsers);
createRef(allUsersRepo, RefNames.refsUsers(accountId));
Account.Id accountId2 = Account.id(2);
AccountCache accountCache = mock(AccountCache.class);
AccountIndexer accountIndexer = mock(AccountIndexer.class);
// Non-user branch because it's not in All-Users.
Ref nonUserBranch = createRef(RefNames.refsUsers(Account.id(3)));
try (ProjectResetter resetProject =
builder(null, accountCache, accountIndexer, null, null, null, null)
.build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
updateRef(nonUserBranch);
createRef(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
createRef(allUsersRepo, RefNames.refsUsers(accountId2));
}
verify(accountCache).evict(accountId);
verify(accountCache).evict(accountId2);
verify(accountIndexer).index(accountId);
verify(accountIndexer).index(accountId2);
verifyNoMoreInteractions(accountCache, accountIndexer);
}
@Test
public void accountEvictionFromAccountCreatorIfUserBranchIsDeleted() throws Exception {
Account.Id accountId = Account.id(1);
Project.NameKey allUsers = Project.nameKey(AllUsersNameProvider.DEFAULT);
Repository allUsersRepo = repoManager.createRepository(allUsers);
AccountCreator accountCreator = mock(AccountCreator.class);
try (ProjectResetter resetProject =
builder(accountCreator, null, null, null, null, null, null)
.build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
createRef(allUsersRepo, RefNames.refsUsers(accountId));
}
verify(accountCreator, only()).evict(ImmutableSet.of(accountId));
}
@Test
public void groupEviction() throws Exception {
AccountGroup.UUID uuid1 = AccountGroup.uuid("abcd1");

View File

@ -47,7 +47,6 @@ import static java.util.stream.Collectors.toSet;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.github.rholder.retry.StopStrategies;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
@ -143,7 +142,6 @@ import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import com.jcraft.jsch.KeyPair;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@ -218,10 +216,6 @@ public class AccountIT extends AbstractDaemonTest {
@Inject protected Emails emails;
@Inject
@Named("accounts")
private LoadingCache<Account.Id, AccountState> accountsCache;
@Inject private AccountOperations accountOperations;
@Inject protected GroupOperations groupOperations;
@ -2408,10 +2402,6 @@ public class AccountIT extends AbstractDaemonTest {
}
private void assertStaleAccountAndReindex(Account.Id accountId) throws IOException {
// Evict account from cache to be sure that we use the index state for staleness checks. This
// has to happen directly on the accounts cache because AccountCacheImpl triggers a reindex for
// the account.
accountsCache.invalidate(accountId);
assertThat(stalenessChecker.check(accountId).isStale()).isTrue();
// Reindex fixes staleness

View File

@ -85,18 +85,6 @@ public class AccountIndexerIT {
assertThat(matchedAccountStates.get(0).account().id()).isEqualTo(accountId);
}
@Test
public void indexingUpdatesStaleCache() throws Exception {
Account.Id accountId = createAccount("foo");
loadAccountToCache(accountId);
String status = "ooo";
updateAccountWithoutCacheOrIndex(accountId, newAccountUpdate().setStatus(status).build());
assertThat(accountCache.get(accountId).get().account().status()).isNull();
accountIndexer.index(accountId);
assertThat(accountCache.get(accountId).get().account().status()).isEqualTo(status);
}
@Test
public void reindexingStaleAccountUpdatesTheIndex() throws Exception {
Account.Id accountId = createAccount("foo");
@ -140,7 +128,6 @@ public class AccountIndexerIT {
}
private void reloadAccountToCache(Account.Id accountId) {
accountCache.evict(accountId);
loadAccountToCache(accountId);
}

View File

@ -1,64 +0,0 @@
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.entities;
import com.google.common.truth.Truth;
import com.google.common.truth.extensions.proto.ProtoTruth;
import com.google.gerrit.server.cache.proto.Cache.AccountProto;
import java.sql.Timestamp;
import java.time.Instant;
import org.junit.Test;
/**
* Test to ensure that we are serializing and deserializing {@link Account} correctly. This is part
* of the {@code AccountCache}.
*/
public class AccountCacheTest {
@Test
public void roundTrip() throws Exception {
Account account =
Account.builder(Account.id(1), Timestamp.from(Instant.EPOCH))
.setFullName("foo bar")
.setDisplayName("foo")
.setActive(false)
.setMetaId("dead..beef")
.setStatus("OOO")
.setPreferredEmail("foo@bar.tld")
.build();
byte[] serialized = Account.Serializer.INSTANCE.serialize(account);
ProtoTruth.assertThat(AccountProto.parseFrom(serialized))
.isEqualTo(
AccountProto.newBuilder()
.setId(1)
.setRegisteredOn(0)
.setFullName("foo bar")
.setDisplayName("foo")
.setInactive(true)
.setMetaId("dead..beef")
.setStatus("OOO")
.setPreferredEmail("foo@bar.tld")
.build());
Truth.assertThat(Account.Serializer.INSTANCE.deserialize(serialized)).isEqualTo(account);
}
@Test
public void roundTripNullFields() throws Exception {
Account account = Account.builder(Account.id(1), Timestamp.from(Instant.EPOCH)).build();
byte[] serialized = Account.Serializer.INSTANCE.serialize(account);
ProtoTruth.assertThat(AccountProto.parseFrom(serialized))
.isEqualTo(AccountProto.newBuilder().setId(1).setRegisteredOn(0).build());
Truth.assertThat(Account.Serializer.INSTANCE.deserialize(serialized)).isEqualTo(account);
}
}

View File

@ -0,0 +1,145 @@
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.account;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.truth.Truth;
import com.google.common.truth.extensions.proto.ProtoTruth;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.cache.proto.Cache;
import com.google.gerrit.server.config.CachedPreferences;
import java.sql.Timestamp;
import java.time.Instant;
import org.junit.Test;
/**
* Test to ensure that we are serializing and deserializing {@link Account} correctly. This is part
* of the {@code AccountCache}.
*/
public class AccountCacheTest {
private static final Account ACCOUNT =
Account.builder(Account.id(1), Timestamp.from(Instant.EPOCH)).build();
private static final Cache.AccountProto ACCOUNT_PROTO =
Cache.AccountProto.newBuilder().setId(1).setRegisteredOn(0).build();
private static final CachedAccountDetails.Serializer SERIALIZER =
CachedAccountDetails.Serializer.INSTANCE;
@Test
public void account_roundTrip() throws Exception {
Account account =
Account.builder(Account.id(1), Timestamp.from(Instant.EPOCH))
.setFullName("foo bar")
.setDisplayName("foo")
.setActive(false)
.setMetaId("dead..beef")
.setStatus("OOO")
.setPreferredEmail("foo@bar.tld")
.build();
CachedAccountDetails original =
CachedAccountDetails.create(account, ImmutableMap.of(), CachedPreferences.fromString(""));
byte[] serialized = SERIALIZER.serialize(original);
Cache.AccountDetailsProto expected =
Cache.AccountDetailsProto.newBuilder()
.setAccount(
Cache.AccountProto.newBuilder()
.setId(1)
.setRegisteredOn(0)
.setFullName("foo bar")
.setDisplayName("foo")
.setInactive(true)
.setMetaId("dead..beef")
.setStatus("OOO")
.setPreferredEmail("foo@bar.tld"))
.build();
ProtoTruth.assertThat(Cache.AccountDetailsProto.parseFrom(serialized)).isEqualTo(expected);
Truth.assertThat(SERIALIZER.deserialize(serialized)).isEqualTo(original);
}
@Test
public void account_roundTripNullFields() throws Exception {
CachedAccountDetails original =
CachedAccountDetails.create(ACCOUNT, ImmutableMap.of(), CachedPreferences.fromString(""));
byte[] serialized = SERIALIZER.serialize(original);
Cache.AccountDetailsProto expected =
Cache.AccountDetailsProto.newBuilder().setAccount(ACCOUNT_PROTO).build();
ProtoTruth.assertThat(Cache.AccountDetailsProto.parseFrom(serialized)).isEqualTo(expected);
Truth.assertThat(SERIALIZER.deserialize(serialized)).isEqualTo(original);
}
@Test
public void config_roundTrip() throws Exception {
CachedAccountDetails original =
CachedAccountDetails.create(
ACCOUNT, ImmutableMap.of(), CachedPreferences.fromString("[general]\n\tfoo = bar"));
byte[] serialized = SERIALIZER.serialize(original);
Cache.AccountDetailsProto expected =
Cache.AccountDetailsProto.newBuilder()
.setAccount(ACCOUNT_PROTO)
.setUserPreferences("[general]\n\tfoo = bar")
.build();
ProtoTruth.assertThat(Cache.AccountDetailsProto.parseFrom(serialized)).isEqualTo(expected);
Truth.assertThat(SERIALIZER.deserialize(serialized)).isEqualTo(original);
}
@Test
public void projectWatch_roundTrip() throws Exception {
ProjectWatches.ProjectWatchKey key =
ProjectWatches.ProjectWatchKey.create(Project.nameKey("pro/ject"), "*");
CachedAccountDetails original =
CachedAccountDetails.create(
ACCOUNT,
ImmutableMap.of(key, ImmutableSet.of(ProjectWatches.NotifyType.ALL_COMMENTS)),
CachedPreferences.fromString(""));
byte[] serialized = SERIALIZER.serialize(original);
Cache.AccountDetailsProto expected =
Cache.AccountDetailsProto.newBuilder()
.setAccount(ACCOUNT_PROTO)
.addProjectWatchProto(
Cache.ProjectWatchProto.newBuilder()
.setProject("pro/ject")
.setFilter("*")
.addNotifyType("ALL_COMMENTS"))
.build();
ProtoTruth.assertThat(Cache.AccountDetailsProto.parseFrom(serialized)).isEqualTo(expected);
Truth.assertThat(SERIALIZER.deserialize(serialized)).isEqualTo(original);
}
@Test
public void projectWatch_roundTripNullFilter() throws Exception {
ProjectWatches.ProjectWatchKey key =
ProjectWatches.ProjectWatchKey.create(Project.nameKey("pro/ject"), null);
CachedAccountDetails original =
CachedAccountDetails.create(
ACCOUNT,
ImmutableMap.of(key, ImmutableSet.of(ProjectWatches.NotifyType.ALL_COMMENTS)),
CachedPreferences.fromString(""));
byte[] serialized = SERIALIZER.serialize(original);
Cache.AccountDetailsProto expected =
Cache.AccountDetailsProto.newBuilder()
.setAccount(ACCOUNT_PROTO)
.addProjectWatchProto(
Cache.ProjectWatchProto.newBuilder()
.setProject("pro/ject")
.addNotifyType("ALL_COMMENTS"))
.build();
ProtoTruth.assertThat(Cache.AccountDetailsProto.parseFrom(serialized)).isEqualTo(expected);
Truth.assertThat(SERIALIZER.deserialize(serialized)).isEqualTo(original);
}
}

View File

@ -1,50 +0,0 @@
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.account;
import static com.google.common.truth.Truth.assertThat;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.EditPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.json.OutputFormat;
import com.google.gson.Gson;
import org.junit.Test;
public class PreferencesTest {
private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
@Test
public void generalPreferencesRoundTrip() {
GeneralPreferencesInfo original = GeneralPreferencesInfo.defaults();
assertThat(GSON.toJson(original))
.isEqualTo(GSON.toJson(Preferences.General.fromInfo(original).toInfo()));
}
@Test
public void diffPreferencesRoundTrip() {
DiffPreferencesInfo original = DiffPreferencesInfo.defaults();
assertThat(GSON.toJson(original))
.isEqualTo(GSON.toJson(Preferences.Diff.fromInfo(original).toInfo()));
}
@Test
public void editPreferencesRoundTrip() {
EditPreferencesInfo original = EditPreferencesInfo.defaults();
assertThat(GSON.toJson(original))
.isEqualTo(GSON.toJson(Preferences.Edit.fromInfo(original).toInfo()));
}
}

View File

@ -1,50 +0,0 @@
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.account;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.truth.extensions.proto.ProtoTruth;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.cache.proto.Cache.ProjectWatchKeyProto;
import org.junit.Test;
/**
* Test to ensure that we are serializing and deserializing {@link ProjectWatches.ProjectWatchKey}
* correctly. This is part of the {@code AccountCache}.
*/
public class ProjectWatchCacheTest {
@Test
public void keyRoundTrip() throws Exception {
ProjectWatches.ProjectWatchKey key =
ProjectWatches.ProjectWatchKey.create(Project.nameKey("pro/ject"), "*");
byte[] serialized = ProjectWatches.ProjectWatchKey.Serializer.INSTANCE.serialize(key);
ProtoTruth.assertThat(ProjectWatchKeyProto.parseFrom(serialized))
.isEqualTo(ProjectWatchKeyProto.newBuilder().setProject("pro/ject").setFilter("*").build());
assertThat(ProjectWatches.ProjectWatchKey.Serializer.INSTANCE.deserialize(serialized))
.isEqualTo(key);
}
@Test
public void keyRoundTripNullFilter() throws Exception {
ProjectWatches.ProjectWatchKey key =
ProjectWatches.ProjectWatchKey.create(Project.nameKey("pro/ject"), null);
byte[] serialized = ProjectWatches.ProjectWatchKey.Serializer.INSTANCE.serialize(key);
ProtoTruth.assertThat(ProjectWatchKeyProto.parseFrom(serialized))
.isEqualTo(ProjectWatchKeyProto.newBuilder().setProject("pro/ject").build());
assertThat(ProjectWatches.ProjectWatchKey.Serializer.INSTANCE.deserialize(serialized))
.isEqualTo(key);
}
}

View File

@ -790,7 +790,6 @@ public abstract class AbstractQueryAccountsTest extends GerritServerTests {
for (String email : emails) {
accountManager.link(id, AuthRequest.forEmail(email));
}
accountCache.evict(id);
accountIndexer.index(id);
}

View File

@ -287,11 +287,12 @@ message PureRevertKeyProto {
bytes claimed_revert = 3;
}
// Key for com.google.gerrit.server.account.ProjectWatches.ProjectWatcheKey.
// Next ID: 3
message ProjectWatchKeyProto {
// Key for com.google.gerrit.server.account.ProjectWatches.
// Next ID: 4
message ProjectWatchProto {
string project = 1;
string filter = 2;
repeated string notify_type = 3;
}
// Serialized form of
@ -307,3 +308,18 @@ message AccountProto {
string status = 7;
string meta_id = 8;
}
// Serialized form of com.google.gerrit.server.account.CachedAccountDetails.Key.
// Next ID: 3
message AccountKeyProto {
int32 account_id = 1;
bytes id = 2;
}
// Serialized form of com.google.gerrit.server.account.CachedAccountDetails.
// Next ID: 4
message AccountDetailsProto {
AccountProto account = 1;
repeated ProjectWatchProto project_watch_proto = 2;
string user_preferences = 3;
}