LCOV - code coverage report
Current view: top level - server/account - GroupCacheImpl.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 115 135 85.2 %
Date: 2022-11-19 15:00:39 Functions: 34 34 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2009 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.account;
      16             : 
      17             : import static com.google.common.collect.ImmutableMap.toImmutableMap;
      18             : import static com.google.common.collect.ImmutableSet.toImmutableSet;
      19             : 
      20             : import com.google.common.cache.CacheLoader;
      21             : import com.google.common.cache.LoadingCache;
      22             : import com.google.common.collect.ImmutableMap;
      23             : import com.google.common.collect.ImmutableSet;
      24             : import com.google.common.collect.Iterables;
      25             : import com.google.common.flogger.FluentLogger;
      26             : import com.google.gerrit.common.Nullable;
      27             : import com.google.gerrit.entities.AccountGroup;
      28             : import com.google.gerrit.entities.InternalGroup;
      29             : import com.google.gerrit.entities.RefNames;
      30             : import com.google.gerrit.proto.Protos;
      31             : import com.google.gerrit.server.cache.CacheModule;
      32             : import com.google.gerrit.server.cache.proto.Cache;
      33             : import com.google.gerrit.server.cache.serialize.CacheSerializer;
      34             : import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
      35             : import com.google.gerrit.server.cache.serialize.ProtobufSerializer;
      36             : import com.google.gerrit.server.cache.serialize.entities.InternalGroupSerializer;
      37             : import com.google.gerrit.server.config.AllUsersName;
      38             : import com.google.gerrit.server.git.GitRepositoryManager;
      39             : import com.google.gerrit.server.group.db.Groups;
      40             : import com.google.gerrit.server.logging.Metadata;
      41             : import com.google.gerrit.server.logging.TraceContext;
      42             : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
      43             : import com.google.gerrit.server.query.group.InternalGroupQuery;
      44             : import com.google.inject.Inject;
      45             : import com.google.inject.Module;
      46             : import com.google.inject.Provider;
      47             : import com.google.inject.Singleton;
      48             : import com.google.inject.TypeLiteral;
      49             : import com.google.inject.name.Named;
      50             : import java.util.ArrayList;
      51             : import java.util.Collection;
      52             : import java.util.HashMap;
      53             : import java.util.Iterator;
      54             : import java.util.List;
      55             : import java.util.Map;
      56             : import java.util.Optional;
      57             : import java.util.Set;
      58             : import java.util.concurrent.ExecutionException;
      59             : import org.bouncycastle.util.Strings;
      60             : import org.eclipse.jgit.lib.ObjectId;
      61             : import org.eclipse.jgit.lib.Ref;
      62             : import org.eclipse.jgit.lib.Repository;
      63             : 
      64             : /** Tracks group objects in memory for efficient access. */
      65             : @Singleton
      66             : public class GroupCacheImpl implements GroupCache {
      67         152 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      68             : 
      69             :   private static final String BYID_NAME = "groups";
      70             :   private static final String BYNAME_NAME = "groups_byname";
      71             :   private static final String BYUUID_NAME = "groups_byuuid";
      72             :   private static final String BYUUID_NAME_PERSISTED = "groups_byuuid_persisted";
      73             : 
      74             :   public static Module module() {
      75         152 :     return new CacheModule() {
      76             :       @Override
      77             :       protected void configure() {
      78         152 :         cache(BYID_NAME, AccountGroup.Id.class, new TypeLiteral<Optional<InternalGroup>>() {})
      79         152 :             .maximumWeight(Long.MAX_VALUE)
      80         152 :             .loader(ByIdLoader.class);
      81             : 
      82         152 :         cache(BYNAME_NAME, String.class, new TypeLiteral<Optional<InternalGroup>>() {})
      83         152 :             .maximumWeight(Long.MAX_VALUE)
      84         152 :             .loader(ByNameLoader.class);
      85             : 
      86             :         // We split the group cache into two parts for performance reasons:
      87             :         // 1) An in-memory part that has only the group ref uuid as key.
      88             :         // 2) A persisted part that has the group ref uuid and sha1 of the ref as key.
      89             :         //
      90             :         // When loading dashboards or returning change query results we potentially
      91             :         // need to access many groups.
      92             :         // We want the persisted cache to be immutable and we want it to be impossible that a
      93             :         // value for a given key is out of date. We therefore require the sha-1 in the key. That
      94             :         // is in line with the rest of the caches in Gerrit.
      95             :         //
      96             :         // Splitting the cache into two chunks internally in this class allows us to retain
      97             :         // the existing performance guarantees of not requiring reads for the repo for values
      98             :         // cached in-memory but also to persist the cache which leads to a much improved
      99             :         // cold-start behavior and in-memory miss latency.
     100             : 
     101         152 :         cache(BYUUID_NAME, String.class, new TypeLiteral<Optional<InternalGroup>>() {})
     102         152 :             .maximumWeight(Long.MAX_VALUE)
     103         152 :             .loader(ByUUIDInMemoryLoader.class);
     104             : 
     105         152 :         persist(
     106             :                 BYUUID_NAME_PERSISTED,
     107             :                 Cache.GroupKeyProto.class,
     108         152 :                 new TypeLiteral<InternalGroup>() {})
     109         152 :             .loader(PersistedByUUIDLoader.class)
     110         152 :             .keySerializer(new ProtobufSerializer<>(Cache.GroupKeyProto.parser()))
     111         152 :             .valueSerializer(PersistedInternalGroupSerializer.INSTANCE)
     112         152 :             .diskLimit(1 << 30) // 1 GiB
     113         152 :             .version(1)
     114         152 :             .maximumWeight(0);
     115             : 
     116         152 :         bind(GroupCacheImpl.class);
     117         152 :         bind(GroupCache.class).to(GroupCacheImpl.class);
     118         152 :       }
     119             :     };
     120             :   }
     121             : 
     122             :   private final LoadingCache<AccountGroup.Id, Optional<InternalGroup>> byId;
     123             :   private final LoadingCache<String, Optional<InternalGroup>> byName;
     124             :   private final LoadingCache<String, Optional<InternalGroup>> byUUID;
     125             : 
     126             :   @Inject
     127             :   GroupCacheImpl(
     128             :       @Named(BYID_NAME) LoadingCache<AccountGroup.Id, Optional<InternalGroup>> byId,
     129             :       @Named(BYNAME_NAME) LoadingCache<String, Optional<InternalGroup>> byName,
     130         151 :       @Named(BYUUID_NAME) LoadingCache<String, Optional<InternalGroup>> byUUID) {
     131         151 :     this.byId = byId;
     132         151 :     this.byName = byName;
     133         151 :     this.byUUID = byUUID;
     134         151 :   }
     135             : 
     136             :   @Override
     137             :   public Optional<InternalGroup> get(AccountGroup.Id groupId) {
     138             :     try {
     139           7 :       return byId.get(groupId);
     140           0 :     } catch (ExecutionException e) {
     141           0 :       logger.atWarning().withCause(e).log("Cannot load group %s", groupId);
     142           0 :       return Optional.empty();
     143             :     }
     144             :   }
     145             : 
     146             :   @Override
     147             :   public Optional<InternalGroup> get(AccountGroup.NameKey name) {
     148         150 :     if (name == null) {
     149           0 :       return Optional.empty();
     150             :     }
     151             :     try {
     152         150 :       return byName.get(name.get());
     153           0 :     } catch (ExecutionException e) {
     154           0 :       logger.atWarning().withCause(e).log("Cannot look up group %s by name", name.get());
     155           0 :       return Optional.empty();
     156             :     }
     157             :   }
     158             : 
     159             :   @Override
     160             :   public Optional<InternalGroup> get(AccountGroup.UUID groupUuid) {
     161         151 :     if (groupUuid == null) {
     162           0 :       return Optional.empty();
     163             :     }
     164             : 
     165             :     try {
     166         151 :       return byUUID.get(groupUuid.get());
     167           0 :     } catch (ExecutionException e) {
     168           0 :       logger.atWarning().withCause(e).log("Cannot look up group %s by uuid", groupUuid.get());
     169           0 :       return Optional.empty();
     170             :     }
     171             :   }
     172             : 
     173             :   @Override
     174             :   public Map<AccountGroup.UUID, InternalGroup> get(Collection<AccountGroup.UUID> groupUuids) {
     175             :     try {
     176         150 :       Set<String> groupUuidsStringSet =
     177         150 :           groupUuids.stream().map(u -> u.get()).collect(toImmutableSet());
     178         150 :       return byUUID.getAll(groupUuidsStringSet).entrySet().stream()
     179         150 :           .filter(g -> g.getValue().isPresent())
     180         150 :           .collect(toImmutableMap(g -> AccountGroup.uuid(g.getKey()), g -> g.getValue().get()));
     181           0 :     } catch (ExecutionException e) {
     182           0 :       logger.atWarning().withCause(e).log("Cannot look up groups %s by uuids", groupUuids);
     183           0 :       return ImmutableMap.of();
     184             :     }
     185             :   }
     186             : 
     187             :   @Override
     188             :   public void evict(AccountGroup.Id groupId) {
     189         151 :     if (groupId != null) {
     190         151 :       logger.atFine().log("Evict group %s by ID", groupId.get());
     191         151 :       byId.invalidate(groupId);
     192             :     }
     193         151 :   }
     194             : 
     195             :   @Override
     196             :   public void evict(AccountGroup.NameKey groupName) {
     197         151 :     if (groupName != null) {
     198         151 :       logger.atFine().log("Evict group '%s' by name", groupName.get());
     199         151 :       byName.invalidate(groupName.get());
     200             :     }
     201         151 :   }
     202             : 
     203             :   @Override
     204             :   public void evict(AccountGroup.UUID groupUuid) {
     205         151 :     if (groupUuid != null) {
     206         151 :       logger.atFine().log("Evict group %s by UUID", groupUuid.get());
     207         151 :       byUUID.invalidate(groupUuid.get());
     208             :     }
     209         151 :   }
     210             : 
     211             :   @Override
     212             :   public void evict(Collection<AccountGroup.UUID> groupUuids) {
     213          15 :     if (groupUuids != null && !groupUuids.isEmpty()) {
     214          15 :       logger.atFine().log("Evict groups %s by UUID", groupUuids);
     215          15 :       byUUID.invalidateAll(groupUuids);
     216             :     }
     217          15 :   }
     218             : 
     219             :   static class ByIdLoader extends CacheLoader<AccountGroup.Id, Optional<InternalGroup>> {
     220             :     private final Provider<InternalGroupQuery> groupQueryProvider;
     221             : 
     222             :     @Inject
     223         152 :     ByIdLoader(Provider<InternalGroupQuery> groupQueryProvider) {
     224         152 :       this.groupQueryProvider = groupQueryProvider;
     225         152 :     }
     226             : 
     227             :     @Override
     228             :     public Optional<InternalGroup> load(AccountGroup.Id key) throws Exception {
     229           7 :       try (TraceTimer ignored =
     230           7 :           TraceContext.newTimer(
     231           7 :               "Loading group by ID", Metadata.builder().groupId(key.get()).build())) {
     232           7 :         return groupQueryProvider.get().byId(key);
     233             :       }
     234             :     }
     235             :   }
     236             : 
     237             :   static class ByNameLoader extends CacheLoader<String, Optional<InternalGroup>> {
     238             :     private final Provider<InternalGroupQuery> groupQueryProvider;
     239             : 
     240             :     @Inject
     241         152 :     ByNameLoader(Provider<InternalGroupQuery> groupQueryProvider) {
     242         152 :       this.groupQueryProvider = groupQueryProvider;
     243         152 :     }
     244             : 
     245             :     @Override
     246             :     public Optional<InternalGroup> load(String name) throws Exception {
     247         150 :       try (TraceTimer ignored =
     248         150 :           TraceContext.newTimer(
     249         150 :               "Loading group by name", Metadata.builder().groupName(name).build())) {
     250         150 :         return groupQueryProvider.get().byName(AccountGroup.nameKey(name));
     251             :       }
     252             :     }
     253             :   }
     254             : 
     255             :   static class ByUUIDInMemoryLoader extends CacheLoader<String, Optional<InternalGroup>> {
     256             :     private final LoadingCache<Cache.GroupKeyProto, InternalGroup> persistedCache;
     257             :     private final GitRepositoryManager repoManager;
     258             :     private final AllUsersName allUsersName;
     259             : 
     260             :     @Inject
     261             :     ByUUIDInMemoryLoader(
     262             :         @Named(BYUUID_NAME_PERSISTED)
     263             :             LoadingCache<Cache.GroupKeyProto, InternalGroup> persistedCache,
     264             :         GitRepositoryManager repoManager,
     265         152 :         AllUsersName allUsersName) {
     266         152 :       this.persistedCache = persistedCache;
     267         152 :       this.repoManager = repoManager;
     268         152 :       this.allUsersName = allUsersName;
     269         152 :     }
     270             : 
     271             :     @Override
     272             :     public Optional<InternalGroup> load(String uuid) throws Exception {
     273         151 :       return loadAll(ImmutableSet.of(uuid)).get(uuid);
     274             :     }
     275             : 
     276             :     @Override
     277             :     public Map<String, Optional<InternalGroup>> loadAll(Iterable<? extends String> uuids)
     278             :         throws Exception {
     279         151 :       Map<String, Optional<InternalGroup>> toReturn = new HashMap<>();
     280         151 :       if (Iterables.isEmpty(uuids)) {
     281           0 :         return toReturn;
     282             :       }
     283         151 :       Iterator<? extends String> uuidIterator = uuids.iterator();
     284         151 :       List<Cache.GroupKeyProto> keyList = new ArrayList<>();
     285         151 :       try (TraceTimer ignored =
     286         151 :               TraceContext.newTimer(
     287             :                   "Loading group from serialized cache",
     288         151 :                   Metadata.builder().cacheName(BYUUID_NAME_PERSISTED).build());
     289         151 :           Repository allUsers = repoManager.openRepository(allUsersName)) {
     290         151 :         while (uuidIterator.hasNext()) {
     291         151 :           String currentUuid = uuidIterator.next();
     292         151 :           String ref = RefNames.refsGroups(AccountGroup.uuid(currentUuid));
     293         151 :           Ref sha1 = allUsers.exactRef(ref);
     294         151 :           if (sha1 == null) {
     295          30 :             toReturn.put(currentUuid, Optional.empty());
     296          30 :             continue;
     297             :           }
     298             :           Cache.GroupKeyProto key =
     299         151 :               Cache.GroupKeyProto.newBuilder()
     300         151 :                   .setUuid(currentUuid)
     301         151 :                   .setRevision(ObjectIdConverter.create().toByteString(sha1.getObjectId()))
     302         151 :                   .build();
     303         151 :           keyList.add(key);
     304         151 :         }
     305             :       }
     306         151 :       persistedCache.getAll(keyList).entrySet().stream()
     307         151 :           .forEach(g -> toReturn.put(g.getKey().getUuid(), Optional.of(g.getValue())));
     308         151 :       return toReturn;
     309             :     }
     310             :   }
     311             : 
     312             :   static class PersistedByUUIDLoader extends CacheLoader<Cache.GroupKeyProto, InternalGroup> {
     313             :     private final Groups groups;
     314             : 
     315             :     @Inject
     316         152 :     PersistedByUUIDLoader(Groups groups) {
     317         152 :       this.groups = groups;
     318         152 :     }
     319             : 
     320             :     @Override
     321             :     public InternalGroup load(Cache.GroupKeyProto key) throws Exception {
     322         151 :       try (TraceTimer ignored =
     323         151 :           TraceContext.newTimer(
     324         151 :               "Loading group by UUID", Metadata.builder().groupUuid(key.getUuid()).build())) {
     325         151 :         ObjectId sha1 = ObjectIdConverter.create().fromByteString(key.getRevision());
     326         151 :         Optional<InternalGroup> loadedGroup =
     327         151 :             groups.getGroup(AccountGroup.uuid(key.getUuid()), sha1);
     328         151 :         if (!loadedGroup.isPresent()) {
     329           0 :           throw new IllegalStateException(
     330           0 :               String.format(
     331             :                   "group %s should have the sha-1 %s, but " + "it was not found",
     332           0 :                   key.getUuid(), sha1.getName()));
     333             :         }
     334         151 :         return loadedGroup.get();
     335             :       }
     336             :     }
     337             :   }
     338             : 
     339         152 :   private enum PersistedInternalGroupSerializer implements CacheSerializer<InternalGroup> {
     340         152 :     INSTANCE;
     341             : 
     342             :     @Override
     343             :     public byte[] serialize(InternalGroup value) {
     344          15 :       if (value == null) {
     345           0 :         return new byte[0];
     346             :       }
     347          15 :       return Protos.toByteArray(InternalGroupSerializer.serialize(value));
     348             :     }
     349             : 
     350             :     @Nullable
     351             :     @Override
     352             :     public InternalGroup deserialize(byte[] in) {
     353          15 :       if (Strings.fromByteArray(in).isEmpty()) {
     354           0 :         return null;
     355             :       }
     356          15 :       return InternalGroupSerializer.deserialize(
     357          15 :           Protos.parseUnchecked(Cache.InternalGroupProto.parser(), in));
     358             :     }
     359             :   }
     360             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750