All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.approval.RecursiveApprovalCopier Maven / Gradle / Ivy

There is a newer version: 3.11.0-rc3
Show newest version
// Copyright (C) 2022 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.approval;

import static com.google.common.collect.ImmutableList.toImmutableList;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.git.RefUpdateUtil;
import com.google.gerrit.server.FanOutExecutor;
import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.internal.storage.file.RefDirectory;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.ReceiveCommand;

public class RecursiveApprovalCopier {
  private static final FluentLogger logger = FluentLogger.forEnclosingClass();

  private static final int SLICE_MAX_REFS = 1000;

  private final BatchUpdate.Factory batchUpdateFactory;
  private final GitRepositoryManager repositoryManager;
  private final InternalUser.Factory internalUserFactory;
  private final ApprovalsUtil approvalsUtil;
  private final ChangeNotes.Factory changeNotesFactory;
  private final ListeningExecutorService executor;

  private final ConcurrentHashMap> pendingRefUpdates =
      new ConcurrentHashMap<>();

  private volatile boolean failedForAtLeastOneProject;

  private final AtomicInteger totalCopyApprovalsTasks = new AtomicInteger();
  private final AtomicInteger finishedCopyApprovalsTasks = new AtomicInteger();

  private final AtomicInteger totalRefUpdates = new AtomicInteger();
  private final AtomicInteger finishedRefUpdates = new AtomicInteger();

  @Inject
  public RecursiveApprovalCopier(
      BatchUpdate.Factory batchUpdateFactory,
      GitRepositoryManager repositoryManager,
      InternalUser.Factory internalUserFactory,
      ApprovalsUtil approvalsUtil,
      ChangeNotes.Factory changeNotesFactory,
      @FanOutExecutor ExecutorService executor) {
    this.batchUpdateFactory = batchUpdateFactory;
    this.repositoryManager = repositoryManager;
    this.internalUserFactory = internalUserFactory;
    this.approvalsUtil = approvalsUtil;
    this.changeNotesFactory = changeNotesFactory;
    this.executor = MoreExecutors.listeningDecorator(executor);
  }

  /**
   * This method assumes it is used as a standalone program having exclusive access to the Git
   * repositories. Therefore, it will (safely) skip locking of the loose refs when performing batch
   * ref-updates.
   */
  public void persistStandalone()
      throws RepositoryNotFoundException, IOException, InterruptedException, ExecutionException {
    persist(repositoryManager.list(), null, false);

    if (failedForAtLeastOneProject) {
      throw new RuntimeException("There were errors, check the logs for details");
    }
  }

  public void persist(Project.NameKey project, @Nullable Consumer labelsCopiedListener)
      throws IOException, RepositoryNotFoundException, InterruptedException, ExecutionException {
    persist(ImmutableList.of(project), labelsCopiedListener, true);
  }

  private void persist(
      Collection projects,
      @Nullable Consumer labelsCopiedListener,
      boolean shouldLockLooseRefs)
      throws InterruptedException, ExecutionException, RepositoryNotFoundException, IOException {
    List> copyApprovalsTasks = new LinkedList<>();
    for (Project.NameKey project : projects) {
      copyApprovalsTasks.addAll(submitCopyApprovalsTasks(project, labelsCopiedListener));
    }
    Futures.successfulAsList(copyApprovalsTasks).get();

    List> batchRefUpdateTasks =
        submitBatchRefUpdateTasks(shouldLockLooseRefs);
    Futures.successfulAsList(batchRefUpdateTasks).get();
  }

