blob: a20ebed17081fdff6568c85d85d5e29d055b4760 [file] [log] [blame]
// Copyright (C) 2014 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.googlesource.gerrit.plugins.quota;
import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP;
import static com.google.gerrit.server.quota.QuotaResponse.error;
import static com.google.gerrit.server.quota.QuotaResponse.noOp;
import static com.google.gerrit.server.quota.QuotaResponse.ok;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Ordering;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.quota.QuotaEnforcer;
import com.google.gerrit.server.quota.QuotaRequestContext;
import com.google.gerrit.server.quota.QuotaResponse;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.lang.mutable.MutableLong;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.internal.storage.file.GC;
import org.eclipse.jgit.internal.storage.file.GC.RepoStatistics;
import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class MaxRepositorySizeQuota implements QuotaEnforcer, RepoSizeCache {
private static final Logger log = LoggerFactory.getLogger(MaxRepositorySizeQuota.class);
static final String REPO_SIZE_CACHE = "repo_size";
static Module module() {
return new CacheModule() {
@Override
protected void configure() {
persist(REPO_SIZE_CACHE, Project.NameKey.class, AtomicLong.class)
.loader(Loader.class)
.expireAfterWrite(Duration.ofDays(1));
bind(RepoSizeCache.class).to(MaxRepositorySizeQuota.class);
}
};
}
protected final LoadingCache<Project.NameKey, AtomicLong> cache;
private final QuotaFinder quotaFinder;
private final ProjectCache projectCache;
@Inject
protected MaxRepositorySizeQuota(
QuotaFinder quotaFinder,
@Named(REPO_SIZE_CACHE) LoadingCache<Project.NameKey, AtomicLong> cache,
ProjectCache projectCache) {
this.quotaFinder = quotaFinder;
this.cache = cache;
this.projectCache = projectCache;
}
protected Optional<Long> getMaxPackSize(Project.NameKey project) {
QuotaSection quotaSection = quotaFinder.firstMatching(project);
if (quotaSection == null) {
return Optional.empty();
}
Long maxRepoSize = quotaSection.getMaxRepoSize();
Long maxTotalSize = quotaSection.getMaxTotalSize();
if (maxRepoSize == null && maxTotalSize == null) {
return Optional.empty();
}
try {
Long maxPackSize1 = null;
if (maxRepoSize != null) {
maxPackSize1 = Math.max(0, maxRepoSize - cache.get(project).get());
}
Long maxPackSize2 = null;
if (maxTotalSize != null) {
long totalSize = 0;
for (Project.NameKey p : projectCache.all()) {
if (quotaSection.matches(p)) {
totalSize += cache.get(p).get();
}
}
maxPackSize2 = Math.max(0, maxTotalSize - totalSize);
}
return Optional.ofNullable(
Ordering.<Long>natural().nullsLast().min(maxPackSize1, maxPackSize2));
} catch (ExecutionException e) {
log.warn("Couldn't calculate maxPackSize for {}", project, e);
return Optional.empty();
}
}
@Singleton
static class Loader extends CacheLoader<Project.NameKey, AtomicLong> {
private final GitRepositoryManager gitManager;
private final boolean useGitObjectCount;
@Inject
Loader(
GitRepositoryManager gitManager, PluginConfigFactory cfg, @PluginName String pluginName) {
this.gitManager = gitManager;
this.useGitObjectCount =
cfg.getFromGerritConfig(pluginName).getBoolean("useGitObjectCount", false);
}
@Override
public AtomicLong load(Project.NameKey project) throws IOException {
try (Repository git = gitManager.openRepository(project)) {
if (useGitObjectCount) {
return new AtomicLong(getDiskUsageByGitObjectCount(git));
}
return new AtomicLong(getDiskUsage(git.getDirectory()));
}
}
private static long getDiskUsage(File dir) throws IOException {
final MutableLong size = new MutableLong();
Files.walkFileTree(
dir.toPath(),
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs)
throws IOException {
if (attrs.isRegularFile()) {
size.add(attrs.size());
}
return FileVisitResult.CONTINUE;
}
});
return size.longValue();
}
private long getDiskUsageByGitObjectCount(Repository repo) throws IOException {
RepoStatistics stats = new GC((FileRepository) repo).getStatistics();
return stats.sizeOfLooseObjects + stats.sizeOfPackedObjects;
}
}
@Override
public long get(Project.NameKey p) {
try {
return cache.get(p).get();
} catch (ExecutionException e) {
log.warn("Error creating RepoSizeEvent", e);
return 0;
}
}
@Override
public void evict(Project.NameKey p) {
cache.invalidate(p);
}
@Override
public void set(Project.NameKey p, long size) {
try {
cache.get(p).set(size);
} catch (ExecutionException e) {
log.warn("Error setting the size of project {}", p, e);
}
}
@Override
public QuotaResponse dryRun(String quotaGroup, QuotaRequestContext ctx, long numTokens) {
if (!REPOSITORY_SIZE_GROUP.equals(quotaGroup)) {
return noOp();
}
return ctx.project()
.flatMap(p -> getMaxPackSize(p))
.map(v -> requestQuota(ctx, numTokens, v, false))
.orElse(noOp());
}
@Override
public void refill(String quotaGroup, QuotaRequestContext ctx, long numTokens) {
if (!REPOSITORY_SIZE_GROUP.equals(quotaGroup)) {
return;
}
ctx.project()
.ifPresent(
p -> {
try {
cache.get(p).getAndUpdate(current -> current > numTokens ? current - numTokens : 0);
} catch (ExecutionException e) {
log.warn("Refilling [{}] bytes for repository {} failed", numTokens, p, e);
}
});
}
@Override
public QuotaResponse requestTokens(String quotaGroup, QuotaRequestContext ctx, long numTokens) {
if (!REPOSITORY_SIZE_GROUP.equals(quotaGroup)) {
return noOp();
}
return ctx.project()
.flatMap(p -> getMaxPackSize(p))
.map(v -> requestQuota(ctx, numTokens, v, true))
.orElse(noOp());
}
@Override
public QuotaResponse availableTokens(String quotaGroup, QuotaRequestContext ctx) {
if (!REPOSITORY_SIZE_GROUP.equals(quotaGroup)) {
return noOp();
}
return ctx.project().flatMap(p -> getMaxPackSize(p)).map(v -> ok(v)).orElse(noOp());
}
private QuotaResponse requestQuota(
QuotaRequestContext ctx, long requested, Long availableSpace, boolean deduct) {
Project.NameKey r = ctx.project().get();
if (availableSpace >= requested) {
if (deduct) {
try {
cache.get(r).getAndAdd(requested);
} catch (ExecutionException e) {
String msg = String.format("Quota request [%d] failed for repository %s", requested, r);
log.warn(msg, e);
return error(msg);
}
return ok();
}
}
return error(
String.format(
"Requested space [%d] is bigger then available [%d] for repository %s",
requested, availableSpace, r));
}
}