LCOV - code coverage report
Current view: top level - server/project - ProjectCacheImpl.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 143 179 79.9 %
Date: 2022-11-19 15:00:39 Functions: 36 41 87.8 %

          Line data    Source code
       1             : // Copyright (C) 2008 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.project;
      16             : 
      17             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      18             : import static java.nio.charset.StandardCharsets.UTF_8;
      19             : import static java.util.stream.Collectors.toSet;
      20             : 
      21             : import com.google.common.annotations.VisibleForTesting;
      22             : import com.google.common.cache.CacheLoader;
      23             : import com.google.common.cache.LoadingCache;
      24             : import com.google.common.collect.ImmutableSet;
      25             : import com.google.common.collect.ImmutableSortedSet;
      26             : import com.google.common.collect.Sets;
      27             : import com.google.common.collect.Streams;
      28             : import com.google.common.flogger.FluentLogger;
      29             : import com.google.common.hash.Hashing;
      30             : import com.google.common.util.concurrent.Futures;
      31             : import com.google.common.util.concurrent.ListenableFuture;
      32             : import com.google.common.util.concurrent.ListeningExecutorService;
      33             : import com.google.gerrit.common.Nullable;
      34             : import com.google.gerrit.entities.AccountGroup;
      35             : import com.google.gerrit.entities.CachedProjectConfig;
      36             : import com.google.gerrit.entities.Project;
      37             : import com.google.gerrit.entities.RefNames;
      38             : import com.google.gerrit.exceptions.StorageException;
      39             : import com.google.gerrit.index.project.ProjectIndexer;
      40             : import com.google.gerrit.lifecycle.LifecycleModule;
      41             : import com.google.gerrit.metrics.Counter2;
      42             : import com.google.gerrit.metrics.Description;
      43             : import com.google.gerrit.metrics.Description.Units;
      44             : import com.google.gerrit.metrics.Field;
      45             : import com.google.gerrit.metrics.MetricMaker;
      46             : import com.google.gerrit.metrics.Timer0;
      47             : import com.google.gerrit.proto.Protos;
      48             : import com.google.gerrit.server.CacheRefreshExecutor;
      49             : import com.google.gerrit.server.cache.CacheModule;
      50             : import com.google.gerrit.server.cache.proto.Cache;
      51             : import com.google.gerrit.server.cache.serialize.CacheSerializer;
      52             : import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
      53             : import com.google.gerrit.server.cache.serialize.ProtobufSerializer;
      54             : import com.google.gerrit.server.cache.serialize.entities.CachedProjectConfigSerializer;
      55             : import com.google.gerrit.server.config.AllProjectsConfigProvider;
      56             : import com.google.gerrit.server.config.AllProjectsName;
      57             : import com.google.gerrit.server.config.AllUsersName;
      58             : import com.google.gerrit.server.config.GerritServerConfig;
      59             : import com.google.gerrit.server.git.GitRepositoryManager;
      60             : import com.google.gerrit.server.logging.Metadata;
      61             : import com.google.gerrit.server.logging.TraceContext;
      62             : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
      63             : import com.google.inject.Inject;
      64             : import com.google.inject.Module;
      65             : import com.google.inject.Provider;
      66             : import com.google.inject.Singleton;
      67             : import com.google.inject.TypeLiteral;
      68             : import com.google.inject.name.Named;
      69             : import com.google.protobuf.ByteString;
      70             : import java.io.IOException;
      71             : import java.time.Duration;
      72             : import java.util.Arrays;
      73             : import java.util.Objects;
      74             : import java.util.Optional;
      75             : import java.util.Set;
      76             : import java.util.concurrent.ExecutionException;
      77             : import java.util.concurrent.locks.Lock;
      78             : import java.util.concurrent.locks.ReentrantLock;
      79             : import org.eclipse.jgit.errors.ConfigInvalidException;
      80             : import org.eclipse.jgit.errors.RepositoryNotFoundException;
      81             : import org.eclipse.jgit.lib.Config;
      82             : import org.eclipse.jgit.lib.ObjectId;
      83             : import org.eclipse.jgit.lib.Ref;
      84             : import org.eclipse.jgit.lib.Repository;
      85             : import org.eclipse.jgit.lib.StoredConfig;
      86             : 
      87             : /**
      88             :  * Cache of project information, including access rights.
      89             :  *
      90             :  * <p>The data of a project is the project's project.config in refs/meta/config parsed out as an
      91             :  * immutable value. It's keyed purely by the refs/meta/config SHA-1. We also cache the same value
      92             :  * keyed by name. The latter mapping can become outdated, so data must be evicted explicitly.
      93             :  */
      94             : @Singleton
      95             : public class ProjectCacheImpl implements ProjectCache {
      96         152 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      97             : 
      98             :   public static final String CACHE_NAME = "projects";
      99             : 
     100             :   public static final String PERSISTED_CACHE_NAME = "persisted_projects";
     101             : 
     102             :   public static final String CACHE_LIST = "project_list";
     103             : 
     104             :   public static Module module() {
     105         152 :     return new CacheModule() {
     106             :       @Override
     107             :       protected void configure() {
     108             :         // We split the project cache into two parts for performance reasons:
     109             :         // 1) An in-memory part that has only the project name as key.
     110             :         // 2) A persisted part that has the name and revision as key.
     111             :         //
     112             :         // When loading dashboards or returning change query results we potentially
     113             :         // need to access hundreds of projects because each change could originate in
     114             :         // a different project and, through inheritance, require us to check even more
     115             :         // projects when evaluating permissions. It's not feasible to read the revision
     116             :         // of refs/meta/config from each of these repos as that would require opening
     117             :         // them all and reading their ref list or table.
     118             :         // At the same time, we want the persisted cache to be immutable and we want it
     119             :         // to be impossible that a value for a given key is out of date. We therefore
     120             :         // require a revision in the key. That is in line with the rest of the caches in
     121             :         // Gerrit.
     122             :         //
     123             :         // Splitting the cache into two chunks internally in this class allows us to retain
     124             :         // the existing performance guarantees of not requiring reads for the repo for values
     125             :         // cached in-memory but also to persist the cache which leads to a much improved
     126             :         // cold-start behavior and in-memory miss latency.
     127         152 :         cache(CACHE_NAME, Project.NameKey.class, CachedProjectConfig.class)
     128         152 :             .loader(InMemoryLoader.class)
     129         152 :             .refreshAfterWrite(Duration.ofMinutes(15))
     130         152 :             .expireAfterWrite(Duration.ofHours(1));
     131             : 
     132         152 :         persist(PERSISTED_CACHE_NAME, Cache.ProjectCacheKeyProto.class, CachedProjectConfig.class)
     133         152 :             .loader(PersistedLoader.class)
     134         152 :             .keySerializer(new ProtobufSerializer<>(Cache.ProjectCacheKeyProto.parser()))
     135         152 :             .valueSerializer(PersistedProjectConfigSerializer.INSTANCE)
     136         152 :             .diskLimit(1 << 30) // 1 GiB
     137         152 :             .version(4)
     138         152 :             .maximumWeight(0);
     139             : 
     140         152 :         cache(CACHE_LIST, ListKey.class, new TypeLiteral<ImmutableSortedSet<Project.NameKey>>() {})
     141         152 :             .maximumWeight(1)
     142         152 :             .loader(Lister.class);
     143             : 
     144         152 :         bind(ProjectCacheImpl.class);
     145         152 :         bind(ProjectCache.class).to(ProjectCacheImpl.class);
     146             : 
     147         152 :         install(
     148         152 :             new LifecycleModule() {
     149             :               @Override
     150             :               protected void configure() {
     151         152 :                 listener().to(ProjectCacheWarmer.class);
     152         152 :               }
     153             :             });
     154         152 :         install(
     155         152 :             new LifecycleModule() {
     156             :               @Override
     157             :               protected void configure() {
     158         152 :                 listener().to(PeriodicProjectListCacheWarmer.LifeCycle.class);
     159         152 :               }
     160             :             });
     161         152 :       }
     162             :     };
     163             :   }
     164             : 
     165             :   private final Config config;
     166             :   private final AllProjectsName allProjectsName;
     167             :   private final AllUsersName allUsersName;
     168             :   private final LoadingCache<Project.NameKey, CachedProjectConfig> inMemoryProjectCache;
     169             :   private final LoadingCache<ListKey, ImmutableSortedSet<Project.NameKey>> list;
     170             :   private final Lock listLock;
     171             :   private final Provider<ProjectIndexer> indexer;
     172             :   private final Timer0 guessRelevantGroupsLatency;
     173             :   private final ProjectState.Factory projectStateFactory;
     174             : 
     175             :   @Inject
     176             :   ProjectCacheImpl(
     177             :       @GerritServerConfig Config config,
     178             :       AllProjectsName allProjectsName,
     179             :       AllUsersName allUsersName,
     180             :       @Named(CACHE_NAME) LoadingCache<Project.NameKey, CachedProjectConfig> inMemoryProjectCache,
     181             :       @Named(CACHE_LIST) LoadingCache<ListKey, ImmutableSortedSet<Project.NameKey>> list,
     182             :       Provider<ProjectIndexer> indexer,
     183             :       MetricMaker metricMaker,
     184         152 :       ProjectState.Factory projectStateFactory) {
     185         152 :     this.config = config;
     186         152 :     this.allProjectsName = allProjectsName;
     187         152 :     this.allUsersName = allUsersName;
     188         152 :     this.inMemoryProjectCache = inMemoryProjectCache;
     189         152 :     this.list = list;
     190         152 :     this.listLock = new ReentrantLock(true /* fair */);
     191         152 :     this.indexer = indexer;
     192         152 :     this.projectStateFactory = projectStateFactory;
     193             : 
     194         152 :     this.guessRelevantGroupsLatency =
     195         152 :         metricMaker.newTimer(
     196             :             "group/guess_relevant_groups_latency",
     197             :             new Description("Latency for guessing relevant groups")
     198         152 :                 .setCumulative()
     199         152 :                 .setUnit(Units.NANOSECONDS));
     200         152 :   }
     201             : 
     202             :   @Override
     203             :   public ProjectState getAllProjects() {
     204         151 :     return get(allProjectsName).orElseThrow(illegalState(allProjectsName));
     205             :   }
     206             : 
     207             :   @Override
     208             :   public ProjectState getAllUsers() {
     209          16 :     return get(allUsersName).orElseThrow(illegalState(allUsersName));
     210             :   }
     211             : 
     212             :   @Override
     213             :   public Optional<ProjectState> get(@Nullable Project.NameKey projectName) {
     214         151 :     if (projectName == null) {
     215           0 :       return Optional.empty();
     216             :     }
     217             : 
     218             :     try {
     219         151 :       return Optional.of(inMemoryProjectCache.get(projectName)).map(projectStateFactory::create);
     220         143 :     } catch (ExecutionException e) {
     221         143 :       if ((e.getCause() instanceof RepositoryNotFoundException)) {
     222         143 :         logger.atFine().log("Cannot find project %s", projectName.get());
     223         143 :         return Optional.empty();
     224             :       }
     225           2 :       throw new StorageException(
     226           2 :           String.format("project state of project %s not available", projectName.get()), e);
     227             :     }
     228             :   }
     229             : 
     230             :   @Override
     231             :   public void evict(Project.NameKey p) {
     232         100 :     if (p != null) {
     233         100 :       logger.atFine().log("Evict project '%s'", p.get());
     234         100 :       inMemoryProjectCache.invalidate(p);
     235             :     }
     236         100 :   }
     237             : 
     238             :   @Override
     239             :   public void evictAndReindex(Project p) {
     240          61 :     evictAndReindex(p.getNameKey());
     241          61 :   }
     242             : 
     243             :   @Override
     244             :   public void evictAndReindex(Project.NameKey p) {
     245          92 :     evict(p);
     246          92 :     indexer.get().index(p);
     247          92 :   }
     248             : 
     249             :   @Override
     250             :   public void remove(Project p) {
     251           0 :     remove(p.getNameKey());
     252           0 :   }
     253             : 
     254             :   @Override
     255             :   public void remove(Project.NameKey name) {
     256           0 :     listLock.lock();
     257             :     try {
     258           0 :       list.put(
     259             :           ListKey.ALL,
     260           0 :           ImmutableSortedSet.copyOf(Sets.difference(list.get(ListKey.ALL), ImmutableSet.of(name))));
     261           0 :     } catch (ExecutionException e) {
     262           0 :       logger.atWarning().withCause(e).log("Cannot list available projects");
     263             :     } finally {
     264           0 :       listLock.unlock();
     265             :     }
     266           0 :     evictAndReindex(name);
     267           0 :   }
     268             : 
     269             :   @Override
     270             :   public void onCreateProject(Project.NameKey newProjectName) throws IOException {
     271         144 :     listLock.lock();
     272             :     try {
     273         144 :       list.put(
     274             :           ListKey.ALL,
     275         144 :           ImmutableSortedSet.copyOf(
     276         144 :               Sets.union(list.get(ListKey.ALL), ImmutableSet.of(newProjectName))));
     277           0 :     } catch (ExecutionException e) {
     278           0 :       logger.atWarning().withCause(e).log("Cannot list available projects");
     279             :     } finally {
     280         144 :       listLock.unlock();
     281             :     }
     282         144 :     indexer.get().index(newProjectName);
     283         144 :   }
     284             : 
     285             :   @Override
     286             :   public ImmutableSortedSet<Project.NameKey> all() {
     287             :     try {
     288          34 :       return list.get(ListKey.ALL);
     289           0 :     } catch (ExecutionException e) {
     290           0 :       logger.atWarning().withCause(e).log("Cannot list available projects");
     291           0 :       return ImmutableSortedSet.of();
     292             :     }
     293             :   }
     294             : 
     295             :   @Override
     296             :   public void refreshProjectList() {
     297           0 :     list.refresh(ListKey.ALL);
     298           0 :   }
     299             : 
     300             :   @Override
     301             :   public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
     302           1 :     try (Timer0.Context ignored = guessRelevantGroupsLatency.start()) {
     303           1 :       return Streams.concat(
     304           1 :               Arrays.stream(config.getStringList("groups", /* subsection= */ null, "relevantGroup"))
     305           1 :                   .map(AccountGroup::uuid),
     306           1 :               all().stream()
     307           1 :                   .map(n -> inMemoryProjectCache.getIfPresent(n))
     308           1 :                   .filter(Objects::nonNull)
     309           1 :                   .flatMap(p -> p.getAllGroupUUIDs().stream())
     310             :                   // getAllGroupUUIDs shouldn't really return null UUIDs, but harden
     311             :                   // against them just in case there is a bug or corner case.
     312           1 :                   .filter(id -> id != null && id.get() != null))
     313           1 :           .collect(toSet());
     314             :     }
     315             :   }
     316             : 
     317             :   @Override
     318             :   public ImmutableSortedSet<Project.NameKey> byName(String pfx) {
     319           1 :     Project.NameKey start = Project.nameKey(pfx);
     320           1 :     Project.NameKey end = Project.nameKey(pfx + Character.MAX_VALUE);
     321             :     try {
     322             :       // Right endpoint is exclusive, but U+FFFF is a non-character so no project ends with it.
     323           1 :       return list.get(ListKey.ALL).subSet(start, end);
     324           0 :     } catch (ExecutionException e) {
     325           0 :       logger.atWarning().withCause(e).log("Cannot look up projects for prefix %s", pfx);
     326           0 :       return ImmutableSortedSet.of();
     327             :     }
     328             :   }
     329             : 
     330             :   /**
     331             :    * Returns a {@code MurMur128} hash of the contents of {@code etc/All-Projects-project.config}.
     332             :    */
     333             :   public static byte[] allProjectsFileProjectConfigHash(Optional<StoredConfig> allProjectsConfig) {
     334             :     // Hash the contents of All-Projects-project.config
     335             :     // This is a way for administrators to orchestrate project.config changes across many Gerrit
     336             :     // instances.
     337             :     // When this file changes, we need to make sure we disregard persistently cached project
     338             :     // state.
     339         151 :     if (!allProjectsConfig.isPresent()) {
     340             :       // If the project.config file is not present, this is equal to an empty config file:
     341           0 :       return Hashing.murmur3_128().hashString("", UTF_8).asBytes();
     342             :     }
     343             :     try {
     344         151 :       allProjectsConfig.get().load();
     345           0 :     } catch (IOException | ConfigInvalidException e) {
     346           0 :       throw new IllegalStateException(e);
     347         151 :     }
     348         151 :     return Hashing.murmur3_128().hashString(allProjectsConfig.get().toText(), UTF_8).asBytes();
     349             :   }
     350             : 
     351             :   @Singleton
     352             :   static class InMemoryLoader extends CacheLoader<Project.NameKey, CachedProjectConfig> {
     353             :     private final LoadingCache<Cache.ProjectCacheKeyProto, CachedProjectConfig> persistedCache;
     354             :     private final GitRepositoryManager repoManager;
     355             :     private final ListeningExecutorService cacheRefreshExecutor;
     356             :     private final Counter2<String, Boolean> refreshCounter;
     357             :     private final AllProjectsName allProjectsName;
     358             :     private final AllProjectsConfigProvider allProjectsConfigProvider;
     359             : 
     360             :     @Inject
     361             :     InMemoryLoader(
     362             :         @Named(PERSISTED_CACHE_NAME)
     363             :             LoadingCache<Cache.ProjectCacheKeyProto, CachedProjectConfig> persistedCache,
     364             :         GitRepositoryManager repoManager,
     365             :         @CacheRefreshExecutor ListeningExecutorService cacheRefreshExecutor,
     366             :         MetricMaker metricMaker,
     367             :         AllProjectsName allProjectsName,
     368         152 :         AllProjectsConfigProvider allProjectsConfigProvider) {
     369         152 :       this.persistedCache = persistedCache;
     370         152 :       this.repoManager = repoManager;
     371         152 :       this.cacheRefreshExecutor = cacheRefreshExecutor;
     372         152 :       refreshCounter =
     373         152 :           metricMaker.newCounter(
     374             :               "caches/refresh_count",
     375             :               new Description(
     376             :                       "The number of refreshes per cache with an indicator if a reload was"
     377             :                           + " necessary.")
     378         152 :                   .setRate(),
     379         152 :               Field.ofString("cache", Metadata.Builder::className)
     380         152 :                   .description("The name of the cache.")
     381         152 :                   .build(),
     382         152 :               Field.ofBoolean("outdated", Metadata.Builder::outdated)
     383         152 :                   .description("Whether the cache entry was outdated on reload.")
     384         152 :                   .build());
     385         152 :       this.allProjectsName = allProjectsName;
     386         152 :       this.allProjectsConfigProvider = allProjectsConfigProvider;
     387         152 :     }
     388             : 
     389             :     @Override
     390             :     public CachedProjectConfig load(Project.NameKey key) throws IOException, ExecutionException {
     391         151 :       try (TraceTimer ignored =
     392         151 :               TraceContext.newTimer(
     393             :                   "Loading project from serialized cache",
     394         151 :                   Metadata.builder().projectName(key.get()).build());
     395         151 :           Repository git = repoManager.openRepository(key)) {
     396             :         Cache.ProjectCacheKeyProto.Builder keyProto =
     397         151 :             Cache.ProjectCacheKeyProto.newBuilder().setProject(key.get());
     398         151 :         Ref configRef = git.exactRef(RefNames.REFS_CONFIG);
     399         151 :         if (key.get().equals(allProjectsName.get())) {
     400         151 :           Optional<StoredConfig> allProjectsConfig = allProjectsConfigProvider.get(allProjectsName);
     401         151 :           byte[] fileHash = allProjectsFileProjectConfigHash(allProjectsConfig);
     402         151 :           keyProto.setGlobalConfigRevision(ByteString.copyFrom(fileHash));
     403             :         }
     404         151 :         if (configRef != null) {
     405         151 :           keyProto.setRevision(ObjectIdConverter.create().toByteString(configRef.getObjectId()));
     406             :         }
     407         151 :         return persistedCache.get(keyProto.build());
     408             :       }
     409             :     }
     410             : 
     411             :     @Override
     412             :     public ListenableFuture<CachedProjectConfig> reload(
     413             :         Project.NameKey key, CachedProjectConfig oldState) throws Exception {
     414           0 :       try (TraceTimer ignored =
     415           0 :           TraceContext.newTimer(
     416           0 :               "Reload project", Metadata.builder().projectName(key.get()).build())) {
     417           0 :         try (Repository git = repoManager.openRepository(key)) {
     418           0 :           Ref configRef = git.exactRef(RefNames.REFS_CONFIG);
     419           0 :           if (configRef != null && configRef.getObjectId().equals(oldState.getRevision().get())) {
     420           0 :             refreshCounter.increment(CACHE_NAME, false);
     421           0 :             return Futures.immediateFuture(oldState);
     422             :           }
     423           0 :         }
     424             : 
     425             :         // Repository is not thread safe, so we have to open it on the thread that does the loading.
     426             :         // Just invoke the loader on the other thread.
     427           0 :         refreshCounter.increment(CACHE_NAME, true);
     428           0 :         return cacheRefreshExecutor.submit(() -> load(key));
     429           0 :       }
     430             :     }
     431             :   }
     432             : 
     433             :   @Singleton
     434             :   static class PersistedLoader
     435             :       extends CacheLoader<Cache.ProjectCacheKeyProto, CachedProjectConfig> {
     436             :     private final GitRepositoryManager repoManager;
     437             :     private final ProjectConfig.Factory projectConfigFactory;
     438             : 
     439             :     @Inject
     440         152 :     PersistedLoader(GitRepositoryManager repoManager, ProjectConfig.Factory projectConfigFactory) {
     441         152 :       this.repoManager = repoManager;
     442         152 :       this.projectConfigFactory = projectConfigFactory;
     443         152 :     }
     444             : 
     445             :     @Override
     446             :     public CachedProjectConfig load(Cache.ProjectCacheKeyProto key) throws Exception {
     447         151 :       Project.NameKey nameKey = Project.nameKey(key.getProject());
     448             :       ObjectId revision =
     449         151 :           key.getRevision().isEmpty()
     450           3 :               ? null
     451         151 :               : ObjectIdConverter.create().fromByteString(key.getRevision());
     452         151 :       try (TraceTimer ignored =
     453         151 :           TraceContext.newTimer(
     454         151 :               "Loading project from repo", Metadata.builder().projectName(nameKey.get()).build())) {
     455         151 :         try (Repository git = repoManager.openRepository(nameKey)) {
     456         151 :           ProjectConfig cfg = projectConfigFactory.create(nameKey);
     457         151 :           cfg.load(git, revision);
     458         151 :           return cfg.getCacheable();
     459             :         }
     460             :       }
     461             :     }
     462             :   }
     463             : 
     464         152 :   private enum PersistedProjectConfigSerializer implements CacheSerializer<CachedProjectConfig> {
     465         152 :     INSTANCE;
     466             : 
     467             :     @Override
     468             :     public byte[] serialize(CachedProjectConfig value) {
     469          15 :       return Protos.toByteArray(CachedProjectConfigSerializer.serialize(value));
     470             :     }
     471             : 
     472             :     @Override
     473             :     public CachedProjectConfig deserialize(byte[] in) {
     474          15 :       return CachedProjectConfigSerializer.deserialize(
     475          15 :           Protos.parseUnchecked(Cache.CachedProjectConfigProto.parser(), in));
     476             :     }
     477             :   }
     478             : 
     479             :   static class ListKey {
     480         147 :     static final ListKey ALL = new ListKey();
     481             : 
     482             :     private ListKey() {}
     483             :   }
     484             : 
     485             :   static class Lister extends CacheLoader<ListKey, ImmutableSortedSet<Project.NameKey>> {
     486             :     private final GitRepositoryManager mgr;
     487             : 
     488             :     @Inject
     489         152 :     Lister(GitRepositoryManager mgr) {
     490         152 :       this.mgr = mgr;
     491         152 :     }
     492             : 
     493             :     @Override
     494             :     public ImmutableSortedSet<Project.NameKey> load(ListKey key) throws Exception {
     495         147 :       try (TraceTimer timer = TraceContext.newTimer("Loading project list")) {
     496         147 :         return ImmutableSortedSet.copyOf(mgr.list());
     497             :       }
     498             :     }
     499             :   }
     500             : 
     501             :   @VisibleForTesting
     502             :   public void evictAllByName() {
     503           1 :     inMemoryProjectCache.invalidateAll();
     504           1 :   }
     505             : 
     506             :   @VisibleForTesting
     507             :   public long sizeAllByName() {
     508           1 :     return inMemoryProjectCache.size();
     509             :   }
     510             : }

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