blob: 2f9ef84744b119f95f8f2470d5d2a8a434524fbf [file] [log] [blame]
// Copyright (C) 2024 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.collect.ImmutableMultiset.toImmutableMultiset;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toMap;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multiset;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.restapi.BadRequestException;
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.server.notedb.LimitExceededException;
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.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.update.BatchUpdate.ChangesHandle;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
@Singleton
public class BatchUpdates {
public class Result {
private final Map<Change.Id, ChangeData> changeDatas;
private Result() {
this(new HashMap<>());
}
private Result(Map<Change.Id, ChangeData> changeDatas) {
this.changeDatas = changeDatas;
}
/**
* Returns the updated {@link ChangeData} for the given project and change ID.
*
* <p>If the requested {@link ChangeData} was already loaded after the {@link BatchUpdate} has
* been executed the cached {@link ChangeData} instance is returned, otherwise the requested
* {@link ChangeData} is loaded and put into the cache.
*/
public ChangeData getChangeData(Project.NameKey projectName, Change.Id changeId) {
return changeDatas.computeIfAbsent(
changeId, id -> changeDataFactory.create(projectName, changeId));
}
}
private final ChangeData.Factory changeDataFactory;
@Inject
BatchUpdates(ChangeData.Factory changeDataFactory) {
this.changeDataFactory = changeDataFactory;
}
@CanIgnoreReturnValue
public Result execute(
Collection<BatchUpdate> updates, ImmutableList<BatchUpdateListener> listeners, boolean dryrun)
throws UpdateException, RestApiException {
requireNonNull(listeners);
if (updates.isEmpty()) {
return new Result();
}
checkDifferentProject(updates);
try {
List<ListenableFuture<ChangeData>> indexFutures = new ArrayList<>();
List<ChangesHandle> changesHandles = new ArrayList<>(updates.size());
try {
for (BatchUpdate u : updates) {
u.executeUpdateRepo();
}
notifyAfterUpdateRepo(listeners);
for (BatchUpdate u : updates) {
changesHandles.add(u.executeChangeOps(listeners, dryrun));
}
for (ChangesHandle h : changesHandles) {
h.execute();
if (h.requiresReindex()) {
indexFutures.addAll(h.startIndexFutures());
}
}
notifyAfterUpdateRefs(listeners);
notifyAfterUpdateChanges(listeners);
} finally {
for (ChangesHandle h : changesHandles) {
h.close();
}
}
Map<Change.Id, ChangeData> changeDatas =
Futures.allAsList(indexFutures).get().stream()
// filter out null values that were returned for change deletions
.filter(Objects::nonNull)
.collect(toMap(cd -> cd.change().getId(), Function.identity()));
// Fire ref update events only after all mutations are finished, since callers may assume a
// patch set ref being created means the change was created, or a branch advancing meaning
// some changes were closed.
updates.forEach(BatchUpdate::fireRefChangeEvents);
if (!dryrun) {
for (BatchUpdate u : updates) {
u.executePostOps(changeDatas);
}
}
return new Result(changeDatas);
} catch (Exception e) {
wrapAndThrowException(e);
return new Result();
}
}
private static void notifyAfterUpdateRepo(ImmutableList<BatchUpdateListener> listeners)
throws Exception {
for (BatchUpdateListener listener : listeners) {
listener.afterUpdateRepos();
}
}
private static void notifyAfterUpdateRefs(ImmutableList<BatchUpdateListener> listeners)
throws Exception {
for (BatchUpdateListener listener : listeners) {
listener.afterUpdateRefs();
}
}
private static void notifyAfterUpdateChanges(ImmutableList<BatchUpdateListener> listeners)
throws Exception {
for (BatchUpdateListener listener : listeners) {
listener.afterUpdateChanges();
}
}
private static void checkDifferentProject(Collection<BatchUpdate> updates) {
Multiset<Project.NameKey> projectCounts =
updates.stream().map(u -> u.getProject()).collect(toImmutableMultiset());
checkArgument(
projectCounts.entrySet().size() == updates.size(),
"updates must all be for different projects, got: %s",
projectCounts);
}
private static void wrapAndThrowException(Exception e) throws UpdateException, RestApiException {
// Convert common non-REST exception types with user-visible messages to corresponding REST
// exception types.
if (e instanceof InvalidChangeOperationException || e instanceof LimitExceededException) {
throw new ResourceConflictException(e.getMessage(), e);
} else if (e instanceof NoSuchChangeException
|| e instanceof NoSuchRefException
|| e instanceof NoSuchProjectException) {
throw new ResourceNotFoundException(e.getMessage(), e);
} else if (e instanceof CommentsRejectedException) {
// SC_BAD_REQUEST is not ideal because it's not a syntactic error, but there is no better
// status code and it's isolated in monitoring.
throw new BadRequestException(e.getMessage(), e);
}
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);
// Otherwise, wrap in a generic UpdateException, which does not include a user-visible message.
throw new UpdateException(e);
}
}