diff --git a/README b/README index 9b892021..80d92f79 100644 --- a/README +++ b/README @@ -1,3 +1,16 @@ Work in progress on a plugin to dynamically allocate labels in order to allow for throttling the number of concurrent builds of a project allowed to run on a given node at one time. + +Contributing +------------ + +### Run / Debug cycle: + +Execute `mvn hpi:run`. This will compile the plugin and launch a Jenkins instance on http://localhost:8080 + +### Create Package (.hpi): + +Execute `mvn hpi:hpi`. This will create `throttle-concurrent.hpi` in the `target/` directory + +For other mvn targets, see: https://jenkins-ci.org/maven-hpi-plugin/ diff --git a/pom.xml b/pom.xml index 84f54aa3..6c9fd81f 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ THE SOFTWARE. org.jenkins-ci.plugins plugin - 1.424 + 1.509.4 throttle-concurrents diff --git a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleJobProperty.java b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleJobProperty.java index 98a8c193..b8b27255 100644 --- a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleJobProperty.java +++ b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleJobProperty.java @@ -16,6 +16,7 @@ import hudson.matrix.MatrixBuild; import hudson.matrix.MatrixProject; +import java.util.Arrays; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -36,27 +37,33 @@ public class ThrottleJobProperty extends JobProperty> { // Moving category to categories, to support, well, multiple categories per job. @Deprecated transient String category; - + private Integer maxConcurrentPerNode; private Integer maxConcurrentTotal; private List categories; private boolean throttleEnabled; private String throttleOption; + private boolean limitOneJobWithMatchingParams; private transient boolean throttleConfiguration; private @CheckForNull ThrottleMatrixProjectOptions matrixOptions; + private String paramsToUseForLimit; + private transient List paramsToCompare; + /** * Store a config version so we're able to migrate config on various * functionality upgrades. */ private Long configVersion; - + @DataBoundConstructor public ThrottleJobProperty(Integer maxConcurrentPerNode, Integer maxConcurrentTotal, List categories, boolean throttleEnabled, String throttleOption, + boolean limitOneJobWithMatchingParams, + String paramsToUseForLimit, @CheckForNull ThrottleMatrixProjectOptions matrixOptions ) { this.maxConcurrentPerNode = maxConcurrentPerNode == null ? 0 : maxConcurrentPerNode; @@ -66,9 +73,22 @@ public ThrottleJobProperty(Integer maxConcurrentPerNode, new CopyOnWriteArrayList(categories); this.throttleEnabled = throttleEnabled; this.throttleOption = throttleOption; + this.limitOneJobWithMatchingParams = limitOneJobWithMatchingParams; this.matrixOptions = matrixOptions; - } + this.paramsToUseForLimit = paramsToUseForLimit; + if ((this.paramsToUseForLimit != null)) { + if ((this.paramsToUseForLimit.length() > 0)) { + this.paramsToCompare = Arrays.asList(this.paramsToUseForLimit.split(",")); + } + else { + this.paramsToCompare = new ArrayList(); + } + } + else { + this.paramsToCompare = new ArrayList(); + } + } /** * Migrates deprecated/obsolete data @@ -95,6 +115,7 @@ public Object readResolve() { maxConcurrentTotal = 0; } } + configVersion = 1L; // Handle the throttleConfiguration in custom builds (not released) @@ -126,28 +147,36 @@ public boolean getThrottleEnabled() { return throttleEnabled; } + public boolean isLimitOneJobWithMatchingParams() { + return limitOneJobWithMatchingParams; + } + public String getThrottleOption() { return throttleOption; } - + public List getCategories() { return categories; } - + public Integer getMaxConcurrentPerNode() { if (maxConcurrentPerNode == null) maxConcurrentPerNode = 0; - + return maxConcurrentPerNode; } public Integer getMaxConcurrentTotal() { if (maxConcurrentTotal == null) maxConcurrentTotal = 0; - + return maxConcurrentTotal; } + public String getParamsToUseForLimit() { + return paramsToUseForLimit; + } + @CheckForNull public ThrottleMatrixProjectOptions getMatrixOptions() { return matrixOptions; @@ -171,6 +200,23 @@ public boolean isThrottleMatrixConfigurations() { : ThrottleMatrixProjectOptions.DEFAULT.isThrottleMatrixConfigurations(); } + public List getParamsToCompare() { + if (paramsToCompare == null) { + if ((paramsToUseForLimit != null)) { + if ((paramsToUseForLimit.length() > 0)) { + paramsToCompare = Arrays.asList(paramsToUseForLimit.split(",")); + } + else { + paramsToCompare = new ArrayList(); + } + } + else { + paramsToCompare = new ArrayList(); + } + } + return paramsToCompare; + } + static List getCategoryTasks(String category) { assert category != null && !category.equals(""); List categoryTasks = new ArrayList(); @@ -198,6 +244,7 @@ static List getCategoryTasks(String category) { } return categoryTasks; } + private static Item getItem(ItemGroup group, String name) { if (group instanceof Jenkins) { return ((Jenkins) group).getItemMap().get(name); @@ -205,11 +252,11 @@ private static Item getItem(ItemGroup group, String name) { return group.getItem(name); } } - + @Extension public static final class DescriptorImpl extends JobPropertyDescriptor { private List categories; - + /** Map from category names, to properties including that category. */ private transient Map> propertiesByCategory = new HashMap>(); @@ -235,7 +282,7 @@ public DescriptorImpl() { public String getDisplayName() { return "Throttle Concurrent Builds"; } - + @Override @SuppressWarnings("rawtypes") public boolean isApplicable(Class jobType) { @@ -279,10 +326,9 @@ public FormValidation doCheckMaxConcurrentTotal(@QueryParameter String value) { return checkNullOrInt(value); } - public ThrottleCategory getCategoryByName(String categoryName) { ThrottleCategory category = null; - + for (ThrottleCategory tc : categories) { if (tc.getCategoryName().equals(categoryName)) { category = tc; @@ -295,7 +341,7 @@ public ThrottleCategory getCategoryByName(String categoryName) { public void setCategories(List categories) { this.categories = new CopyOnWriteArrayList(categories); } - + public List getCategories() { if (categories == null) { categories = new CopyOnWriteArrayList(); @@ -308,14 +354,14 @@ public ListBoxModel doFillCategoryItems() { ListBoxModel m = new ListBoxModel(); m.add("(none)", ""); - + for (ThrottleCategory tc : getCategories()) { m.add(tc.getCategoryName()); } return m; } - + } public static final class ThrottleCategory extends AbstractDescribableImpl { @@ -335,18 +381,18 @@ public ThrottleCategory(String categoryName, this.nodeLabeledPairs = nodeLabeledPairs == null ? new ArrayList() : nodeLabeledPairs; } - + public Integer getMaxConcurrentPerNode() { if (maxConcurrentPerNode == null) maxConcurrentPerNode = 0; - + return maxConcurrentPerNode; } - + public Integer getMaxConcurrentTotal() { if (maxConcurrentTotal == null) maxConcurrentTotal = 0; - + return maxConcurrentTotal; } @@ -360,7 +406,7 @@ public List getNodeLabeledPairs() { return nodeLabeledPairs; } - + @Extension public static class DescriptorImpl extends Descriptor { @Override diff --git a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java index a0440387..8f22de38 100644 --- a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java +++ b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java @@ -3,6 +3,8 @@ import hudson.Extension; import hudson.matrix.MatrixConfiguration; import hudson.matrix.MatrixProject; +import hudson.model.AbstractProject; +import hudson.model.ParameterValue; import hudson.model.Computer; import hudson.model.Executor; import hudson.model.Hudson; @@ -10,10 +12,15 @@ import hudson.model.Node; import hudson.model.Queue; import hudson.model.Queue.Task; +import hudson.model.queue.WorkUnit; import hudson.model.labels.LabelAtom; import hudson.model.queue.CauseOfBlockage; import hudson.model.queue.QueueTaskDispatcher; +import hudson.model.Action; +import hudson.model.ParametersAction; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Set; import java.util.logging.Level; @@ -37,7 +44,6 @@ public CauseOfBlockage canTake(Node node, Task task) { if (tjp!=null && tjp.getThrottleEnabled()) { CauseOfBlockage cause = canRun(task, tjp); if (cause != null) return cause; - if (tjp.getThrottleOption().equals("project")) { if (tjp.getMaxConcurrentPerNode().intValue() > 0) { int maxConcurrentPerNode = tjp.getMaxConcurrentPerNode().intValue(); @@ -92,11 +98,14 @@ else if (tjp.getThrottleOption().equals("category")) { public CauseOfBlockage canRun(Queue.Item item) { ThrottleJobProperty tjp = getThrottleJobProperty(item.task); if (tjp!=null && tjp.getThrottleEnabled()) { + if (tjp.isLimitOneJobWithMatchingParams() && isAnotherBuildWithSameParametersRunningOnAnyNode(item)) { + return CauseOfBlockage.fromMessage(Messages._ThrottleQueueTaskDispatcher_OnlyOneWithMatchingParameters()); + } return canRun(item.task, tjp); } return null; } - + @Nonnull private ThrottleMatrixProjectOptions getMatrixOptions(Task task) { ThrottleJobProperty tjp = getThrottleJobProperty(task); @@ -178,6 +187,106 @@ else if (tjp.getThrottleOption().equals("category")) { return null; } + private boolean isAnotherBuildWithSameParametersRunningOnAnyNode(Queue.Item item) { + if (isAnotherBuildWithSameParametersRunningOnNode(Hudson.getInstance(), item)) { + return true; + } + + for (Node node : Hudson.getInstance().getNodes()) { + if (isAnotherBuildWithSameParametersRunningOnNode(node, item)) { + return true; + } + } + return false; + } + + private boolean isAnotherBuildWithSameParametersRunningOnNode(Node node, Queue.Item item) { + ThrottleJobProperty tjp = getThrottleJobProperty(item.task); + Computer computer = node.toComputer(); + List paramsToCompare = tjp.getParamsToCompare(); + List itemParams = getParametersFromQueueItem(item); + + if (paramsToCompare.size() > 0) { + itemParams = doFilterParams(paramsToCompare, itemParams); + } + + if (computer != null) { + for (Executor exec : computer.getExecutors()) { + if (item != null && item.task != null) { + // TODO: refactor into a nameEquals helper method + if (exec.getCurrentExecutable() != null && + exec.getCurrentExecutable().getParent() != null && + exec.getCurrentExecutable().getParent().getOwnerTask() != null && + exec.getCurrentExecutable().getParent().getOwnerTask().getName().equals(item.task.getDisplayName())) { + List executingUnitParams = getParametersFromWorkUnit(exec.getCurrentWorkUnit()); + executingUnitParams = doFilterParams(paramsToCompare, executingUnitParams); + + if (executingUnitParams.containsAll(itemParams)) { + LOGGER.log(Level.FINE, "build (" + exec.getCurrentWorkUnit() + + ") with identical parameters (" + + executingUnitParams + ") is already running."); + return true; + } + } + } + } + } + return false; + } + + /** + * Filter job parameters to only include parameters used for throttling + * @param String array containing a list of params to be used for throttling. + * @param List of ParameterValue objects of the job. + * @return List with only the desired params in the list. + */ + private List doFilterParams(List params, List OriginalParams) { + if (params.isEmpty()) { + return OriginalParams; + } + + List newParams = new ArrayList(); + + for (ParameterValue p : OriginalParams) { + if (params.contains(p.getName())) { + newParams.add(p); + } + } + return newParams; + } + + public List getParametersFromWorkUnit(WorkUnit unit) { + List paramsList = new ArrayList(); + + if (unit != null && unit.context != null && unit.context.actions != null) { + List actions = unit.context.actions; + for (Action action : actions) { + if (action instanceof ParametersAction) { + ParametersAction params = (ParametersAction) action; + if (params != null) { + paramsList = params.getParameters(); + } + } + } + } + return paramsList; + } + + public List getParametersFromQueueItem(Queue.Item item) { + List paramsList; + + ParametersAction params = item.getAction(ParametersAction.class); + if (params != null) { + paramsList = params.getParameters(); + } + else + { + paramsList = new ArrayList(); + } + return paramsList; + } + + @CheckForNull private ThrottleJobProperty getThrottleJobProperty(Task task) { if (task instanceof Job) { @@ -204,9 +313,9 @@ private int buildsOfProjectOnNode(Node node, Task task) { Computer computer = node.toComputer(); if (computer != null) { //Not all nodes are certain to become computers, like nodes with 0 executors. // Count flyweight tasks that might not consume an actual executor. - for (Executor e : computer.getOneOffExecutors()) { - runCount += buildsOnExecutor(task, e); - } + for (Executor e : computer.getOneOffExecutors()) { + runCount += buildsOnExecutor(task, e); + } for (Executor e : computer.getExecutors()) { runCount += buildsOnExecutor(task, e); diff --git a/src/main/resources/hudson/plugins/throttleconcurrents/Messages.properties b/src/main/resources/hudson/plugins/throttleconcurrents/Messages.properties index 949b92e3..98caeb7d 100644 --- a/src/main/resources/hudson/plugins/throttleconcurrents/Messages.properties +++ b/src/main/resources/hudson/plugins/throttleconcurrents/Messages.properties @@ -1,5 +1,6 @@ ThrottleQueueTaskDispatcher.MaxCapacityOnNode=Already running {0} builds on node ThrottleQueueTaskDispatcher.MaxCapacityTotal=Already running {0} builds across all nodes ThrottleQueueTaskDispatcher.BuildPending=A build is pending launch +ThrottleQueueTaskDispatcher.OnlyOneWithMatchingParameters=A build with matching parameters is already running ThrottleMatrixProjectOptions.DisplayName=Additional options for Matrix projects \ No newline at end of file diff --git a/src/main/resources/hudson/plugins/throttleconcurrents/ThrottleJobProperty/config.jelly b/src/main/resources/hudson/plugins/throttleconcurrents/ThrottleJobProperty/config.jelly index e18c2cf3..042b5b48 100644 --- a/src/main/resources/hudson/plugins/throttleconcurrents/ThrottleJobProperty/config.jelly +++ b/src/main/resources/hudson/plugins/throttleconcurrents/ThrottleJobProperty/config.jelly @@ -20,6 +20,18 @@ field="maxConcurrentPerNode"> + + + + + + + diff --git a/src/main/resources/hudson/plugins/throttleconcurrents/ThrottleJobProperty/help-limitOneJobWithMatchingParams.html b/src/main/resources/hudson/plugins/throttleconcurrents/ThrottleJobProperty/help-limitOneJobWithMatchingParams.html new file mode 100644 index 00000000..973eab0c --- /dev/null +++ b/src/main/resources/hudson/plugins/throttleconcurrents/ThrottleJobProperty/help-limitOneJobWithMatchingParams.html @@ -0,0 +1,6 @@ +
+

If this box is checked, only one instance of the job with matching parameters will be allowed to run at a given time. + Other instances of this job with different parameters will be allowed to run concurrently.

+

Optionally, provide a comma-separated list of parameters to use when comparing jobs. If blank, all parameters + must match for a job to be limited to one running instance.

+
diff --git a/src/test/java/hudson/plugins/throttleconcurrents/ThrottleJobPropertyTest.java b/src/test/java/hudson/plugins/throttleconcurrents/ThrottleJobPropertyTest.java index ec532db8..09a7cd4f 100644 --- a/src/test/java/hudson/plugins/throttleconcurrents/ThrottleJobPropertyTest.java +++ b/src/test/java/hudson/plugins/throttleconcurrents/ThrottleJobPropertyTest.java @@ -22,11 +22,11 @@ public void testGetCategoryProjects() throws Exception { String alpha = "alpha", beta = "beta", gamma = "gamma"; // category names FreeStyleProject p1 = createFreeStyleProject("p1"); FreeStyleProject p2 = createFreeStyleProject("p2"); - p2.addProperty(new ThrottleJobProperty(1, 1, Arrays.asList(alpha), false, THROTTLE_OPTION_CATEGORY, ThrottleMatrixProjectOptions.DEFAULT)); + p2.addProperty(new ThrottleJobProperty(1, 1, Arrays.asList(alpha), false, THROTTLE_OPTION_CATEGORY, false, "", ThrottleMatrixProjectOptions.DEFAULT)); FreeStyleProject p3 = createFreeStyleProject("p3"); - p3.addProperty(new ThrottleJobProperty(1, 1, Arrays.asList(alpha, beta), true, THROTTLE_OPTION_CATEGORY, ThrottleMatrixProjectOptions.DEFAULT)); + p3.addProperty(new ThrottleJobProperty(1, 1, Arrays.asList(alpha, beta), true, THROTTLE_OPTION_CATEGORY, false, "", ThrottleMatrixProjectOptions.DEFAULT)); FreeStyleProject p4 = createFreeStyleProject("p4"); - p4.addProperty(new ThrottleJobProperty(1, 1, Arrays.asList(beta, gamma), true, THROTTLE_OPTION_CATEGORY, ThrottleMatrixProjectOptions.DEFAULT)); + p4.addProperty(new ThrottleJobProperty(1, 1, Arrays.asList(beta, gamma), true, THROTTLE_OPTION_CATEGORY, false, "", ThrottleMatrixProjectOptions.DEFAULT)); // TODO when core dep ≥1.480.3, add cloudbees-folder as a test dependency so we can check jobs inside folders assertProjects(alpha, p3); assertProjects(beta, p3, p4); @@ -45,7 +45,7 @@ public void testGetCategoryProjects() throws Exception { public void testToString_withNulls(){ - ThrottleJobProperty tjp = new ThrottleJobProperty(0,0, null, false, null, ThrottleMatrixProjectOptions.DEFAULT); + ThrottleJobProperty tjp = new ThrottleJobProperty(0,0, null, false, null, false, "", ThrottleMatrixProjectOptions.DEFAULT); assertNotNull(tjp.toString()); } @@ -55,10 +55,13 @@ public void testThrottleJob_constructor_should_store_arguments() { List expectedCategories = Collections.emptyList(); boolean expectedThrottleEnabled = anyBoolean(); String expectedThrottleOption = anyString(); + boolean expectedLimitOneJobWithMatchingParams = anyBoolean(); + String expectedParamsToUseForLimit = anyString(); ThrottleJobProperty property = new ThrottleJobProperty(expectedMaxConcurrentPerNode, expectedMaxConcurrentTotal, expectedCategories, expectedThrottleEnabled, expectedThrottleOption, + expectedLimitOneJobWithMatchingParams, expectedParamsToUseForLimit, ThrottleMatrixProjectOptions.DEFAULT); assertEquals(expectedMaxConcurrentPerNode, property.getMaxConcurrentPerNode()); @@ -80,6 +83,8 @@ public void testThrottleJob_should_copy_categories_to_concurrency_safe_list() { unsafeList, anyBoolean(), "throttle_option", + anyBoolean(), + anyString(), ThrottleMatrixProjectOptions.DEFAULT); List storedCategories = property.getCategories(); @@ -95,6 +100,8 @@ public void testThrottleJob_constructor_handles_null_categories(){ null, anyBoolean(), "throttle_option", + anyBoolean(), + anyString(), ThrottleMatrixProjectOptions.DEFAULT); assertEquals(Collections.emptyList(), property.getCategories());