Navigation Menu

Skip to content

Commit

Permalink
[JENKINS-30269] Add step configuration page + limit waiting builds
Browse files Browse the repository at this point in the history
  • Loading branch information
amuniz committed Mar 21, 2016
1 parent ba87427 commit 9617986
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 10 deletions.
Expand Up @@ -5,13 +5,18 @@
import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;

import hudson.Extension;
import hudson.model.AutoCompletionCandidates;

public class LockStep extends AbstractStepImpl implements Serializable {

public final String resource;

public Integer maxWaiting;

@DataBoundConstructor
public LockStep(String resource) {
if (resource == null || resource.isEmpty()) {
Expand All @@ -23,6 +28,11 @@ public LockStep(String resource) {
this.resource = resource;
}

@DataBoundSetter
public void setMaxWaiting(Integer maxWaiting) {
this.maxWaiting = maxWaiting;
}

@Extension
public static final class DescriptorImpl extends AbstractStepDescriptorImpl {

Expand All @@ -37,13 +47,17 @@ public String getFunctionName() {

@Override
public String getDisplayName() {
return "Lock";
return "Lock shared resources to manage concurrency";
}

@Override
public boolean takesImplicitBlockArgument() {
return true;
}

public AutoCompletionCandidates doAutoCompleteResource(@QueryParameter String value) {
return RequiredResourcesProperty.DescriptorImpl.doAutoCompleteResourceNames(value);
}
}

private static final long serialVersionUID = 1L;
Expand Down
@@ -1,5 +1,7 @@
package org.jenkins.plugins.lockableresources;

import static java.util.logging.Level.WARNING;

import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ScheduledFuture;
Expand All @@ -19,8 +21,11 @@
import com.google.common.base.Function;
import com.google.inject.Inject;

import hudson.model.Executor;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import jenkins.model.CauseOfInterruption;
import jenkins.util.Timer;

public class LockStepExecution extends AbstractStepExecutionImpl {
Expand Down Expand Up @@ -77,8 +82,9 @@ public void run() {
private synchronized boolean lockAndProceed() {
LockableResourcesStruct resourceHolder = new LockableResourcesStruct(step.resource);
LOGGER.finest("Trying to acquire [" + step.resource + "] by " + run.getExternalizableId());
// force load from disk to avoid not commited memory between threads
LockableResourcesManager.get().addToQueue(resourceHolder.required, run);
// Apply maxWaiting if needed
Run<?, ?> older = LockableResourcesManager.get().addToQueue(resourceHolder.required, run, step.maxWaiting);
applyMaxWaiting(older, resourceHolder);
if (LockableResourcesManager.get().isNextInQueue(resourceHolder.required, run) &&
LockableResourcesManager.get().lock(resourceHolder.required, run)) {
listener.getLogger().println("Lock acquired on [" + step.resource + "]");
Expand All @@ -89,11 +95,27 @@ private synchronized boolean lockAndProceed() {
start();
return true;
} else {
//listener.getLogger().println(LockableResourcesManager.get().getLockCause(step.resource));
return false;
}
}

private void applyMaxWaiting(Run<?, ?> older, LockableResourcesStruct resourceHolder) {
if (older != null) {
// No more builds accepted waiting, cancel the older one
Executor e = older.getExecutor();
if (e != null) {
if (older.equals(run)) {
e.interrupt(Result.NOT_BUILT, new CanceledCause("The wait limit was reached and there was a newer build already waiting to lock [" +step.resource + "]"));
} else {
e.interrupt(Result.NOT_BUILT, new CanceledCause(run));
}
} else{
LOGGER.log(WARNING, "could not cancel an older flow because it has no assigned executor");
}
LockableResourcesManager.get().removeFromQueue(resourceHolder.required, older);
}
}

private static void retry(final String id, final long delay) {
// retry only if the the execution of this step has not being somehow stopped
StepExecution.applyAll(LockStepExecution.class, new Function<LockStepExecution, Void>() {
Expand Down Expand Up @@ -149,6 +171,43 @@ public void stop(Throwable cause) throws Exception {
getContext().onFailure(cause);
}

/**
* Records that a build was canceled because it reached a milestone but a
* newer build already passed it, or a newer build
* {@link Milestone#wentAway(Run)} from the last milestone the build passed.
*/
public static final class CanceledCause extends CauseOfInterruption {

private static final long serialVersionUID = 1;

private final String newerBuild;
private final String cause;

CanceledCause(Run<?, ?> newerBuild) {
this.newerBuild = newerBuild.getExternalizableId();
this.cause = null;
}

CanceledCause(String cause) {
this.cause = cause;
this.newerBuild = null;
}

public Run<?, ?> getNewerBuild() {
return Run.fromExternalizableId(newerBuild);
}

@Override
public String getShortDescription() {
if (newerBuild != null) {
return "Superseded by " + getNewerBuild().getDisplayName();
} else {
return cause;
}
}

}

private static final long serialVersionUID = 1L;

}
Expand Up @@ -108,11 +108,19 @@ public String getLabels() {
}

public void addToQueue(Run<?, ?> b) {
if (!queuedBuilds.contains(b.getExternalizableId())) {
if (!isInQueue(b)) {
queuedBuilds.add(b.getExternalizableId());
}
}

public boolean isInQueue(Run<?, ?> b) {
return queuedBuilds.contains(b.getExternalizableId());
}

public Integer getBuildsInQueue() {
return queuedBuilds.size();
}

public void removeFromQueue(Run<?, ?> b) {
if (queuedBuilds.size() > 0) {
if (queuedBuilds.get(0).equals(b.getExternalizableId())) {
Expand All @@ -124,6 +132,24 @@ public void removeFromQueue(Run<?, ?> b) {
// else no items in queue, return quietly
}

public String getOlderBuildInQueue(Run<?, ?> build) {
String older = null;
List<String> builds = new ArrayList<String>();
builds.addAll(queuedBuilds);
builds.add(build.getExternalizableId());
for (String b : builds) {
String job = b.split("#")[0];
String number = b.split("#")[1];
if (older == null && job.equals(build.getParent().getFullName())) {
older = b;
} else if (job.equals(build.getParent().getFullName()) &&
new Integer(number) < new Integer(older.split("#")[1])) {
older = b;
}
}
return older;
}

public boolean isNextInQueue(Run<?, ?> b) {
if (queuedBuilds.size() > 0) {
return queuedBuilds.get(0).equals(b.getExternalizableId());
Expand Down
Expand Up @@ -231,11 +231,33 @@ public synchronized boolean isNextInQueue(List<LockableResource> resources, Run<
return true;
}

public synchronized void addToQueue(List<LockableResource> resources, Run<?, ?> build) {
/**
* Returns null if the build is accepted into the queue, or the older build in the queue if not accepted.
* The returned build could be in incoming one if it is the older.
*/
public synchronized Run<?, ?> addToQueue(List<LockableResource> resources, Run<?, ?> build, Integer limit) {
boolean modified = false;
Run<?, ?> older = null;
for (LockableResource r : resources) {
r.addToQueue(build);
if (!r.isInQueue(build)) {
if (limit != null && r.getBuildsInQueue() >= limit) {
String olderId = r.getOlderBuildInQueue(build);
// The incoming build is not the older one, so add it to the queue
if (!olderId.equals(build.getExternalizableId())) {
r.addToQueue(build);
}
older = Run.fromExternalizableId(olderId);
// If the build can not aquire one of the requested resources, then it does not make sense to anything else
break;
}
r.addToQueue(build);
modified = true;
}
}
save();
if (modified) {
save();
}
return older;
}

public synchronized void removeFromQueue(List<LockableResource> resources, Run<?, ?> build) {
Expand All @@ -251,7 +273,8 @@ public synchronized void unlock(List<LockableResource> resourcesToUnLock,
// Seach the resource in the internal list un unlock it
for (LockableResource internal : resources) {
if (internal.getName().equals(r.getName())) {
if (build == null || build.getExternalizableId().equals(internal.getBuild().getExternalizableId())) {
if (build == null ||
(internal.getBuild() != null && build.getExternalizableId().equals(internal.getBuild().getExternalizableId()))) {
internal.unqueue();
internal.setBuild(null);
save();
Expand Down
Expand Up @@ -208,7 +208,7 @@ public AutoCompletionCandidates doAutoCompleteLabelName(
return c;
}

public AutoCompletionCandidates doAutoCompleteResourceNames(
public static AutoCompletionCandidates doAutoCompleteResourceNames(
@QueryParameter String value) {
AutoCompletionCandidates c = new AutoCompletionCandidates();

Expand Down
@@ -0,0 +1,9 @@
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:entry title="${%Resource}" field="resource">
<f:textbox/>
</f:entry>
<f:entry title="${%Limit waiting builds}" field="maxWaiting">
<f:number clazz="positive-number"/>
</f:entry>
</j:jelly>
@@ -0,0 +1,11 @@
<div>
<p>
By default builds a queued without any restriction on waiting to lock a common resource
Setting this parameter a limit is forced in the number of waiting builds. Any build trying to lock after
the limit is reached will trigger a cancel round trip which will abort the older waiting build.
</p>
<p>
For example, if builds #3 and #4 are waiting to lock the resource and build #5 reaches the lock step,
then build #3 will be cancelled in favor of #5 which is newer.
</p>
</div>
@@ -0,0 +1,5 @@
<div>
<p>
The resource name to lock as defined in Global settings.
</p>
</div>
Expand Up @@ -105,6 +105,45 @@ public void evaluate() throws Throwable {
}
});
}

@Test
public void maxWaiting() {
story.addStep(new Statement() {
@Override
public void evaluate() throws Throwable {
defineResource("resource1");
WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p");
p.setDefinition(new CpsFlowDefinition(
"lock(resource: 'resource1', maxWaiting: 2) {\n" +
" semaphore 'wait-inside'\n" +
"}\n" +
"echo 'Finish'"
));
WorkflowRun b1 = p.scheduleBuild2(0).waitForStart();
SemaphoreStep.waitForStart("wait-inside/1", b1);

WorkflowRun b2 = p.scheduleBuild2(0).waitForStart();
// Ensure that b2 reaches the lock before b3
story.j.waitForMessage("[resource1] is locked by p#1", b2);
WorkflowRun b3 = p.scheduleBuild2(0).waitForStart();
// Both 2 and 3 are waiting for locking resource1
story.j.waitForMessage("[resource1] is locked by p#1", b3);

WorkflowRun b4 = p.scheduleBuild2(0).waitForStart();
story.j.waitForMessage("Superseded by #4", b2);

// Unlock resource1
SemaphoreStep.success("wait-inside/1", null);
story.j.waitForMessage("Lock released on resouce [resource1]", b1);

story.j.waitForMessage("Lock acquired on [resource1]", b3);
SemaphoreStep.success("wait-inside/2", null);
story.j.waitForMessage("Lock acquired on [resource1]", b4);
SemaphoreStep.success("wait-inside/3", null);
story.j.waitForMessage("Finish", b4);
}
});
}

private void defineResource(String r) {
LockableResourcesManager.get().getResources().add(new LockableResource(r));
Expand Down

0 comments on commit 9617986

Please sign in to comment.