diff options
Diffstat (limited to 'java/com/google/gerrit/acceptance/ProjectResetter.java')
-rw-r--r-- | java/com/google/gerrit/acceptance/ProjectResetter.java | 414 |
1 files changed, 414 insertions, 0 deletions
diff --git a/java/com/google/gerrit/acceptance/ProjectResetter.java b/java/com/google/gerrit/acceptance/ProjectResetter.java new file mode 100644 index 0000000000..cc263c68b6 --- /dev/null +++ b/java/com/google/gerrit/acceptance/ProjectResetter.java @@ -0,0 +1,414 @@ +// 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.google.gerrit.acceptance; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.gerrit.reviewdb.client.RefNames.REFS_USERS; +import static java.util.stream.Collectors.toSet; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.Sets; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.index.RefState; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.account.GroupCache; +import com.google.gerrit.server.account.GroupIncludeCache; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.index.account.AccountIndexer; +import com.google.gerrit.server.index.group.GroupIndexer; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.project.RefPatternMatcher; +import com.google.inject.Inject; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Repository; + +/** + * Saves the states of given projects and resets the project states on close. + * + * <p>Saving the project states is done by saving the states of all refs in the project. On close + * those refs are reset to the saved states. Refs that were newly created are deleted. + * + * <p>By providing ref patterns per project it can be controlled which refs should be reset on + * close. + * + * <p>If resetting touches {@code refs/meta/config} branches the corresponding projects are evicted + * from the project cache. + * + * <p>If resetting touches user branches or the {@code refs/meta/external-ids} branch the + * corresponding accounts are evicted from the account cache and also if needed from the cache in + * {@link AccountCreator}. + * + * <p>At the moment this class has the following limitations: + * + * <ul> + * <li>Resetting group branches doesn't evict the corresponding groups from the group cache. + * <li>Changes are not reindexed if change meta refs are reset. + * <li>Changes are not reindexed if starred-changes refs in All-Users are reset. + * <li>If accounts are deleted changes may still refer to these accounts (e.g. as reviewers). + * </ul> + * + * Primarily this class is intended to reset the states of the All-Projects and All-Users projects + * after each test. These projects rarely contain changes and it's currently not a problem if these + * changes get stale. For creating changes each test gets a brand new project. Since this project is + * not used outside of the test method that creates it, it doesn't need to be reset. + */ +public class ProjectResetter implements AutoCloseable { + public static class Builder { + public interface Factory { + Builder builder(); + } + + private final GitRepositoryManager repoManager; + private final AllUsersName allUsersName; + @Nullable private final AccountCreator accountCreator; + @Nullable private final AccountCache accountCache; + @Nullable private final AccountIndexer accountIndexer; + @Nullable private final GroupCache groupCache; + @Nullable private final GroupIncludeCache groupIncludeCache; + @Nullable private final GroupIndexer groupIndexer; + @Nullable private final ProjectCache projectCache; + + @Inject + public Builder( + GitRepositoryManager repoManager, + AllUsersName allUsersName, + @Nullable AccountCreator accountCreator, + @Nullable AccountCache accountCache, + @Nullable AccountIndexer accountIndexer, + @Nullable GroupCache groupCache, + @Nullable GroupIncludeCache groupIncludeCache, + @Nullable GroupIndexer groupIndexer, + @Nullable ProjectCache projectCache) { + this.repoManager = repoManager; + this.allUsersName = allUsersName; + this.accountCreator = accountCreator; + this.accountCache = accountCache; + this.accountIndexer = accountIndexer; + this.groupCache = groupCache; + this.groupIncludeCache = groupIncludeCache; + this.groupIndexer = groupIndexer; + this.projectCache = projectCache; + } + + public ProjectResetter build(ProjectResetter.Config input) throws IOException { + return new ProjectResetter( + repoManager, + allUsersName, + accountCreator, + accountCache, + accountIndexer, + groupCache, + groupIncludeCache, + groupIndexer, + projectCache, + input.refsByProject); + } + } + + public static class Config { + private final Multimap<Project.NameKey, String> refsByProject; + + public Config() { + this.refsByProject = MultimapBuilder.hashKeys().arrayListValues().build(); + } + + public Config reset(Project.NameKey project, String... refPatterns) { + List<String> refPatternList = Arrays.asList(refPatterns); + if (refPatternList.isEmpty()) { + refPatternList = ImmutableList.of(RefNames.REFS + "*"); + } + refsByProject.putAll(project, refPatternList); + return this; + } + } + + @Inject private GitRepositoryManager repoManager; + @Inject private AllUsersName allUsersName; + @Inject @Nullable private AccountCreator accountCreator; + @Inject @Nullable private AccountCache accountCache; + @Inject @Nullable private GroupCache groupCache; + @Inject @Nullable private GroupIncludeCache groupIncludeCache; + @Inject @Nullable private GroupIndexer groupIndexer; + @Inject @Nullable private AccountIndexer accountIndexer; + @Inject @Nullable private ProjectCache projectCache; + + private final Multimap<Project.NameKey, String> refsPatternByProject; + + // State to which to reset to. + private final Multimap<Project.NameKey, RefState> savedRefStatesByProject; + + // Results of the resetting + private Multimap<Project.NameKey, String> keptRefsByProject; + private Multimap<Project.NameKey, String> restoredRefsByProject; + private Multimap<Project.NameKey, String> deletedRefsByProject; + + private ProjectResetter( + GitRepositoryManager repoManager, + AllUsersName allUsersName, + @Nullable AccountCreator accountCreator, + @Nullable AccountCache accountCache, + @Nullable AccountIndexer accountIndexer, + @Nullable GroupCache groupCache, + @Nullable GroupIncludeCache groupIncludeCache, + @Nullable GroupIndexer groupIndexer, + @Nullable ProjectCache projectCache, + Multimap<Project.NameKey, String> refPatternByProject) + throws IOException { + this.repoManager = repoManager; + this.allUsersName = allUsersName; + this.accountCreator = accountCreator; + this.accountCache = accountCache; + this.accountIndexer = accountIndexer; + this.groupCache = groupCache; + this.groupIndexer = groupIndexer; + this.groupIncludeCache = groupIncludeCache; + this.projectCache = projectCache; + this.refsPatternByProject = refPatternByProject; + this.savedRefStatesByProject = readRefStates(); + } + + @Override + public void close() throws Exception { + keptRefsByProject = MultimapBuilder.hashKeys().arrayListValues().build(); + restoredRefsByProject = MultimapBuilder.hashKeys().arrayListValues().build(); + deletedRefsByProject = MultimapBuilder.hashKeys().arrayListValues().build(); + + restoreRefs(); + deleteNewlyCreatedRefs(); + evictCachesAndReindex(); + } + + /** Read the states of all matching refs. */ + private Multimap<Project.NameKey, RefState> readRefStates() throws IOException { + Multimap<Project.NameKey, RefState> refStatesByProject = + MultimapBuilder.hashKeys().arrayListValues().build(); + for (Map.Entry<Project.NameKey, Collection<String>> e : + refsPatternByProject.asMap().entrySet()) { + try (Repository repo = repoManager.openRepository(e.getKey())) { + Collection<Ref> refs = repo.getRefDatabase().getRefs(); + for (String refPattern : e.getValue()) { + RefPatternMatcher matcher = RefPatternMatcher.getMatcher(refPattern); + for (Ref ref : refs) { + if (matcher.match(ref.getName(), null)) { + refStatesByProject.put(e.getKey(), RefState.create(ref.getName(), ref.getObjectId())); + } + } + } + } + } + return refStatesByProject; + } + + private void restoreRefs() throws IOException { + for (Map.Entry<Project.NameKey, Collection<RefState>> e : + savedRefStatesByProject.asMap().entrySet()) { + try (Repository repo = repoManager.openRepository(e.getKey())) { + for (RefState refState : e.getValue()) { + if (refState.match(repo)) { + keptRefsByProject.put(e.getKey(), refState.ref()); + continue; + } + Ref ref = repo.exactRef(refState.ref()); + RefUpdate updateRef = repo.updateRef(refState.ref()); + updateRef.setExpectedOldObjectId(ref != null ? ref.getObjectId() : ObjectId.zeroId()); + updateRef.setNewObjectId(refState.id()); + updateRef.setForceUpdate(true); + RefUpdate.Result result = updateRef.update(); + checkState( + result == RefUpdate.Result.FORCED || result == RefUpdate.Result.NEW, + "resetting branch %s in %s failed", + refState.ref(), + e.getKey()); + restoredRefsByProject.put(e.getKey(), refState.ref()); + } + } + } + } + + private void deleteNewlyCreatedRefs() throws IOException { + for (Map.Entry<Project.NameKey, Collection<String>> e : + refsPatternByProject.asMap().entrySet()) { + try (Repository repo = repoManager.openRepository(e.getKey())) { + Collection<Ref> nonRestoredRefs = + repo.getRefDatabase().getRefs().stream() + .filter( + r -> + !keptRefsByProject.containsEntry(e.getKey(), r.getName()) + && !restoredRefsByProject.containsEntry(e.getKey(), r.getName())) + .collect(toSet()); + for (String refPattern : e.getValue()) { + RefPatternMatcher matcher = RefPatternMatcher.getMatcher(refPattern); + for (Ref ref : nonRestoredRefs) { + if (matcher.match(ref.getName(), null) + && !deletedRefsByProject.containsEntry(e.getKey(), ref.getName())) { + RefUpdate updateRef = repo.updateRef(ref.getName()); + updateRef.setExpectedOldObjectId(ref.getObjectId()); + updateRef.setNewObjectId(ObjectId.zeroId()); + updateRef.setForceUpdate(true); + RefUpdate.Result result = updateRef.delete(); + checkState( + result == RefUpdate.Result.FORCED, + "deleting branch %s in %s failed", + ref.getName(), + e.getKey()); + deletedRefsByProject.put(e.getKey(), ref.getName()); + } + } + } + } + } + } + + private void evictCachesAndReindex() throws IOException { + evictAndReindexProjects(); + evictAndReindexAccounts(); + evictAndReindexGroups(); + + // TODO(ekempin): Reindex changes if starred-changes refs in All-Users were modified. + } + + /** Evict projects for which the config was changed. */ + private void evictAndReindexProjects() throws IOException { + if (projectCache == null) { + return; + } + + for (Project.NameKey project : + Sets.union( + projectsWithConfigChanges(restoredRefsByProject), + projectsWithConfigChanges(deletedRefsByProject))) { + projectCache.evict(project); + } + } + + private Set<Project.NameKey> projectsWithConfigChanges( + Multimap<Project.NameKey, String> projects) { + return projects.entries().stream() + .filter(e -> e.getValue().equals(RefNames.REFS_CONFIG)) + .map(Map.Entry::getKey) + .collect(toSet()); + } + + /** Evict accounts that were modified. */ + private void evictAndReindexAccounts() throws IOException { + Set<Account.Id> deletedAccounts = accountIds(deletedRefsByProject.get(allUsersName).stream()); + if (accountCreator != null) { + accountCreator.evict(deletedAccounts); + } + if (accountCache != null || accountIndexer != null) { + Set<Account.Id> modifiedAccounts = + new HashSet<>(accountIds(restoredRefsByProject.get(allUsersName).stream())); + + if (restoredRefsByProject.get(allUsersName).contains(RefNames.REFS_EXTERNAL_IDS) + || deletedRefsByProject.get(allUsersName).contains(RefNames.REFS_EXTERNAL_IDS)) { + // The external IDs have been modified but we don't know which accounts were affected. + // Make sure all accounts are evicted and reindexed. + try (Repository repo = repoManager.openRepository(allUsersName)) { + for (Account.Id id : accountIds(repo)) { + evictAndReindexAccount(id); + } + } + + // Remove deleted accounts from the cache and index. + for (Account.Id id : deletedAccounts) { + evictAndReindexAccount(id); + } + } else { + // Evict and reindex all modified and deleted accounts. + for (Account.Id id : Sets.union(modifiedAccounts, deletedAccounts)) { + evictAndReindexAccount(id); + } + } + } + } + + /** Evict groups that were modified. */ + private void evictAndReindexGroups() throws IOException { + if (groupCache != null || groupIndexer != null) { + Set<AccountGroup.UUID> modifiedGroups = + new HashSet<>(groupUUIDs(restoredRefsByProject.get(allUsersName))); + Set<AccountGroup.UUID> deletedGroups = + new HashSet<>(groupUUIDs(deletedRefsByProject.get(allUsersName))); + + // Evict and reindex all modified and deleted groups. + for (AccountGroup.UUID uuid : Sets.union(modifiedGroups, deletedGroups)) { + evictAndReindexGroup(uuid); + } + } + } + + private void evictAndReindexAccount(Account.Id accountId) throws IOException { + if (accountCache != null) { + accountCache.evict(accountId); + } + if (groupIncludeCache != null) { + groupIncludeCache.evictGroupsWithMember(accountId); + } + if (accountIndexer != null) { + accountIndexer.index(accountId); + } + } + + private void evictAndReindexGroup(AccountGroup.UUID uuid) throws IOException { + if (groupCache != null) { + groupCache.evict(uuid); + } + + if (groupIncludeCache != null) { + groupIncludeCache.evictParentGroupsOf(uuid); + } + + if (groupIndexer != null) { + groupIndexer.index(uuid); + } + } + + private static Set<Account.Id> accountIds(Repository repo) throws IOException { + return accountIds(repo.getRefDatabase().getRefsByPrefix(REFS_USERS).stream().map(Ref::getName)); + } + + private static Set<Account.Id> accountIds(Stream<String> refs) { + return refs.filter(r -> r.startsWith(REFS_USERS)) + .map(Account.Id::fromRef) + .filter(Objects::nonNull) + .collect(toSet()); + } + + private Set<AccountGroup.UUID> groupUUIDs(Collection<String> refs) { + return refs.stream() + .filter(RefNames::isRefsGroups) + .map(AccountGroup.UUID::fromRef) + .filter(Objects::nonNull) + .collect(toSet()); + } +} |