| // Copyright (C) 2012 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.git; |
| |
| import com.google.common.collect.Sets; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.common.data.GarbageCollectionResult; |
| import com.google.gerrit.common.data.GarbageCollectionResult.GcError; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.extensions.events.GarbageCollectorListener; |
| import com.google.gerrit.server.config.GcConfig; |
| import com.google.gerrit.server.extensions.events.AbstractNoNotifyEvent; |
| import com.google.gerrit.server.plugincontext.PluginSetContext; |
| import com.google.inject.Inject; |
| import java.io.PrintWriter; |
| import java.util.List; |
| import java.util.Properties; |
| import java.util.Set; |
| import org.eclipse.jgit.api.GarbageCollectCommand; |
| import org.eclipse.jgit.api.Git; |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.lib.ConfigConstants; |
| import org.eclipse.jgit.lib.NullProgressMonitor; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.lib.TextProgressMonitor; |
| import org.eclipse.jgit.storage.pack.PackConfig; |
| |
| /** Serial execution of GC on a list of repositories. */ |
| public class GarbageCollection { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private final GitRepositoryManager repoManager; |
| private final GarbageCollectionQueue gcQueue; |
| private final GcConfig gcConfig; |
| private final PluginSetContext<GarbageCollectorListener> listeners; |
| |
| public interface Factory { |
| GarbageCollection create(); |
| } |
| |
| @Inject |
| GarbageCollection( |
| GitRepositoryManager repoManager, |
| GarbageCollectionQueue gcQueue, |
| GcConfig config, |
| PluginSetContext<GarbageCollectorListener> listeners) { |
| this.repoManager = repoManager; |
| this.gcQueue = gcQueue; |
| this.gcConfig = config; |
| this.listeners = listeners; |
| } |
| |
| @CanIgnoreReturnValue |
| public GarbageCollectionResult run(List<Project.NameKey> projectNames) { |
| return run(projectNames, null); |
| } |
| |
| @CanIgnoreReturnValue |
| public GarbageCollectionResult run(List<Project.NameKey> projectNames, PrintWriter writer) { |
| return run(projectNames, gcConfig.isAggressive(), writer); |
| } |
| |
| /** Runs GC on the given projects, serially. Progress is written to writer if non-null. */ |
| @CanIgnoreReturnValue |
| public GarbageCollectionResult run( |
| List<Project.NameKey> projectNames, boolean aggressive, @Nullable PrintWriter writer) { |
| GarbageCollectionResult result = new GarbageCollectionResult(); |
| Set<Project.NameKey> projectsToGc = gcQueue.addAll(projectNames); |
| for (Project.NameKey projectName : |
| Sets.difference(Sets.newHashSet(projectNames), projectsToGc)) { |
| result.addError(new GcError(GcError.Type.GC_ALREADY_SCHEDULED, projectName)); |
| } |
| for (Project.NameKey p : projectsToGc) { |
| try (Repository repo = repoManager.openRepository(p)) { |
| logGcConfiguration(p, repo, aggressive); |
| print(writer, "collecting garbage for \"" + p + "\":\n"); |
| GarbageCollectCommand gc = |
| Git.wrap( |
| repo instanceof DelegateRepository |
| ? ((DelegateRepository) repo).delegate() |
| : repo) |
| .gc(); |
| gc.setAggressive(aggressive); |
| logGcInfo(p, "before:", gc.getStatistics()); |
| gc.setProgressMonitor( |
| writer != null ? new TextProgressMonitor(writer) : NullProgressMonitor.INSTANCE); |
| Properties statistics = gc.call(); |
| logGcInfo(p, "after: ", statistics); |
| print(writer, "done.\n\n"); |
| fire(p, statistics); |
| } catch (RepositoryNotFoundException e) { |
| logGcError(writer, p, e); |
| result.addError(new GcError(GcError.Type.REPOSITORY_NOT_FOUND, p)); |
| } catch (Exception e) { |
| logGcError(writer, p, e); |
| result.addError(new GcError(GcError.Type.GC_FAILED, p)); |
| } finally { |
| gcQueue.gcFinished(p); |
| } |
| } |
| return result; |
| } |
| |
| private void fire(Project.NameKey p, Properties statistics) { |
| if (!listeners.iterator().hasNext()) { |
| return; |
| } |
| Event event = new Event(p, statistics); |
| listeners.runEach(l -> l.onGarbageCollected(event)); |
| } |
| |
| private static void logGcInfo(Project.NameKey projectName, String msg) { |
| logGcInfo(projectName, msg, null); |
| } |
| |
| private static void logGcInfo(Project.NameKey projectName, String msg, Properties statistics) { |
| StringBuilder b = new StringBuilder(); |
| b.append("[").append(projectName.get()).append("] "); |
| b.append(msg); |
| if (statistics != null) { |
| b.append(" "); |
| String s = statistics.toString(); |
| if (s.startsWith("{") && s.endsWith("}")) { |
| s = s.substring(1, s.length() - 1); |
| } |
| b.append(s); |
| } |
| logger.atInfo().log("%s", b); |
| } |
| |
| private static void logGcConfiguration( |
| Project.NameKey projectName, Repository repo, boolean aggressive) { |
| StringBuilder b = new StringBuilder(); |
| Config cfg = repo.getConfig(); |
| b.append("gc.aggressive=").append(aggressive).append("; "); |
| b.append(formatConfigValues(cfg, ConfigConstants.CONFIG_GC_SECTION, null)); |
| for (String subsection : cfg.getSubsections(ConfigConstants.CONFIG_GC_SECTION)) { |
| b.append(formatConfigValues(cfg, ConfigConstants.CONFIG_GC_SECTION, subsection)); |
| } |
| if (b.length() == 0) { |
| b.append("no set"); |
| } |
| |
| logGcInfo(projectName, "gc config: " + b.toString()); |
| logGcInfo(projectName, "pack config: " + new PackConfig(repo).toString()); |
| } |
| |
| private static String formatConfigValues(Config config, String section, String subsection) { |
| StringBuilder b = new StringBuilder(); |
| Set<String> names = config.getNames(section, subsection); |
| for (String name : names) { |
| String value = config.getString(section, subsection, name); |
| b.append(section); |
| if (subsection != null) { |
| b.append(".").append(subsection); |
| } |
| b.append("."); |
| b.append(name).append("=").append(value); |
| b.append("; "); |
| } |
| return b.toString(); |
| } |
| |
| private static void logGcError(PrintWriter writer, Project.NameKey projectName, Exception e) { |
| print(writer, "failed.\n\n"); |
| StringBuilder b = new StringBuilder(); |
| b.append("[").append(projectName.get()).append("]"); |
| logger.atSevere().withCause(e).log("%s", b); |
| } |
| |
| private static void print(PrintWriter writer, String message) { |
| if (writer != null) { |
| writer.print(message); |
| } |
| } |
| |
| private static class Event extends AbstractNoNotifyEvent |
| implements GarbageCollectorListener.Event { |
| private final Project.NameKey p; |
| private final Properties statistics; |
| |
| Event(Project.NameKey p, Properties statistics) { |
| this.p = p; |
| this.statistics = statistics; |
| } |
| |
| @Override |
| public String getProjectName() { |
| return p.get(); |
| } |
| |
| @Override |
| public Properties getStatistics() { |
| return statistics; |
| } |
| } |
| } |