Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
766475a
basic support for limiting concurrent job runs by parameters
joemiller Feb 25, 2013
931f183
Merge pull request #1 from pantheon-systems/limit_concurrency_by_job_…
joemiller Feb 25, 2013
75a1598
allow limiting job concurrency against a list of parameters
joemiller Feb 26, 2013
07d2291
added some quick commands to help other devs work on the plugin
joemiller Feb 26, 2013
65bc248
Merge pull request #2 from pantheon-systems/selectable_parameters
joemiller Feb 26, 2013
05b8ed7
fix bug with jenkins >1.510 causing stackoverflow. bump version
joemiller Jun 19, 2013
0be7eff
fix bug where parameter-based job concurrency stops working after jen…
joemiller Aug 20, 2013
d8d2017
Successful build against 1.509.4
Hornswoggles Feb 20, 2014
8319b20
merging in 1.8.1 upstream changes
Hornswoggles Feb 20, 2014
f4c0ce2
Fixing NPE
Hornswoggles Feb 20, 2014
ed78fdb
Merge pull request #3 from pantheon-systems/upstream
Hornswoggles Feb 21, 2014
805556e
updating tests so they pass
Hornswoggles Feb 21, 2014
a01b866
Merge branch 'upstream'
Hornswoggles Feb 21, 2014
801200e
fix npes
Feb 24, 2014
77ad29d
helper function
Feb 24, 2014
3421a91
Merge pull request #4 from pantheon-systems/fix_npe_again
kibra Feb 24, 2014
7520a2c
replace deprecated method
Mar 19, 2014
12b7aac
fix test by changing select options to upper case
Mar 19, 2014
1a32b7d
add .idea directory to .gitignore
Mar 19, 2014
cc0da5a
fix comment typo
zihaoyu Mar 20, 2014
5284d6d
Merge pull request #5 from zihaoyu/develop
Hornswoggles Mar 20, 2014
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
*.iml
*.ipr
*.iws
.idea
target
work
# eclipse =>
Expand Down
13 changes: 13 additions & 0 deletions README
Original file line number Diff line number Diff line change
@@ -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/
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ THE SOFTWARE.
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>1.424</version>
<version>1.509.4</version>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to change the version? I don't see real dependencies from Jenkins core.
I also recommend to avoid 1.509.4 at least. See https://groups.google.com/forum/#!topic/jenkinsci-dev/9fMrUsVh6zU

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure, possibly. It's been a while since we started this patch and
I don't recall if we bumped version for a reason.

Would the proper approach to testing this be to simply revert the version
in the pom.xml and run the test suite?

thanks

On Fri, Apr 4, 2014 at 10:46 AM, Oleg Nenashev [email protected]:

In pom.xml:

@@ -26,7 +26,7 @@ THE SOFTWARE.

org.jenkins-ci.plugins
plugin

  • 1.424
  • 1.509.4

Do we really need to change the version? I don't see real dependencies
from Jenkins core.
I also recommend to avoid 1.509.4 at least. See
https://groups.google.com/forum/#!topic/jenkinsci-dev/9fMrUsVh6zU

Reply to this email directly or view it on GitHubhttps://github.com//pull/9/files#r11305151
.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test suites don't provide enough test coverage. BTW, it is better than nothing

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throttling by parameter doesnt work on 1.424 I will go through each LTS release between 1.424 and 1.509. Starting with 1.424.6

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LTS 1.447 builds and works. Although the helpful message does not show up in the queue
LTS 1.466 builds and works and has the helpful message in the queue

</parent>

