summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/update/BatchUpdate.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/update/BatchUpdate.java')
-rw-r--r--java/com/google/gerrit/server/update/BatchUpdate.java409
1 files changed, 409 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
new file mode 100644
index 0000000000..b3472d2dab
--- /dev/null
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -0,0 +1,409 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.update;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableMultiset.toImmutableMultiset;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.Multiset;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.validators.OnSubmitValidators;
+import com.google.gerrit.server.logging.RequestId;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.NoSuchRefException;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.TimeZone;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * Helper for a set of updates that should be applied for a site.
+ *
+ * <p>An update operation can be divided into three phases:
+ *
+ * <ol>
+ * <li>Git reference updates
+ * <li>Database updates
+ * <li>Post-update steps
+ * <li>
+ * </ol>
+ *
+ * A single conceptual operation, such as a REST API call or a merge operation, may make multiple
+ * changes at each step, which all need to be serialized relative to each other. Moreover, for
+ * consistency, <em>all</em> git ref updates must be performed before <em>any</em> database updates,
+ * since database updates might refer to newly-created patch set refs. And all post-update steps,
+ * such as hooks, should run only after all storage mutations have completed.
+ *
+ * <p>Depending on the backend used, each step might support batching, for example in a {@code
+ * BatchRefUpdate} or one or more database transactions. All operations in one phase must complete
+ * successfully before proceeding to the next phase.
+ */
+public abstract class BatchUpdate implements AutoCloseable {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ public static Module module() {
+ return new FactoryModule() {
+ @Override
+ public void configure() {
+ factory(ReviewDbBatchUpdate.AssistedFactory.class);
+ factory(NoteDbBatchUpdate.AssistedFactory.class);
+ }
+ };
+ }
+
+ @Singleton
+ public static class Factory {
+ private final NotesMigration migration;
+ private final ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory;
+ private final NoteDbBatchUpdate.AssistedFactory noteDbBatchUpdateFactory;
+
+ // TODO(dborowitz): Make this non-injectable to force all callers to use RetryHelper.
+ @Inject
+ Factory(
+ NotesMigration migration,
+ ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory,
+ NoteDbBatchUpdate.AssistedFactory noteDbBatchUpdateFactory) {
+ this.migration = migration;
+ this.reviewDbBatchUpdateFactory = reviewDbBatchUpdateFactory;
+ this.noteDbBatchUpdateFactory = noteDbBatchUpdateFactory;
+ }
+
+ public BatchUpdate create(
+ ReviewDb db, Project.NameKey project, CurrentUser user, Timestamp when) {
+ if (migration.disableChangeReviewDb()) {
+ return noteDbBatchUpdateFactory.create(db, project, user, when);
+ }
+ return reviewDbBatchUpdateFactory.create(db, project, user, when);
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public void execute(
+ Collection<BatchUpdate> updates, BatchUpdateListener listener, boolean dryRun)
+ throws UpdateException, RestApiException {
+ requireNonNull(listener);
+ checkDifferentProject(updates);
+ // It's safe to downcast all members of the input collection in this case, because the only
+ // way a caller could have gotten any BatchUpdates in the first place is to call the create
+ // method above, which always returns instances of the type we expect. Just to be safe,
+ // copy them into an ImmutableList so there is no chance the callee can pollute the input
+ // collection.
+ if (migration.disableChangeReviewDb()) {
+ ImmutableList<NoteDbBatchUpdate> noteDbUpdates =
+ (ImmutableList) ImmutableList.copyOf(updates);
+ NoteDbBatchUpdate.execute(noteDbUpdates, listener, dryRun);
+ } else {
+ ImmutableList<ReviewDbBatchUpdate> reviewDbUpdates =
+ (ImmutableList) ImmutableList.copyOf(updates);
+ ReviewDbBatchUpdate.execute(reviewDbUpdates, listener, dryRun);
+ }
+ }
+
+ private static void checkDifferentProject(Collection<BatchUpdate> updates) {
+ Multiset<Project.NameKey> projectCounts =
+ updates.stream().map(u -> u.project).collect(toImmutableMultiset());
+ checkArgument(
+ projectCounts.entrySet().size() == updates.size(),
+ "updates must all be for different projects, got: %s",
+ projectCounts);
+ }
+ }
+
+ static Order getOrder(Collection<? extends BatchUpdate> updates, BatchUpdateListener listener) {
+ Order o = null;
+ for (BatchUpdate u : updates) {
+ if (o == null) {
+ o = u.order;
+ } else if (u.order != o) {
+ throw new IllegalArgumentException("cannot mix execution orders");
+ }
+ }
+ if (o != Order.REPO_BEFORE_DB) {
+ checkArgument(
+ listener == BatchUpdateListener.NONE,
+ "BatchUpdateListener not supported for order %s",
+ o);
+ }
+ return o;
+ }
+
+ static boolean getUpdateChangesInParallel(Collection<? extends BatchUpdate> updates) {
+ checkArgument(!updates.isEmpty());
+ Boolean p = null;
+ for (BatchUpdate u : updates) {
+ if (p == null) {
+ p = u.updateChangesInParallel;
+ } else if (u.updateChangesInParallel != p) {
+ throw new IllegalArgumentException("cannot mix parallel and non-parallel operations");
+ }
+ }
+ // Properly implementing this would involve hoisting the parallel loop up
+ // even further. As of this writing, the only user is ReceiveCommits,
+ // which only executes a single BatchUpdate at a time. So bail for now.
+ checkArgument(
+ !p || updates.size() <= 1,
+ "cannot execute ChangeOps in parallel with more than 1 BatchUpdate");
+ return p;
+ }
+
+ static void wrapAndThrowException(Exception e) throws UpdateException, RestApiException {
+ Throwables.throwIfUnchecked(e);
+
+ // Propagate REST API exceptions thrown by operations; they commonly throw exceptions like
+ // ResourceConflictException to indicate an atomic update failure.
+ Throwables.throwIfInstanceOf(e, UpdateException.class);
+ Throwables.throwIfInstanceOf(e, RestApiException.class);
+
+ // Convert other common non-REST exception types with user-visible messages to corresponding
+ // REST exception types
+ if (e instanceof InvalidChangeOperationException) {
+ throw new ResourceConflictException(e.getMessage(), e);
+ } else if (e instanceof NoSuchChangeException
+ || e instanceof NoSuchRefException
+ || e instanceof NoSuchProjectException) {
+ throw new ResourceNotFoundException(e.getMessage(), e);
+ }
+
+ // Otherwise, wrap in a generic UpdateException, which does not include a user-visible message.
+ throw new UpdateException(e);
+ }
+
+ protected GitRepositoryManager repoManager;
+
+ protected final Project.NameKey project;
+ protected final CurrentUser user;
+ protected final Timestamp when;
+ protected final TimeZone tz;
+
+ protected final ListMultimap<Change.Id, BatchUpdateOp> ops =
+ MultimapBuilder.linkedHashKeys().arrayListValues().build();
+ protected final Map<Change.Id, Change> newChanges = new HashMap<>();
+ protected final List<RepoOnlyOp> repoOnlyOps = new ArrayList<>();
+
+ protected RepoView repoView;
+ protected BatchRefUpdate batchRefUpdate;
+ protected Order order;
+ protected OnSubmitValidators onSubmitValidators;
+ protected PushCertificate pushCert;
+ protected String refLogMessage;
+
+ private boolean updateChangesInParallel;
+
+ protected BatchUpdate(
+ GitRepositoryManager repoManager,
+ PersonIdent serverIdent,
+ Project.NameKey project,
+ CurrentUser user,
+ Timestamp when) {
+ this.repoManager = repoManager;
+ this.project = project;
+ this.user = user;
+ this.when = when;
+ tz = serverIdent.getTimeZone();
+ order = Order.REPO_BEFORE_DB;
+ }
+
+ @Override
+ public void close() {
+ if (repoView != null) {
+ repoView.close();
+ }
+ }
+
+ public abstract void execute(BatchUpdateListener listener)
+ throws UpdateException, RestApiException;
+
+ public void execute() throws UpdateException, RestApiException {
+ execute(BatchUpdateListener.NONE);
+ }
+
+ protected abstract Context newContext();
+
+ public BatchUpdate setRepository(Repository repo, RevWalk revWalk, ObjectInserter inserter) {
+ checkState(this.repoView == null, "repo already set");
+ repoView = new RepoView(repo, revWalk, inserter);
+ return this;
+ }
+
+ public BatchUpdate setPushCertificate(@Nullable PushCertificate pushCert) {
+ this.pushCert = pushCert;
+ return this;
+ }
+
+ public BatchUpdate setRefLogMessage(@Nullable String refLogMessage) {
+ this.refLogMessage = refLogMessage;
+ return this;
+ }
+
+ public BatchUpdate setOrder(Order order) {
+ this.order = order;
+ return this;
+ }
+
+ /**
+ * Add a validation step for intended ref operations, which will be performed at the end of {@link
+ * RepoOnlyOp#updateRepo(RepoContext)} step.
+ */
+ public BatchUpdate setOnSubmitValidators(OnSubmitValidators onSubmitValidators) {
+ this.onSubmitValidators = onSubmitValidators;
+ return this;
+ }
+
+ /**
+ * Execute {@link BatchUpdateOp#updateChange(ChangeContext)} in parallel for each change.
+ *
+ * <p>This improves performance of writing to multiple changes in separate ReviewDb transactions.
+ * When only NoteDb is used, updates to all changes are written in a single batch ref update, so
+ * parallelization is not used and this option is ignored.
+ */
+ public BatchUpdate updateChangesInParallel() {
+ this.updateChangesInParallel = true;
+ return this;
+ }
+
+ protected void initRepository() throws IOException {
+ if (repoView == null) {
+ repoView = new RepoView(repoManager, project);
+ }
+ }
+
+ protected RepoView getRepoView() throws IOException {
+ initRepository();
+ return repoView;
+ }
+
+ protected CurrentUser getUser() {
+ return user;
+ }
+
+ protected Optional<AccountState> getAccount() {
+ return user.isIdentifiedUser()
+ ? Optional.of(user.asIdentifiedUser().state())
+ : Optional.empty();
+ }
+
+ protected RevWalk getRevWalk() throws IOException {
+ initRepository();
+ return repoView.getRevWalk();
+ }
+
+ public Map<String, ReceiveCommand> getRefUpdates() {
+ return repoView != null ? repoView.getCommands().getCommands() : ImmutableMap.of();
+ }
+
+ public BatchUpdate addOp(Change.Id id, BatchUpdateOp op) {
+ checkArgument(!(op instanceof InsertChangeOp), "use insertChange");
+ requireNonNull(op);
+ ops.put(id, op);
+ return this;
+ }
+
+ public BatchUpdate addRepoOnlyOp(RepoOnlyOp op) {
+ checkArgument(!(op instanceof BatchUpdateOp), "use addOp()");
+ repoOnlyOps.add(op);
+ return this;
+ }
+
+ public BatchUpdate insertChange(InsertChangeOp op) throws IOException {
+ Context ctx = newContext();
+ Change c = op.createChange(ctx);
+ checkArgument(
+ !newChanges.containsKey(c.getId()), "only one op allowed to create change %s", c.getId());
+ newChanges.put(c.getId(), c);
+ ops.get(c.getId()).add(0, op);
+ return this;
+ }
+
+ protected static void logDebug(String msg, Throwable t) {
+ // Only log if there is a requestId assigned, since those are the
+ // expensive/complicated requests like MergeOp. Doing it every time would be
+ // noisy.
+ if (RequestId.isSet()) {
+ logger.atFine().withCause(t).log("%s", msg);
+ }
+ }
+
+ protected static void logDebug(String msg) {
+ // Only log if there is a requestId assigned, since those are the
+ // expensive/complicated requests like MergeOp. Doing it every time would be
+ // noisy.
+ if (RequestId.isSet()) {
+ logger.atFine().log(msg);
+ }
+ }
+
+ protected static void logDebug(String msg, @Nullable Object arg) {
+ // Only log if there is a requestId assigned, since those are the
+ // expensive/complicated requests like MergeOp. Doing it every time would be
+ // noisy.
+ if (RequestId.isSet()) {
+ logger.atFine().log(msg, arg);
+ }
+ }
+
+ protected static void logDebug(String msg, @Nullable Object arg1, @Nullable Object arg2) {
+ // Only log if there is a requestId assigned, since those are the
+ // expensive/complicated requests like MergeOp. Doing it every time would be
+ // noisy.
+ if (RequestId.isSet()) {
+ logger.atFine().log(msg, arg1, arg2);
+ }
+ }
+
+ protected static void logDebug(
+ String msg, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3) {
+ // Only log if there is a requestId assigned, since those are the
+ // expensive/complicated requests like MergeOp. Doing it every time would be
+ // noisy.
+ if (RequestId.isSet()) {
+ logger.atFine().log(msg, arg1, arg2, arg3);
+ }
+ }
+}