diff --git a/.gitignore b/.gitignore index 77fb9a7d..c9c57ab6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.iml *.ipr *.iws +.idea target work # eclipse => 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 b52da6b2..0e9c62c9 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 aca9ca63..b208b558 100644 --- a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleJobProperty.java +++ b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleJobProperty.java @@ -37,6 +37,8 @@ public class ThrottleJobProperty extends JobProperty> { private List categories; private boolean throttleEnabled; private String throttleOption; + private boolean limitOneJobWithMatchingParams; + private String limitOneJobByParams; /** * Store a config version so we're able to migrate config on various @@ -49,12 +51,16 @@ public ThrottleJobProperty(Integer maxConcurrentPerNode, Integer maxConcurrentTotal, List categories, boolean throttleEnabled, - String throttleOption) { + String throttleOption, + boolean limitOneJobWithMatchingParams, + String limitOneJobByParams) { this.maxConcurrentPerNode = maxConcurrentPerNode == null ? 0 : maxConcurrentPerNode; this.maxConcurrentTotal = maxConcurrentTotal == null ? 0 : maxConcurrentTotal; this.categories = categories; this.throttleEnabled = throttleEnabled; this.throttleOption = throttleOption; + this.limitOneJobWithMatchingParams = limitOneJobWithMatchingParams; + this.limitOneJobByParams = limitOneJobByParams; } @@ -83,6 +89,7 @@ public Object readResolve() { maxConcurrentTotal = 0; } } + configVersion = 1L; return this; @@ -109,6 +116,14 @@ public boolean getThrottleEnabled() { return throttleEnabled; } + public boolean getlimitOneJobWithMatchingParams() { + return limitOneJobWithMatchingParams; + } + + public String getLimitOneJobByParams() { + return limitOneJobByParams; + } + public String getThrottleOption() { return throttleOption; } diff --git a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java index e923d390..d3abd654 100644 --- a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java +++ b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java @@ -4,16 +4,23 @@ 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; 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.Arrays; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Set; import java.util.logging.Level; @@ -23,7 +30,8 @@ public class ThrottleQueueTaskDispatcher extends QueueTaskDispatcher { @Override - public CauseOfBlockage canTake(Node node, Task task) { + public CauseOfBlockage canTake(Node node, Queue.BuildableItem item) { + Task task = item.task; if (task instanceof MatrixConfiguration) { return null; } @@ -32,7 +40,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(); @@ -83,10 +90,14 @@ else if (tjp.getThrottleOption().equals("category")) { return null; } - // @Override on jenkins 4.127+ , but still compatible with 1.399 + // @Override on jenkins 1.427+ , but still compatible with 1.399 public CauseOfBlockage canRun(Queue.Item item) { ThrottleJobProperty tjp = getThrottleJobProperty(item.task); if (tjp!=null && tjp.getThrottleEnabled()) { + if (tjp.getlimitOneJobWithMatchingParams() && isAnotherBuildWithSameParametersRunningOnAnyNode(item)) { + LOGGER.info("A build with matching parameters is already running."); + return CauseOfBlockage.fromMessage(Messages._ThrottleQueueTaskDispatcher_OnlyOneWithMatchingParameters()); + } return canRun(item.task, tjp); } return null; @@ -147,6 +158,98 @@ 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 = new ArrayList(); + List itemParams = getParametersFromQueueItem(item); + + if (tjp.getLimitOneJobByParams().length() > 0) { + paramsToCompare = Arrays.asList(tjp.getLimitOneJobByParams().split(",")); + 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.info("build (" + exec.getCurrentWorkUnit() + ") with identical parameters (" + + executingUnitParams + ") is already running."); + return true; + } + } + } + } + } + return false; + } + + // takes a String array containing a list of params, a List of ParameterValue objects + // and returns a new 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 = new ArrayList(); + + ParametersAction params = item.getAction(ParametersAction.class); + if (params != null) { + paramsList = params.getParameters(); + } + return paramsList; + } + + private ThrottleJobProperty getThrottleJobProperty(Task task) { if (task instanceof AbstractProject) { AbstractProject p = (AbstractProject) task; diff --git a/src/main/resources/hudson/plugins/throttleconcurrents/Messages.properties b/src/main/resources/hudson/plugins/throttleconcurrents/Messages.properties index bb5815d4..94bf9bdd 100644 --- a/src/main/resources/hudson/plugins/throttleconcurrents/Messages.properties +++ b/src/main/resources/hudson/plugins/throttleconcurrents/Messages.properties @@ -1,3 +1,4 @@ ThrottleQueueTaskDispatcher.MaxCapacityOnNode=Already running {0} builds on node ThrottleQueueTaskDispatcher.MaxCapacityTotal=Already running {0} builds across all nodes -ThrottleQueueTaskDispatcher.BuildPending=A build is pending launch \ No newline at end of file +ThrottleQueueTaskDispatcher.BuildPending=A build is pending launch +ThrottleQueueTaskDispatcher.OnlyOneWithMatchingParameters=A build with matching parameters is already running diff --git a/src/main/resources/hudson/plugins/throttleconcurrents/ThrottleJobProperty/config.jelly b/src/main/resources/hudson/plugins/throttleconcurrents/ThrottleJobProperty/config.jelly index a805a6d2..fd563ef3 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 4551ce96..9c346d65 100644 --- a/src/test/java/hudson/plugins/throttleconcurrents/ThrottleJobPropertyTest.java +++ b/src/test/java/hudson/plugins/throttleconcurrents/ThrottleJobPropertyTest.java @@ -21,11 +21,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)); + p2.addProperty(new ThrottleJobProperty(1, 1, Arrays.asList(alpha), false, THROTTLE_OPTION_CATEGORY, false, "")); FreeStyleProject p3 = createFreeStyleProject("p3"); - p3.addProperty(new ThrottleJobProperty(1, 1, Arrays.asList(alpha, beta), true, THROTTLE_OPTION_CATEGORY)); + p3.addProperty(new ThrottleJobProperty(1, 1, Arrays.asList(alpha, beta), true, THROTTLE_OPTION_CATEGORY, false, "")); FreeStyleProject p4 = createFreeStyleProject("p4"); - p4.addProperty(new ThrottleJobProperty(1, 1, Arrays.asList(beta, gamma), true, THROTTLE_OPTION_CATEGORY)); + p4.addProperty(new ThrottleJobProperty(1, 1, Arrays.asList(beta, gamma), true, THROTTLE_OPTION_CATEGORY, false, "")); // 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); diff --git a/src/test/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcherTest.java b/src/test/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcherTest.java index 529a1870..ecc94851 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("FINE"); select.setSelectedAttribute(option, true); break; }