-
Notifications
You must be signed in to change notification settings - Fork 112
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
base: master
Are you sure you want to change the base?
Changes from 7 commits
766475a
931f183
75a1598
07d2291
65bc248
05b8ed7
0be7eff
d8d2017
8319b20
f4c0ce2
ed78fdb
805556e
a01b866
801200e
77ad29d
3421a91
7520a2c
12b7aac
1a32b7d
cc0da5a
5284d6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Store a list instead of the string to avoid multiple parsings. |
||
|
||
/** | ||
* Store a config version so we're able to migrate config on various | ||
|
@@ -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; | ||
} | ||
|
||
|
||
|
@@ -80,6 +86,7 @@ public Object readResolve() { | |
maxConcurrentTotal = 0; | ||
} | ||
} | ||
|
||
configVersion = 1L; | ||
|
||
return this; | ||
|
@@ -89,6 +96,14 @@ public boolean getThrottleEnabled() { | |
return throttleEnabled; | ||
} | ||
|
||
public boolean getlimitOneJobWithMatchingParams() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAIK, Stapler requires |
||
return limitOneJobWithMatchingParams; | ||
} | ||
|
||
public String getLimitOneJobByParams() { | ||
return limitOneJobByParams; | ||
} | ||
|
||
public String getThrottleOption() { | ||
return throttleOption; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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(); | ||
|
@@ -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."); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
@@ -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(",")); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It makes sense to add the parameter validation to UI form. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
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; | ||
|
@@ -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 |
---|---|---|
@@ -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> |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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]: