Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for limiting job concurrency based on one or more build parameters #9

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 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
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/
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ THE SOFTWARE.
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>1.399</version>
<version>1.518</version>
Copy link
Member

Choose a reason for hiding this comment

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

Could you reduce version dependency to 1.480.x at least? Old LTS releases still alive...

Copy link
Author

Choose a reason for hiding this comment

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

Yes, I'll try that.

On Mon, Nov 4, 2013 at 12:20 PM, Oleg Nenashev [email protected]:

In pom.xml:

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

org.jenkins-ci.plugins
plugin

  • 1.399
  • 1.518

Could you reduce version dependency to 1.480.x at least? Old LTS releases
still alive...


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

</parent>

<artifactId>throttle-concurrents</artifactId>
<packaging>hpi</packaging>
<name>Jenkins Throttle Concurrent Builds Plug-in</name>
<version>1.7.3-SNAPSHOT</version>
<version>1.7.5-SNAPSHOT</version>
<url>http://wiki.jenkins-ci.org/display/JENKINS/Throttle+Concurrent+Builds+Plugin</url>
<description>Plugin to throttle the number of concurrent builds of a single job per node.</description>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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 @@ -46,12 +48,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 @@ -80,6 +86,7 @@ public Object readResolve() {
maxConcurrentTotal = 0;
}
}

configVersion = 1L;

return this;
Expand All @@ -89,6 +96,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 @@ -5,15 +5,20 @@
import hudson.matrix.MatrixProject;
import hudson.model.AbstractBuild;
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.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;
Expand All @@ -35,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 @@ -90,6 +94,10 @@ else if (tjp.getThrottleOption().equals("category")) {
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 @@ -151,6 +159,88 @@ else if (tjp.getThrottleOption().equals("category")) {
}


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 (exec.getCurrentExecutable() != null && exec.getCurrentExecutable().getParent().getOwnerTask().getName() == item.task.getName()) {
List<ParameterValue> 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<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>();

List<Action> actions = unit.context.actions;
for (Action action : actions) {
if (action instanceof ParametersAction) {
ParametersAction params = (ParametersAction) action;
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 Expand Up @@ -224,4 +314,4 @@ private List<AbstractProject<?,?>> getCategoryProjects(String category) {

private static final Logger LOGGER = Logger.getLogger(ThrottleQueueTaskDispatcher.class.getName());

}
}
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>