blob: 6f4fce9434ac47a585495ce72b397a23cd407880 [file] [log] [blame]
// Copyright (C) 2009 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.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.proto.Protos;
import com.google.gerrit.server.cache.CacheModule;
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.cache.serialize.ProtobufSerializer;
import com.google.gerrit.server.cache.serialize.entities.InternalGroupSerializer;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.group.db.Groups;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.query.group.InternalGroupQuery;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import org.bouncycastle.util.Strings;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
/** Tracks group objects in memory for efficient access. */
@Singleton
public class GroupCacheImpl implements GroupCache {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String BYID_NAME = "groups";
private static final String BYNAME_NAME = "groups_byname";
private static final String BYUUID_NAME = "groups_byuuid";
private static final String BYUUID_NAME_PERSISTED = "groups_byuuid_persisted";
public static Module module() {
return new CacheModule() {
@Override
protected void configure() {
cache(BYID_NAME, AccountGroup.Id.class, new TypeLiteral<Optional<InternalGroup>>() {})
.maximumWeight(Long.MAX_VALUE)
.loader(ByIdLoader.class);
cache(BYNAME_NAME, String.class, new TypeLiteral<Optional<InternalGroup>>() {})
.maximumWeight(Long.MAX_VALUE)
.loader(ByNameLoader.class);
// We split the group cache into two parts for performance reasons:
// 1) An in-memory part that has only the group ref uuid as key.
// 2) A persisted part that has the group ref uuid and sha1 of the ref as key.
//
// When loading dashboards or returning change query results we potentially
// need to access many groups.
// We want the persisted cache to be immutable and we want it to be impossible that a
// value for a given key is out of date. We therefore require the sha-1 in the key. That
// is in line with the rest of the caches in Gerrit.
//
// Splitting the cache into two chunks internally in this class allows us to retain
// the existing performance guarantees of not requiring reads for the repo for values
// cached in-memory but also to persist the cache which leads to a much improved
// cold-start behavior and in-memory miss latency.
cache(BYUUID_NAME, String.class, new TypeLiteral<Optional<InternalGroup>>() {})
.maximumWeight(Long.MAX_VALUE)
.loader(ByUUIDInMemoryLoader.class);
persist(
BYUUID_NAME_PERSISTED,
Cache.GroupKeyProto.class,
new TypeLiteral<InternalGroup>() {})
.loader(PersistedByUUIDLoader.class)
.keySerializer(new ProtobufSerializer<>(Cache.GroupKeyProto.parser()))
.valueSerializer(PersistedInternalGroupSerializer.INSTANCE)
.diskLimit(1 << 30) // 1 GiB
.version(1)
.maximumWeight(0);
bind(GroupCacheImpl.class);
bind(GroupCache.class).to(GroupCacheImpl.class);
}
};
}
private final LoadingCache<AccountGroup.Id, Optional<InternalGroup>> byId;
private final LoadingCache<String, Optional<InternalGroup>> byName;
private final LoadingCache<String, Optional<InternalGroup>> byUUID;
private final LoadingCache<Cache.GroupKeyProto, InternalGroup> persistedByUuidCache;
@Inject
GroupCacheImpl(
@Named(BYID_NAME) LoadingCache<AccountGroup.Id, Optional<InternalGroup>> byId,
@Named(BYNAME_NAME) LoadingCache<String, Optional<InternalGroup>> byName,
@Named(BYUUID_NAME) LoadingCache<String, Optional<InternalGroup>> byUUID,
@Named(BYUUID_NAME_PERSISTED)
LoadingCache<Cache.GroupKeyProto, InternalGroup> persistedByUuidCache) {
this.byId = byId;
this.byName = byName;
this.byUUID = byUUID;
this.persistedByUuidCache = persistedByUuidCache;
}
@Override
public Optional<InternalGroup> get(AccountGroup.Id groupId) {
try {
return byId.get(groupId);
} catch (ExecutionException e) {
logger.atWarning().withCause(e).log("Cannot load group %s", groupId);
return Optional.empty();
}
}
@Override
public Optional<InternalGroup> get(AccountGroup.NameKey name) {
if (name == null) {
return Optional.empty();
}
try {
return byName.get(name.get());
} catch (ExecutionException e) {
logger.atWarning().withCause(e).log("Cannot look up group %s by name", name.get());
return Optional.empty();
}
}
@Override
public Optional<InternalGroup> get(AccountGroup.UUID groupUuid) {
if (groupUuid == null) {
return Optional.empty();
}
try {
return byUUID.get(groupUuid.get());
} catch (ExecutionException e) {
logger.atWarning().withCause(e).log("Cannot look up group %s by uuid", groupUuid.get());
return Optional.empty();
}
}
@Override
public Map<AccountGroup.UUID, InternalGroup> get(Collection<AccountGroup.UUID> groupUuids) {
try {
Set<String> groupUuidsStringSet =
groupUuids.stream().map(u -> u.get()).collect(toImmutableSet());
return byUUID.getAll(groupUuidsStringSet).entrySet().stream()
.filter(g -> g.getValue().isPresent())
.collect(toImmutableMap(g -> AccountGroup.uuid(g.getKey()), g -> g.getValue().get()));
} catch (ExecutionException e) {
logger.atWarning().withCause(e).log("Cannot look up groups %s by uuids", groupUuids);
return ImmutableMap.of();
}
}
@Override
public InternalGroup getFromMetaId(AccountGroup.UUID groupUuid, ObjectId metaId)
throws StorageException {
Cache.GroupKeyProto key =
Cache.GroupKeyProto.newBuilder()
.setUuid(groupUuid.get())
.setRevision(ObjectIdConverter.create().toByteString(metaId))
.build();
try {
return persistedByUuidCache.get(key);
} catch (ExecutionException e) {
throw new StorageException(e);
}
}
@Override
public void evict(AccountGroup.Id groupId) {
if (groupId != null) {
logger.atFine().log("Evict group %s by ID", groupId.get());
byId.invalidate(groupId);
}
}
@Override
public void evict(AccountGroup.NameKey groupName) {
if (groupName != null) {
logger.atFine().log("Evict group '%s' by name", groupName.get());
byName.invalidate(groupName.get());
}
}
@Override
public void evict(AccountGroup.UUID groupUuid) {
if (groupUuid != null) {
logger.atFine().log("Evict group %s by UUID", groupUuid.get());
byUUID.invalidate(groupUuid.get());
}
}
@Override
public void evict(Collection<AccountGroup.UUID> groupUuids) {
if (groupUuids != null && !groupUuids.isEmpty()) {
logger.atFine().log("Evict groups %s by UUID", groupUuids);
byUUID.invalidateAll(groupUuids);
}
}
static class ByIdLoader extends CacheLoader<AccountGroup.Id, Optional<InternalGroup>> {
private final Provider<InternalGroupQuery> groupQueryProvider;
@Inject
ByIdLoader(Provider<InternalGroupQuery> groupQueryProvider) {
this.groupQueryProvider = groupQueryProvider;
}
@Override
public Optional<InternalGroup> load(AccountGroup.Id key) throws Exception {
try (TraceTimer ignored =
TraceContext.newTimer(
"Loading group by ID", Metadata.builder().groupId(key.get()).build())) {
return groupQueryProvider.get().byId(key);
}
}
}
static class ByNameLoader extends CacheLoader<String, Optional<InternalGroup>> {
private final Provider<InternalGroupQuery> groupQueryProvider;
@Inject
ByNameLoader(Provider<InternalGroupQuery> groupQueryProvider) {
this.groupQueryProvider = groupQueryProvider;
}
@Override
public Optional<InternalGroup> load(String name) throws Exception {
try (TraceTimer ignored =
TraceContext.newTimer(
"Loading group by name", Metadata.builder().groupName(name).build())) {
return groupQueryProvider.get().byName(AccountGroup.nameKey(name));
}
}
}
static class ByUUIDInMemoryLoader extends CacheLoader<String, Optional<InternalGroup>> {
private final LoadingCache<Cache.GroupKeyProto, InternalGroup> persistedCache;
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
@Inject
ByUUIDInMemoryLoader(
@Named(BYUUID_NAME_PERSISTED)
LoadingCache<Cache.GroupKeyProto, InternalGroup> persistedCache,
GitRepositoryManager repoManager,
AllUsersName allUsersName) {
this.persistedCache = persistedCache;
this.repoManager = repoManager;
this.allUsersName = allUsersName;
}
@Override
public Optional<InternalGroup> load(String uuid) throws Exception {
return loadAll(ImmutableSet.of(uuid)).get(uuid);
}
@Override
public Map<String, Optional<InternalGroup>> loadAll(Iterable<? extends String> uuids)
throws Exception {
Map<String, Optional<InternalGroup>> toReturn = new HashMap<>();
if (Iterables.isEmpty(uuids)) {
return toReturn;
}
Iterator<? extends String> uuidIterator = uuids.iterator();
List<Cache.GroupKeyProto> keyList = new ArrayList<>();
try (TraceTimer ignored =
TraceContext.newTimer(
"Loading group from serialized cache",
Metadata.builder().cacheName(BYUUID_NAME_PERSISTED).build());
Repository allUsers = repoManager.openRepository(allUsersName)) {
while (uuidIterator.hasNext()) {
String currentUuid = uuidIterator.next();
String ref = RefNames.refsGroups(AccountGroup.uuid(currentUuid));
Ref sha1 = allUsers.exactRef(ref);
if (sha1 == null) {
toReturn.put(currentUuid, Optional.empty());
continue;
}
Cache.GroupKeyProto key =
Cache.GroupKeyProto.newBuilder()
.setUuid(currentUuid)
.setRevision(ObjectIdConverter.create().toByteString(sha1.getObjectId()))
.build();
keyList.add(key);
}
}
persistedCache.getAll(keyList).entrySet().stream()
.forEach(g -> toReturn.put(g.getKey().getUuid(), Optional.of(g.getValue())));
return toReturn;
}
}
static class PersistedByUUIDLoader extends CacheLoader<Cache.GroupKeyProto, InternalGroup> {
private final Groups groups;
@Inject
PersistedByUUIDLoader(Groups groups) {
this.groups = groups;
}
@Override
public InternalGroup load(Cache.GroupKeyProto key) throws Exception {
try (TraceTimer ignored =
TraceContext.newTimer(
"Loading group by UUID", Metadata.builder().groupUuid(key.getUuid()).build())) {
ObjectId sha1 = ObjectIdConverter.create().fromByteString(key.getRevision());
Optional<InternalGroup> loadedGroup =
groups.getGroup(AccountGroup.uuid(key.getUuid()), sha1);
if (!loadedGroup.isPresent()) {
throw new IllegalStateException(
String.format(
"group %s should have the sha-1 %s, but " + "it was not found",
key.getUuid(), sha1.getName()));
}
return loadedGroup.get();
}
}
}
private enum PersistedInternalGroupSerializer implements CacheSerializer<InternalGroup> {
INSTANCE;
@Override
public byte[] serialize(InternalGroup value) {
if (value == null) {
return new byte[0];
}
return Protos.toByteArray(InternalGroupSerializer.serialize(value));
}
@Nullable
@Override
public InternalGroup deserialize(byte[] in) {
if (Strings.fromByteArray(in).isEmpty()) {
return null;
}
return InternalGroupSerializer.deserialize(
Protos.parseUnchecked(Cache.InternalGroupProto.parser(), in));
}
}
}