| // Copyright (C) 2017 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.googlesource.gerrit.plugins.findowners; |
| |
| import com.google.common.cache.CacheBuilder; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.server.account.AccountCache; |
| import com.google.gerrit.server.account.Emails; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.permissions.PermissionBackend; |
| import com.google.gerrit.server.project.ProjectState; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import java.time.Duration; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Map; |
| import java.util.WeakHashMap; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.TimeUnit; |
| |
| /** Save OwnersDb in a cache for multiple calls to submit_filter. */ |
| class Cache { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| // The OwnersDb is created from OWNERS files in directories that |
| // contain changed files of a patch set, which belongs to a project |
| // and branch. OwnersDb can be cached if the head of a project branch |
| // and the patch set are not changed. |
| |
| // Although the head of a project branch could be changed by other users, |
| // it is better to assume the same for a patch set during a short period |
| // of time. So multiple checks would have the same result. For example, |
| // one client UI action can trigger multiple HTTP requests. |
| // Each HTTP request has one StoredValues, |
| // and can trigger multiple Prolog submit_filter. |
| // Each submit_filter has one Prolog engine. |
| // It would not be enough to keep the cache in a Prolog engine environment |
| // or a StoredValues. |
| |
| // Since a Java runtime can host multiple Gerrit sites, each site should have |
| // its own cache. We assume that each site has its own GitRepositoryManager. |
| // We use a WeakHashMap to avoid leaking objects in the map. |
| private static final Map<GitRepositoryManager, Cache> cacheMap = |
| Collections.synchronizedMap(new WeakHashMap<GitRepositoryManager, Cache>()); |
| |
| // dbCache key is generated by makeKey. |
| private com.google.common.cache.Cache<String, OwnersDb> dbCache; |
| |
| private Config config; // global config shared by all OwnersDb in dbCache |
| |
| private Cache(Config config) { |
| this.config = config; |
| init(config.getMaxCacheAge(), config.getMaxCacheSize()); |
| } |
| |
| long size() { |
| return (dbCache == null) ? 0 : dbCache.size(); |
| } |
| |
| Cache init(int maxSeconds, int maxSize) { |
| // This should be called once in normal configuration, |
| // but could be called multiple times in unit or integration tests. |
| if (dbCache != null) { |
| dbCache.invalidateAll(); // release all cached objects |
| } |
| if (maxSeconds > 0) { |
| logger.atInfo().log("Initialize Cache with maxSeconds=%d maxSize=%d", maxSeconds, maxSize); |
| dbCache = |
| CacheBuilder.newBuilder() |
| .maximumSize(maxSize) |
| .expireAfterWrite(Duration.ofSeconds(maxSeconds)) |
| .recordStats() |
| .build(); |
| } else { |
| logger.atInfo().log("Cache disabled."); |
| dbCache = null; |
| } |
| return this; |
| } |
| |
| /** Returns a cached or new OwnersDb, for the current patchset. */ |
| OwnersDb get( |
| Boolean useCache, |
| PermissionBackend permissionBackend, |
| ProjectState projectState, |
| AccountCache accountCache, |
| Emails emails, |
| GitRepositoryManager repoManager, |
| ChangeData changeData) { |
| return get( |
| useCache, |
| permissionBackend, |
| projectState, |
| accountCache, |
| emails, |
| repoManager, |
| changeData, |
| changeData.currentPatchSet().id().get()); |
| } |
| |
| /** Returns a cached or new OwnersDb, for the specified patchset. */ |
| OwnersDb get( |
| Boolean useCache, |
| PermissionBackend permissionBackend, |
| ProjectState projectState, |
| AccountCache accountCache, |
| Emails emails, |
| GitRepositoryManager repoManager, |
| ChangeData changeData, |
| int patchset) { |
| String branch = changeData.change().getDest().branch(); |
| String dbKey = Cache.makeKey(changeData.getId().get(), patchset, repoManager); |
| // TODO: get changed files of the given patchset? |
| return get( |
| useCache, |
| permissionBackend, |
| projectState, |
| accountCache, |
| emails, |
| dbKey, |
| repoManager, |
| changeData, |
| branch, |
| changeData.currentFilePaths()); |
| } |
| |
| /** Returns a cached or new OwnersDb, for the specified branch and changed files. */ |
| OwnersDb get( |
| Boolean useCache, |
| PermissionBackend permissionBackend, |
| ProjectState projectState, |
| AccountCache accountCache, |
| Emails emails, |
| String key, |
| GitRepositoryManager repoManager, |
| ChangeData changeData, |
| String branch, |
| Collection<String> files) { |
| if (dbCache == null || !useCache) { // Do not cache OwnersDb |
| logger.atFiner().log("Create new OwnersDb, key=%s", key); |
| return new OwnersDb( |
| permissionBackend, |
| projectState, |
| accountCache, |
| emails, |
| key, |
| repoManager, |
| config, |
| changeData, |
| branch, |
| files); |
| } |
| try { |
| logger.atFiner().log( |
| "Get from cache %s, key=%s, cache size=%d", dbCache, key, dbCache.size()); |
| logger.atFine().atMostEvery(30, TimeUnit.SECONDS).log( |
| "FindOwnersCacheStats: " + dbCache.stats()); |
| return dbCache.get( |
| key, |
| new Callable<OwnersDb>() { |
| @Override |
| public OwnersDb call() { |
| logger.atFiner().log("Create new OwnersDb, key=%s", key); |
| return new OwnersDb( |
| permissionBackend, |
| projectState, |
| accountCache, |
| emails, |
| key, |
| repoManager, |
| config, |
| changeData, |
| branch, |
| files); |
| } |
| }); |
| } catch (ExecutionException e) { |
| logger.atSevere().withCause(e).log( |
| "Cache.get has exception for %s", Config.getChangeId(changeData)); |
| return new OwnersDb( |
| permissionBackend, |
| projectState, |
| accountCache, |
| emails, |
| key, |
| repoManager, |
| config, |
| changeData, |
| branch, |
| files); |
| } |
| } |
| |
| public static String makeKey(int change, int patch, GitRepositoryManager repoManager) { |
| return String.format("%d:%d:%H", change, patch, repoManager); |
| } |
| |
| public static Cache getInstance(Config config, GitRepositoryManager repoManager) { |
| Cache cache = |
| cacheMap.computeIfAbsent(repoManager, (GitRepositoryManager k) -> new Cache(config)); |
| cache.config = config; // Always use newest config, although dbCache could have older data. |
| return cache; |
| } |
| } |