| // Copyright (C) 2021 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.plugins.codeowners.backend; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.entities.BranchNameKey; |
| import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration; |
| import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.inject.Inject; |
| import java.io.IOException; |
| import java.util.HashMap; |
| import java.util.Optional; |
| import java.util.concurrent.TimeUnit; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| |
| /** |
| * Class to load and cache {@link CodeOwnerConfig}s within a request. |
| * |
| * <p>This cache is transient, which means the code owner configs stay cached only for the lifetime |
| * of the {@code TransientCodeOwnerConfigCache} instance. |
| * |
| * <p><strong>Note</strong>: This class is not thread-safe. |
| */ |
| public class TransientCodeOwnerConfigCache implements CodeOwnerConfigLoader { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private final GitRepositoryManager repoManager; |
| private final CodeOwners codeOwners; |
| private final Optional<Integer> maxCacheSize; |
| private final Counters counters; |
| private final HashMap<CacheKey, Optional<CodeOwnerConfig>> cache = new HashMap<>(); |
| |
| @Inject |
| TransientCodeOwnerConfigCache( |
| CodeOwnersPluginConfiguration codeOwnersPluginConfiguration, |
| GitRepositoryManager repoManager, |
| CodeOwners codeOwners, |
| CodeOwnerMetrics codeOwnerMetrics) { |
| this.repoManager = repoManager; |
| this.codeOwners = codeOwners; |
| this.maxCacheSize = |
| codeOwnersPluginConfiguration.getGlobalConfig().getMaxCodeOwnerConfigCacheSize(); |
| this.counters = new Counters(codeOwnerMetrics); |
| } |
| |
| /** |
| * Gets the specified code owner config from the cache, if it was previously retrieved. Otherwise |
| * loads and returns the code owner config. |
| */ |
| @Override |
| public Optional<CodeOwnerConfig> get( |
| CodeOwnerConfig.Key codeOwnerConfigKey, @Nullable ObjectId revision) { |
| CacheKey cacheKey = CacheKey.create(codeOwnerConfigKey, revision); |
| Optional<CodeOwnerConfig> cachedCodeOwnerConfig = cache.get(cacheKey); |
| if (cachedCodeOwnerConfig != null) { |
| counters.incrementCacheReads(); |
| return cachedCodeOwnerConfig; |
| } |
| return loadAndCache(cacheKey); |
| } |
| |
| /** |
| * Gets the specified code owner config from the cache, if it was previously retrieved. Otherwise |
| * loads and returns the code owner config. |
| */ |
| @Override |
| public Optional<CodeOwnerConfig> getFromCurrentRevision(CodeOwnerConfig.Key codeOwnerConfigKey) { |
| return get(codeOwnerConfigKey, /* revision= */ null); |
| } |
| |
| /** Load a code owner config and puts it into the cache. */ |
| private Optional<CodeOwnerConfig> loadAndCache(CacheKey cacheKey) { |
| counters.incrementBackendReads(); |
| Optional<CodeOwnerConfig> codeOwnerConfig; |
| if (cacheKey.revision().isPresent()) { |
| codeOwnerConfig = codeOwners.get(cacheKey.codeOwnerConfigKey(), cacheKey.revision().get()); |
| } else { |
| Optional<ObjectId> revision = getRevision(cacheKey.codeOwnerConfigKey().branchNameKey()); |
| if (revision.isPresent()) { |
| codeOwnerConfig = codeOwners.get(cacheKey.codeOwnerConfigKey(), revision.get()); |
| } else { |
| // branch does not exists, hence the code owner config also doesn't exist |
| codeOwnerConfig = Optional.empty(); |
| } |
| } |
| if (!maxCacheSize.isPresent() || cache.size() < maxCacheSize.get()) { |
| cache.put(cacheKey, codeOwnerConfig); |
| } else if (maxCacheSize.isPresent()) { |
| logger.atWarning().atMostEvery(1, TimeUnit.DAYS).log( |
| "exceeded limit of %s (project = %s)", |
| getClass().getSimpleName(), cacheKey.codeOwnerConfigKey().project()); |
| } |
| return codeOwnerConfig; |
| } |
| |
| /** |
| * Gets the revision for the given branch. |
| * |
| * <p>Returns {@link Optional#empty()} if the branch doesn't exist. |
| */ |
| private Optional<ObjectId> getRevision(BranchNameKey branchNameKey) { |
| try (Repository repo = repoManager.openRepository(branchNameKey.project())) { |
| Ref ref = repo.exactRef(branchNameKey.branch()); |
| if (ref == null) { |
| // branch does not exist |
| return Optional.empty(); |
| } |
| return Optional.of(ref.getObjectId()); |
| } catch (IOException e) { |
| throw new CodeOwnersInternalServerErrorException( |
| String.format( |
| "failed to get revision of branch %s in project %s", |
| branchNameKey.shortName(), branchNameKey.project()), |
| e); |
| } |
| } |
| |
| @AutoValue |
| abstract static class CacheKey { |
| /** The key of the code owner config. */ |
| public abstract CodeOwnerConfig.Key codeOwnerConfigKey(); |
| |
| /** The revision from which the code owner config was loaded. */ |
| public abstract Optional<ObjectId> revision(); |
| |
| public static CacheKey create( |
| CodeOwnerConfig.Key codeOwnerConfigKey, @Nullable ObjectId revision) { |
| return new AutoValue_TransientCodeOwnerConfigCache_CacheKey( |
| codeOwnerConfigKey, Optional.ofNullable(revision)); |
| } |
| } |
| |
| public Counters getCounters() { |
| return counters; |
| } |
| |
| public static class Counters { |
| private final CodeOwnerMetrics codeOwnerMetrics; |
| |
| private int cacheReadCount; |
| private int backendReadCount; |
| |
| private Counters(CodeOwnerMetrics codeOwnerMetrics) { |
| this.codeOwnerMetrics = codeOwnerMetrics; |
| } |
| |
| private void incrementCacheReads() { |
| codeOwnerMetrics.countCodeOwnerConfigCacheReads.increment(); |
| cacheReadCount++; |
| } |
| |
| private void incrementBackendReads() { |
| // we do not increase the countCodeOwnerConfigReads metric here, since this is already done in |
| // CodeOwners |
| backendReadCount++; |
| } |
| |
| public int getBackendReadCount() { |
| return backendReadCount; |
| } |
| |
| public int getCacheReadCount() { |
| return cacheReadCount; |
| } |
| } |
| } |