diff --git a/pom.xml b/pom.xml index 84f54aa3..f428d37f 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ THE SOFTWARE. org.jenkins-ci.plugins plugin - 1.424 + 1.596.1 throttle-concurrents @@ -42,25 +42,33 @@ THE SOFTWARE. All source code is under the MIT license. - + + + UTF-8 + 1.6 + 1.6 + + - org.apache.maven.plugins - maven-enforcer-plugin - 1.0-beta-1 + maven-clean-plugin + ${maven-clean-plugin.version} + + + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${compileSource} + ${compileTarget} + false + false + - - maven-compiler-plugin - - 1.6 - 1.6 - - org.apache.maven.plugins maven-release-plugin @@ -85,7 +93,14 @@ THE SOFTWARE. +3 - + + + + Darren Ball + balldarrens@gmail.com + + + repo.jenkins-ci.org @@ -113,6 +128,12 @@ THE SOFTWARE. 2.0.1 jar + + org.jenkins-ci.plugins + matrix-project + 1.4.1 + + diff --git a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleJobProperty.java b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleJobProperty.java index 98a8c193..cb9b976f 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; @@ -29,6 +30,8 @@ import net.sf.json.JSONObject; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; @@ -42,9 +45,13 @@ public class ThrottleJobProperty extends JobProperty> { 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. @@ -57,6 +64,8 @@ public ThrottleJobProperty(Integer maxConcurrentPerNode, List categories, boolean throttleEnabled, String throttleOption, + boolean limitOneJobWithMatchingParams, + String paramsToUseForLimit, @CheckForNull ThrottleMatrixProjectOptions matrixOptions ) { this.maxConcurrentPerNode = maxConcurrentPerNode == null ? 0 : maxConcurrentPerNode; @@ -66,7 +75,20 @@ 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(ArrayUtils.nullToEmpty(StringUtils.split(this.paramsToUseForLimit))); + } + else { + this.paramsToCompare = new ArrayList(); + } + } + else { + this.paramsToCompare = new ArrayList(); + } } @@ -126,6 +148,10 @@ public boolean getThrottleEnabled() { return throttleEnabled; } + public boolean isLimitOneJobWithMatchingParams() { + return limitOneJobWithMatchingParams; + } + public String getThrottleOption() { return throttleOption; } @@ -148,6 +174,10 @@ public Integer getMaxConcurrentTotal() { return maxConcurrentTotal; } + public String getParamsToUseForLimit() { + return paramsToUseForLimit; + } + @CheckForNull public ThrottleMatrixProjectOptions getMatrixOptions() { return matrixOptions; @@ -171,6 +201,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(); diff --git a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java index a0440387..33c28a14 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; @@ -92,6 +99,9 @@ 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; @@ -178,6 +188,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.getName())) { + 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 params + * @param OriginalParams + * @return + */ + 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) { 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..34ba01b0 100644 --- a/src/main/resources/hudson/plugins/throttleconcurrents/ThrottleJobProperty/config.jelly +++ b/src/main/resources/hudson/plugins/throttleconcurrents/ThrottleJobProperty/config.jelly @@ -20,6 +20,17 @@ 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/ThrottleIntegrationTest.java b/src/test/java/hudson/plugins/throttleconcurrents/ThrottleIntegrationTest.java index 069f6d0d..09ace0b0 100644 --- a/src/test/java/hudson/plugins/throttleconcurrents/ThrottleIntegrationTest.java +++ b/src/test/java/hudson/plugins/throttleconcurrents/ThrottleIntegrationTest.java @@ -123,6 +123,8 @@ public void testThrottlingWithCategory() throws Exception { Arrays.asList(category), // categories true, // throttleEnabled "category", // throttleOption + false, + null, ThrottleMatrixProjectOptions.DEFAULT )); p1.getBuildersList().add(new SleepBuilder(SLEEP_TIME)); @@ -135,6 +137,8 @@ public void testThrottlingWithCategory() throws Exception { Arrays.asList(category), // categories true, // throttleEnabled "category", // throttleOption + false, + null, ThrottleMatrixProjectOptions.DEFAULT )); p2.getBuildersList().add(new SleepBuilder(SLEEP_TIME)); 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()); diff --git a/src/test/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcherTest.java b/src/test/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcherTest.java index 529a1870..d317f977 100644 --- a/src/test/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcherTest.java +++ b/src/test/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcherTest.java @@ -358,7 +358,7 @@ private String configureLogger() input.setValueAttribute(logger); } HtmlSelect select = form.getSelectByName("level"); - HtmlOption option = select.getOptionByValue("fine"); + HtmlOption option = select.getOptionByValue("ALL"); select.setSelectedAttribute(option, true); break; }