Skip to content

Commit

Permalink
[JENKINS-30269] Supress polling for locking and use StepContext to no…
Browse files Browse the repository at this point in the history
…tify locks in the same Pipeline build
  • Loading branch information
amuniz committed Mar 23, 2016
1 parent 9617986 commit a14014b
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 244 deletions.
4 changes: 2 additions & 2 deletions pom.xml
Expand Up @@ -85,14 +85,14 @@
<artifactId>annotations</artifactId>
<version>3.0.0</version>
</dependency>

<!-- Testing scope -->
<dependency>
<groupId>com.infradna.tool</groupId>
<artifactId>bridge-method-annotation</artifactId>
<version>1.14</version>
<optional>true</optional>
</dependency>

<!-- Testing scope -->
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-aggregator</artifactId>
Expand Down
@@ -1,32 +1,19 @@
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;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

import javax.annotation.Nonnull;

import org.jenkins.plugins.lockableresources.queue.LockableResourcesStruct;
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
import org.jenkinsci.plugins.workflow.steps.BodyExecution;
import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback;
import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.jenkinsci.plugins.workflow.steps.StepContextParameter;
import org.jenkinsci.plugins.workflow.steps.StepExecution;

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 All @@ -39,94 +26,38 @@ public class LockStepExecution extends AbstractStepExecutionImpl {
@StepContextParameter
private transient TaskListener listener;

private transient volatile ScheduledFuture<?> task;
private volatile BodyExecution body;
private final String id = UUID.randomUUID().toString();
private static final Logger LOGGER = Logger.getLogger(LockStepExecution.class.getName());

@Override
public boolean start() throws Exception {
listener.getLogger().println("Trying to acquire lock on [" + step.resource + "]");
if (!lockAndProceed()) {
listener.getLogger().println(LockableResourcesManager.get().getLockCause(step.resource));
listener.getLogger().println("Waiting for lock...");
tryLock(0);
}
return false;
}

@Override
public void onResume() {
super.onResume();
//LockableResourcesManager.get().load();
if (body == null) {
// Restarted while waiting to lock the resource
LOGGER.fine("Resuming lock step on [" + run.getExternalizableId() + "], retrying to acquire lock");
tryLock(0);
}
// the body was already started, nothing to do here
}

private void tryLock(long delay) {
task = Timer.get().schedule(new Runnable() {
@Override
public void run() {
task = null;
if (!lockAndProceed()) {
retry(id, 1000); // try to lock every second
}
}
}, delay, TimeUnit.MILLISECONDS);
}

private synchronized boolean lockAndProceed() {
LockableResourcesStruct resourceHolder = new LockableResourcesStruct(step.resource);
LOGGER.finest("Trying to acquire [" + step.resource + "] by " + run.getExternalizableId());
// 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 + "]");
LOGGER.finest("Lock acquired on [" + step.resource + "] by " + run.getExternalizableId());
body = getContext().newBodyInvoker().
withCallback(new Callback(resourceHolder, run)).
withDisplayName(null).
start();
return true;
if(LockableResourcesManager.get().lock(resourceHolder.required, run, getContext())) {
proceed(getContext(), step.resource);
} else {
return false;
// we have to wait
listener.getLogger().println("Resource locked, waiting...");
}
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);
public static void proceed(StepContext context, String resource) {
LockableResourcesStruct resourceHolder = new LockableResourcesStruct(resource);
Run<?, ?> r = null;
try {
r = context.get(Run.class);
context.get(TaskListener.class).getLogger().println("Lock acquired on [" + resource + "]");
} catch (Exception e) {
context.onFailure(e);
return;
}
}

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>() {
@Override
public Void apply(@Nonnull LockStepExecution execution) {
if (execution.id.equals(id)) {
execution.tryLock(delay);
}
return null;
}
});
LOGGER.finest("Lock acquired on [" + resource + "] by " + r.getExternalizableId());
context.newBodyInvoker().
withCallback(new Callback(resourceHolder, r)).
withDisplayName(null).
start();
}