<artifactId>throttle-concurrents</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public class ThrottleJobProperty extends JobProperty<AbstractProject<?,?>> {
private List<String> categories;
private boolean throttleEnabled;
private String throttleOption;
private boolean limitOneJobWithMatchingParams;
private String limitOneJobByParams;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Store a list instead of the string to avoid multiple parsings.
I also think that the field name is quite confusing


/**
* Store a config version so we're able to migrate config on various
Expand All @@ -49,12 +51,16 @@ public ThrottleJobProperty(Integer maxConcurrentPerNode,
Integer maxConcurrentTotal,
List<String> 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;
}


Expand Down Expand Up @@ -83,6 +89,7 @@ public Object readResolve() {
maxConcurrentTotal = 0;
}
}

configVersion = 1L;

return this;
Expand All @@ -109,6 +116,14 @@ public boolean getThrottleEnabled() {
return throttleEnabled;
}

public boolean getlimitOneJobWithMatchingParams() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, Stapler requires isLimitOneJobWithMatchingParams() in order to have a proper behavior for fields.

return limitOneJobWithMatchingParams;
}

public String getLimitOneJobByParams() {
return limitOneJobByParams;
}

public String getThrottleOption() {
return throttleOption;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand All @@ -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();
Expand Down Expand Up @@ -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.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isAnotherBuildWithSameParametersRunningOnAnyNode() has its internal logging of conflicts

return CauseOfBlockage.fromMessage(Messages._ThrottleQueueTaskDispatcher_OnlyOneWithMatchingParameters());
}
return canRun(item.task, tjp);
}
return null;
Expand Down Expand Up @@ -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<String> paramsToCompare = new ArrayList<String>();
List<ParameterValue> itemParams = getParametersFromQueueItem(item);

if (tjp.getLimitOneJobByParams().length() > 0) {
paramsToCompare = Arrays.asList(tjp.getLimitOneJobByParams().split(","));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense to add the parameter validation to UI form.
Otherwise, errors in data formats may be left unnoticed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to calculate the array and save it to a transient field once (on save/load) to decrease the performance impact

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())) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task::getName() is not reliable due to Folders, etc.
You should use Task::getDisplayName() as it has been mentioned in Javadoc

List<ParameterValue> executingUnitParams = getParametersFromWorkUnit(exec.getCurrentWorkUnit());
executingUnitParams = doFilterParams(paramsToCompare, executingUnitParams);

if (executingUnitParams.containsAll(itemParams)) {
LOGGER.info("build (" + exec.getCurrentWorkUnit() + ") with identical parameters (" +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose to decrease the logging level. Seems that the log message will totally spam logs on conflicts
It also makes sense to use formatted outputs to prevent string constructions

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<ParameterValue> with only the desired params in the list.
private List<ParameterValue> doFilterParams(List<String> params, List<ParameterValue> OriginalParams) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use Sun/Oracle Java code style to prevent complains from FindBugs and other utilities

if (params.isEmpty()) {
return OriginalParams;
}

List<ParameterValue> newParams = new ArrayList<ParameterValue>();

for (ParameterValue p : OriginalParams) {
if (params.contains(p.getName())) {
newParams.add(p);
}
}
return newParams;
}

public List<ParameterValue> getParametersFromWorkUnit(WorkUnit unit) {
List<ParameterValue> paramsList = new ArrayList<ParameterValue>();

if (unit != null && unit.context != null && unit.context.actions != null) {
List<Action> 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<ParameterValue> getParametersFromQueueItem(Queue.Item item) {
List<ParameterValue> paramsList = new ArrayList<ParameterValue>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid the creation of objects, will be overridden soon. The else clause in if (params != null) could be preferable


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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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
ThrottleQueueTaskDispatcher.BuildPending=A build is pending launch
ThrottleQueueTaskDispatcher.OnlyOneWithMatchingParameters=A build with matching parameters is already running
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@
field="maxConcurrentPerNode">
<f:textbox />
</f:entry>

<f:optionalBlock name="limitOneJobWithMatchingParams"
title="${%Prevent multiple jobs with identical parameters from running concurrently}"
inline="true"
checked="${instance.limitOneJobWithMatchingParams}">
<f:entry title="${%List of parameters to check (comma-separated)}"
field="limitOneJobByParams"
help="${descriptor.getHelpFile('limitOneJobWithMatchingParams')}">
<f:textbox />
</f:entry>
</f:optionalBlock>

<j:if test="${!empty(descriptor.categories)}">
<f:entry title="${%Multi-Project Throttle Category}">
<j:forEach var="cat" items="${descriptor.categories}">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div>
<p>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.</p>
<p>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.</p>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down