Add namespacing support for task quotas
Make it possible to specify quotas for a project namespace.
It utilizes the already existing syntax for `quota` section.
The project name is correctly decoded when the specific task
has it specified it in its `toString()` implementation. If a
project name was not found quotas from the global (`*`) section
are applied.
Change-Id: I81ede1412c5876b9f89f73e42557fc56532aa464
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaFinder.java b/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaFinder.java
index 59921c7..87e21fd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaFinder.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaFinder.java
@@ -17,6 +17,7 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.project.ProjectCache;
import com.google.inject.Inject;
+import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -32,6 +33,10 @@
public QuotaSection firstMatching(Project.NameKey project) {
Config cfg = projectCache.getAllProjects().getConfig("quota.config").get();
+ return firstMatching(cfg, project);
+ }
+
+ public QuotaSection firstMatching(Config cfg, Project.NameKey project) {
Set<String> namespaces = cfg.getSubsections(QuotaSection.QUOTA);
String p = project.get();
for (String n : namespaces) {
@@ -56,8 +61,17 @@
return null;
}
- public QuotaSection getGlobalNamespacedQuota() {
- Config cfg = projectCache.getAllProjects().getConfig("quota.config").get();
+ public QuotaSection getGlobalNamespacedQuota(Config cfg) {
return new QuotaSection(cfg, "*");
}
+
+ public List<QuotaSection> getQuotaNamespaces(Config cfg) {
+ return cfg.getSubsections(QuotaSection.QUOTA).stream()
+ .map(ns -> new QuotaSection(cfg, ns))
+ .toList();
+ }
+
+ public Config getQuotaConfig() {
+ return projectCache.getAllProjects().getConfig("quota.config").get();
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaSection.java b/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaSection.java
index 3ab706d..a1be14b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaSection.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/QuotaSection.java
@@ -22,33 +22,23 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-public class QuotaSection {
+public record QuotaSection(Config cfg, String namespace, String resolvedNamespace) {
private static final Logger log = LoggerFactory.getLogger(QuotaSection.class);
public static final String QUOTA = "quota";
public static final String KEY_MAX_PROJECTS = "maxProjects";
public static final String KEY_MAX_REPO_SIZE = "maxRepoSize";
public static final String KEY_MAX_TOTAL_SIZE = "maxTotalSize";
- private final Config cfg;
- private final String namespace;
- private final Namespace resolvedNamespace;
-
- QuotaSection(Config cfg, String namespace) {
+ public QuotaSection(Config cfg, String namespace) {
this(cfg, namespace, namespace);
}
- QuotaSection(Config cfg, String namespace, String resolvedNamespace) {
- this.cfg = cfg;
- this.namespace = namespace;
- this.resolvedNamespace = new Namespace(resolvedNamespace);
- }
-
public String getNamespace() {
- return resolvedNamespace.get();
+ return resolvedNamespace;
}
public boolean matches(Project.NameKey project) {
- return resolvedNamespace.matches(project);
+ return new Namespace(resolvedNamespace).matches(project);
}
public Integer getMaxProjects() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotas.java b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotas.java
index b59d6f8..73c0552 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotas.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotas.java
@@ -14,14 +14,17 @@
package com.googlesource.gerrit.plugins.quota;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.server.git.WorkQueue;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Singleton;
+import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -29,21 +32,38 @@
public class TaskQuotas implements WorkQueue.TaskParker {
private static final Logger log = LoggerFactory.getLogger(TaskQuotas.class);
private final QuotaFinder quotaFinder;
- private final List<TaskQuota> quotas = new ArrayList<>();
private final Map<Integer, List<TaskQuota>> quotasByTask = new ConcurrentHashMap<>();
+ private final Map<QuotaSection, List<TaskQuota>> quotasByNamespace = new HashMap<>();
+ private final Pattern PROJECT_PATTERN = Pattern.compile("\\s+(.*\\.git)\\s+(\\S+)$");
+ private final Config quotaConfig;
@Inject
public TaskQuotas(QuotaFinder quotaFinder) {
this.quotaFinder = quotaFinder;
+ this.quotaConfig = quotaFinder.getQuotaConfig();
initQuotas();
}
private void initQuotas() {
- quotas.addAll(quotaFinder.getGlobalNamespacedQuota().getAllQuotas());
+ quotasByNamespace.putAll(
+ quotaFinder.getQuotaNamespaces(quotaConfig).stream()
+ .collect(Collectors.toMap(Function.identity(), QuotaSection::getAllQuotas)));
}
@Override
public boolean isReadyToStart(WorkQueue.Task<?> task) {
+ Optional<Project.NameKey> estimatedProject = estimateProject(task);
+ List<TaskQuota> quotas =
+ estimatedProject
+ .map(
+ project -> {
+ return quotasByNamespace.getOrDefault(
+ Optional.ofNullable(quotaFinder.firstMatching(quotaConfig, project))
+ .orElse(quotaFinder.getGlobalNamespacedQuota(quotaConfig)),
+ List.of());
+ })
+ .orElse(List.of());
+
List<TaskQuota> acquiredQuotas = new ArrayList<>();
for (TaskQuota quota : quotas) {
if (quota.isApplicable(task)) {
@@ -79,4 +99,10 @@
Optional.ofNullable(quotasByTask.remove(task.getTaskId()))
.ifPresent(quotas -> quotas.forEach(q -> q.release(task)));
}
+
+ private Optional<Project.NameKey> estimateProject(WorkQueue.Task<?> task) {
+ Matcher matcher = PROJECT_PATTERN.matcher(task.toString());
+
+ return matcher.find() ? Optional.of(Project.nameKey(matcher.group(1))) : Optional.empty();
+ }
}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index e12c482..da693fa 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -244,10 +244,8 @@
-----------
Task quotas provide fine-grained control over queues for administrators.
-Quotas should be specified in the global * section (namespacing support
-has not been implemented yet). Once the defined limit is reached, any
-additional tasks are parked, preventing them from consuming threads and
-allowing other tasks to continue execution.
+Once the defined limit is reached, any additional tasks are parked, preventing
+them from consuming threads and allowing other tasks to continue execution.
The `maxStartForTaskForQueue` setting defines the maximum number of threads
that can be started for a specific task and queue combination. Example: