Add logging for parked tasks
Log task parking events with quota details for better debugging.
Centralize quota logging in quota.log file
Change-Id: If313ec184a8c6a6f603ca431d58a458dbed2e0d0
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/MinStartForQueueQuota.java b/src/main/java/com/googlesource/gerrit/plugins/quota/MinStartForQueueQuota.java
index 33b9a3f..68cdc1d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/MinStartForQueueQuota.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/MinStartForQueueQuota.java
@@ -14,15 +14,15 @@
package com.googlesource.gerrit.plugins.quota;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class MinStartForQueueQuota {
public static final Logger log = LoggerFactory.getLogger(MinStartForQueueQuota.class);
+ public static final String KEY = "minStartForQueue";
// 10 SSH-Interactive-Worker
public static final Pattern CONFIG_PATTERN = Pattern.compile("(\\d+)\\s+(.+)");
@@ -35,16 +35,17 @@
}
if (matcher.matches()) {
- int limit = Integer.parseInt(matcher.group(1));
+ int reservation = Integer.parseInt(matcher.group(1));
String queue = matcher.group(2);
QueueManager.registerReservation(
queue,
new QueueManager.Reservation(
- limit,
+ reservation,
task -> {
return task.getQueueName().equalsIgnoreCase(queue)
&& TaskQuotas.estimateProject(task).map(qs::matches).orElse(false);
- }));
+ },
+ qs.getNamespace()));
} else {
log.error("Invalid configuration entry [{}]", cfg);
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/Module.java b/src/main/java/com/googlesource/gerrit/plugins/quota/Module.java
index 781787a..045801f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/Module.java
@@ -96,6 +96,9 @@
bind(LifecycleListener.class)
.annotatedWith(UniqueAnnotations.create())
.to(PublisherScheduler.class);
+ bind(LifecycleListener.class)
+ .annotatedWith(UniqueAnnotations.create())
+ .to(TaskQuotaLogFile.class);
DynamicSet.bind(binder(), UploadValidationListener.class).to(RateLimitUploadListener.class);
bindConstant()
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/ParkedQuotaTransitionLogger.java b/src/main/java/com/googlesource/gerrit/plugins/quota/ParkedQuotaTransitionLogger.java
new file mode 100644
index 0000000..bb40a19
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/ParkedQuotaTransitionLogger.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2025 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.git.WorkQueue.Task;
+import static com.googlesource.gerrit.plugins.quota.QueueManager.Queue;
+import static com.googlesource.gerrit.plugins.quota.QueueManager.QueueInfo;
+import static com.googlesource.gerrit.plugins.quota.QueueManager.Reservation;
+
+import com.google.gerrit.util.logging.NamedFluentLogger;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class ParkedQuotaTransitionLogger {
+ protected static final NamedFluentLogger quotaLog =
+ NamedFluentLogger.forName(TaskQuotaLogFile.NAME);
+ protected static final Map<Integer, TaskQuota> prevParkingQuotaByTaskId =
+ new ConcurrentHashMap<>();
+ protected static final TaskQuota CANNOT_SATISFY_RESERVATIONS_QUOTA =
+ new TaskQuota() {
+ @Override
+ public boolean isApplicable(Task<?> task) {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public boolean isReadyToStart(Task<?> task) {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public void onStop(Task<?> task) {
+ throw new IllegalStateException();
+ }
+ };
+
+ /** Logs only if the reason for parked changes from the previous parking event. */
+ public static void logTaskWithEnforcedQuota(Task<?> t, TaskQuota q) {
+ if (shouldLog(t, q)) {
+ quotaLog.atInfo().log("Task [%s] parked due to quota rule [%s]", t, q);
+ }
+ }
+
+ public static void logTaskWithNoSatisfyingReservation(Task<?> t) {
+ if (!shouldLog(t, CANNOT_SATISFY_RESERVATIONS_QUOTA)) {
+ return;
+ }
+
+ QueueInfo queueInfo = QueueManager.infoByQueue.get(Queue.fromKey(t.getQueueName()));
+ if (queueInfo != null) {
+ queueInfo.reservations.stream()
+ .filter(r -> r.matches(t))
+ .findFirst()
+ .ifPresentOrElse(
+ reservation -> {
+ quotaLog.atInfo().log(
+ "Task [%s] parked because there are no spare unreserved threads in queue [%s], "
+ + "and there are insufficient reserved threads for the %s namespace",
+ t, t.getQueueName(), reservation.namespace());
+ },
+ () -> {
+ quotaLog.atInfo().log(
+ "Task [%s] parked because there are no spare unreserved threads in queue [%s]",
+ t, t.getQueueName());
+ });
+ }
+ }
+
+ public static boolean shouldLog(Task<?> t, TaskQuota q) {
+ return prevParkingQuotaByTaskId.put(t.getTaskId(), q) != q;
+ }
+
+ public static void clear(Task<?> t) {
+ prevParkingQuotaByTaskId.remove(t.getTaskId());
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/QueueManager.java b/src/main/java/com/googlesource/gerrit/plugins/quota/QueueManager.java
index 4b0010d..dc6d658 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/QueueManager.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/QueueManager.java
@@ -15,19 +15,18 @@
package com.googlesource.gerrit.plugins.quota;
import com.google.gerrit.server.git.WorkQueue;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
+import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Predicate;
-
-import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class QueueManager {
public static final Logger log = LoggerFactory.getLogger(QueueManager.class);
@@ -101,7 +100,8 @@
}
}
- public record Reservation(int reservedCapacity, Predicate<WorkQueue.Task<?>> taskMatcher) {
+ public record Reservation(
+ int reservedCapacity, Predicate<WorkQueue.Task<?>> taskMatcher, String namespace) {
public boolean matches(WorkQueue.Task<?> task) {
return taskMatcher.test(task);
}
@@ -164,7 +164,8 @@
qName,
reservation.reservedCapacity(),
capacityToReserve);
- queueInfo.addReservation(new Reservation(capacityToReserve, reservation.taskMatcher));
+ queueInfo.addReservation(
+ new Reservation(capacityToReserve, reservation.taskMatcher, reservation.namespace()));
return;
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/SoftMaxPerUserForQueue.java b/src/main/java/com/googlesource/gerrit/plugins/quota/SoftMaxPerUserForQueue.java
index de7c630..19fa018 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/SoftMaxPerUserForQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/SoftMaxPerUserForQueue.java
@@ -24,14 +24,17 @@
import java.util.regex.Pattern;
public class SoftMaxPerUserForQueue implements TaskQuota {
+ public static final String KEY = "softMaxStartPerUserForQueue";
public static final Pattern CONFIG_PATTERN =
Pattern.compile("(\\d+)\\s+(" + String.join("|", QueueManager.Queue.keys()) + ")");
+ private final QuotaSection quotaSection;
private final int softMax;
private final QueueManager.Queue queue;
private final ConcurrentHashMap<String, Integer> taskStartedCountByUser =
new ConcurrentHashMap<>();
- public SoftMaxPerUserForQueue(int softMax, String queueName) {
+ public SoftMaxPerUserForQueue(QuotaSection quotaSection, int softMax, String queueName) {
+ this.quotaSection = quotaSection;
this.softMax = softMax;
this.queue = QueueManager.Queue.fromKey(queueName);
}
@@ -79,7 +82,14 @@
Matcher matcher = CONFIG_PATTERN.matcher(cfg);
return matcher.find()
? Optional.of(
- new SoftMaxPerUserForQueue(Integer.parseInt(matcher.group(1)), matcher.group(2)))
+ new SoftMaxPerUserForQueue(qs, Integer.parseInt(matcher.group(1)), matcher.group(2)))
: Optional.empty();
}
+
+ @Override
+ public String toString() {
+ return KEY
+ + ": softMax [%d], queue [%s], namespace [%s]"
+ .formatted(softMax, queue.getName(), quotaSection.getNamespace());
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaForTask.java b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaForTask.java
index 2d15abe..8375c3b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaForTask.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaForTask.java
@@ -21,7 +21,7 @@
public abstract class TaskQuotaForTask extends TaskQuotaWithPermits {
protected static final Map<String, Set<String>> SUPPORTED_TASKS_BY_GROUP =
Map.of("uploadpack", Set.of("git-upload-pack"), "receivepack", Set.of("git-receive-pack"));
- private final String taskGroup;
+ public final String taskGroup;
public TaskQuotaForTask(String taskGroup, int permits) {
super(permits);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaForTaskForQueue.java b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaForTaskForQueue.java
index ac57a00..535edbf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaForTaskForQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaForTaskForQueue.java
@@ -23,13 +23,17 @@
public class TaskQuotaForTaskForQueue extends TaskQuotaForTask {
public static final Logger log = LoggerFactory.getLogger(TaskQuotaForTaskForQueue.class);
+ public static final String KEY = "maxStartForTaskForQueue";
public static final Pattern CONFIG_PATTERN =
Pattern.compile(
"(\\d+)\\s+(" + String.join("|", SUPPORTED_TASKS_BY_GROUP.keySet()) + ")\\s+(.+)");
- private final String queueName;
+ public final String queueName;
+ protected final QuotaSection quotaSection;
- public TaskQuotaForTaskForQueue(String queueName, String taskGroup, int maxStart) {
+ public TaskQuotaForTaskForQueue(
+ QuotaSection quotaSection, String queueName, String taskGroup, int maxStart) {
super(taskGroup, maxStart);
+ this.quotaSection = quotaSection;
this.queueName = queueName;
}
@@ -43,10 +47,17 @@
if (matcher.matches()) {
return Optional.of(
new TaskQuotaForTaskForQueue(
- matcher.group(3), matcher.group(2), Integer.parseInt(matcher.group(1))));
+ qs, matcher.group(3), matcher.group(2), Integer.parseInt(matcher.group(1))));
} else {
log.error("Invalid configuration entry [{}]", cfg);
return Optional.empty();
}
}
+
+ @Override
+ public String toString() {
+ return KEY
+ + ": task [%s], queue [%s], permits [%d], namespace [%s]"
+ .formatted(taskGroup, queueName, maxPermits, quotaSection.getNamespace());
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaForTaskForQueueForUser.java b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaForTaskForQueueForUser.java
index 7afa870..751559e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaForTaskForQueueForUser.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaForTaskForQueueForUser.java
@@ -23,6 +23,7 @@
public class TaskQuotaForTaskForQueueForUser extends TaskQuotaForTaskForQueue {
public static final Logger log = LoggerFactory.getLogger(TaskQuotaForTaskForQueueForUser.class);
+ public static final String KEY = "maxStartForTaskForUserForQueue";
public static final Pattern CONFIG_PATTERN =
Pattern.compile(
"(\\d+)\\s+("
@@ -33,8 +34,8 @@
private final String user;
public TaskQuotaForTaskForQueueForUser(
- String queueName, String user, String taskGroup, int maxStart) {
- super(queueName, taskGroup, maxStart);
+ QuotaSection quotaSection, String queueName, String user, String taskGroup, int maxStart) {
+ super(quotaSection, queueName, taskGroup, maxStart);
this.user = user;
}
@@ -49,6 +50,7 @@
if (matcher.matches()) {
return Optional.of(
new TaskQuotaForTaskForQueueForUser(
+ qs,
matcher.group(4),
matcher.group(3),
matcher.group(2),
@@ -58,4 +60,11 @@
return Optional.empty();
}
}
+
+ @Override
+ public String toString() {
+ return KEY
+ + ": task [%s], queue [%s], user [%s], permits [%d], namespace [%s]"
+ .formatted(taskGroup, queueName, user, maxPermits, quotaSection.getNamespace());
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaKeys.java b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaKeys.java
index ff0a4cb..65da2a5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaKeys.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaKeys.java
@@ -18,13 +18,13 @@
import java.util.function.BiFunction;
public enum TaskQuotaKeys {
- MAX_START_FOR_TASK_FOR_QUEUE("maxStartForTaskForQueue", TaskQuotaForTaskForQueue::build),
- MIN_START_FOR_TASK_FOR_QUEUE("minStartForQueue", MinStartForQueueQuota::build),
+ MAX_START_FOR_TASK_FOR_QUEUE(TaskQuotaForTaskForQueue.KEY, TaskQuotaForTaskForQueue::build),
+ MIN_START_FOR_TASK_FOR_QUEUE(MinStartForQueueQuota.KEY, MinStartForQueueQuota::build),
MAX_START_FOR_TASK_FOR_USER_FOR_QUEUE(
- "maxStartForTaskForUserForQueue", TaskQuotaForTaskForQueueForUser::build),
+ TaskQuotaForTaskForQueueForUser.KEY, TaskQuotaForTaskForQueueForUser::build),
MAX_START_PER_USER_FOR_TASK_FOR_QUEUE(
- "maxStartPerUserForTaskForQueue", TaskQuotaPerUserForTaskForQueue::build),
- SOFT_MAX_START_FOR_QUEUE_PER_USER("softMaxStartPerUserForQueue", SoftMaxPerUserForQueue::build);
+ TaskQuotaPerUserForTaskForQueue.KEY, TaskQuotaPerUserForTaskForQueue::build),
+ SOFT_MAX_START_FOR_QUEUE_PER_USER(SoftMaxPerUserForQueue.KEY, SoftMaxPerUserForQueue::build);
public final String key;
public final BiFunction<QuotaSection, String, Optional<TaskQuota>> processor;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaLogFile.java b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaLogFile.java
new file mode 100644
index 0000000..66c43ff
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaLogFile.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2025 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 com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.server.util.PluginLogFile;
+import com.google.gerrit.server.util.SystemLog;
+import com.google.inject.Inject;
+import org.apache.log4j.PatternLayout;
+
+public class TaskQuotaLogFile extends PluginLogFile {
+ protected static String NAME = "quota_log";
+
+ @Inject
+ public TaskQuotaLogFile(SystemLog systemLog, ServerInformation serverInfo) {
+ super(systemLog, serverInfo, NAME, new PatternLayout("[%d] %m%n"));
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaPerUserForTaskForQueue.java b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaPerUserForTaskForQueue.java
index a7299a2..dc2ee64 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaPerUserForTaskForQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaPerUserForTaskForQueue.java
@@ -22,10 +22,12 @@
public class TaskQuotaPerUserForTaskForQueue extends TaskQuotaForTaskForQueue {
public static final Logger log = LoggerFactory.getLogger(TaskQuotaPerUserForTaskForQueue.class);
+ public static final String KEY = "maxStartPerUserForTaskForQueue";
private final PerUserTaskQuota perUserTaskQuota;
- public TaskQuotaPerUserForTaskForQueue(String queue, String taskGroup, int maxStart) {
- super(queue, taskGroup, maxStart);
+ public TaskQuotaPerUserForTaskForQueue(
+ QuotaSection quotaSection, String queue, String taskGroup, int maxStart) {
+ super(quotaSection, queue, taskGroup, maxStart);
perUserTaskQuota = new PerUserTaskQuota(maxStart);
}
@@ -44,10 +46,17 @@
if (matcher.matches()) {
return Optional.of(
new TaskQuotaPerUserForTaskForQueue(
- matcher.group(3), matcher.group(2), Integer.parseInt(matcher.group(1))));
+ qs, matcher.group(3), matcher.group(2), Integer.parseInt(matcher.group(1))));
} else {
log.error("Invalid configuration entry [{}]", cfg);
return Optional.empty();
}
}
+
+ @Override
+ public String toString() {
+ return KEY
+ + ": task [%s], queue [%s], permits [%d], namespace [%s]"
+ .formatted(taskGroup, queueName, maxPermits, quotaSection.getNamespace());
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaWithPermits.java b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaWithPermits.java
index ea414ca..ff7fe80 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaWithPermits.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotaWithPermits.java
@@ -5,9 +5,11 @@
public abstract class TaskQuotaWithPermits implements TaskQuota {
protected final AtomicInteger permits;
+ protected final int maxPermits;
public TaskQuotaWithPermits(int maxPermits) {
this.permits = new AtomicInteger(maxPermits);
+ this.maxPermits = maxPermits;
}
public boolean isReadyToStart(WorkQueue.Task<?> task) {
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 63a8bc1..5359bf5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotas.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/quota/TaskQuotas.java
@@ -28,12 +28,9 @@
import javax.inject.Inject;
import javax.inject.Singleton;
import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
@Singleton
public class TaskQuotas implements WorkQueue.TaskParker {
- private static final Logger log = LoggerFactory.getLogger(TaskQuotas.class);
private final QuotaFinder quotaFinder;
private final Map<Integer, List<TaskQuota>> quotasByTask = new ConcurrentHashMap<>();
private final Map<QuotaSection, List<TaskQuota>> quotasByNamespace = new HashMap<>();
@@ -83,8 +80,8 @@
@Override
public boolean isReadyToStart(WorkQueue.Task<?> task) {
- QueueManager.Queue queue = QueueManager.Queue.fromKey(task.getQueueName());
if (!QueueManager.acquire(task)) {
+ ParkedQuotaTransitionLogger.logTaskWithNoSatisfyingReservation(task);
return false;
}
@@ -105,7 +102,7 @@
for (TaskQuota quota : applicableQuotas) {
if (quota.isApplicable(task)) {
if (!quota.isReadyToStart(task)) {
- log.debug("Task [{}] will be parked due task quota rules", task);
+ ParkedQuotaTransitionLogger.logTaskWithEnforcedQuota(task, quota);
QueueManager.release(task);
acquiredQuotas.forEach(q -> q.onStop(task));
return false;
@@ -123,6 +120,7 @@
@Override
public void onNotReadyToStart(WorkQueue.Task<?> task) {
QueueManager.release(task);
+ ParkedQuotaTransitionLogger.clear(task);
Optional.ofNullable(quotasByTask.remove(task.getTaskId()))
.ifPresent(quotas -> quotas.forEach(q -> q.onStop(task)));
}
@@ -133,6 +131,7 @@
@Override
public void onStop(WorkQueue.Task<?> task) {
QueueManager.release(task);
+ ParkedQuotaTransitionLogger.clear(task);
Optional.ofNullable(quotasByTask.remove(task.getTaskId()))
.ifPresent(quotas -> quotas.forEach(q -> q.onStop(task)));
}