diff --git a/pom.xml b/pom.xml
index 84f54aa3..d55aa63f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,7 +26,7 @@ THE SOFTWARE.
org.jenkins-ci.plugins
plugin
- 1.424
+ 1.532.2
throttle-concurrents
@@ -113,6 +113,12 @@ THE SOFTWARE.
2.0.1
jar
+
+ org.jenkins-ci.plugins
+ matrix-project
+ 1.0
+ jar
+
diff --git a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleCategoriesCountersCache.java b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleCategoriesCountersCache.java
new file mode 100644
index 00000000..a981670c
--- /dev/null
+++ b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleCategoriesCountersCache.java
@@ -0,0 +1,230 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2015 CloudBees Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.throttleconcurrents;
+
+import hudson.model.AbstractBuild;
+import hudson.model.JobProperty;
+import hudson.model.queue.CauseOfBlockage;
+import hudson.plugins.throttleconcurrents.util.ThrottleHelper;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import jenkins.model.Jenkins;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Stores additional cache for {@link ThrottleQueueTaskDispatcher}.
+ * The cache is supposed to be used from {@link RunListener}.
+ * @author Oleg Nenashev
+ * @since TODO
+ */
+public class ThrottleCategoriesCountersCache {
+
+ private static final ThrottleCategoriesCountersCache INSTANCE = new ThrottleCategoriesCountersCache();
+
+ public static ThrottleCategoriesCountersCache getInstance() {
+ return INSTANCE;
+ }
+
+ private final Map categoriesMap = new HashMap();
+
+ private @CheckForNull Map categoriesHash = null;
+
+ private CategoryEntry getCategoryEntry(String categoryName) {
+ CategoryEntry res = categoriesMap.get(categoryName);
+ if (res == null) {
+ res = new CategoryEntry(categoryName);
+ categoriesMap.put(categoryName, res);
+ }
+ return res;
+ }
+
+ private Map getCategoriesHash() {
+ if (categoriesHash == null) {
+ categoriesHash = new HashMap();
+ refreshCategoriesCache();
+ }
+ return categoriesHash;
+ }
+
+ public synchronized @CheckForNull CauseOfBlockage canTake(Collection categoryNames, String nodeName) {
+ // Global check
+ for (String categoryName : categoryNames) {
+ int maxValue = getCategoriesHash().get(categoryName).getMaxConcurrentTotal();
+ int totalBuildsCount = getTotalBuildsNumber(categoryName);
+ if (totalBuildsCount >= maxValue) {
+ return CauseOfBlockage.fromMessage(Messages._ThrottleQueueTaskDispatcher_MaxCapacityTotal(totalBuildsCount));
+ }
+ }
+
+ // Per-node check
+ for (String categoryName : categoryNames) {
+ int maxValue = getCategoriesHash().get(categoryName).getMaxConcurrentPerNode();
+ int totalBuildsCountOnNode = getNodeBuildsNumber(categoryName, nodeName);
+ if (totalBuildsCountOnNode >= maxValue) {
+ return CauseOfBlockage.fromMessage(Messages._ThrottleQueueTaskDispatcher_MaxCapacityTotal(totalBuildsCountOnNode));
+ }
+ }
+ return null;
+ }
+
+ public synchronized @CheckForNull CauseOfBlockage canRun(Collection categoryNames) {
+ for (String categoryName : categoryNames) {
+ int maxValue = getCategoriesHash().get(categoryName).getMaxConcurrentTotal();
+ int totalBuildsCount = getTotalBuildsNumber(categoryName);
+ if (totalBuildsCount >= maxValue) {
+ return CauseOfBlockage.fromMessage(Messages._ThrottleQueueTaskDispatcher_MaxCapacityTotal(totalBuildsCount));
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Updates global cache of category properties
+ */
+ public synchronized void refreshCategoriesCache() {
+ Jenkins j = Jenkins.getInstance();
+ if (j == null) {
+ throw new IllegalStateException("Jenkins must be started");
+ }
+
+ ThrottleJobProperty.DescriptorImpl d = j.getDescriptorByType(ThrottleJobProperty.DescriptorImpl.class);
+ if (d == null) {
+ throw new IllegalStateException("Cannot get the Throttle Property descriptor");
+ }
+
+ List categories = d.getCategories();
+ getCategoriesHash().clear();
+ for (ThrottleJobProperty.ThrottleCategory category : categories) {
+ categoriesHash.put(category.getCategoryName(), category);
+ }
+ }
+
+ private int getTotalBuildsNumber(String categoryName) {
+ return getCategoryEntry(categoryName).globalCount.count;
+ }
+
+ private int getNodeBuildsNumber(String categoryName, String nodeName) {
+ return getCategoryEntry(categoryName).getEntry(nodeName).count;
+ }
+
+ //TODO: recalculate cache on jobs update
+
+ public synchronized void fireStarted(AbstractBuild build) {
+ JobProperty jp = ThrottleHelper.getThrottleJobProperty(build.getProject());
+ if (jp == null) {
+ return;
+ }
+ final ThrottleJobProperty tjp = (ThrottleJobProperty)jp;
+ if ( !ThrottleHelper.shouldBeThrottled(build.getParent(), tjp) ) {
+ return;
+ }
+
+ // Update the categories cache
+ if (tjp.getThrottleOption().equals("category")) {
+ final List categories = tjp.getCategories();
+ if (categories != null && !categories.isEmpty()) {
+ for (String categoryName : categories) {
+ getCategoryEntry(categoryName).fireStarted(build);
+ }
+ }
+ }
+
+ }
+
+ public synchronized void fireCompleted(AbstractBuild build) {
+ //TODO: remove code dup
+ JobProperty jp = ThrottleHelper.getThrottleJobProperty(build.getProject());
+ if (jp == null) {
+ return;
+ }
+ final ThrottleJobProperty tjp = (ThrottleJobProperty)jp;
+ if ( !ThrottleHelper.shouldBeThrottled(build.getParent(), tjp) ) {
+ return;
+ }
+
+ // Update the categories cache
+ if (tjp.getThrottleOption().equals("category")) {
+ final List categories = tjp.getCategories();
+ if (categories != null && !categories.isEmpty()) {
+ for (String categoryName : categories) {
+ getCategoryEntry(categoryName).fireCompleted(build);
+ }
+ }
+ }
+ }
+
+ private static class CategoryEntry {
+ private final String categoryName;
+ private final CounterEntry globalCount = new CounterEntry();
+ private final Map nodeCounts = new HashMap();
+
+ public CategoryEntry(String categoryName) {
+ this.categoryName = null;
+ }
+
+ public String getCategoryName() {
+ return categoryName;
+ }
+
+ void fireStarted(AbstractBuild build) {
+ globalCount.inc();
+ getEntry(build.getBuiltOnStr()).inc();
+ }
+
+ void fireCompleted(AbstractBuild build) {
+ globalCount.dec();
+ getEntry(build.getBuiltOnStr()).dec();
+ }
+
+ private @Nonnull CounterEntry getEntry(String nodeName) {
+ String key = StringUtils.isEmpty(nodeName) ? "(master)" : nodeName;
+ CounterEntry res = nodeCounts.get(key);
+ if (res == null) {
+ res = new CounterEntry();
+ nodeCounts.put(key, res);
+ }
+ return res;
+ }
+ }
+
+ private static class CounterEntry {
+ int count=0;
+
+ public int getCount() {
+ return count;
+ }
+
+ public void inc() {
+ count++;
+ }
+
+ public void dec() {
+ count--;
+ }
+ }
+}
diff --git a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleJobProperty.java b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleJobProperty.java
index f2cba356..db98b109 100644
--- a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleJobProperty.java
+++ b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleJobProperty.java
@@ -249,6 +249,7 @@ public boolean isMatrixProject(Job job) {
@Override
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
req.bindJSON(this, formData);
+ ThrottleCategoriesCountersCache.getInstance().refreshCategoriesCache();
save();
return true;
}
diff --git a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java
index eb1abece..3c94d853 100644
--- a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java
+++ b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java
@@ -24,8 +24,61 @@
@Extension
public class ThrottleQueueTaskDispatcher extends QueueTaskDispatcher {
+ /**
+ * Fast and unreliable implementation of can take.
+ * It's designed to be a first-stage check in
+ * {@link ThrottleQueueTaskDispatcher#canTake(hudson.model.Node, hudson.model.Queue.Task)}.
+ * @param node
+ * @param task
+ * @param tjp Non-null job property of the task
+ * @return {@link CauseOfBlockage} if Jenkins cannot take the fob.
+ * null means that Jenkins may be able to take the job, but
+ * a full check is required.
+ */
+ private @CheckForNull CauseOfBlockage fastCanTake(Node node, AbstractProject prj, ThrottleJobProperty tjp) {
+ // We presume that the throttling eligibility has been checked before
+ // The only supported type is AbstractProject
+
+ if (tjp.getThrottleOption().equals("category")) {
+ final CauseOfBlockage categoriesCheckResult =
+ ThrottleCategoriesCountersCache.getInstance().canTake(
+ tjp.getCategories(), node.getNodeName());
+ if (categoriesCheckResult != null) {
+ return categoriesCheckResult;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Fast and unreliable implementation of canRun.
+ * It's designed to be a first-stage check in
+ * {@link ThrottleQueueTaskDispatcher#canTake(hudson.model.Node, hudson.model.Queue.Task)}.
+ * @param node
+ * @param task
+ * @param tjp Non-null job property of the task
+ * @return {@link CauseOfBlockage} if Jenkins cannot take the fob.
+ * null means that Jenkins may be able to take the job, but
+ * a full check is required.
+ */
+ private @CheckForNull CauseOfBlockage fastCanRun(AbstractProject prj, ThrottleJobProperty tjp) {
+ // We presume that the throttling eligibility has been checked before
+ // The only supported type is AbstractProject
+ if (tjp.getThrottleOption().equals("category")) {
+ final CauseOfBlockage categoriesCheckResult =
+ ThrottleCategoriesCountersCache.getInstance().canRun(tjp.getCategories());
+ if (categoriesCheckResult != null) {
+ return categoriesCheckResult;
+ }
+ }
+
+ return null;
+ }
+
@Override
- public CauseOfBlockage canTake(Node node, Task task) {
+ public CauseOfBlockage canTake(Node node, Queue.BuildableItem item) {
+ Task task =item.task;
ThrottleJobProperty tjp = getThrottleJobProperty(task);
@@ -34,8 +87,14 @@ public CauseOfBlockage canTake(Node node, Task task) {
return null;
}
+ // Perform fast inaccurate check and exit if it vetoes the task
+ final CauseOfBlockage fastCheckResult = fastCanTake(node, (AbstractProject) task, tjp);
+ if (fastCheckResult != null) {
+ return fastCheckResult;
+ }
+
if (tjp!=null && tjp.getThrottleEnabled()) {
- CauseOfBlockage cause = canRun(task, tjp);
+ CauseOfBlockage cause = canRun(task, tjp); // TODO: ? isPending() is not required for canTake()
if (cause != null) return cause;
if (tjp.getThrottleOption().equals("project")) {
@@ -91,9 +150,23 @@ else if (tjp.getThrottleOption().equals("category")) {
// @Override on jenkins 4.127+ , but still compatible with 1.399
public CauseOfBlockage canRun(Queue.Item item) {
ThrottleJobProperty tjp = getThrottleJobProperty(item.task);
- if (tjp!=null && tjp.getThrottleEnabled()) {
- return canRun(item.task, tjp);
+ if (!shouldBeThrottled(item.task, tjp)) {
+ return null;
}
+
+ // Perform fast inaccurate check and exit if it vetoes the task
+ final CauseOfBlockage fastCheckResult = fastCanRun((AbstractProject)item.task, tjp);
+ if (fastCheckResult != null) {
+ return fastCheckResult;
+ }
+
+ // TODO: fast check of nodes
+
+ // We do not check everything else and rely on CanTake
+ // It has been done to make canRun as a spot-check only
+ if (tjp!=null && tjp.getThrottleEnabled()) {
+ return canRun(item, tjp);
+ }
return null;
}
@@ -123,17 +196,25 @@ private boolean shouldBeThrottled(@Nonnull Task task, @CheckForNull ThrottleJobP
return true;
}
+ @Deprecated
public CauseOfBlockage canRun(Task task, ThrottleJobProperty tjp) {
- if (!shouldBeThrottled(task, tjp)) {
- return null;
- }
- if (Hudson.getInstance().getQueue().isPending(task)) {
+ return canRun(Hudson.getInstance().getQueue().getItem(task), tjp);
+ }
+
+ private boolean isPending(Queue.Item item) {
+ return (item instanceof Queue.BuildableItem)
+ ? ((Queue.BuildableItem)item).isPending() : false;
+ }
+
+ public CauseOfBlockage canRun(Queue.Item item, ThrottleJobProperty tjp) {
+
+ if (isPending(item)) {
return CauseOfBlockage.fromMessage(Messages._ThrottleQueueTaskDispatcher_BuildPending());
}
if (tjp.getThrottleOption().equals("project")) {
if (tjp.getMaxConcurrentTotal().intValue() > 0) {
int maxConcurrentTotal = tjp.getMaxConcurrentTotal().intValue();
- int totalRunCount = buildsOfProjectOnAllNodes(task);
+ int totalRunCount = buildsOfProjectOnAllNodes(item.task);
if (totalRunCount >= maxConcurrentTotal) {
return CauseOfBlockage.fromMessage(Messages._ThrottleQueueTaskDispatcher_MaxCapacityTotal(totalRunCount));
diff --git a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleRunListener.java b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleRunListener.java
new file mode 100644
index 00000000..369d1e31
--- /dev/null
+++ b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleRunListener.java
@@ -0,0 +1,53 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2015 CloudBees Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.throttleconcurrents;
+
+import hudson.Extension;
+import hudson.model.AbstractBuild;
+import hudson.model.Run;
+import hudson.model.TaskListener;
+import hudson.model.listeners.RunListener;
+
+/**
+ * Performs monitoring of {@link Run}s to fill {@link ThrottleCategoriesCountersCache}.
+ * @author Oleg Nenashev
+ * @since TODO
+ */
+@Extension
+public class ThrottleRunListener extends RunListener {
+
+ @Override
+ public void onStarted(Run r, TaskListener listener) {
+ if (r instanceof AbstractBuild) {
+ ThrottleCategoriesCountersCache.getInstance().fireStarted((AbstractBuild)r);
+ }
+ }
+
+ @Override
+ public void onCompleted(Run r, TaskListener listener) {
+ if (r instanceof AbstractBuild) {
+ ThrottleCategoriesCountersCache.getInstance().fireCompleted((AbstractBuild)r);
+ }
+ }
+}
diff --git a/src/main/java/hudson/plugins/throttleconcurrents/util/ThrottleHelper.java b/src/main/java/hudson/plugins/throttleconcurrents/util/ThrottleHelper.java
new file mode 100644
index 00000000..b02066d1
--- /dev/null
+++ b/src/main/java/hudson/plugins/throttleconcurrents/util/ThrottleHelper.java
@@ -0,0 +1,83 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2015 CloudBees Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.plugins.throttleconcurrents.util;
+
+import hudson.matrix.MatrixConfiguration;
+import hudson.matrix.MatrixProject;
+import hudson.model.AbstractProject;
+import hudson.model.Queue;
+import hudson.plugins.throttleconcurrents.ThrottleJobProperty;
+import hudson.plugins.throttleconcurrents.ThrottleMatrixProjectOptions;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+
+/**
+ * A set of helper methods.
+ * @author Oleg Nenashev
+ * @since TODO
+ */
+public class ThrottleHelper {
+
+ @CheckForNull
+ public static ThrottleJobProperty getThrottleJobProperty(Queue.Task task) {
+ if (task instanceof AbstractProject) {
+ return getThrottleJobProperty((AbstractProject,?>) task);
+ }
+ return null;
+ }
+
+ @CheckForNull
+ public static ThrottleJobProperty getThrottleJobProperty(AbstractProject,?> prj) {
+ if (prj instanceof MatrixConfiguration) {
+ prj = (AbstractProject,?>)((MatrixConfiguration)prj).getParent();
+ }
+ ThrottleJobProperty tjp = prj.getProperty(ThrottleJobProperty.class);
+ return tjp;
+ }
+
+ /**
+ * Checks if the task should be throttled
+ * @param Type of task to be checked. Designed for {@link Queue.Task} and {@link AbstractProject}.
+ * @param task Task to be checked
+ * @param tjp Property of the task being evaluated
+ * @return
+ */
+ public static boolean shouldBeThrottled(@Nonnull TTask task, @CheckForNull ThrottleJobProperty tjp) {
+ if (tjp == null) return false;
+ if (!tjp.getThrottleEnabled()) return false;
+
+ // Handle matrix options
+ ThrottleMatrixProjectOptions matrixOptions = tjp.getMatrixOptions();
+ if (matrixOptions == null) matrixOptions = ThrottleMatrixProjectOptions.DEFAULT;
+ if (!matrixOptions.isThrottleMatrixConfigurations() && task instanceof MatrixConfiguration) {
+ return false;
+ }
+ if (!matrixOptions.isThrottleMatrixBuilds()&& task instanceof MatrixProject) {
+ return false;
+ }
+
+ // Allow throttling by default
+ return true;
+ }
+}