private static final class Callback extends BodyExecutionCallback.TailCall {
Expand All @@ -150,10 +81,10 @@ private void unlock(StepContext context) throws IOException, InterruptedExceptio
if (run == null && buildExternalizableId != null) {
run = Run.fromExternalizableId(buildExternalizableId);
}
LockableResourcesManager.get().unlock(resourceHolder.required, run);
// It's granted to contain one (and only one for now)
LockableResourcesManager.get().unlock(resourceHolder.required, run, context);
context.get(TaskListener.class).getLogger().println("Lock released on resouce [" + resourceHolder.required.get(0) + "]");
LOGGER.finest("Lock released on [" + resourceHolder.required.get(0) + "]");
context.onSuccess(null);
}

private static final long serialVersionUID = 1L;
Expand All @@ -162,52 +93,7 @@ private void unlock(StepContext context) throws IOException, InterruptedExceptio

@Override
public void stop(Throwable cause) throws Exception {
if (body != null) {
body.cancel(cause);
}
if (task != null) {
task.cancel(false);
}
getContext().onFailure(cause);
// NO-OP
}

/**
* 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 @@ -22,16 +22,19 @@
import hudson.model.User;
import hudson.tasks.Mailer.UserProperty;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import jenkins.model.Jenkins;

import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.jinterop.winreg.IJIWinReg.saveFile;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
Expand Down Expand Up @@ -64,9 +67,9 @@ public class LockableResource extends AbstractDescribableImpl<LockableResource>

/**
* Only used when this lockable resource is tried to be locked by {@link LockStep},
* otherwise (freestyle builds) regular Jenkins builds queue is used.
* otherwise (freestyle builds) regular Jenkins queue is used.
*/
private List<String> queuedBuilds = new ArrayList<String>();
private List<StepContext> queuedContexts = new ArrayList<StepContext>();

@Deprecated
public LockableResource(
Expand Down Expand Up @@ -107,54 +110,9 @@ public String getLabels() {
return labels;
}

public void addToQueue(Run<?, ?> b) {
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())) {
queuedBuilds.remove(0);
} else {
throw new IllegalArgumentException("Trying to unqueue a wrong build: " + b.getExternalizableId());
}
}
// 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());
}
return true;
return queuedContexts.size();
}

public boolean isValidLabel(String candidate, Map<String, Object> params) {
Expand Down Expand Up @@ -331,6 +289,55 @@ private void validateQueuingTimeout() {
}
}

public void queueAdd(StepContext context) {
queuedContexts.add(context);
}

/**
* It removes the returned context from the queue and set the holding build (@link {@link #build}) to null
* if there are no more contexts for that build.
*/
@CheckForNull
public StepContext getNextQueuedContext() {
if (queuedContexts.size() > 0) {
StepContext nextContext = queuedContexts.remove(0);
Run<?, ?> nextContextRun;
try {
nextContextRun = nextContext.get(Run.class);
} catch (Exception e) {
// This should not happen, but let's remove it and get the next
queuedContexts.remove(nextContext);
return getNextQueuedContext();
}
if (!isThereAnotherContextForTheSameRun(nextContextRun)) {
setBuild(null);
}
return nextContext;
}
return null;
}

private boolean isThereAnotherContextForTheSameRun(Run<?, ?> nextContextRun) {
boolean thereIsAnotherContextForTheSameRun = false;
Iterator<StepContext> it = queuedContexts.iterator();
while (it.hasNext()) {
StepContext c = it.next();
Run<?, ?> waitingRun;
try {
waitingRun = c.get(Run.class);
} catch (Exception e) {
// this should not happen, but let's remove it anyway
it.remove();
continue;
}
if (nextContextRun.getExternalizableId().equals(waitingRun.getExternalizableId())) {
thereIsAnotherContextForTheSameRun = true;
break;
}
}
return thereIsAnotherContextForTheSameRun;
}

@DataBoundSetter
public void setReservedBy(String userName) {
this.reservedBy = Util.fixEmptyAndTrim(userName);
Expand Down

0 comments on commit a14014b

Please sign in to comment.