  private List> submitCopyApprovalsTasks(
      Project.NameKey project, @Nullable Consumer labelsCopiedListener)
      throws RepositoryNotFoundException, IOException {
    List> futures = new LinkedList<>();
    try (Repository repository = repositoryManager.openRepository(project)) {
      ImmutableList allMetaRefs =
          repository.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES).stream()
              .filter(r -> r.getName().endsWith(RefNames.META_SUFFIX))
              .collect(toImmutableList());

      totalCopyApprovalsTasks.addAndGet(allMetaRefs.size());

      for (List slice : Lists.partition(allMetaRefs, SLICE_MAX_REFS)) {
        futures.add(
            executor.submit(
                () -> {
                  copyApprovalsForSlice(project, slice, labelsCopiedListener);
                  return null;
                }));
      }
    }
    return futures;
  }

  private void copyApprovalsForSlice(
      Project.NameKey project, List slice, @Nullable Consumer labelsCopiedListener)
      throws Exception {
    try {
      copyApprovalsForSlice(project, slice, labelsCopiedListener, false);
    } catch (Exception e) {
      failedForAtLeastOneProject = true;
      logger.atSevere().withCause(e).log(
          "Error in a slice of project %s, will retry and skip corrupt meta-refs", project);
      copyApprovalsForSlice(project, slice, labelsCopiedListener, true);
    }
    logProgress();
  }

  private void copyApprovalsForSlice(
      Project.NameKey project,
      List slice,
      @Nullable Consumer labelsCopiedListener,
      boolean checkForCorruptMetaRefs)
      throws Exception {
    logger.atInfo().log("copy-approvals for a slice of %s project", project);
    try (BatchUpdate bu =
        batchUpdateFactory.create(project, internalUserFactory.create(), TimeUtil.nowTs())) {
      for (Ref metaRef : slice) {
        Change.Id changeId = Change.Id.fromRef(metaRef.getName());
        if (checkForCorruptMetaRefs && isCorrupt(project, changeId)) {
          logger.atSevere().log("skipping corrupt meta-ref %s", metaRef.getName());
          continue;
        }
        bu.addOp(
            changeId,
            new RecursiveApprovalCopier.PersistCopiedVotesOp(approvalsUtil, labelsCopiedListener));
      }

      BatchRefUpdate bru = bu.prepareRefUpdates();
      if (bru != null) {
        List cmds = bru.getCommands();
        pendingRefUpdates.compute(
            project,
            (p, u) -> {
              if (u == null) {
                return new LinkedList<>(cmds);
              }
              u.addAll(cmds);
              return u;
            });
        totalRefUpdates.addAndGet(cmds.size());
      }

      finishedCopyApprovalsTasks.addAndGet(slice.size());
    }
  }

  private List> submitBatchRefUpdateTasks(boolean shouldLockLooseRefs) {
    logger.atInfo().log("submitting batch ref-update tasks");
    List> futures = new LinkedList<>();
    for (Map.Entry> e : pendingRefUpdates.entrySet()) {
      Project.NameKey project = e.getKey();
      List updates = e.getValue();
      futures.add(
          executor.submit(
              () -> {
                executeRefUpdates(project, updates, shouldLockLooseRefs);
                return null;
              }));
    }
    return futures;
  }

  private void executeRefUpdates(
      Project.NameKey project, List updates, boolean shouldLockLooseRefs)
      throws RepositoryNotFoundException, IOException {
    logger.atInfo().log(
        "executing batch ref-update for project %s, size %d", project, updates.size());
    try (Repository repository = repositoryManager.openRepository(project)) {
      RefDatabase refdb = repository.getRefDatabase();
      BatchRefUpdate bu;
      if (refdb instanceof RefDirectory) {
        bu = ((RefDirectory) refdb).newBatchUpdate(shouldLockLooseRefs);
      } else {
        bu = refdb.newBatchUpdate();
      }
      bu.addCommand(updates);
      RefUpdateUtil.executeChecked(bu, repository);

      finishedRefUpdates.addAndGet(updates.size());
      logProgress();
    }
  }

  private boolean isCorrupt(Project.NameKey project, Change.Id changeId) {
    Change c = ChangeNotes.Factory.newChange(project, changeId);
    try {
      changeNotesFactory.createForBatchUpdate(c, true);
      return false;
    } catch (Exception e) {
      logger.atSevere().withCause(e).log(e.getMessage());
      return true;
    }
  }

  public void persist(Change change) throws UpdateException, RestApiException {
    Project.NameKey project = change.getProject();
    try (BatchUpdate bu =
        batchUpdateFactory.create(project, internalUserFactory.create(), TimeUtil.nowTs())) {
      Change.Id changeId = change.getId();
      bu.addOp(changeId, new PersistCopiedVotesOp(approvalsUtil, null));
      bu.execute();
    }
  }

  private void logProgress() {
    logger.atInfo().log(
        "copy-approvals tasks done: %d/%d, ref-update tasks done: %d/%d",
        finishedCopyApprovalsTasks.get(),
        totalCopyApprovalsTasks.get(),
        finishedRefUpdates.get(),
        totalRefUpdates.get());
  }

  private static class PersistCopiedVotesOp implements BatchUpdateOp {
    private final ApprovalsUtil approvalsUtil;
    private final Consumer listener;

    PersistCopiedVotesOp(
        ApprovalsUtil approvalsUtil, @Nullable Consumer labelsCopiedListener) {
      this.approvalsUtil = approvalsUtil;
      this.listener = labelsCopiedListener;
    }

    @Override
    public boolean updateChange(ChangeContext ctx) throws IOException {
      Change change = ctx.getChange();
      ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId(), change.getLastUpdatedOn());
      approvalsUtil.persistCopiedApprovals(
          ctx.getNotes(),
          ctx.getNotes().getCurrentPatchSet(),
          ctx.getRevWalk(),
          ctx.getRepoView().getConfig(),
          update);

      boolean labelsCopied = update.hasCopiedApprovals();

      if (labelsCopied && listener != null) {
        listener.accept(change);
      }

      return labelsCopied;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy