blob: 4c7e268839d0dba4be6afc54527428c148ad8005 [file] [log] [blame]
// 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.google.gerrit.server.notedb;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.entities.RefNames.REFS_DRAFT_COMMENTS;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
import static org.eclipse.jgit.lib.Constants.EMPTY_TREE_ID;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.git.RefUpdateUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.DeleteZombieComments;
import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.update.context.RefUpdateContext;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.ReceiveCommand;
/**
* This class can be used to clean zombie draft comments from NoteDB.
*
* <p>See {@link DeleteZombieComments} for more info.
*/
public class DeleteZombieCommentsRefs extends DeleteZombieComments<Ref> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
// Number of refs deleted at once in a batch ref-update.
// Log progress after deleting every CHUNK_SIZE refs
private static final int CHUNK_SIZE = 3000;
private final GitRepositoryManager repoManager;
private final AllUsersName allUsers;
@Nullable private final ChangeUpdate.Factory changeUpdateFactory;
@Nullable private final IdentifiedUser.GenericFactory userFactory;
private Repository allUsersRepo;
public interface Factory {
DeleteZombieCommentsRefs create(int cleanupPercentage);
DeleteZombieCommentsRefs create(int cleanupPercentage, boolean dryRun);
}
@AssistedInject
public DeleteZombieCommentsRefs(
@Assisted Integer cleanupPercentage,
GitRepositoryManager repoManager,
AllUsersName allUsers,
DraftCommentsReader draftCommentsReader,
ChangeNotes.Factory changeNotesFactory,
CommentsUtil commentsUtil,
ChangeUpdate.Factory changeUpdateFactory,
IdentifiedUser.GenericFactory userFactory) {
this(
cleanupPercentage,
/* dryRun= */ true,
(msg) -> {},
repoManager,
allUsers,
draftCommentsReader,
changeNotesFactory,
commentsUtil,
changeUpdateFactory,
userFactory);
}
@AssistedInject
public DeleteZombieCommentsRefs(
@Assisted Integer cleanupPercentage,
@Assisted boolean dryRun,
GitRepositoryManager repoManager,
AllUsersName allUsers,
DraftCommentsReader draftCommentsReader,
ChangeNotes.Factory changeNotesFactory,
CommentsUtil commentsUtil,
ChangeUpdate.Factory changeUpdateFactory,
IdentifiedUser.GenericFactory userFactory) {
this(
cleanupPercentage,
dryRun,
(msg) -> {},
repoManager,
allUsers,
draftCommentsReader,
changeNotesFactory,
commentsUtil,
changeUpdateFactory,
userFactory);
}
public DeleteZombieCommentsRefs(
AllUsersName allUsers,
GitRepositoryManager repoManager,
Integer cleanupPercentage,
Consumer<String> uiConsumer) {
this(
cleanupPercentage,
/* dryRun= */ false,
uiConsumer,
repoManager,
allUsers,
null,
null,
null,
null,
null);
}
private DeleteZombieCommentsRefs(
Integer cleanupPercentage,
boolean dryRun,
Consumer<String> uiConsumer,
GitRepositoryManager repoManager,
AllUsersName allUsers,
@Nullable DraftCommentsReader draftCommentsReader,
@Nullable ChangeNotes.Factory changeNotesFactory,
@Nullable CommentsUtil commentsUtil,
@Nullable ChangeUpdate.Factory changeUpdateFactory,
@Nullable IdentifiedUser.GenericFactory userFactory) {
super(
cleanupPercentage,
dryRun,
uiConsumer,
repoManager,
draftCommentsReader,
changeNotesFactory,
commentsUtil);
this.allUsers = allUsers;
this.repoManager = repoManager;
this.changeUpdateFactory = changeUpdateFactory;
this.userFactory = userFactory;
}
@Override
public void setup() throws IOException {
allUsersRepo = repoManager.openRepository(allUsers);
}
@Override
public void close() throws IOException {
allUsersRepo.close();
}
@Override
protected List<Ref> listAllDrafts() throws IOException {
return allUsersRepo.getRefDatabase().getRefsByPrefix(REFS_DRAFT_COMMENTS);
}
@Override
protected List<Ref> listEmptyDrafts() throws IOException {
List<Ref> zombieRefs = filterZombieRefs(allUsersRepo, listAllDrafts());
logInfo(
String.format(
"Found a total of %d zombie draft refs in %s repo.",
zombieRefs.size(), allUsers.get()));
return zombieRefs;
}
/**
* An earlier bug in the deletion of draft comments {@code
* refs/draft-comments/$change_id_short/$change_id/$user_id} caused some draft refs to remain in
* Git and not get deleted. These refs point to an empty tree. We delete such refs.
*/
@Override
protected void deleteEmptyDraftsByKey(Collection<Ref> refs) throws IOException {
long zombieRefsCnt = refs.size();
long deletedRefsCnt = 0;
long startTime = System.currentTimeMillis();
for (List<Ref> refsBatch : Iterables.partition(refs, CHUNK_SIZE)) {
deleteZombieDraftsBatch(refsBatch);
long elapsed = (System.currentTimeMillis() - startTime) / 1000;
deletedRefsCnt += refsBatch.size();
logProgress(deletedRefsCnt, zombieRefsCnt, elapsed);
}
}
@Override
protected void deleteZombieDrafts(ListMultimap<Ref, HumanComment> drafts) throws IOException {
for (Map.Entry<Ref, Collection<HumanComment>> e : drafts.asMap().entrySet()) {
deleteZombieDraftsForChange(
getAccountId(e.getKey()), getChangeNotes(getChangeId(e.getKey())), e.getValue());
}
}
@Override
protected Change.Id getChangeId(Ref ref) {
return Change.Id.fromAllUsersRef(ref.getName());
}
@Override
protected Account.Id getAccountId(Ref ref) {
return Account.Id.fromRef(ref.getName());
}
@Override
protected String loggable(Ref ref) {
return ref.getName();
}
private List<Ref> filterZombieRefs(Repository allUsersRepo, List<Ref> allDraftRefs)
throws IOException {
List<Ref> zombieRefs = new ArrayList<>((int) (allDraftRefs.size() * 0.5));
for (Ref ref : allDraftRefs) {
if (isZombieRef(allUsersRepo, ref)) {
zombieRefs.add(ref);
}
}
return zombieRefs;
}
private boolean isZombieRef(Repository allUsersRepo, Ref ref) throws IOException {
return allUsersRepo.parseCommit(ref.getObjectId()).getTree().getId().equals(EMPTY_TREE_ID);
}
private void logProgress(long deletedRefsCount, long allRefsCount, long elapsed) {
logInfo(
String.format(
"Deleted %d/%d zombie draft refs (%d seconds)",
deletedRefsCount, allRefsCount, elapsed));
}
private void deleteZombieDraftsBatch(Collection<Ref> refsBatch) throws IOException {
try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
ImmutableList<ReceiveCommand> deleteCommands =
refsBatch.stream()
.map(
zombieRef ->
new ReceiveCommand(
zombieRef.getObjectId(), ObjectId.zeroId(), zombieRef.getName()))
.collect(toImmutableList());
BatchRefUpdate bru = allUsersRepo.getRefDatabase().newBatchUpdate();
bru.setAtomic(true);
bru.addCommand(deleteCommands);
RefUpdateUtil.executeChecked(bru, allUsersRepo);
}
}
/**
* Accepts a list of draft (zombie) comments for the same change and delete them by executing a
* {@link ChangeUpdate} on NoteDb. The update is executed using the user account who created this
* draft.
*/
private void deleteZombieDraftsForChange(
Account.Id accountId, ChangeNotes changeNotes, Collection<HumanComment> draftsToDelete)
throws IOException {
if (changeUpdateFactory == null || userFactory == null) {
return;
}
ChangeUpdate changeUpdate =
changeUpdateFactory.create(changeNotes, userFactory.create(accountId), TimeUtil.now());
draftsToDelete.forEach(c -> changeUpdate.deleteComment(c));
changeUpdate.commit();
logger.atInfo().log(
"Deleted zombie draft comments with UUIDs %s",
draftsToDelete.stream().map(d -> d.key.uuid).collect(toImmutableList()));
}
}