Merge branch 'stable-3.2' * stable-3.2: Set version to 3.2.3 for release Change-Id: Idaf7c2ed5a601b6d091268147057b79899a4801a
diff --git a/Jenkinsfile b/Jenkinsfile index a090baf..deb1d46 100644 --- a/Jenkinsfile +++ b/Jenkinsfile
@@ -19,7 +19,7 @@ stage('build') { steps { gerritReview labels: [Verified: 0], message: "Build started: ${env.BUILD_URL}" - sh 'mvn test' + sh "bash -c '. set-java.sh 11 && mvn test'" } } }
diff --git a/pom.xml b/pom.xml index 9ad8148..c67aee8 100644 --- a/pom.xml +++ b/pom.xml
@@ -4,7 +4,7 @@ <groupId>com.gerritforge</groupId> <artifactId>global-refdb</artifactId> - <version>3.2.3</version> + <version>3.3.0-rc1</version> <packaging>jar</packaging> <name>global-refdb</name> @@ -38,7 +38,7 @@ <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> - <gerrit.version>3.2.3</gerrit.version> + <gerrit.version>3.3.0-rc1</gerrit.version> </properties> <dependencies> @@ -75,8 +75,8 @@ <artifactId>maven-compiler-plugin</artifactId> <version>3.6.1</version> <configuration> - <source>1.8</source> - <target>1.8</target> + <source>11</source> + <target>11</target> </configuration> </plugin> <plugin> @@ -95,7 +95,7 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-javadoc-plugin</artifactId> - <version>3.1.1</version> + <version>3.2.0</version> <executions> <execution> <id>attach-javadocs</id>
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/BatchRefUpdateValidator.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/BatchRefUpdateValidator.java new file mode 100644 index 0000000..0961b3c --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/BatchRefUpdateValidator.java
@@ -0,0 +1,178 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.OutOfSyncException; +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement; +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement.EnforcePolicy; +import com.google.common.flogger.FluentLogger; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.transport.ReceiveCommand; + +public class BatchRefUpdateValidator extends RefUpdateValidator { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + public static interface Factory { + BatchRefUpdateValidator create(String projectName, RefDatabase refDb); + } + + public interface BatchValidationWrapper { + void apply(BatchRefUpdate batchRefUpdate, NoParameterVoidFunction arg) throws IOException; + } + + @Inject + public BatchRefUpdateValidator( + SharedRefDatabaseWrapper sharedRefDb, + ValidationMetrics validationMetrics, + SharedRefEnforcement refEnforcement, + LockWrapper.Factory lockWrapperFactory, + ProjectsFilter projectsFilter, + @Assisted String projectName, + @Assisted RefDatabase refDb) { + super( + sharedRefDb, + validationMetrics, + refEnforcement, + lockWrapperFactory, + projectsFilter, + projectName, + refDb); + } + + public void executeBatchUpdateWithValidation( + BatchRefUpdate batchRefUpdate, NoParameterVoidFunction batchRefUpdateFunction) + throws IOException { + if (refEnforcement.getPolicy(projectName) == EnforcePolicy.IGNORED + || !isGlobalProject(projectName)) { + batchRefUpdateFunction.invoke(); + return; + } + + try { + doExecuteBatchUpdate(batchRefUpdate, batchRefUpdateFunction); + } catch (IOException e) { + logger.atWarning().withCause(e).log( + "Failed to execute Batch Update on project %s", projectName); + if (refEnforcement.getPolicy(projectName) == EnforcePolicy.REQUIRED) { + throw e; + } + } + } + + private void doExecuteBatchUpdate( + BatchRefUpdate batchRefUpdate, NoParameterVoidFunction delegateUpdate) throws IOException { + + List<ReceiveCommand> commands = batchRefUpdate.getCommands(); + if (commands.isEmpty()) { + return; + } + + List<RefPair> refsToUpdate = getRefsPairs(commands).collect(Collectors.toList()); + List<RefPair> refsFailures = + refsToUpdate.stream().filter(RefPair::hasFailed).collect(Collectors.toList()); + if (!refsFailures.isEmpty()) { + String allFailuresMessage = + refsFailures.stream() + .map(refPair -> String.format("Failed to fetch ref %s", refPair.compareRef.getName())) + .collect(Collectors.joining(", ")); + Exception firstFailureException = refsFailures.get(0).exception; + + logger.atSevere().withCause(firstFailureException).log(allFailuresMessage); + throw new IOException(allFailuresMessage, firstFailureException); + } + + try (CloseableSet<AutoCloseable> locks = new CloseableSet<>()) { + refsToUpdate = compareAndGetLatestLocalRefs(refsToUpdate, locks); + delegateUpdate.invoke(); + updateSharedRefDb(batchRefUpdate.getCommands().stream(), refsToUpdate); + } catch (OutOfSyncException e) { + List<ReceiveCommand> receiveCommands = batchRefUpdate.getCommands(); + logger.atWarning().withCause(e).log( + String.format( + "Batch ref-update failing because node is out of sync with the shared ref-db. Set all commands Result to LOCK_FAILURE [%d]", + receiveCommands.size())); + receiveCommands.forEach((command) -> command.setResult(ReceiveCommand.Result.LOCK_FAILURE)); + } + } + + private void updateSharedRefDb(Stream<ReceiveCommand> commandStream, List<RefPair> refsToUpdate) + throws IOException { + if (commandStream + .filter(cmd -> cmd.getResult() != ReceiveCommand.Result.OK) + .findFirst() + .isPresent()) { + return; + } + + for (RefPair refPair : refsToUpdate) { + updateSharedDbOrThrowExceptionFor(refPair); + } + } + + private Stream<RefPair> getRefsPairs(List<ReceiveCommand> receivedCommands) { + return receivedCommands.stream().map(this::getRefPairForCommand); + } + + private RefPair getRefPairForCommand(ReceiveCommand command) { + try { + switch (command.getType()) { + case CREATE: + return new RefPair(nullRef(command.getRefName()), getNewRef(command)); + + case UPDATE: + case UPDATE_NONFASTFORWARD: + return new RefPair(getCurrentRef(command.getRefName()), getNewRef(command)); + + case DELETE: + return new RefPair(getCurrentRef(command.getRefName()), ObjectId.zeroId()); + + default: + return new RefPair( + command.getRef(), + new IllegalArgumentException("Unsupported command type " + command.getType())); + } + } catch (IOException e) { + return new RefPair(command.getRef(), e); + } + } + + private ObjectId getNewRef(ReceiveCommand command) { + return command.getNewId(); + } + + private List<RefPair> compareAndGetLatestLocalRefs( + List<RefPair> refsToUpdate, CloseableSet<AutoCloseable> locks) throws IOException { + List<RefPair> latestRefsToUpdate = new ArrayList<>(); + for (RefPair refPair : refsToUpdate) { + latestRefsToUpdate.add(compareAndGetLatestLocalRef(refPair, locks)); + } + return latestRefsToUpdate; + } + + private static final Ref nullRef(String refName) { + return new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, refName, ObjectId.zeroId()); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LibModule.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LibModule.java new file mode 100644 index 0000000..49b20d8 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LibModule.java
@@ -0,0 +1,37 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import com.gerritforge.gerrit.globalrefdb.GlobalRefDatabase; +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.NoopSharedRefDatabase; +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.gerrit.lifecycle.LifecycleModule; +import com.google.inject.Scopes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LibModule extends LifecycleModule { + private static final Logger log = LoggerFactory.getLogger(LibModule.class); + + @Override + protected void configure() { + DynamicItem.itemOf(binder(), GlobalRefDatabase.class); + DynamicItem.bind(binder(), GlobalRefDatabase.class) + .to(NoopSharedRefDatabase.class) + .in(Scopes.SINGLETON); + + log.info("Shared ref-db engine: none"); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LibModuleLogFile.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LibModuleLogFile.java new file mode 100644 index 0000000..075bcc9 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LibModuleLogFile.java
@@ -0,0 +1,32 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import com.google.gerrit.server.util.SystemLog; +import org.apache.log4j.AsyncAppender; +import org.apache.log4j.Layout; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; + +public abstract class LibModuleLogFile { + + public LibModuleLogFile(SystemLog systemLog, String logName, Layout layout) { + AsyncAppender asyncAppender = systemLog.createAsyncAppender(logName, layout, true, true); + Logger logger = LogManager.getLogger(logName); + logger.removeAppender(logName); + logger.addAppender(asyncAppender); + logger.setAdditivity(false); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LockWrapper.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LockWrapper.java new file mode 100644 index 0000000..18d75a3 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/LockWrapper.java
@@ -0,0 +1,50 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +public class LockWrapper implements AutoCloseable { + public interface Factory { + LockWrapper create( + @Assisted("project") String project, + @Assisted("refName") String refName, + @Assisted AutoCloseable lock); + } + + private final String project; + private final String refName; + private final AutoCloseable lock; + private final SharedRefLogger sharedRefLogger; + + @Inject + public LockWrapper( + SharedRefLogger sharedRefLogger, + @Assisted("project") String project, + @Assisted("refName") String refName, + @Assisted AutoCloseable lock) { + this.lock = lock; + this.sharedRefLogger = sharedRefLogger; + this.project = project; + this.refName = refName; + } + + @Override + public void close() throws Exception { + lock.close(); + sharedRefLogger.logLockRelease(project, refName); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/Log4jSharedRefLogger.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/Log4jSharedRefLogger.java new file mode 100644 index 0000000..5653ee2 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/Log4jSharedRefLogger.java
@@ -0,0 +1,141 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.entities.Project; +import com.google.gerrit.extensions.common.GitPerson; +import com.google.gerrit.json.OutputFormat; +import com.google.gerrit.server.CommonConverters; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.util.SystemLog; +import com.google.gson.Gson; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.io.IOException; +import org.apache.log4j.PatternLayout; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class Log4jSharedRefLogger extends LibModuleLogFile implements SharedRefLogger { + private static final String LOG_NAME = "sharedref_log"; + private Logger sharedRefDBLog; + private final GitRepositoryManager gitRepositoryManager; + private static final Gson gson = OutputFormat.JSON_COMPACT.newGson(); + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + @Inject + public Log4jSharedRefLogger(SystemLog systemLog, GitRepositoryManager gitRepositoryManager) { + super(systemLog, LOG_NAME, new PatternLayout("[%d{ISO8601}] [%t] %-5p : %m%n")); + this.gitRepositoryManager = gitRepositoryManager; + sharedRefDBLog = LoggerFactory.getLogger(LOG_NAME); + } + + @Override + public void logRefUpdate(String project, Ref currRef, ObjectId newRefValue) { + if (!ObjectId.zeroId().equals(newRefValue)) { + try (Repository repository = gitRepositoryManager.openRepository(Project.nameKey(project)); + RevWalk walk = new RevWalk(repository)) { + GitPerson committer = null; + String commitMessage = null; + if (newRefValue != null) { + int objectType = walk.parseAny(newRefValue).getType(); + switch (objectType) { + case OBJ_COMMIT: + RevCommit commit = walk.parseCommit(newRefValue); + committer = CommonConverters.toGitPerson(commit.getCommitterIdent()); + commitMessage = commit.getShortMessage(); + break; + case OBJ_BLOB: + break; + default: + throw new IncorrectObjectTypeException(newRefValue, Constants.typeString(objectType)); + } + } + sharedRefDBLog.info( + gson.toJson( + new SharedRefLogEntry.UpdateRef( + project, + currRef.getName(), + currRef.getObjectId().getName(), + newRefValue == null ? ObjectId.zeroId().name() : newRefValue.getName(), + committer, + commitMessage))); + } catch (IOException e) { + logger.atSevere().withCause(e).log( + "Cannot log sharedRefDB interaction for ref %s on project %s", + currRef.getName(), project); + } + } else { + sharedRefDBLog.info( + gson.toJson( + new SharedRefLogEntry.DeleteRef( + project, currRef.getName(), currRef.getObjectId().getName()))); + } + } + + @Override + public <T> void logRefUpdate(String project, String refName, T currRef, T newRefValue) { + if (newRefValue != null) { + sharedRefDBLog.info( + gson.toJson( + new SharedRefLogEntry.UpdateRef( + project, refName, safeToString(currRef), safeToString(newRefValue), null, null))); + } else { + sharedRefDBLog.info( + gson.toJson(new SharedRefLogEntry.DeleteRef(project, refName, safeToString(currRef)))); + } + } + + @Override + public void logProjectDelete(String project) { + sharedRefDBLog.info(gson.toJson(new SharedRefLogEntry.DeleteProject(project))); + } + + @Override + public void logLockAcquisition(String project, String refName) { + sharedRefDBLog.info(gson.toJson(new SharedRefLogEntry.LockAcquire(project, refName))); + } + + @Override + public void logLockRelease(String project, String refName) { + sharedRefDBLog.info(gson.toJson(new SharedRefLogEntry.LockRelease(project, refName))); + } + + @VisibleForTesting + public void setLogger(Logger logger) { + this.sharedRefDBLog = logger; + } + + private <T> String safeToString(T currRef) { + if (currRef == null) { + return "<null>"; + } + return currRef.toString(); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectDeletedSharedDbCleanup.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectDeletedSharedDbCleanup.java new file mode 100644 index 0000000..ec72bcc --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectDeletedSharedDbCleanup.java
@@ -0,0 +1,53 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.entities.Project; +import com.google.gerrit.extensions.events.ProjectDeletedListener; +import com.google.inject.Inject; + +public class ProjectDeletedSharedDbCleanup implements ProjectDeletedListener { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private final SharedRefDatabaseWrapper sharedDb; + + private final ValidationMetrics validationMetrics; + + @Inject + public ProjectDeletedSharedDbCleanup( + SharedRefDatabaseWrapper sharedDb, ValidationMetrics validationMetrics) { + this.sharedDb = sharedDb; + this.validationMetrics = validationMetrics; + } + + @Override + public void onProjectDeleted(Event event) { + String projectName = event.getProjectName(); + logger.atInfo().log( + "Deleting project '%s'. Will perform a cleanup in Shared-Ref database.", projectName); + + try { + sharedDb.remove(Project.nameKey(projectName)); + } catch (GlobalRefDbSystemError e) { + validationMetrics.incrementSplitBrain(); + logger.atSevere().withCause(e).log( + "Project '%s' deleted from GIT but it was not able to cleanup" + + " from Shared-Ref database", + projectName); + } + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectsFilter.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectsFilter.java new file mode 100644 index 0000000..3121df4 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectsFilter.java
@@ -0,0 +1,112 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import com.google.common.base.Strings; +import com.google.common.collect.Sets; +import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.Project.NameKey; +import com.google.gerrit.server.events.Event; +import com.google.gerrit.server.events.ProjectEvent; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.util.List; +import java.util.Set; + +// import com.google.gerrit.entities.AccessSection; + +@Singleton +public class ProjectsFilter { + public enum PatternType { + REGEX, + WILDCARD, + EXACT_MATCH; + + public static PatternType getPatternType(String pattern) { + // TODO: Use AccessSection + if (pattern.startsWith("^")) { + // if (pattern.startsWith(AccessSection.REGEX_PREFIX)) { + return REGEX; + } else if (pattern.endsWith("*")) { + return WILDCARD; + } else { + return EXACT_MATCH; + } + } + } + + private Set<NameKey> globalProjects = Sets.newConcurrentHashSet(); + private Set<NameKey> localProjects = Sets.newConcurrentHashSet(); + private final List<String> projectPatterns; + + @Inject + public ProjectsFilter(SharedRefDbConfiguration cfg) { + projectPatterns = cfg.projects().getPatterns(); + } + + public boolean matches(Event event) { + if (event == null) { + throw new IllegalArgumentException("Event object cannot be null"); + } + if (event instanceof ProjectEvent) { + return matches(((ProjectEvent) event).getProjectNameKey()); + } + return false; + } + + public boolean matches(String projectName) { + return matches(NameKey.parse(projectName)); + } + + public boolean matches(Project.NameKey name) { + if (name == null || Strings.isNullOrEmpty(name.get())) { + throw new IllegalArgumentException( + String.format("Project name cannot be null or empty, but was %s", name)); + } + if (projectPatterns.isEmpty() || globalProjects.contains(name)) { + return true; + } + + if (localProjects.contains(name)) { + return false; + } + + String projectName = name.get(); + + for (String pattern : projectPatterns) { + if (matchesPattern(projectName, pattern)) { + globalProjects.add(name); + return true; + } + } + localProjects.add(name); + return false; + } + + private boolean matchesPattern(String projectName, String pattern) { + boolean match = false; + switch (PatternType.getPatternType(pattern)) { + case REGEX: + match = projectName.matches(pattern); + break; + case WILDCARD: + match = projectName.startsWith(pattern.substring(0, pattern.length() - 1)); + break; + case EXACT_MATCH: + match = projectName.equals(pattern); + } + return match; + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefPair.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefPair.java new file mode 100644 index 0000000..7156a7e --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefPair.java
@@ -0,0 +1,47 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; + +public class RefPair { + public final Ref compareRef; + public final ObjectId putValue; + public final Exception exception; + + RefPair(Ref oldRef, ObjectId newRefValue) { + if (oldRef == null) { + throw new IllegalArgumentException("Required not-null ref in RefPair"); + } + this.compareRef = oldRef; + this.putValue = newRefValue; + this.exception = null; + } + + RefPair(Ref newRef, Exception e) { + this.compareRef = newRef; + this.exception = e; + this.putValue = ObjectId.zeroId(); + } + + public String getName() { + return compareRef.getName(); + } + + public boolean hasFailed() { + return exception != null; + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidator.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidator.java new file mode 100644 index 0000000..123e382 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidator.java
@@ -0,0 +1,295 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError; +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.OutOfSyncException; +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedDbSplitBrainException; +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedLockException; +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement; +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement.EnforcePolicy; +import com.google.common.base.MoreObjects; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.entities.Project; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import java.io.IOException; +import java.util.HashMap; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefUpdate; + +public class RefUpdateValidator { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + public static final String SHARED_REF_DB_VERSIONING_REF = "refs/global-refdb/version"; + + protected final SharedRefDatabaseWrapper sharedRefDb; + protected final ValidationMetrics validationMetrics; + + protected final String projectName; + private final LockWrapper.Factory lockWrapperFactory; + protected final RefDatabase refDb; + protected final SharedRefEnforcement refEnforcement; + protected final ProjectsFilter projectsFilter; + + public static interface Factory { + RefUpdateValidator create(String projectName, RefDatabase refDb); + } + + public interface ExceptionThrowingSupplier<T, E extends Exception> { + T create() throws E; + } + + public interface RefValidationWrapper { + RefUpdate.Result apply(NoParameterFunction<RefUpdate.Result> arg, RefUpdate refUpdate) + throws IOException; + } + + public interface NoParameterFunction<T> { + T invoke() throws IOException; + } + + public interface NoParameterVoidFunction { + void invoke() throws IOException; + } + + @Inject + public RefUpdateValidator( + SharedRefDatabaseWrapper sharedRefDb, + ValidationMetrics validationMetrics, + SharedRefEnforcement refEnforcement, + LockWrapper.Factory lockWrapperFactory, + ProjectsFilter projectsFilter, + @Assisted String projectName, + @Assisted RefDatabase refDb) { + this.sharedRefDb = sharedRefDb; + this.validationMetrics = validationMetrics; + this.lockWrapperFactory = lockWrapperFactory; + this.refDb = refDb; + this.projectName = projectName; + this.refEnforcement = refEnforcement; + this.projectsFilter = projectsFilter; + } + + public RefUpdate.Result executeRefUpdate( + RefUpdate refUpdate, NoParameterFunction<RefUpdate.Result> refUpdateFunction) + throws IOException { + if (isProjectVersionUpdate(refUpdate.getName()) + || !isGlobalProject(projectName) + || refEnforcement.getPolicy(projectName) == EnforcePolicy.IGNORED) { + return refUpdateFunction.invoke(); + } + + try { + return doExecuteRefUpdate(refUpdate, refUpdateFunction); + } catch (SharedDbSplitBrainException e) { + validationMetrics.incrementSplitBrain(); + + logger.atWarning().withCause(e).log( + "Unable to execute ref-update on project=%s ref=%s", + projectName, refUpdate.getRef().getName()); + if (refEnforcement.getPolicy(projectName) == EnforcePolicy.REQUIRED) { + throw e; + } + } + return null; + } + + private Boolean isProjectVersionUpdate(String refName) { + Boolean isProjectVersionUpdate = refName.equals(SHARED_REF_DB_VERSIONING_REF); + logger.atFine().log("Is project version update? " + isProjectVersionUpdate); + return isProjectVersionUpdate; + } + + private <T extends Throwable> void softFailBasedOnEnforcement(T e, EnforcePolicy policy) + throws T { + logger.atWarning().withCause(e).log( + String.format( + "Failure while running with policy enforcement %s. Error message: %s", + policy, e.getMessage())); + if (policy == EnforcePolicy.REQUIRED) { + throw e; + } + } + + protected Boolean isGlobalProject(String projectName) { + Boolean isGlobalProject = projectsFilter.matches(projectName); + logger.atFine().log("Is global project? " + isGlobalProject); + return isGlobalProject; + } + + protected RefUpdate.Result doExecuteRefUpdate( + RefUpdate refUpdate, NoParameterFunction<RefUpdate.Result> refUpdateFunction) + throws IOException { + try (CloseableSet<AutoCloseable> locks = new CloseableSet<>()) { + RefPair refPairForUpdate = newRefPairFrom(refUpdate); + compareAndGetLatestLocalRef(refPairForUpdate, locks); + RefUpdate.Result result = refUpdateFunction.invoke(); + if (isSuccessful(result)) { + updateSharedDbOrThrowExceptionFor(refPairForUpdate); + } + return result; + } catch (OutOfSyncException e) { + logger.atWarning().withCause(e).log( + String.format("Local node is out of sync with ref-db: %s", e.getMessage())); + + return RefUpdate.Result.LOCK_FAILURE; + } + } + + protected void updateSharedDbOrThrowExceptionFor(RefPair refPair) throws IOException { + // We are not checking refs that should be ignored + final EnforcePolicy refEnforcementPolicy = + refEnforcement.getPolicy(projectName, refPair.getName()); + if (refEnforcementPolicy == EnforcePolicy.IGNORED) return; + + String errorMessage = + String.format( + "Not able to persist the data in Zookeeper for project '%s' and ref '%s'," + + "the cluster is now in Split Brain since the commit has been " + + "persisted locally but not in SharedRef the value %s", + projectName, refPair.getName(), refPair.putValue); + boolean succeeded; + try { + succeeded = + sharedRefDb.compareAndPut( + Project.nameKey(projectName), refPair.compareRef, refPair.putValue); + } catch (GlobalRefDbSystemError e) { + logger.atWarning().withCause(e).log( + "Not able to persist the data in Zookeeper for project '{}' and ref '{}', message: {}", + projectName, + refPair.getName(), + e.getMessage()); + throw new SharedDbSplitBrainException(errorMessage, e); + } + + if (!succeeded) { + throw new SharedDbSplitBrainException(errorMessage); + } + } + + protected RefPair compareAndGetLatestLocalRef(RefPair refPair, CloseableSet<AutoCloseable> locks) + throws SharedLockException, OutOfSyncException, IOException { + String refName = refPair.getName(); + EnforcePolicy refEnforcementPolicy = refEnforcement.getPolicy(projectName, refName); + if (refEnforcementPolicy == EnforcePolicy.IGNORED) { + return refPair; + } + + locks.addResourceIfNotExist( + String.format("%s-%s", projectName, refName), + () -> + lockWrapperFactory.create( + projectName, refName, sharedRefDb.lockRef(Project.nameKey(projectName), refName))); + + RefPair latestRefPair = getLatestLocalRef(refPair); + if (sharedRefDb.isUpToDate(Project.nameKey(projectName), latestRefPair.compareRef)) { + return latestRefPair; + } + + if (isNullRef(latestRefPair.compareRef) + || sharedRefDb.exists(Project.nameKey(projectName), refName)) { + validationMetrics.incrementSplitBrainPrevention(); + + softFailBasedOnEnforcement( + new OutOfSyncException(projectName, latestRefPair.compareRef), refEnforcementPolicy); + } + + return latestRefPair; + } + + private boolean isNullRef(Ref ref) { + return ref.getObjectId().equals(ObjectId.zeroId()); + } + + private RefPair getLatestLocalRef(RefPair refPair) throws IOException { + Ref latestRef = refDb.exactRef(refPair.getName()); + return new RefPair( + latestRef == null ? nullRef(refPair.getName()) : latestRef, refPair.putValue); + } + + private Ref nullRef(String name) { + return new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, name, ObjectId.zeroId()); + } + + protected boolean isSuccessful(RefUpdate.Result result) { + switch (result) { + case NEW: + case FORCED: + case FAST_FORWARD: + case NO_CHANGE: + case RENAMED: + return true; + + case REJECTED_OTHER_REASON: + case REJECTED_MISSING_OBJECT: + case REJECTED_CURRENT_BRANCH: + case NOT_ATTEMPTED: + case LOCK_FAILURE: + case IO_FAILURE: + case REJECTED: + default: + return false; + } + } + + protected RefPair newRefPairFrom(RefUpdate refUpdate) throws IOException { + return new RefPair(getCurrentRef(refUpdate.getName()), refUpdate.getNewObjectId()); + } + + protected Ref getCurrentRef(String refName) throws IOException { + return MoreObjects.firstNonNull(refDb.findRef(refName), nullRef(refName)); + } + + public static class CloseableSet<T extends AutoCloseable> implements AutoCloseable { + private final HashMap<String, AutoCloseable> elements; + + public CloseableSet() { + this(new HashMap<>()); + } + + public CloseableSet(HashMap<String, AutoCloseable> elements) { + this.elements = elements; + } + + public void addResourceIfNotExist( + String key, ExceptionThrowingSupplier<T, SharedLockException> resourceFactory) + throws SharedLockException { + if (!elements.containsKey(key)) { + elements.put(key, resourceFactory.create()); + } + } + + @Override + public void close() { + elements.values().stream() + .forEach( + closeable -> { + try { + closeable.close(); + } catch (Exception closingException) { + logger.atSevere().withCause(closingException).log( + "Exception trying to release resource %s, " + + "the locked resources won't be accessible in all cluster unless" + + " the lock is removed from ZK manually", + closeable); + } + }); + } + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapper.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapper.java new file mode 100644 index 0000000..d9c8051 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapper.java
@@ -0,0 +1,102 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import com.gerritforge.gerrit.globalrefdb.GlobalRefDatabase; +import com.gerritforge.gerrit.globalrefdb.GlobalRefDbLockException; +import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError; +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.NoopSharedRefDatabase; +import com.google.common.annotations.VisibleForTesting; +import com.google.gerrit.entities.Project; +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.inject.Inject; +import java.util.Optional; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; + +public class SharedRefDatabaseWrapper implements GlobalRefDatabase { + private static final GlobalRefDatabase NOOP_REFDB = new NoopSharedRefDatabase(); + + @Inject(optional = true) + private DynamicItem<GlobalRefDatabase> sharedRefDbDynamicItem; + + private final SharedRefLogger sharedRefLogger; + + @Inject + public SharedRefDatabaseWrapper(SharedRefLogger sharedRefLogger) { + this.sharedRefLogger = sharedRefLogger; + } + + @VisibleForTesting + public SharedRefDatabaseWrapper( + DynamicItem<GlobalRefDatabase> sharedRefDbDynamicItem, SharedRefLogger sharedRefLogger) { + this.sharedRefLogger = sharedRefLogger; + this.sharedRefDbDynamicItem = sharedRefDbDynamicItem; + } + + @Override + public boolean isUpToDate(Project.NameKey project, Ref ref) throws GlobalRefDbLockException { + return sharedRefDb().isUpToDate(project, ref); + } + + @Override + public boolean compareAndPut(Project.NameKey project, Ref currRef, ObjectId newRefValue) + throws GlobalRefDbSystemError { + boolean succeeded = sharedRefDb().compareAndPut(project, currRef, newRefValue); + if (succeeded) { + sharedRefLogger.logRefUpdate(project.get(), currRef, newRefValue); + } + return succeeded; + } + + @Override + public <T> boolean compareAndPut(Project.NameKey project, String refName, T currValue, T newValue) + throws GlobalRefDbSystemError { + boolean succeeded = sharedRefDb().compareAndPut(project, refName, currValue, newValue); + if (succeeded) { + sharedRefLogger.logRefUpdate(project.get(), refName, currValue, newValue); + } + return succeeded; + } + + @Override + public AutoCloseable lockRef(Project.NameKey project, String refName) + throws GlobalRefDbLockException { + AutoCloseable locker = sharedRefDb().lockRef(project, refName); + sharedRefLogger.logLockAcquisition(project.get(), refName); + return locker; + } + + @Override + public boolean exists(Project.NameKey project, String refName) { + return sharedRefDb().exists(project, refName); + } + + @Override + public void remove(Project.NameKey project) throws GlobalRefDbSystemError { + sharedRefDb().remove(project); + sharedRefLogger.logProjectDelete(project.get()); + } + + @Override + public <T> Optional<T> get(Project.NameKey nameKey, String s, Class<T> clazz) + throws GlobalRefDbSystemError { + return sharedRefDb().get(nameKey, s, clazz); + } + + private GlobalRefDatabase sharedRefDb() { + return Optional.ofNullable(sharedRefDbDynamicItem).map(di -> di.get()).orElse(NOOP_REFDB); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbBatchRefUpdate.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbBatchRefUpdate.java new file mode 100644 index 0000000..5d23c32 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbBatchRefUpdate.java
@@ -0,0 +1,185 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.PushCertificate; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.util.time.ProposedTimestamp; + +public class SharedRefDbBatchRefUpdate extends BatchRefUpdate { + + private final BatchRefUpdate batchRefUpdate; + private final String project; + private final BatchRefUpdateValidator.Factory batchRefValidatorFactory; + private final RefDatabase refDb; + + public static interface Factory { + SharedRefDbBatchRefUpdate create(String project, RefDatabase refDb); + } + + @Inject + public SharedRefDbBatchRefUpdate( + BatchRefUpdateValidator.Factory batchRefValidatorFactory, + @Assisted String project, + @Assisted RefDatabase refDb) { + super(refDb); + this.refDb = refDb; + this.project = project; + this.batchRefUpdate = refDb.newBatchUpdate(); + this.batchRefValidatorFactory = batchRefValidatorFactory; + } + + @Override + public int hashCode() { + return batchRefUpdate.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return batchRefUpdate.equals(obj); + } + + @Override + public boolean isAllowNonFastForwards() { + return batchRefUpdate.isAllowNonFastForwards(); + } + + @Override + public BatchRefUpdate setAllowNonFastForwards(boolean allow) { + return batchRefUpdate.setAllowNonFastForwards(allow); + } + + @Override + public PersonIdent getRefLogIdent() { + return batchRefUpdate.getRefLogIdent(); + } + + @Override + public BatchRefUpdate setRefLogIdent(PersonIdent pi) { + return batchRefUpdate.setRefLogIdent(pi); + } + + @Override + public String getRefLogMessage() { + return batchRefUpdate.getRefLogMessage(); + } + + @Override + public boolean isRefLogIncludingResult() { + return batchRefUpdate.isRefLogIncludingResult(); + } + + @Override + public BatchRefUpdate setRefLogMessage(String msg, boolean appendStatus) { + return batchRefUpdate.setRefLogMessage(msg, appendStatus); + } + + @Override + public BatchRefUpdate disableRefLog() { + return batchRefUpdate.disableRefLog(); + } + + @Override + public BatchRefUpdate setForceRefLog(boolean force) { + return batchRefUpdate.setForceRefLog(force); + } + + @Override + public boolean isRefLogDisabled() { + return batchRefUpdate.isRefLogDisabled(); + } + + @Override + public BatchRefUpdate setAtomic(boolean atomic) { + return batchRefUpdate.setAtomic(atomic); + } + + @Override + public boolean isAtomic() { + return batchRefUpdate.isAtomic(); + } + + @Override + public void setPushCertificate(PushCertificate cert) { + batchRefUpdate.setPushCertificate(cert); + } + + @Override + public List<ReceiveCommand> getCommands() { + return batchRefUpdate.getCommands(); + } + + @Override + public BatchRefUpdate addCommand(ReceiveCommand cmd) { + return batchRefUpdate.addCommand(cmd); + } + + @Override + public BatchRefUpdate addCommand(ReceiveCommand... cmd) { + return batchRefUpdate.addCommand(cmd); + } + + @Override + public BatchRefUpdate addCommand(Collection<ReceiveCommand> cmd) { + return batchRefUpdate.addCommand(cmd); + } + + @Override + public List<String> getPushOptions() { + return batchRefUpdate.getPushOptions(); + } + + @Override + public List<ProposedTimestamp> getProposedTimestamps() { + return batchRefUpdate.getProposedTimestamps(); + } + + @Override + public BatchRefUpdate addProposedTimestamp(ProposedTimestamp ts) { + return batchRefUpdate.addProposedTimestamp(ts); + } + + @Override + public void execute(RevWalk walk, ProgressMonitor monitor, List<String> options) + throws IOException { + batchRefValidatorFactory + .create(project, refDb) + .executeBatchUpdateWithValidation( + batchRefUpdate, () -> batchRefUpdate.execute(walk, monitor, options)); + } + + @Override + public void execute(RevWalk walk, ProgressMonitor monitor) throws IOException { + batchRefValidatorFactory + .create(project, refDb) + .executeBatchUpdateWithValidation( + batchRefUpdate, () -> batchRefUpdate.execute(walk, monitor)); + } + + @Override + public String toString() { + return batchRefUpdate.toString(); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbConfiguration.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbConfiguration.java new file mode 100644 index 0000000..3708f74 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbConfiguration.java
@@ -0,0 +1,127 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import static com.google.common.base.Suppliers.memoize; +import static com.google.common.base.Suppliers.ofInstance; + +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement.EnforcePolicy; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import java.io.IOException; +import java.util.List; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SharedRefDbConfiguration { + private static final Logger log = LoggerFactory.getLogger(SharedRefDbConfiguration.class); + + private final Supplier<Projects> projects; + private final Supplier<SharedRefDatabase> sharedRefDb; + + public SharedRefDbConfiguration(Config config) { + Supplier<Config> lazyCfg = lazyLoad(config); + projects = memoize(() -> new Projects(lazyCfg)); + sharedRefDb = memoize(() -> new SharedRefDatabase(lazyCfg)); + } + + public SharedRefDatabase getSharedRefDb() { + return sharedRefDb.get(); + } + + public Projects projects() { + return projects.get(); + } + + private Supplier<Config> lazyLoad(Config config) { + if (config instanceof FileBasedConfig) { + return memoize( + () -> { + FileBasedConfig fileConfig = (FileBasedConfig) config; + String fileConfigFileName = fileConfig.getFile().getPath(); + try { + log.info("Loading configuration from {}", fileConfigFileName); + fileConfig.load(); + } catch (IOException | ConfigInvalidException e) { + log.error("Unable to load configuration from " + fileConfigFileName, e); + } + return fileConfig; + }); + } + return ofInstance(config); + } + + public static class SharedRefDatabase { + public static final String SECTION = "ref-database"; + public static final String ENABLE_KEY = "enabled"; + public static final String SUBSECTION_ENFORCEMENT_RULES = "enforcementRules"; + + private final boolean enabled; + private final Multimap<EnforcePolicy, String> enforcementRules; + + private SharedRefDatabase(Supplier<Config> cfg) { + enabled = getBoolean(cfg, SECTION, null, ENABLE_KEY, false); + + enforcementRules = MultimapBuilder.hashKeys().arrayListValues().build(); + for (EnforcePolicy policy : EnforcePolicy.values()) { + enforcementRules.putAll( + policy, getList(cfg, SECTION, SUBSECTION_ENFORCEMENT_RULES, policy.name())); + } + } + + public boolean isEnabled() { + return enabled; + } + + public Multimap<EnforcePolicy, String> getEnforcementRules() { + return enforcementRules; + } + + private List<String> getList( + Supplier<Config> cfg, String section, String subsection, String name) { + return ImmutableList.copyOf(cfg.get().getStringList(section, subsection, name)); + } + } + + public static class Projects { + public static final String SECTION = "projects"; + public static final String PATTERN_KEY = "pattern"; + public List<String> patterns; + + public Projects(Supplier<Config> cfg) { + patterns = ImmutableList.copyOf(cfg.get().getStringList(SECTION, null, PATTERN_KEY)); + } + + public List<String> getPatterns() { + return patterns; + } + } + + static boolean getBoolean( + Supplier<Config> cfg, String section, String subsection, String name, boolean defaultValue) { + try { + return cfg.get().getBoolean(section, subsection, name, defaultValue); + } catch (IllegalArgumentException e) { + log.error("invalid value for {}; using default value {}", name, defaultValue); + log.debug("Failed to retrieve boolean value: {}", e.getMessage(), e); + return defaultValue; + } + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbGitRepositoryManager.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbGitRepositoryManager.java new file mode 100644 index 0000000..b533c05 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbGitRepositoryManager.java
@@ -0,0 +1,61 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import com.google.gerrit.entities.Project; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.LocalDiskRepositoryManager; +import com.google.gerrit.server.git.RepositoryCaseMismatchException; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.SortedSet; +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.lib.Repository; + +@Singleton +public class SharedRefDbGitRepositoryManager implements GitRepositoryManager { + private final GitRepositoryManager gitRepositoryManager; + private final SharedRefDbRepository.Factory sharedRefDbRepoFactory; + + @Inject + public SharedRefDbGitRepositoryManager( + SharedRefDbRepository.Factory sharedRefDbRepoFactory, + LocalDiskRepositoryManager localDiskRepositoryManager) { + this.sharedRefDbRepoFactory = sharedRefDbRepoFactory; + this.gitRepositoryManager = localDiskRepositoryManager; + } + + @Override + public Repository openRepository(Project.NameKey name) + throws RepositoryNotFoundException, IOException { + return wrap(name, gitRepositoryManager.openRepository(name)); + } + + @Override + public Repository createRepository(Project.NameKey name) + throws RepositoryCaseMismatchException, RepositoryNotFoundException, IOException { + return wrap(name, gitRepositoryManager.createRepository(name)); + } + + @Override + public SortedSet<Project.NameKey> list() { + return gitRepositoryManager.list(); + } + + private Repository wrap(Project.NameKey projectName, Repository projectRepo) { + return sharedRefDbRepoFactory.create(projectName.get(), projectRepo); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefDatabase.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefDatabase.java new file mode 100644 index 0000000..8597c49 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefDatabase.java
@@ -0,0 +1,160 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefRename; +import org.eclipse.jgit.lib.RefUpdate; + +public class SharedRefDbRefDatabase extends RefDatabase { + private final SharedRefDbRefUpdate.Factory refUpdateFactory; + private final SharedRefDbBatchRefUpdate.Factory batchRefUpdateFactory; + private final String projectName; + private final RefDatabase refDatabase; + + public interface Factory { + public SharedRefDbRefDatabase create(String projectName, RefDatabase refDatabase); + } + + @Inject + public SharedRefDbRefDatabase( + SharedRefDbRefUpdate.Factory refUpdateFactory, + SharedRefDbBatchRefUpdate.Factory batchRefUpdateFactory, + @Assisted String projectName, + @Assisted RefDatabase refDatabase) { + this.refUpdateFactory = refUpdateFactory; + this.batchRefUpdateFactory = batchRefUpdateFactory; + this.projectName = projectName; + this.refDatabase = refDatabase; + } + + @Override + public int hashCode() { + return refDatabase.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return refDatabase.equals(obj); + } + + @Override + public void create() throws IOException { + refDatabase.create(); + } + + @Override + public void close() { + refDatabase.close(); + } + + @Override + public boolean isNameConflicting(String name) throws IOException { + return refDatabase.isNameConflicting(name); + } + + @Override + public Collection<String> getConflictingNames(String name) throws IOException { + return refDatabase.getConflictingNames(name); + } + + @Override + public RefUpdate newUpdate(String name, boolean detach) throws IOException { + return wrapRefUpdate(refDatabase.newUpdate(name, detach)); + } + + @Override + public RefRename newRename(String fromName, String toName) throws IOException { + return refDatabase.newRename(fromName, toName); + } + + @Override + public BatchRefUpdate newBatchUpdate() { + return batchRefUpdateFactory.create(projectName, refDatabase); + } + + @Override + public boolean performsAtomicTransactions() { + return refDatabase.performsAtomicTransactions(); + } + + @Override + public String toString() { + return refDatabase.toString(); + } + + @Override + public Ref exactRef(String name) throws IOException { + return refDatabase.exactRef(name); + } + + @Override + public Map<String, Ref> exactRef(String... refs) throws IOException { + return refDatabase.exactRef(refs); + } + + @Override + public Ref firstExactRef(String... refs) throws IOException { + return refDatabase.firstExactRef(refs); + } + + @Override + public List<Ref> getRefs() throws IOException { + return refDatabase.getRefs(); + } + + @SuppressWarnings("deprecation") + @Override + public Map<String, Ref> getRefs(String prefix) throws IOException { + return refDatabase.getRefs(prefix); + } + + @Override + public List<Ref> getRefsByPrefix(String prefix) throws IOException { + return refDatabase.getRefsByPrefix(prefix); + } + + @Override + public boolean hasRefs() throws IOException { + return refDatabase.hasRefs(); + } + + @Override + public List<Ref> getAdditionalRefs() throws IOException { + return refDatabase.getAdditionalRefs(); + } + + @Override + public Ref peel(Ref ref) throws IOException { + return refDatabase.peel(ref); + } + + @Override + public void refresh() { + refDatabase.refresh(); + } + + RefUpdate wrapRefUpdate(RefUpdate refUpdate) { + return refUpdateFactory.create(projectName, refUpdate, refDatabase); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefUpdate.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefUpdate.java new file mode 100644 index 0000000..eb68b14 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefUpdate.java
@@ -0,0 +1,237 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import java.io.IOException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.PushCertificate; + +public class SharedRefDbRefUpdate extends RefUpdate { + + protected final RefUpdate refUpdateBase; + private final String projectName; + private final RefUpdateValidator.Factory refValidatorFactory; + private final RefUpdateValidator refUpdateValidator; + + public interface Factory { + SharedRefDbRefUpdate create(String projectName, RefUpdate refUpdate, RefDatabase refDb); + } + + @Inject + public SharedRefDbRefUpdate( + RefUpdateValidator.Factory refValidatorFactory, + @Assisted String projectName, + @Assisted RefUpdate refUpdate, + @Assisted RefDatabase refDb) { + super(refUpdate.getRef()); + refUpdateBase = refUpdate; + this.projectName = projectName; + this.refValidatorFactory = refValidatorFactory; + refUpdateValidator = this.refValidatorFactory.create(this.projectName, refDb); + } + + @Override + protected RefDatabase getRefDatabase() { + return notImplementedException(); + } + + private <T> T notImplementedException() { + throw new IllegalStateException("This method should have never been invoked"); + } + + @Override + protected Repository getRepository() { + return notImplementedException(); + } + + @Override + protected boolean tryLock(boolean deref) throws IOException { + return notImplementedException(); + } + + @Override + protected void unlock() { + notImplementedException(); + } + + @Override + protected Result doUpdate(Result result) throws IOException { + return notImplementedException(); + } + + @Override + protected Result doDelete(Result result) throws IOException { + return notImplementedException(); + } + + @Override + protected Result doLink(String target) throws IOException { + return notImplementedException(); + } + + @Override + public Result update() throws IOException { + return refUpdateValidator.executeRefUpdate(refUpdateBase, refUpdateBase::update); + } + + @Override + public Result update(RevWalk rev) throws IOException { + return refUpdateValidator.executeRefUpdate(refUpdateBase, () -> refUpdateBase.update(rev)); + } + + @Override + public Result delete() throws IOException { + return refUpdateValidator.executeRefUpdate(refUpdateBase, refUpdateBase::delete); + } + + @Override + public Result delete(RevWalk walk) throws IOException { + return refUpdateValidator.executeRefUpdate(refUpdateBase, () -> refUpdateBase.delete(walk)); + } + + @Override + public int hashCode() { + return refUpdateBase.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return refUpdateBase.equals(obj); + } + + @Override + public String toString() { + return refUpdateBase.toString(); + } + + @Override + public String getName() { + return refUpdateBase.getName(); + } + + @Override + public Ref getRef() { + return refUpdateBase.getRef(); + } + + @Override + public ObjectId getNewObjectId() { + return refUpdateBase.getNewObjectId(); + } + + @Override + public void setDetachingSymbolicRef() { + refUpdateBase.setDetachingSymbolicRef(); + } + + @Override + public boolean isDetachingSymbolicRef() { + return refUpdateBase.isDetachingSymbolicRef(); + } + + @Override + public void setNewObjectId(AnyObjectId id) { + refUpdateBase.setNewObjectId(id); + } + + @Override + public ObjectId getExpectedOldObjectId() { + return refUpdateBase.getExpectedOldObjectId(); + } + + @Override + public void setExpectedOldObjectId(AnyObjectId id) { + refUpdateBase.setExpectedOldObjectId(id); + } + + @Override + public boolean isForceUpdate() { + return refUpdateBase.isForceUpdate(); + } + + @Override + public void setForceUpdate(boolean b) { + refUpdateBase.setForceUpdate(b); + } + + @Override + public PersonIdent getRefLogIdent() { + return refUpdateBase.getRefLogIdent(); + } + + @Override + public void setRefLogIdent(PersonIdent pi) { + refUpdateBase.setRefLogIdent(pi); + } + + @Override + public String getRefLogMessage() { + return refUpdateBase.getRefLogMessage(); + } + + @Override + public void setRefLogMessage(String msg, boolean appendStatus) { + refUpdateBase.setRefLogMessage(msg, appendStatus); + } + + @Override + public void disableRefLog() { + refUpdateBase.disableRefLog(); + } + + @Override + public void setForceRefLog(boolean force) { + refUpdateBase.setForceRefLog(force); + } + + @Override + public ObjectId getOldObjectId() { + return refUpdateBase.getOldObjectId(); + } + + @Override + public void setPushCertificate(PushCertificate cert) { + refUpdateBase.setPushCertificate(cert); + } + + @Override + public Result getResult() { + return refUpdateBase.getResult(); + } + + @Override + public Result forceUpdate() throws IOException { + return refUpdateValidator.executeRefUpdate(refUpdateBase, refUpdateBase::forceUpdate); + } + + @Override + public Result link(String target) throws IOException { + return refUpdateBase.link(target); + } + + @Override + public void setCheckConflicting(boolean check) { + refUpdateBase.setCheckConflicting(check); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRepository.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRepository.java new file mode 100644 index 0000000..cdb65da --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRepository.java
@@ -0,0 +1,56 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import com.google.gerrit.server.git.DelegateRepository; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import java.io.IOException; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Repository; + +public class SharedRefDbRepository extends DelegateRepository { + + private final SharedRefDbRefDatabase sharedRefDatabase; + + public interface Factory { + public SharedRefDbRepository create(String projectName, Repository repository); + } + + @Inject + public SharedRefDbRepository( + SharedRefDbRefDatabase.Factory refDbFactory, + @Assisted String projectName, + @Assisted Repository repository) { + super(repository); + this.sharedRefDatabase = refDbFactory.create(projectName, repository.getRefDatabase()); + } + + @Override + public RefDatabase getRefDatabase() { + return sharedRefDatabase; + } + + @Override + public RefUpdate updateRef(String ref) throws IOException { + return sharedRefDatabase.wrapRefUpdate(delegate.updateRef(ref)); + } + + @Override + public RefUpdate updateRef(String ref, boolean detach) throws IOException { + return sharedRefDatabase.wrapRefUpdate(delegate.updateRef(ref, detach)); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogEntry.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogEntry.java new file mode 100644 index 0000000..d580057 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogEntry.java
@@ -0,0 +1,100 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.common.GitPerson; + +public class SharedRefLogEntry { + + public enum Type { + UPDATE_REF, + DELETE_REF, + DELETE_PROJECT, + LOCK_ACQUIRE, + LOCK_RELEASE + } + + public String projectName; + public Type type; + + public static class UpdateRef extends SharedRefLogEntry { + + public String refName; + public String oldId; + public String newId; + public GitPerson committer; + public String comment; + + UpdateRef( + String projectName, + String refName, + String oldId, + String newId, + @Nullable GitPerson committer, + @Nullable String comment) { + this.type = Type.UPDATE_REF; + this.projectName = projectName; + this.refName = refName; + this.oldId = oldId; + this.newId = newId; + this.committer = committer; + this.comment = comment; + } + } + + public static class DeleteProject extends SharedRefLogEntry { + + DeleteProject(String projectName) { + this.type = Type.DELETE_PROJECT; + this.projectName = projectName; + } + } + + public static class DeleteRef extends SharedRefLogEntry { + + public String refName; + public String oldId; + + DeleteRef(String projectName, String refName, String oldId) { + this.type = Type.DELETE_REF; + this.projectName = projectName; + this.refName = refName; + this.oldId = oldId; + } + } + + public static class LockAcquire extends SharedRefLogEntry { + + public String refName; + + LockAcquire(String projectName, String refName) { + this.type = Type.LOCK_ACQUIRE; + this.projectName = projectName; + this.refName = refName; + } + } + + public static class LockRelease extends SharedRefLogEntry { + + public String refName; + + LockRelease(String projectName, String refName) { + this.type = Type.LOCK_RELEASE; + this.projectName = projectName; + this.refName = refName; + } + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogger.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogger.java new file mode 100644 index 0000000..faef27b --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogger.java
@@ -0,0 +1,31 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; + +public interface SharedRefLogger { + + void logRefUpdate(String project, Ref currRef, ObjectId newRefValue); + + <T> void logRefUpdate(String project, String refName, T currRef, T newRefValue); + + void logProjectDelete(String project); + + void logLockAcquisition(String project, String refName); + + void logLockRelease(String project, String refName); +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ValidationMetrics.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ValidationMetrics.java new file mode 100644 index 0000000..678091b --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/ValidationMetrics.java
@@ -0,0 +1,72 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import com.google.gerrit.metrics.Counter1; +import com.google.gerrit.metrics.Description; +import com.google.gerrit.metrics.Field; +import com.google.gerrit.metrics.MetricMaker; +import com.google.gerrit.server.logging.PluginMetadata; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class ValidationMetrics { + private static final String GIT_UPDATE_SPLIT_BRAIN_PREVENTED = "git_update_split_brain_prevented"; + private static final String GIT_UPDATE_SPLIT_BRAIN = "git_update_split_brain"; + + private final Counter1<String> splitBrainPreventionCounter; + private final Counter1<String> splitBrainCounter; + + @Inject + public ValidationMetrics(MetricMaker metricMaker) { + this.splitBrainPreventionCounter = + metricMaker.newCounter( + "global-refdb/validation/git_update_split_brain_prevented", + rateDescription("errors", "Rate of REST API error responses"), + stringField( + GIT_UPDATE_SPLIT_BRAIN_PREVENTED, + "Ref-update operations, split-brain detected and prevented")); + + this.splitBrainCounter = + metricMaker.newCounter( + "global-refdb/validation/git_update_split_brain", + rateDescription("errors", "Rate of REST API error responses"), + stringField( + GIT_UPDATE_SPLIT_BRAIN, + "Ref-update operation left node in a split-brain scenario")); + } + + public void incrementSplitBrainPrevention() { + splitBrainPreventionCounter.increment(GIT_UPDATE_SPLIT_BRAIN_PREVENTED); + } + + public void incrementSplitBrain() { + splitBrainCounter.increment(GIT_UPDATE_SPLIT_BRAIN); + } + + public Field<String> stringField(String metadataKey, String description) { + return Field.ofString( + metadataKey, + (metadataBuilder, fieldValue) -> + metadataBuilder.addPluginMetadata(PluginMetadata.create(metadataKey, fieldValue))) + .description(description) + .build(); + } + + public Description rateDescription(String unit, String description) { + return new Description(description).setRate().setUnit(unit); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/CustomSharedRefEnforcementByProject.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/CustomSharedRefEnforcementByProject.java new file mode 100644 index 0000000..b1eca36 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/CustomSharedRefEnforcementByProject.java
@@ -0,0 +1,103 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb; + +import static com.google.common.base.Suppliers.memoize; + +import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbConfiguration; +import com.google.common.base.MoreObjects; +import com.google.common.base.Splitter; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Inject; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class CustomSharedRefEnforcementByProject implements SharedRefEnforcement { + private static final String ALL = ".*"; + + private final Supplier<Map<String, Map<String, EnforcePolicy>>> predefEnforcements; + + @Inject + public CustomSharedRefEnforcementByProject(SharedRefDbConfiguration config) { + this.predefEnforcements = memoize(() -> parseDryRunEnforcementsToMap(config)); + } + + private static Map<String, Map<String, EnforcePolicy>> parseDryRunEnforcementsToMap( + SharedRefDbConfiguration config) { + Map<String, Map<String, EnforcePolicy>> enforcementMap = new HashMap<>(); + + for (Map.Entry<EnforcePolicy, String> enforcementEntry : + config.getSharedRefDb().getEnforcementRules().entries()) { + parseEnforcementEntry(enforcementMap, enforcementEntry); + } + + return enforcementMap; + } + + private static void parseEnforcementEntry( + Map<String, Map<String, EnforcePolicy>> enforcementMap, + Map.Entry<EnforcePolicy, String> enforcementEntry) { + Iterator<String> projectAndRef = Splitter.on(':').split(enforcementEntry.getValue()).iterator(); + EnforcePolicy enforcementPolicy = enforcementEntry.getKey(); + + if (projectAndRef.hasNext()) { + String projectName = emptyToAll(projectAndRef.next()); + String refName = emptyToAll(projectAndRef.hasNext() ? projectAndRef.next() : ALL); + + Map<String, EnforcePolicy> existingOrDefaultRef = + enforcementMap.getOrDefault(projectName, new HashMap<>()); + + existingOrDefaultRef.put(refName, enforcementPolicy); + + enforcementMap.put(projectName, existingOrDefaultRef); + } + } + + private static String emptyToAll(String value) { + return value.trim().isEmpty() ? ALL : value; + } + + @Override + public EnforcePolicy getPolicy(String projectName, String refName) { + if (isRefToBeIgnoredBySharedRefDb(refName)) { + return EnforcePolicy.IGNORED; + } + + return getRefEnforcePolicy(projectName, refName); + } + + private EnforcePolicy getRefEnforcePolicy(String projectName, String refName) { + Map<String, EnforcePolicy> orDefault = + predefEnforcements + .get() + .getOrDefault( + projectName, predefEnforcements.get().getOrDefault(ALL, ImmutableMap.of())); + + return MoreObjects.firstNonNull( + orDefault.getOrDefault(refName, orDefault.get(ALL)), EnforcePolicy.REQUIRED); + } + + @Override + public EnforcePolicy getPolicy(String projectName) { + Map<String, EnforcePolicy> policiesForProject = + predefEnforcements + .get() + .getOrDefault( + projectName, predefEnforcements.get().getOrDefault(ALL, ImmutableMap.of())); + return policiesForProject.getOrDefault(ALL, EnforcePolicy.REQUIRED); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/DefaultSharedRefEnforcement.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/DefaultSharedRefEnforcement.java new file mode 100644 index 0000000..9c2ef35 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/DefaultSharedRefEnforcement.java
@@ -0,0 +1,28 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb; + +public class DefaultSharedRefEnforcement implements SharedRefEnforcement { + + @Override + public EnforcePolicy getPolicy(String projectName, String refName) { + return isRefToBeIgnoredBySharedRefDb(refName) ? EnforcePolicy.IGNORED : EnforcePolicy.REQUIRED; + } + + @Override + public EnforcePolicy getPolicy(String projectName) { + return EnforcePolicy.REQUIRED; + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabase.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabase.java new file mode 100644 index 0000000..4810c64 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabase.java
@@ -0,0 +1,63 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb; + +import com.gerritforge.gerrit.globalrefdb.GlobalRefDatabase; +import com.gerritforge.gerrit.globalrefdb.GlobalRefDbLockException; +import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError; +import com.google.gerrit.entities.Project; +import java.util.Optional; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; + +public class NoopSharedRefDatabase implements GlobalRefDatabase { + + @Override + public boolean isUpToDate(Project.NameKey project, Ref ref) throws GlobalRefDbLockException { + return true; + } + + @Override + public boolean compareAndPut(Project.NameKey project, Ref currRef, ObjectId newRefValue) + throws GlobalRefDbSystemError { + return true; + } + + @Override + public <T> boolean compareAndPut(Project.NameKey project, String refName, T currValue, T newValue) + throws GlobalRefDbSystemError { + return false; + } + + @Override + public AutoCloseable lockRef(Project.NameKey project, String refName) + throws GlobalRefDbLockException { + return () -> {}; + } + + @Override + public boolean exists(Project.NameKey project, String refName) { + return false; + } + + @Override + public void remove(Project.NameKey project) throws GlobalRefDbSystemError {} + + @Override + public <T> Optional<T> get(Project.NameKey project, String refName, Class<T> clazz) + throws GlobalRefDbSystemError { + return Optional.empty(); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/OutOfSyncException.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/OutOfSyncException.java new file mode 100644 index 0000000..c8dc304 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/OutOfSyncException.java
@@ -0,0 +1,34 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb; + +import java.io.IOException; +import org.eclipse.jgit.lib.Ref; + +/** Local project/ref is out of sync with the shared refdb */ +public class OutOfSyncException extends IOException { + private static final long serialVersionUID = 1L; + + public OutOfSyncException(String project, Ref localRef) { + super( + localRef == null + ? String.format( + "Local ref doesn't exists locally for project %s but exists in the shared ref-db", + project) + : String.format( + "Local ref %s (ObjectId=%s) on project %s is out of sync with the shared ref-db", + localRef.getName(), localRef.getObjectId().getName(), project)); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedDbSplitBrainException.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedDbSplitBrainException.java new file mode 100644 index 0000000..aece4cb --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedDbSplitBrainException.java
@@ -0,0 +1,29 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb; + +import java.io.IOException; + +public class SharedDbSplitBrainException extends IOException { + private static final long serialVersionUID = 1L; + + public SharedDbSplitBrainException(String message) { + super(message); + } + + public SharedDbSplitBrainException(String message, Throwable cause) { + super(message, cause); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedLockException.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedLockException.java new file mode 100644 index 0000000..69c3f33 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedLockException.java
@@ -0,0 +1,26 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb; + +import java.io.IOException; + +/** Unable to lock a project/ref resource. */ +public class SharedLockException extends IOException { + private static final long serialVersionUID = 1L; + + public SharedLockException(String project, String refName, Exception cause) { + super(String.format("Unable to lock project %s on ref %s", project, refName), cause); + } +}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedRefEnforcement.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedRefEnforcement.java new file mode 100644 index 0000000..d2c1ed6 --- /dev/null +++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedRefEnforcement.java
@@ -0,0 +1,53 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb; + +/** Type of enforcement to implement between the local and shared RefDb. */ +public interface SharedRefEnforcement { + public enum EnforcePolicy { + IGNORED, + REQUIRED; + } + + /** + * Get the enforcement policy for a project/refName. + * + * @param projectName project to be enforced + * @param refName ref name to be enforced + * @return the {@link EnforcePolicy} value + */ + public EnforcePolicy getPolicy(String projectName, String refName); + + /** + * Get the enforcement policy for a project + * + * @param projectName + * @return the {@link EnforcePolicy} value + */ + public EnforcePolicy getPolicy(String projectName); + + /** + * Check if a refName should be ignored by shared Ref-Db + * + * @param refName + * @return true if ref should be ignored; false otherwise + */ + default boolean isRefToBeIgnoredBySharedRefDb(String refName) { + return refName == null + || refName.startsWith("refs/draft-comments") + || (refName.startsWith("refs/changes") && !refName.endsWith("/meta")) + || refName.startsWith("refs/cache-automerge"); + } +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/BatchRefUpdateValidatorTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/BatchRefUpdateValidatorTest.java new file mode 100644 index 0000000..e1c8478 --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/BatchRefUpdateValidatorTest.java
@@ -0,0 +1,194 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import static com.google.common.truth.Truth.assertThat; +import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.DefaultSharedRefEnforcement; +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.RefFixture; +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement; +import com.google.gerrit.entities.Project; +import com.google.gerrit.metrics.DisabledMetricMaker; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.eclipse.jgit.internal.storage.file.RefDirectory; +import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class BatchRefUpdateValidatorTest extends LocalDiskRepositoryTestCase implements RefFixture { + @Rule public TestName nameRule = new TestName(); + + private Repository diskRepo; + private TestRepository<Repository> repo; + private RefDirectory refdir; + private RevCommit A; + private RevCommit B; + + @Mock SharedRefDatabaseWrapper sharedRefDatabase; + + @Mock SharedRefEnforcement tmpRefEnforcement; + @Mock ProjectsFilter projectsFilter; + + @Before + public void setup() throws Exception { + super.setUp(); + when(projectsFilter.matches(anyString())).thenReturn(true); + gitRepoSetup(); + } + + private void gitRepoSetup() throws Exception { + diskRepo = createBareRepository(); + refdir = (RefDirectory) diskRepo.getRefDatabase(); + repo = new TestRepository<>(diskRepo); + A = repo.commit().create(); + B = repo.commit(repo.getRevWalk().parseCommit(A)); + } + + @Test + public void immutableChangeShouldNotBeWrittenIntoZk() throws Exception { + String AN_IMMUTABLE_REF = "refs/changes/01/1/1"; + + List<ReceiveCommand> cmds = Arrays.asList(new ReceiveCommand(A, B, AN_IMMUTABLE_REF, UPDATE)); + + BatchRefUpdate batchRefUpdate = newBatchUpdate(cmds); + BatchRefUpdateValidator BatchRefUpdateValidator = newDefaultValidator(A_TEST_PROJECT_NAME); + + BatchRefUpdateValidator.executeBatchUpdateWithValidation( + batchRefUpdate, () -> execute(batchRefUpdate)); + + verify(sharedRefDatabase, never()) + .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class)); + } + + @Test + public void compareAndPutShouldAlwaysIngoreAlwaysDraftCommentsEvenOutOfOrder() throws Exception { + String DRAFT_COMMENT = "refs/draft-comments/56/450756/1013728"; + List<ReceiveCommand> cmds = Arrays.asList(new ReceiveCommand(A, B, DRAFT_COMMENT, UPDATE)); + + BatchRefUpdate batchRefUpdate = newBatchUpdate(cmds); + BatchRefUpdateValidator BatchRefUpdateValidator = newDefaultValidator(A_TEST_PROJECT_NAME); + + BatchRefUpdateValidator.executeBatchUpdateWithValidation( + batchRefUpdate, () -> execute(batchRefUpdate)); + + verify(sharedRefDatabase, never()) + .compareAndPut(A_TEST_PROJECT_NAME_KEY, newRef(DRAFT_COMMENT, A.getId()), B.getId()); + } + + @Test + public void validationShouldFailWhenLocalRefDbIsOutOfSync() throws Exception { + String AN_OUT_OF_SYNC_REF = "refs/changes/01/1/1"; + BatchRefUpdate batchRefUpdate = + newBatchUpdate( + Collections.singletonList(new ReceiveCommand(A, B, AN_OUT_OF_SYNC_REF, UPDATE))); + BatchRefUpdateValidator batchRefUpdateValidator = + getRefValidatorForEnforcement(A_TEST_PROJECT_NAME, tmpRefEnforcement); + + doReturn(SharedRefEnforcement.EnforcePolicy.REQUIRED) + .when(batchRefUpdateValidator.refEnforcement) + .getPolicy(A_TEST_PROJECT_NAME, AN_OUT_OF_SYNC_REF); + lenient() + .doReturn(false) + .when(sharedRefDatabase) + .isUpToDate(A_TEST_PROJECT_NAME_KEY, newRef(AN_OUT_OF_SYNC_REF, AN_OBJECT_ID_1)); + + batchRefUpdateValidator.executeBatchUpdateWithValidation( + batchRefUpdate, () -> execute(batchRefUpdate)); + + final List<ReceiveCommand> commands = batchRefUpdate.getCommands(); + assertThat(commands.size()).isEqualTo(1); + commands.forEach( + (command) -> assertThat(command.getResult()).isEqualTo(ReceiveCommand.Result.LOCK_FAILURE)); + } + + @Test + public void shouldNotUpdateSharedRefDbWhenProjectIsLocal() throws Exception { + when(projectsFilter.matches(anyString())).thenReturn(true); + + String AN_OUT_OF_SYNC_REF = "refs/changes/01/1/1"; + BatchRefUpdate batchRefUpdate = + newBatchUpdate( + Collections.singletonList(new ReceiveCommand(A, B, AN_OUT_OF_SYNC_REF, UPDATE))); + BatchRefUpdateValidator batchRefUpdateValidator = + getRefValidatorForEnforcement(A_TEST_PROJECT_NAME, tmpRefEnforcement); + + batchRefUpdateValidator.executeBatchUpdateWithValidation( + batchRefUpdate, () -> execute(batchRefUpdate)); + + verify(sharedRefDatabase, never()) + .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class)); + } + + private BatchRefUpdateValidator newDefaultValidator(String projectName) { + return getRefValidatorForEnforcement(projectName, new DefaultSharedRefEnforcement()); + } + + private BatchRefUpdateValidator getRefValidatorForEnforcement( + String projectName, SharedRefEnforcement sharedRefEnforcement) { + return new BatchRefUpdateValidator( + sharedRefDatabase, + new ValidationMetrics(new DisabledMetricMaker()), + sharedRefEnforcement, + new DummyLockWrapper(), + projectsFilter, + projectName, + diskRepo.getRefDatabase()); + } + + private Void execute(BatchRefUpdate u) throws IOException { + try (RevWalk rw = new RevWalk(diskRepo)) { + u.execute(rw, NullProgressMonitor.INSTANCE); + } + return null; + } + + private BatchRefUpdate newBatchUpdate(List<ReceiveCommand> cmds) { + BatchRefUpdate u = refdir.newBatchUpdate(); + u.addCommand(cmds); + return u; + } + + @Override + public String testBranch() { + return "branch_" + nameRule.getMethodName(); + } +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/DisabledSharedRefLogger.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/DisabledSharedRefLogger.java new file mode 100644 index 0000000..f9a1e57 --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/DisabledSharedRefLogger.java
@@ -0,0 +1,38 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.junit.Ignore; + +@Ignore +public class DisabledSharedRefLogger implements SharedRefLogger { + + @Override + public void logRefUpdate(String project, Ref currRef, ObjectId newRefValue) {} + + @Override + public void logProjectDelete(String project) {} + + @Override + public void logLockAcquisition(String project, String refName) {} + + @Override + public void logLockRelease(String project, String refName) {} + + @Override + public <T> void logRefUpdate(String project, String refName, T currRef, T newRefValue) {} +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/DummyLockWrapper.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/DummyLockWrapper.java new file mode 100644 index 0000000..84d6912 --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/DummyLockWrapper.java
@@ -0,0 +1,26 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import org.junit.Ignore; + +@Ignore +public class DummyLockWrapper implements LockWrapper.Factory { + + @Override + public LockWrapper create(String project, String refName, AutoCloseable lock) { + return new LockWrapper(new DisabledSharedRefLogger(), project, refName, lock); + } +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/Log4jSharedRefLoggerTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/Log4jSharedRefLoggerTest.java new file mode 100644 index 0000000..2c89091 --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/Log4jSharedRefLoggerTest.java
@@ -0,0 +1,165 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.json.OutputFormat; +import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.notedb.Sequences; +import com.google.gerrit.server.util.SystemLog; +import com.google.gson.Gson; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.log4j.LogManager; +import org.apache.log4j.PatternLayout; +import org.apache.log4j.WriterAppender; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Log4jSharedRefLoggerTest extends AbstractDaemonTest { + + private static final Gson gson = OutputFormat.JSON_COMPACT.newGson(); + private StringWriter logWriter; + private Log4jSharedRefLogger log4jSharedRefLogger; + + @Before + public void setUp() throws IOException { + this.logWriter = new StringWriter(); + this.log4jSharedRefLogger = newLog4jSharedRefLogger(); + } + + @Test + public void shouldLogProjectDeletion() { + log4jSharedRefLogger.logProjectDelete(project.get()); + + SharedRefLogEntry.DeleteProject gotLogEntry = + gson.fromJson(logWriter.toString(), SharedRefLogEntry.DeleteProject.class); + + assertThat(gotLogEntry.type).isEqualTo(SharedRefLogEntry.Type.DELETE_PROJECT); + assertThat(gotLogEntry.projectName).isEqualTo(project.get()); + } + + @Test + public void shouldLogUpdateRef() throws Exception { + final String refName = "refs/remotes/origin/master"; + Ref currRef = repo().exactRef(refName); + PushOneCommit.Result result = pushTo(refName); + ObjectId newRefValue = result.getCommit().toObjectId(); + + log4jSharedRefLogger.logRefUpdate(project.get(), currRef, newRefValue); + + SharedRefLogEntry.UpdateRef gotLogEntry = + gson.fromJson(logWriter.toString(), SharedRefLogEntry.UpdateRef.class); + + assertThat(gotLogEntry.type).isEqualTo(SharedRefLogEntry.Type.UPDATE_REF); + assertThat(gotLogEntry.projectName).isEqualTo(project.get()); + assertThat(gotLogEntry.refName).isEqualTo(refName); + assertThat(gotLogEntry.oldId).isEqualTo(currRef.getObjectId().getName()); + assertThat(gotLogEntry.newId).isEqualTo(newRefValue.getName()); + assertThat(gotLogEntry.comment).isNotNull(); + assertThat(gotLogEntry.committer).isNotNull(); + } + + @Test + public void shouldLogDeleteRef() throws Exception { + final String refName = "refs/remotes/origin/master"; + Ref currRef = repo().exactRef(refName); + + log4jSharedRefLogger.logRefUpdate(project.get(), currRef, ObjectId.zeroId()); + + SharedRefLogEntry.DeleteRef gotLogEntry = + gson.fromJson(logWriter.toString(), SharedRefLogEntry.DeleteRef.class); + + assertThat(gotLogEntry.type).isEqualTo(SharedRefLogEntry.Type.DELETE_REF); + assertThat(gotLogEntry.projectName).isEqualTo(project.get()); + assertThat(gotLogEntry.refName).isEqualTo(refName); + assertThat(gotLogEntry.oldId).isEqualTo(currRef.getObjectId().getName()); + } + + @Test + public void shouldLogBlobRefs() throws Exception { + Repository allUsersRepo = repoManager.openRepository(allUsers); + String blobRefName = RefNames.REFS_SEQUENCES + Sequences.NAME_ACCOUNTS; + Ref currRef = allUsersRepo.exactRef(blobRefName); + log4jSharedRefLogger.logRefUpdate(allUsers.get(), currRef, currRef.getObjectId()); + + SharedRefLogEntry.UpdateRef gotLogEntry = + gson.fromJson(logWriter.toString(), SharedRefLogEntry.UpdateRef.class); + + assertThat(gotLogEntry.type).isEqualTo(SharedRefLogEntry.Type.UPDATE_REF); + assertThat(gotLogEntry.projectName).isEqualTo(allUsers.get()); + assertThat(gotLogEntry.refName).isEqualTo(blobRefName); + assertThat(gotLogEntry.oldId).isEqualTo(currRef.getObjectId().getName()); + assertThat(gotLogEntry.newId).isEqualTo(currRef.getObjectId().getName()); + assertThat(gotLogEntry.comment).isNull(); + assertThat(gotLogEntry.committer).isNull(); + } + + @Test + public void shouldLogLockAcquisition() { + String refName = "refs/foo/bar"; + log4jSharedRefLogger.logLockAcquisition(project.get(), refName); + + SharedRefLogEntry.LockAcquire gotLogEntry = + gson.fromJson(logWriter.toString(), SharedRefLogEntry.LockAcquire.class); + + assertThat(gotLogEntry.type).isEqualTo(SharedRefLogEntry.Type.LOCK_ACQUIRE); + assertThat(gotLogEntry.projectName).isEqualTo(project.get()); + assertThat(gotLogEntry.refName).isEqualTo(refName); + } + + @Test + public void shouldLogLockRelease() { + String refName = "refs/foo/bar"; + log4jSharedRefLogger.logLockRelease(project.get(), refName); + + SharedRefLogEntry.LockAcquire gotLogEntry = + gson.fromJson(logWriter.toString(), SharedRefLogEntry.LockAcquire.class); + + assertThat(gotLogEntry.type).isEqualTo(SharedRefLogEntry.Type.LOCK_RELEASE); + assertThat(gotLogEntry.projectName).isEqualTo(project.get()); + assertThat(gotLogEntry.refName).isEqualTo(refName); + } + + private Log4jSharedRefLogger newLog4jSharedRefLogger() throws IOException { + final Log4jSharedRefLogger log4jSharedRefLogger = + new Log4jSharedRefLogger(new SystemLog(new SitePaths(newPath()), baseConfig), repoManager); + log4jSharedRefLogger.setLogger(logWriterLogger()); + return log4jSharedRefLogger; + } + + private Logger logWriterLogger() { + org.apache.log4j.Logger logger = LogManager.getLogger("logWriterLogger"); + logger.addAppender(new WriterAppender(new PatternLayout("%m"), logWriter)); + return LoggerFactory.getLogger("logWriterLogger"); + } + + private static Path newPath() throws IOException { + Path tmp = Files.createTempFile("gerrit_test_", "_site"); + Files.deleteIfExists(tmp); + return tmp; + } +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectDeletedSharedDbCleanupTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectDeletedSharedDbCleanupTest.java new file mode 100644 index 0000000..cf383bf --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectDeletedSharedDbCleanupTest.java
@@ -0,0 +1,58 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.RefFixture; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.events.ProjectDeletedListener; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ProjectDeletedSharedDbCleanupTest implements RefFixture { + @Rule public TestName nameRule = new TestName(); + + @Mock ValidationMetrics mockValidationMetrics; + @Mock SharedRefDatabaseWrapper sharedRefDatabase; + + @Test + public void aDeleteProjectEventShouldCleanupProjectFromZk() throws Exception { + String projectName = A_TEST_PROJECT_NAME; + ProjectDeletedSharedDbCleanup projectDeletedSharedDbCleanup = + new ProjectDeletedSharedDbCleanup(sharedRefDatabase, mockValidationMetrics); + + ProjectDeletedListener.Event event = + new ProjectDeletedListener.Event() { + @Override + public String getProjectName() { + return projectName; + } + + @Override + public NotifyHandling getNotify() { + return NotifyHandling.NONE; + } + }; + + projectDeletedSharedDbCleanup.onProjectDeleted(event); + + Mockito.verify(sharedRefDatabase, Mockito.times(1)).remove(A_TEST_PROJECT_NAME_KEY); + } +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectsFilterTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectsFilterTest.java new file mode 100644 index 0000000..a8da17e --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/ProjectsFilterTest.java
@@ -0,0 +1,180 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbConfiguration.Projects; +import com.google.common.collect.Lists; +import com.google.gerrit.entities.Project.NameKey; +import com.google.gerrit.server.events.Event; +import com.google.gerrit.server.events.ProjectEvent; +import com.google.gerrit.server.events.RefUpdatedEvent; +import com.google.gerrit.testing.GerritJUnit; +import java.util.Collections; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ProjectsFilterTest { + + @Mock private SharedRefDbConfiguration configuration; + @Mock private Projects projects; + + private ProjectsFilter objectUnderTest; + + @Test + public void shouldMatchByExactProjectName() { + when(projects.getPatterns()).thenReturn(Lists.newArrayList("test_project")); + when(configuration.projects()).thenReturn(projects); + + objectUnderTest = new ProjectsFilter(configuration); + + assertThat(objectUnderTest.matches(NameKey.parse("test_project"))).isTrue(); + assertThat(objectUnderTest.matches(NameKey.parse("test_project2"))).isFalse(); + } + + @Test + public void shouldMatchByWildcard() { + when(projects.getPatterns()).thenReturn(Lists.newArrayList("test_project*")); + when(configuration.projects()).thenReturn(projects); + + objectUnderTest = new ProjectsFilter(configuration); + + assertThat(objectUnderTest.matches(NameKey.parse("test_project"))).isTrue(); + assertThat(objectUnderTest.matches(NameKey.parse("test_project2"))).isTrue(); + assertThat(objectUnderTest.matches(NameKey.parse("2test_project"))).isFalse(); + } + + @Test + public void shouldMatchByRegex() { + when(projects.getPatterns()).thenReturn(Lists.newArrayList("^test_(project|project2)")); + when(configuration.projects()).thenReturn(projects); + + objectUnderTest = new ProjectsFilter(configuration); + + assertThat(objectUnderTest.matches(NameKey.parse("test_project"))).isTrue(); + assertThat(objectUnderTest.matches(NameKey.parse("test_project2"))).isTrue(); + assertThat(objectUnderTest.matches(NameKey.parse("test_project3"))).isFalse(); + } + + @Test + public void shouldExcludeByRegex() { + when(projects.getPatterns()).thenReturn(Lists.newArrayList("^(?:(?!test_project3).)*$")); + when(configuration.projects()).thenReturn(projects); + + objectUnderTest = new ProjectsFilter(configuration); + + assertThat(objectUnderTest.matches(NameKey.parse("test_project"))).isTrue(); + assertThat(objectUnderTest.matches(NameKey.parse("test_project2"))).isTrue(); + assertThat(objectUnderTest.matches(NameKey.parse("test_project3"))).isFalse(); + } + + @Test + public void shouldExcludeByMultipleProjectsRegexPattern() { + when(projects.getPatterns()) + .thenReturn(Lists.newArrayList("^(?:(?!(test_project3|test_project4)).)*$")); + when(configuration.projects()).thenReturn(projects); + + objectUnderTest = new ProjectsFilter(configuration); + + assertThat(objectUnderTest.matches(NameKey.parse("test_project"))).isTrue(); + assertThat(objectUnderTest.matches(NameKey.parse("test_project2"))).isTrue(); + assertThat(objectUnderTest.matches(NameKey.parse("test_project3"))).isFalse(); + assertThat(objectUnderTest.matches(NameKey.parse("test_project4"))).isFalse(); + } + + @Test + public void shouldMatchWhenNoPatternProvided() { + when(projects.getPatterns()).thenReturn(Collections.emptyList()); + when(configuration.projects()).thenReturn(projects); + + objectUnderTest = new ProjectsFilter(configuration); + + assertThat(objectUnderTest.matches(NameKey.parse("test_project"))).isTrue(); + } + + @Test + public void shouldMatchProjectEvent() { + ProjectEvent event = mock(ProjectEvent.class); + when(event.getProjectNameKey()).thenReturn(NameKey.parse("test_project")); + when(projects.getPatterns()).thenReturn(Lists.newArrayList("test_project")); + when(configuration.projects()).thenReturn(projects); + + objectUnderTest = new ProjectsFilter(configuration); + + assertThat(objectUnderTest.matches(event)).isTrue(); + } + + @Test + public void shouldMatchRefUpdatedEvent() { + RefUpdatedEvent event = mock(RefUpdatedEvent.class); + when(event.getProjectNameKey()).thenReturn(NameKey.parse("test_project")); + when(projects.getPatterns()).thenReturn(Lists.newArrayList("test_project")); + when(configuration.projects()).thenReturn(projects); + + objectUnderTest = new ProjectsFilter(configuration); + + assertThat(objectUnderTest.matches(event)).isTrue(); + } + + @Test + public void shouldExcludedNonProjectEvents() { + Event event = mock(Event.class); + when(projects.getPatterns()).thenReturn(Lists.newArrayList("test_project)")); + when(configuration.projects()).thenReturn(projects); + + objectUnderTest = new ProjectsFilter(configuration); + + assertThat(objectUnderTest.matches(event)).isFalse(); + } + + @Test + public void shouldThrowExceptionWhenProjecNameIsNull() { + when(projects.getPatterns()).thenReturn(Collections.emptyList()); + when(configuration.projects()).thenReturn(projects); + + objectUnderTest = new ProjectsFilter(configuration); + + GerritJUnit.assertThrows( + IllegalArgumentException.class, () -> objectUnderTest.matches((NameKey) null)); + } + + @Test + public void shouldThrowExceptionWhenProjecNameIsEmpty() { + when(projects.getPatterns()).thenReturn(Collections.emptyList()); + when(configuration.projects()).thenReturn(projects); + + objectUnderTest = new ProjectsFilter(configuration); + + GerritJUnit.assertThrows( + IllegalArgumentException.class, () -> objectUnderTest.matches(NameKey.parse(""))); + } + + @Test + public void shouldThrowExceptionWhenEventIsNull() { + when(projects.getPatterns()).thenReturn(Collections.emptyList()); + when(configuration.projects()).thenReturn(projects); + + objectUnderTest = new ProjectsFilter(configuration); + + GerritJUnit.assertThrows( + IllegalArgumentException.class, () -> objectUnderTest.matches((Event) null)); + } +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidatorTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidatorTest.java new file mode 100644 index 0000000..2c7431f --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidatorTest.java
@@ -0,0 +1,218 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.DefaultSharedRefEnforcement; +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.RefFixture; +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedDbSplitBrainException; +import com.google.gerrit.entities.Project; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class RefUpdateValidatorTest implements RefFixture { + private static final DefaultSharedRefEnforcement defaultRefEnforcement = + new DefaultSharedRefEnforcement(); + + @Mock SharedRefDatabaseWrapper sharedRefDb; + + @Mock SharedRefLogger sharedRefLogger; + + @Mock RefDatabase localRefDb; + + @Mock ValidationMetrics validationMetrics; + + @Mock RefUpdate refUpdate; + + @Mock ProjectsFilter projectsFilter; + + String refName; + Ref oldUpdateRef; + Ref newUpdateRef; + Ref localRef; + + RefUpdateValidator refUpdateValidator; + + @Before + public void setupMocks() throws Exception { + refName = aBranchRef(); + oldUpdateRef = newRef(refName, AN_OBJECT_ID_1); + newUpdateRef = newRef(refName, AN_OBJECT_ID_2); + localRef = newRef(refName, AN_OBJECT_ID_3); + + doReturn(localRef).when(localRefDb).findRef(refName); + doReturn(localRef).when(localRefDb).exactRef(refName); + doReturn(oldUpdateRef).when(refUpdate).getRef(); + doReturn(newUpdateRef.getObjectId()).when(refUpdate).getNewObjectId(); + doReturn(refName).when(refUpdate).getName(); + lenient().doReturn(oldUpdateRef.getObjectId()).when(refUpdate).getOldObjectId(); + + doReturn(true).when(projectsFilter).matches(anyString()); + + refUpdateValidator = newRefUpdateValidator(sharedRefDb); + } + + @Test + public void validationShouldSucceedWhenSharedRefDbIsNoop() throws Exception { + SharedRefDatabaseWrapper noopSharedRefDbWrapper = new SharedRefDatabaseWrapper(sharedRefLogger); + + Result result = + newRefUpdateValidator(noopSharedRefDbWrapper).executeRefUpdate(refUpdate, () -> Result.NEW); + assertThat(result).isEqualTo(Result.NEW); + } + + @Test + public void validationShouldSucceedWhenLocalRefDbIsUpToDate() throws Exception { + lenient() + .doReturn(false) + .when(sharedRefDb) + .isUpToDate(any(Project.NameKey.class), any(Ref.class)); + doReturn(true).when(sharedRefDb).isUpToDate(A_TEST_PROJECT_NAME_KEY, localRef); + lenient() + .doReturn(false) + .when(sharedRefDb) + .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class)); + doReturn(true) + .when(sharedRefDb) + .compareAndPut(A_TEST_PROJECT_NAME_KEY, localRef, newUpdateRef.getObjectId()); + + Result result = refUpdateValidator.executeRefUpdate(refUpdate, () -> Result.NEW); + + assertThat(result).isEqualTo(Result.NEW); + } + + @Test + public void sharedRefDbShouldBeUpdatedWithRefDeleted() throws Exception { + doReturn(ObjectId.zeroId()).when(refUpdate).getNewObjectId(); + doReturn(true).when(sharedRefDb).isUpToDate(any(Project.NameKey.class), any(Ref.class)); + lenient() + .doReturn(false) + .when(sharedRefDb) + .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class)); + doReturn(true) + .when(sharedRefDb) + .compareAndPut(A_TEST_PROJECT_NAME_KEY, localRef, ObjectId.zeroId()); + doReturn(localRef).doReturn(null).when(localRefDb).findRef(refName); + + Result result = refUpdateValidator.executeRefUpdate(refUpdate, () -> Result.FORCED); + + assertThat(result).isEqualTo(Result.FORCED); + } + + @Test + public void sharedRefDbShouldBeUpdatedWithNewRefCreated() throws Exception { + Ref localNullRef = nullRef(refName); + + doReturn(true).when(sharedRefDb).isUpToDate(any(Project.NameKey.class), any(Ref.class)); + lenient() + .doReturn(false) + .when(sharedRefDb) + .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class)); + doReturn(true) + .when(sharedRefDb) + .compareAndPut(A_TEST_PROJECT_NAME_KEY, localNullRef, newUpdateRef.getObjectId()); + doReturn(localNullRef).doReturn(newUpdateRef).when(localRefDb).findRef(refName); + + Result result = refUpdateValidator.executeRefUpdate(refUpdate, () -> Result.NEW); + + assertThat(result).isEqualTo(Result.NEW); + } + + @Test + public void validationShouldFailWhenLocalRefDbIsOutOfSync() throws Exception { + lenient() + .doReturn(true) + .when(sharedRefDb) + .isUpToDate(any(Project.NameKey.class), any(Ref.class)); + doReturn(true).when(sharedRefDb).exists(A_TEST_PROJECT_NAME_KEY, refName); + doReturn(false).when(sharedRefDb).isUpToDate(A_TEST_PROJECT_NAME_KEY, localRef); + + Result result = refUpdateValidator.executeRefUpdate(refUpdate, () -> Result.NEW); + + assertThat(result).isEqualTo(Result.LOCK_FAILURE); + } + + @Test(expected = SharedDbSplitBrainException.class) + public void shouldTrowSplitBrainWhenLocalRefDbIsUpToDateButFinalCompareAndPutIsFailing() + throws Exception { + lenient() + .doReturn(false) + .when(sharedRefDb) + .isUpToDate(any(Project.NameKey.class), any(Ref.class)); + doReturn(true).when(sharedRefDb).isUpToDate(A_TEST_PROJECT_NAME_KEY, localRef); + lenient() + .doReturn(true) + .when(sharedRefDb) + .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class)); + doReturn(false) + .when(sharedRefDb) + .compareAndPut(A_TEST_PROJECT_NAME_KEY, localRef, newUpdateRef.getObjectId()); + + refUpdateValidator.executeRefUpdate(refUpdate, () -> Result.NEW); + } + + @Test + public void shouldNotUpdateSharedRefDbWhenFinalCompareAndPutIsFailing() throws Exception { + lenient() + .doReturn(false) + .when(sharedRefDb) + .isUpToDate(any(Project.NameKey.class), any(Ref.class)); + doReturn(true).when(sharedRefDb).isUpToDate(A_TEST_PROJECT_NAME_KEY, localRef); + + Result result = refUpdateValidator.executeRefUpdate(refUpdate, () -> Result.LOCK_FAILURE); + + verify(sharedRefDb, never()) + .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class)); + assertThat(result).isEqualTo(Result.LOCK_FAILURE); + } + + @Test + public void shouldNotUpdateSharedRefDbWhenProjectIsLocal() throws Exception { + when(projectsFilter.matches(anyString())).thenReturn(false); + + refUpdateValidator.executeRefUpdate(refUpdate, () -> Result.NEW); + + verify(sharedRefDb, never()) + .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class)); + } + + private RefUpdateValidator newRefUpdateValidator(SharedRefDatabaseWrapper refDbWrapper) { + return new RefUpdateValidator( + refDbWrapper, + validationMetrics, + defaultRefEnforcement, + new DummyLockWrapper(), + projectsFilter, + A_TEST_PROJECT_NAME, + localRefDb); + } +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbBatchRefUpdateTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbBatchRefUpdateTest.java new file mode 100644 index 0000000..e52cc5f --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbBatchRefUpdateTest.java
@@ -0,0 +1,211 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import static java.util.Arrays.asList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.DefaultSharedRefEnforcement; +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.RefFixture; +import java.io.IOException; +import java.util.Collections; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.transport.ReceiveCommand.Result; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SharedRefDbBatchRefUpdateTest implements RefFixture { + + @Mock SharedRefDatabaseWrapper sharedRefDb; + @Mock BatchRefUpdate batchRefUpdate; + @Mock BatchRefUpdateValidator batchRefUpdateValidator; + @Mock RefDatabase refDatabase; + @Mock RevWalk revWalk; + @Mock ProgressMonitor progressMonitor; + @Mock ValidationMetrics validationMetrics; + @Mock ProjectsFilter projectsFilter; + + private final Ref oldRef = + new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, A_TEST_REF_NAME, AN_OBJECT_ID_1); + private final Ref newRef = + new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, A_TEST_REF_NAME, AN_OBJECT_ID_2); + ReceiveCommand receiveCommandBeforeExecution = + createReceiveCommand( + oldRef.getObjectId(), newRef.getObjectId(), oldRef.getName(), Result.NOT_ATTEMPTED); + + ReceiveCommand successReceiveCommandAfterExecution = + createReceiveCommand(oldRef.getObjectId(), newRef.getObjectId(), oldRef.getName(), Result.OK); + + ReceiveCommand rejectReceiveCommandAfterExecution = + createReceiveCommand( + oldRef.getObjectId(), + newRef.getObjectId(), + oldRef.getName(), + Result.REJECTED_NONFASTFORWARD); + + private ReceiveCommand createReceiveCommand( + ObjectId oldRefObjectId, ObjectId newRefObjectId, String refName, Result result) { + ReceiveCommand receiveCommand = new ReceiveCommand(oldRefObjectId, newRefObjectId, refName); + receiveCommand.setResult(result); + return receiveCommand; + } + + private SharedRefDbBatchRefUpdate sharedRefDbRefUpdate; + + @Rule public TestName nameRule = new TestName(); + + @Override + public String testBranch() { + return "branch_" + nameRule.getMethodName(); + } + + @Before + public void setup() { + when(projectsFilter.matches(anyString())).thenReturn(true); + } + + @SuppressWarnings("deprecation") + private void setMockRequiredReturnValues() throws IOException { + + doReturn(batchRefUpdate).when(refDatabase).newBatchUpdate(); + + when(batchRefUpdate.getCommands()) + .thenReturn(asList(receiveCommandBeforeExecution)) + .thenReturn(asList(successReceiveCommandAfterExecution)); + + doReturn(oldRef).when(refDatabase).getRef(A_TEST_REF_NAME); + doReturn(oldRef).when(refDatabase).exactRef(A_TEST_REF_NAME); + + sharedRefDbRefUpdate = getSharedRefDbBatchRefUpdateWithDefaultPolicyEnforcement(); + + verifyZeroInteractions(validationMetrics); + } + + @Test + public void executeAndDelegateSuccessfullyWithNoExceptions() throws Exception { + setMockRequiredReturnValues(); + + // When compareAndPut against sharedDb succeeds + doReturn(true).when(sharedRefDb).isUpToDate(A_TEST_PROJECT_NAME_KEY, oldRef); + doReturn(true) + .when(sharedRefDb) + .compareAndPut(eq(A_TEST_PROJECT_NAME_KEY), refEquals(oldRef), eq(newRef.getObjectId())); + sharedRefDbRefUpdate.execute(revWalk, progressMonitor, Collections.emptyList()); + verify(sharedRefDb) + .compareAndPut(eq(A_TEST_PROJECT_NAME_KEY), refEquals(oldRef), eq(newRef.getObjectId())); + } + + private Ref refEquals(Ref oldRef) { + return argThat(new RefMatcher(oldRef)); + } + + @Test(expected = IOException.class) + public void executeAndFailsWithExceptions() throws IOException { + sharedRefDbRefUpdate = getSharedRefDbBatchRefUpdateWithMockedValidator(); + doThrow(new IOException("IO Test Exception")) + .when(batchRefUpdateValidator) + .executeBatchUpdateWithValidation(any(), any()); + + sharedRefDbRefUpdate.execute(revWalk, progressMonitor, Collections.emptyList()); + } + + @Test + public void executeSuccessfullyWithNoExceptionsWhenOutOfSync() throws IOException { + setMockRequiredReturnValues(); + doReturn(true).when(sharedRefDb).exists(A_TEST_PROJECT_NAME_KEY, A_TEST_REF_NAME); + doReturn(false).when(sharedRefDb).isUpToDate(A_TEST_PROJECT_NAME_KEY, oldRef); + + sharedRefDbRefUpdate.execute(revWalk, progressMonitor, Collections.emptyList()); + + verify(validationMetrics).incrementSplitBrainPrevention(); + } + + @Test + public void executeSuccessfullyWithNoExceptionsWhenEmptyList() throws IOException { + doReturn(batchRefUpdate).when(refDatabase).newBatchUpdate(); + doReturn(Collections.emptyList()).when(batchRefUpdate).getCommands(); + + sharedRefDbRefUpdate = getSharedRefDbBatchRefUpdateWithDefaultPolicyEnforcement(); + + sharedRefDbRefUpdate.execute(revWalk, progressMonitor, Collections.emptyList()); + } + + private SharedRefDbBatchRefUpdate getSharedRefDbBatchRefUpdateWithDefaultPolicyEnforcement() { + BatchRefUpdateValidator.Factory batchRefValidatorFactory = + new BatchRefUpdateValidator.Factory() { + @Override + public BatchRefUpdateValidator create(String projectName, RefDatabase refDb) { + return new BatchRefUpdateValidator( + sharedRefDb, + validationMetrics, + new DefaultSharedRefEnforcement(), + new DummyLockWrapper(), + projectsFilter, + projectName, + refDb); + } + }; + return new SharedRefDbBatchRefUpdate( + batchRefValidatorFactory, A_TEST_PROJECT_NAME, refDatabase); + } + + private SharedRefDbBatchRefUpdate getSharedRefDbBatchRefUpdateWithMockedValidator() { + BatchRefUpdateValidator.Factory batchRefValidatorFactory = + new BatchRefUpdateValidator.Factory() { + @Override + public BatchRefUpdateValidator create(String projectName, RefDatabase refDb) { + return batchRefUpdateValidator; + } + }; + return new SharedRefDbBatchRefUpdate( + batchRefValidatorFactory, A_TEST_PROJECT_NAME, refDatabase); + } + + protected static class RefMatcher implements ArgumentMatcher<Ref> { + private Ref left; + + public RefMatcher(Ref ref) { + this.left = ref; + } + + @Override + public boolean matches(Ref right) { + return left.getName().equals(right.getName()) + && left.getObjectId().equals(right.getObjectId()); + } + } +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbGitRepositoryManagerTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbGitRepositoryManagerTest.java new file mode 100644 index 0000000..b2b8ad6 --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbGitRepositoryManagerTest.java
@@ -0,0 +1,82 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.RefFixture; +import com.google.gerrit.server.git.LocalDiskRepositoryManager; +import org.eclipse.jgit.lib.Repository; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SharedRefDbGitRepositoryManagerTest implements RefFixture { + + @Mock LocalDiskRepositoryManager localDiskRepositoryManagerMock; + + @Mock SharedRefDbRepository.Factory sharedRefDbRepositoryFactoryMock; + + @Mock Repository repositoryMock; + + @Mock SharedRefDbRepository sharedRefDbRepositoryMock; + + SharedRefDbGitRepositoryManager msRepoMgr; + + @Override + public String testBranch() { + return "foo"; + } + + @Before + public void setUp() throws Exception { + doReturn(sharedRefDbRepositoryMock) + .when(sharedRefDbRepositoryFactoryMock) + .create(A_TEST_PROJECT_NAME, repositoryMock); + msRepoMgr = + new SharedRefDbGitRepositoryManager( + sharedRefDbRepositoryFactoryMock, localDiskRepositoryManagerMock); + } + + @Test + public void openRepositoryShouldCreateSharedRefDbRepositoryWrapper() throws Exception { + doReturn(repositoryMock) + .when(localDiskRepositoryManagerMock) + .openRepository(A_TEST_PROJECT_NAME_KEY); + + msRepoMgr.openRepository(A_TEST_PROJECT_NAME_KEY); + + verifyThatSharedRefDbRepositoryWrapperHasBeenCreated(); + } + + @Test + public void createRepositoryShouldCreateSharedRefDbRepositoryWrapper() throws Exception { + doReturn(repositoryMock) + .when(localDiskRepositoryManagerMock) + .createRepository(A_TEST_PROJECT_NAME_KEY); + + msRepoMgr.createRepository(A_TEST_PROJECT_NAME_KEY); + + verifyThatSharedRefDbRepositoryWrapperHasBeenCreated(); + } + + private void verifyThatSharedRefDbRepositoryWrapperHasBeenCreated() { + verify(sharedRefDbRepositoryFactoryMock).create(A_TEST_PROJECT_NAME, repositoryMock); + } +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefDatabaseTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefDatabaseTest.java new file mode 100644 index 0000000..352b136 --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRefDatabaseTest.java
@@ -0,0 +1,59 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.RefFixture; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefUpdate; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SharedRefDbRefDatabaseTest implements RefFixture { + + @Rule public TestName nameRule = new TestName(); + + @Mock SharedRefDbRefUpdate.Factory refUpdateFactoryMock; + @Mock SharedRefDbBatchRefUpdate.Factory refBatchUpdateFactoryMock; + + @Mock RefDatabase refDatabaseMock; + + @Mock RefUpdate refUpdateMock; + + @Override + public String testBranch() { + return "branch_" + nameRule.getMethodName(); + } + + @Test + public void newUpdateShouldCreateSharedRefDbRefUpdate() throws Exception { + String refName = aBranchRef(); + SharedRefDbRefDatabase sharedRefDbRefDb = + new SharedRefDbRefDatabase( + refUpdateFactoryMock, refBatchUpdateFactoryMock, A_TEST_PROJECT_NAME, refDatabaseMock); + doReturn(refUpdateMock).when(refDatabaseMock).newUpdate(refName, false); + + sharedRefDbRefDb.newUpdate(refName, false); + + verify(refUpdateFactoryMock).create(A_TEST_PROJECT_NAME, refUpdateMock, refDatabaseMock); + } +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRepositoryTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRepositoryTest.java new file mode 100644 index 0000000..5bb4fb4 --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbRepositoryTest.java
@@ -0,0 +1,97 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.RefFixture; +import java.io.IOException; +import org.eclipse.jgit.lib.ObjectDatabase; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.eclipse.jgit.lib.Repository; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SharedRefDbRepositoryTest implements RefFixture { + + @Mock SharedRefDbRefDatabase.Factory sharedRefDbRefDbFactory; + @Mock SharedRefDbRefDatabase sharedRefDb; + @Mock RefDatabase genericRefDb; + @Mock ObjectDatabase objectDatabase; + + @Mock SharedRefDbRefUpdate sharedRefDbRefUpdate; + + @Mock Repository repository; + + private final String PROJECT_NAME = "ProjectName"; + private final String REFS_HEADS_MASTER = "refs/heads/master"; + + @Override + public String testBranch() { + return null; + } + + private void setMockitoCommon() { + doReturn(true).when(repository).isBare(); + doReturn(genericRefDb).when(repository).getRefDatabase(); + doReturn(objectDatabase).when(repository).getObjectDatabase(); + doReturn(sharedRefDb).when(sharedRefDbRefDbFactory).create(PROJECT_NAME, genericRefDb); + } + + @Test + public void shouldInvokeSharedRefDbRefDbFactoryCreate() { + setMockitoCommon(); + try (SharedRefDbRepository sharedRefDbRepository = + new SharedRefDbRepository(sharedRefDbRefDbFactory, PROJECT_NAME, repository)) { + + sharedRefDbRepository.getRefDatabase(); + verify(sharedRefDbRefDbFactory).create(PROJECT_NAME, genericRefDb); + } + } + + @Test + public void shouldInvokeNewUpdateInSharedRefDbRefDatabase() throws IOException { + setMockitoCommon(); + try (SharedRefDbRepository sharedRefDbRepository = + new SharedRefDbRepository(sharedRefDbRefDbFactory, PROJECT_NAME, repository)) { + sharedRefDbRepository.getRefDatabase().newUpdate(REFS_HEADS_MASTER, false); + + verify(sharedRefDb).newUpdate(REFS_HEADS_MASTER, false); + } + } + + @Test + public void shouldInvokeUpdateInSharedRefDbRefUpdate() throws IOException { + setMockitoCommon(); + doReturn(Result.NEW).when(sharedRefDbRefUpdate).update(); + doReturn(sharedRefDbRefUpdate).when(sharedRefDb).newUpdate(REFS_HEADS_MASTER, false); + + try (SharedRefDbRepository sharedRefDbRepository = + new SharedRefDbRepository(sharedRefDbRefDbFactory, PROJECT_NAME, repository)) { + + Result updateResult = + sharedRefDbRepository.getRefDatabase().newUpdate(REFS_HEADS_MASTER, false).update(); + + verify(sharedRefDbRefUpdate).update(); + assertThat(updateResult).isEqualTo(Result.NEW); + } + } +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/CustomSharedRefEnforcementByProjectTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/CustomSharedRefEnforcementByProjectTest.java new file mode 100644 index 0000000..4cf6800 --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/CustomSharedRefEnforcementByProjectTest.java
@@ -0,0 +1,150 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb; + +import static com.google.common.truth.Truth.assertThat; + +import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbConfiguration; +import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbConfiguration.SharedRefDatabase; +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement.EnforcePolicy; +import java.util.Arrays; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Ref; +import org.junit.Before; +import org.junit.Test; + +public class CustomSharedRefEnforcementByProjectTest implements RefFixture { + + SharedRefEnforcement refEnforcement; + + @Before + public void setUp() { + Config sharedRefDbConfig = new Config(); + sharedRefDbConfig.setStringList( + SharedRefDatabase.SECTION, + SharedRefDatabase.SUBSECTION_ENFORCEMENT_RULES, + EnforcePolicy.IGNORED.name(), + Arrays.asList( + "ProjectOne", + "ProjectTwo:refs/heads/master/test", + "ProjectTwo:refs/heads/master/test2")); + + refEnforcement = newCustomRefEnforcement(sharedRefDbConfig); + } + + @Test + public void projectOneShouldReturnDesiredForAllRefs() { + Ref aRef = newRef("refs/heads/master/2", AN_OBJECT_ID_1); + assertThat(refEnforcement.getPolicy("ProjectOne", aRef.getName())) + .isEqualTo(EnforcePolicy.IGNORED); + } + + @Test + public void projectOneEnforcementShouldAlwaysPrevail() { + Ref aRef = newRef("refs/heads/master/test", AN_OBJECT_ID_1); + assertThat(refEnforcement.getPolicy("ProjectOne", aRef.getName())) + .isEqualTo(EnforcePolicy.IGNORED); + } + + @Test + public void aNonListedProjectShouldRequireRefForMasterTest() { + Ref aRef = newRef("refs/heads/master/test", AN_OBJECT_ID_1); + assertThat(refEnforcement.getPolicy("NonListedProject", aRef.getName())) + .isEqualTo(EnforcePolicy.REQUIRED); + } + + @Test + public void projectTwoSpecificRefShouldReturnIgnoredPolicy() { + Ref refOne = newRef("refs/heads/master/test", AN_OBJECT_ID_1); + Ref refTwo = newRef("refs/heads/master/test2", AN_OBJECT_ID_1); + + assertThat(refEnforcement.getPolicy("ProjectTwo", refOne.getName())) + .isEqualTo(EnforcePolicy.IGNORED); + assertThat(refEnforcement.getPolicy("ProjectTwo", refTwo.getName())) + .isEqualTo(EnforcePolicy.IGNORED); + } + + @Test + public void aNonListedProjectShouldReturnRequired() { + Ref refOne = newRef("refs/heads/master/newChange", AN_OBJECT_ID_1); + assertThat(refEnforcement.getPolicy("NonListedProject", refOne.getName())) + .isEqualTo(EnforcePolicy.REQUIRED); + } + + @Test + public void aNonListedRefInProjectShouldReturnRequired() { + Ref refOne = newRef("refs/heads/master/test3", AN_OBJECT_ID_1); + assertThat(refEnforcement.getPolicy("ProjectTwo", refOne.getName())) + .isEqualTo(EnforcePolicy.REQUIRED); + } + + @Test + public void aNonListedProjectAndRefShouldReturnRequired() { + Ref refOne = newRef("refs/heads/master/test3", AN_OBJECT_ID_1); + assertThat(refEnforcement.getPolicy("NonListedProject", refOne.getName())) + .isEqualTo(EnforcePolicy.REQUIRED); + } + + @Test + public void getProjectPolicyForProjectOneShouldReturnIgnored() { + assertThat(refEnforcement.getPolicy("ProjectOne")).isEqualTo(EnforcePolicy.IGNORED); + } + + @Test + public void getProjectPolicyForProjectTwoShouldReturnRequired() { + assertThat(refEnforcement.getPolicy("ProjectTwo")).isEqualTo(EnforcePolicy.REQUIRED); + } + + @Test + public void getProjectPolicyForNonListedProjectShouldReturnRequired() { + assertThat(refEnforcement.getPolicy("NonListedProject")).isEqualTo(EnforcePolicy.REQUIRED); + } + + @Test + public void getProjectPolicyForNonListedProjectWhenSingleProject() { + SharedRefEnforcement customEnforcement = + newCustomRefEnforcementWithValue(EnforcePolicy.IGNORED, ":refs/heads/master"); + + assertThat(customEnforcement.getPolicy("NonListedProject")).isEqualTo(EnforcePolicy.REQUIRED); + } + + @Test + public void getANonListedProjectWhenOnlyOneProjectIsListedShouldReturnRequired() { + SharedRefEnforcement customEnforcement = + newCustomRefEnforcementWithValue(EnforcePolicy.IGNORED, "AProject:"); + assertThat(customEnforcement.getPolicy("NonListedProject", "refs/heads/master")) + .isEqualTo(EnforcePolicy.REQUIRED); + } + + private SharedRefEnforcement newCustomRefEnforcementWithValue( + EnforcePolicy policy, String... projectAndRefs) { + Config sharedRefDbConfiguration = new Config(); + sharedRefDbConfiguration.setStringList( + SharedRefDatabase.SECTION, + SharedRefDatabase.SUBSECTION_ENFORCEMENT_RULES, + policy.name(), + Arrays.asList(projectAndRefs)); + return newCustomRefEnforcement(sharedRefDbConfiguration); + } + + private SharedRefEnforcement newCustomRefEnforcement(Config sharedRefDbConfig) { + return new CustomSharedRefEnforcementByProject(new SharedRefDbConfiguration(sharedRefDbConfig)); + } + + @Override + public String testBranch() { + return "fooBranch"; + } +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/DefaultSharedRefEnforcementTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/DefaultSharedRefEnforcementTest.java new file mode 100644 index 0000000..a9f0cb2 --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/DefaultSharedRefEnforcementTest.java
@@ -0,0 +1,80 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb; + +import static com.google.common.truth.Truth.assertThat; + +import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement.EnforcePolicy; +import org.eclipse.jgit.lib.Ref; +import org.junit.Test; + +public class DefaultSharedRefEnforcementTest implements RefFixture { + + SharedRefEnforcement refEnforcement = new DefaultSharedRefEnforcement(); + + @Test + public void anImmutableChangeShouldBeIgnored() { + Ref immutableChangeRef = newRef(A_REF_NAME_OF_A_PATCHSET, AN_OBJECT_ID_1); + assertThat(refEnforcement.getPolicy(A_TEST_PROJECT_NAME, immutableChangeRef.getName())) + .isEqualTo(EnforcePolicy.IGNORED); + } + + @Test + public void aChangeMetaShouldNotBeIgnored() { + Ref immutableChangeRef = newRef("refs/changes/01/1/meta", AN_OBJECT_ID_1); + assertThat(refEnforcement.getPolicy(A_TEST_PROJECT_NAME, immutableChangeRef.getName())) + .isEqualTo(EnforcePolicy.REQUIRED); + } + + @Test + public void aCacheAutomergeShouldBeIgnored() { + Ref immutableChangeRef = newRef("refs/cache-automerge/01/1/1000000", AN_OBJECT_ID_1); + assertThat(refEnforcement.getPolicy(A_TEST_PROJECT_NAME, immutableChangeRef.getName())) + .isEqualTo(EnforcePolicy.IGNORED); + } + + @Test + public void aDraftCommentsShouldBeIgnored() { + Ref immutableChangeRef = newRef("refs/draft-comments/01/1/1000000", AN_OBJECT_ID_1); + assertThat(refEnforcement.getPolicy(A_TEST_PROJECT_NAME, immutableChangeRef.getName())) + .isEqualTo(EnforcePolicy.IGNORED); + } + + @Test + public void regularRefHeadsMasterShouldNotBeIgnored() { + Ref immutableChangeRef = newRef("refs/heads/master", AN_OBJECT_ID_1); + assertThat(refEnforcement.getPolicy(A_TEST_PROJECT_NAME, immutableChangeRef.getName())) + .isEqualTo(EnforcePolicy.REQUIRED); + } + + @Test + public void regularCommitShouldNotBeIgnored() { + Ref immutableChangeRef = newRef("refs/heads/stable-2.16", AN_OBJECT_ID_1); + assertThat(refEnforcement.getPolicy(A_TEST_PROJECT_NAME, immutableChangeRef.getName())) + .isEqualTo(EnforcePolicy.REQUIRED); + } + + @Test + public void allUsersExternalIdsRefShouldBeRequired() { + Ref refOne = newRef("refs/meta/external-ids", AN_OBJECT_ID_1); + assertThat(refEnforcement.getPolicy("All-Users", refOne.getName())) + .isEqualTo(EnforcePolicy.REQUIRED); + } + + @Override + public String testBranch() { + return "fooBranch"; + } +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabaseTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabaseTest.java new file mode 100644 index 0000000..bc1435c --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabaseTest.java
@@ -0,0 +1,42 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb; + +import static com.google.common.truth.Truth.assertThat; + +import org.eclipse.jgit.lib.Ref; +import org.junit.Test; + +public class NoopSharedRefDatabaseTest implements RefFixture { + + private Ref sampleRef = newRef(A_TEST_REF_NAME, AN_OBJECT_ID_1); + private NoopSharedRefDatabase objectUnderTest = new NoopSharedRefDatabase(); + + @Test + public void isUpToDateShouldAlwaysReturnTrue() { + assertThat(objectUnderTest.isUpToDate(A_TEST_PROJECT_NAME_KEY, sampleRef)).isTrue(); + } + + @Test + public void compareAndPutShouldAlwaysReturnTrue() { + assertThat(objectUnderTest.compareAndPut(A_TEST_PROJECT_NAME_KEY, sampleRef, AN_OBJECT_ID_2)) + .isTrue(); + } + + @Test + public void existsShouldAlwaysReturnFalse() { + assertThat(objectUnderTest.exists(A_TEST_PROJECT_NAME_KEY, A_TEST_REF_NAME)).isFalse(); + } +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefFixture.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefFixture.java new file mode 100644 index 0000000..fa6a825 --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefFixture.java
@@ -0,0 +1,54 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb; + +import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.RefNames; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.Ref; +import org.junit.Ignore; + +@Ignore +public interface RefFixture { + + static final String ALLOWED_CHARS = "abcdefghilmnopqrstuvz"; + static final String ALLOWED_DIGITS = "1234567890"; + static final String ALLOWED_NAME_CHARS = + ALLOWED_CHARS + ALLOWED_CHARS.toUpperCase() + ALLOWED_DIGITS; + static final String A_TEST_PROJECT_NAME = "A_TEST_PROJECT_NAME"; + static final Project.NameKey A_TEST_PROJECT_NAME_KEY = Project.nameKey(A_TEST_PROJECT_NAME); + static final ObjectId AN_OBJECT_ID_1 = new ObjectId(1, 2, 3, 4, 5); + static final ObjectId AN_OBJECT_ID_2 = new ObjectId(1, 2, 3, 4, 6); + static final ObjectId AN_OBJECT_ID_3 = new ObjectId(1, 2, 3, 4, 7); + static final String A_TEST_REF_NAME = "refs/heads/master"; + static final String A_REF_NAME_OF_A_PATCHSET = "refs/changes/01/1/1"; + + default String aBranchRef() { + return RefNames.REFS_HEADS + testBranch(); + } + + default String testBranch() { + return "aTestBranch"; + } + + default Ref newRef(String refName, ObjectId objectId) { + return new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, refName, objectId); + } + + default Ref nullRef(String refName) { + return newRef(refName, ObjectId.zeroId()); + } +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefSharedDatabaseTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefSharedDatabaseTest.java new file mode 100644 index 0000000..2d3d8b8 --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefSharedDatabaseTest.java
@@ -0,0 +1,47 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb; + +import static com.google.common.truth.Truth.assertThat; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Ref.Storage; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +public class RefSharedDatabaseTest implements RefFixture { + @Rule public TestName nameRule = new TestName(); + + @Override + public String testBranch() { + return "branch_" + nameRule.getMethodName(); + } + + @Test + public void shouldCreateANewRef() { + + ObjectId objectId = AN_OBJECT_ID_1; + String refName = aBranchRef(); + + Ref aNewRef = new ObjectIdRef.Unpeeled(Storage.NETWORK, refName, objectId); + + assertThat(aNewRef.getName()).isEqualTo(refName); + assertThat(aNewRef.getObjectId()).isEqualTo(objectId); + assertThat(aNewRef.getStorage()).isEqualTo(Storage.NETWORK); + } +}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefUpdateStub.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefUpdateStub.java new file mode 100644 index 0000000..e78ba6d --- /dev/null +++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/RefUpdateStub.java
@@ -0,0 +1,114 @@ +// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb; + +import java.io.IOException; +import org.apache.commons.lang.NotImplementedException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; +import org.junit.Ignore; + +@Ignore +public class RefUpdateStub extends RefUpdate { + + public static RefUpdate forSuccessfulCreate(Ref newRef) { + return new RefUpdateStub(Result.NEW, null, newRef, newRef.getObjectId()); + } + + public static RefUpdate forSuccessfulUpdate(Ref oldRef, ObjectId newObjectId) { + return new RefUpdateStub(Result.FAST_FORWARD, null, oldRef, newObjectId); + } + + public static RefUpdate forSuccessfulDelete(Ref oldRef) { + return new RefUpdateStub(null, Result.FORCED, oldRef, ObjectId.zeroId()); + } + + private final Result updateResult; + private final Result deleteResult; + + public RefUpdateStub(Result updateResult, Result deleteResult, Ref oldRef, ObjectId newObjectId) { + super(oldRef); + this.setNewObjectId(newObjectId); + this.updateResult = updateResult; + this.deleteResult = deleteResult; + } + + @Override + protected RefDatabase getRefDatabase() { + throw new NotImplementedException("Method not implemented yet, not assumed you needed it!!"); + } + + @Override + protected Repository getRepository() { + throw new NotImplementedException("Method not implemented yet, not assumed you needed it!!"); + } + + @Override + protected boolean tryLock(boolean deref) throws IOException { + throw new NotImplementedException("Method not implemented yet, not assumed you needed it!!"); + } + + @Override + protected void unlock() { + throw new NotImplementedException("Method not implemented yet, not assumed you needed it!!"); + } + + @Override + protected Result doUpdate(Result desiredResult) throws IOException { + throw new NotImplementedException("Method not implemented, shouldn't be called!!"); + } + + @Override + protected Result doDelete(Result desiredResult) throws IOException { + throw new NotImplementedException("Method not implemented, shouldn't be called!!"); + } + + @Override + protected Result doLink(String target) throws IOException { + throw new NotImplementedException("Method not implemented yet, not assumed you needed it!!"); + } + + @Override + public Result update() throws IOException { + if (updateResult != null) return updateResult; + + throw new NotImplementedException("Not assumed you needed to stub this call!!"); + } + + @Override + public Result update(RevWalk walk) throws IOException { + if (updateResult != null) return updateResult; + + throw new NotImplementedException("Not assumed you needed to stub this call!!"); + } + + @Override + public Result delete() throws IOException { + if (deleteResult != null) return deleteResult; + + throw new NotImplementedException("Not assumed you needed to stub this call!!"); + } + + @Override + public Result delete(RevWalk walk) throws IOException { + if (deleteResult != null) return deleteResult; + + throw new NotImplementedException("Not assumed you needed to stub this call!!"); + } +}