Skip to content

Commit

Permalink
Merge pull request #42 from amuniz/pr-26-fix
Browse files Browse the repository at this point in the history
[JENKINS-34268][JENKINS-34273] Lock multiple resources with specific quantity
  • Loading branch information
amuniz committed Dec 16, 2016
2 parents ba48550 + 843be82 commit 974572d
Show file tree
Hide file tree
Showing 14 changed files with 640 additions and 124 deletions.
@@ -0,0 +1,47 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Copyright (c) 2016, Florian Hug. All rights reserved. *
* *
* This file is part of the Jenkins Lockable Resources Plugin and is *
* published under the MIT license. *
* *
* See the "LICENSE.txt" file for more information. *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

package org.jenkins.plugins.lockableresources;

import hudson.init.InitMilestone;
import hudson.init.Initializer;

import java.util.ArrayList;
import java.util.List;

import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.jenkins.plugins.lockableresources.queue.LockableResourcesStruct;
import org.jenkins.plugins.lockableresources.queue.QueuedContextStruct;
import org.jenkins.plugins.lockableresources.LockableResource;
import org.jenkins.plugins.lockableresources.LockableResourcesManager;

import java.util.logging.Level;
import java.util.logging.Logger;

public final class BackwardCompatibility {
private static final Logger LOG = Logger.getLogger(BackwardCompatibility.class.getName());

@Initializer(after = InitMilestone.JOB_LOADED)
public static void compatibilityMigration() {
LOG.log(Level.FINE, "lockable-resource-plugin compatibility migration task run");
List<LockableResource> resources = LockableResourcesManager.get().getResources();
for (LockableResource resource : resources) {
List<StepContext> queuedContexts = resource.getQueuedContexts();
if (queuedContexts.size() > 0) {
for (StepContext queuedContext : queuedContexts) {
List<String> resourcesNames = new ArrayList<String>();
resourcesNames.add(resource.getName());
LockableResourcesStruct resourceHolder = new LockableResourcesStruct(resourcesNames, "", 0);
LockableResourcesManager.get().queueContext(queuedContext, resourceHolder, resource.getName());
}
queuedContexts.clear();
}
}
}
}
72 changes: 68 additions & 4 deletions src/main/java/org/jenkins/plugins/lockableresources/LockStep.java
Expand Up @@ -10,26 +10,50 @@

import hudson.Extension;
import hudson.model.AutoCompletionCandidates;
import hudson.util.FormValidation;
import hudson.Util;

import edu.umd.cs.findbugs.annotations.Nullable;
import edu.umd.cs.findbugs.annotations.CheckForNull;

public class LockStep extends AbstractStepImpl implements Serializable {

public final String resource;
@CheckForNull
public String resource = null;

@CheckForNull
public String label = null;

public int quantity = 0;

public boolean inversePrecedence = false;

// it should be LockStep() - without params. But keeping this for backward compatibility
// so `lock('resource1')` still works and `lock(label: 'label1', quantity: 3)` works too (resource is not required)
@DataBoundConstructor
public LockStep(String resource) {
if (resource == null || resource.isEmpty()) {
throw new IllegalArgumentException("must specify resource");
if (resource != null && !resource.isEmpty()) {
this.resource = resource;
}
this.resource = resource;
}

@DataBoundSetter
public void setInversePrecedence(boolean inversePrecedence) {
this.inversePrecedence = inversePrecedence;
}

@DataBoundSetter
public void setLabel(String label) {
if (label != null && !label.isEmpty()) {
this.label = label;
}
}

@DataBoundSetter
public void setQuantity(int quantity) {
this.quantity = quantity;
}

@Extension
public static final class DescriptorImpl extends AbstractStepDescriptorImpl {

Expand All @@ -55,6 +79,46 @@ public boolean takesImplicitBlockArgument() {
public AutoCompletionCandidates doAutoCompleteResource(@QueryParameter String value) {
return RequiredResourcesProperty.DescriptorImpl.doAutoCompleteResourceNames(value);
}

public static FormValidation doCheckLabel(@QueryParameter String value, @QueryParameter String resource) {
String resourceLabel = Util.fixEmpty(value);
String resourceName = Util.fixEmpty(resource);
if (resourceLabel != null && resourceName != null) {
return FormValidation.error("Label and resource name cannot be specified simultaneously.");
}
if ((resourceLabel == null) && (resourceName == null)) {
return FormValidation.error("Either label or resource name must be specified.");
}
return FormValidation.ok();
}

public static FormValidation doCheckResource(@QueryParameter String value, @QueryParameter String label) {
return doCheckLabel(label, value);
}
}

public String toString() {
// a label takes always priority
if (this.label != null) {
if (this.quantity > 0) {
return "Label: " + this.label + ", Quantity: " + this.quantity;
}
return "Label: " + this.label;
}
// make sure there is an actual resource specified
if (this.resource != null) {
return this.resource;
}
return "[no resource/label specified - probably a bug]";
}

/**
* Label and resource are mutual exclusive.
*/
public void validate() throws Exception {
if (label != null && !label.isEmpty() && resource != null && !resource.isEmpty()) {
throw new IllegalArgumentException("Label and resource name cannot be specified simultaneously.");
}
}

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

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand Down Expand Up @@ -30,51 +31,59 @@ public class LockStepExecution extends AbstractStepExecutionImpl {

@Override
public boolean start() throws Exception {
if (LockableResourcesManager.get().createResource(step.resource)) {
listener.getLogger().println("Resource [" + step.resource + "] did not exist. Created.");
step.validate();

listener.getLogger().println("Trying to acquire lock on [" + step + "]");
List<String> resources = new ArrayList<String>();
if (step.resource != null) {
if (LockableResourcesManager.get().createResource(step.resource)) {
listener.getLogger().println("Resource [" + step + "] did not exist. Created.");
}
resources.add(step.resource);
}
listener.getLogger().println("Trying to acquire lock on [" + step.resource + "]");
LockableResourcesStruct resourceHolder = new LockableResourcesStruct(step.resource);
if(!LockableResourcesManager.get().lock(resourceHolder.required, run, getContext(), step.inversePrecedence)) {
// we have to wait
listener.getLogger().println("[" + step.resource + "] is locked, waiting...");
} // proceed is called inside lock otherwise
LockableResourcesStruct resourceHolder = new LockableResourcesStruct(resources, step.label, step.quantity);
// determine if there are enough resources available to proceed
List<LockableResource> available = LockableResourcesManager.get().checkResourcesAvailability(resourceHolder, listener.getLogger(), null);
if (available == null || !LockableResourcesManager.get().lock(available, run, getContext(), step.toString(), step.inversePrecedence)) {
listener.getLogger().println("[" + step + "] is locked, waiting...");
LockableResourcesManager.get().queueContext(getContext(), resourceHolder, step.toString());
} // proceed is called inside lock if execution is possible
return false;
}

public static void proceed(StepContext context, String resource, boolean inversePrecedence) {
LockableResourcesStruct resourceHolder = new LockableResourcesStruct(resource);
public static void proceed(List<String> resourcenames, StepContext context, String resourceDescription, boolean inversePrecedence) {
Run<?, ?> r = null;
try {
r = context.get(Run.class);
context.get(TaskListener.class).getLogger().println("Lock acquired on [" + resource + "]");
context.get(TaskListener.class).getLogger().println("Lock acquired on [" + resourceDescription + "]");
} catch (Exception e) {
context.onFailure(e);
return;
}

LOGGER.finest("Lock acquired on [" + resource + "] by " + r.getExternalizableId());
LOGGER.finest("Lock acquired on [" + resourceDescription + "] by " + r.getExternalizableId());
context.newBodyInvoker().
withCallback(new Callback(resourceHolder, inversePrecedence)).
withCallback(new Callback(resourcenames, resourceDescription, inversePrecedence)).
withDisplayName(null).
start();
}

private static final class Callback extends BodyExecutionCallback.TailCall {

private final LockableResourcesStruct resourceHolder;
private final List<String> resourceNames;
private final String resourceDescription;
private final boolean inversePrecedence;

Callback(LockableResourcesStruct resourceHolder, boolean inversePrecedence) {
// It's granted to contain one item (and only one for now)
this.resourceHolder = resourceHolder;
Callback(List<String> resourceNames, String resourceDescription, boolean inversePrecedence) {
this.resourceNames = resourceNames;
this.resourceDescription = resourceDescription;
this.inversePrecedence = inversePrecedence;
}

protected void finished(StepContext context) throws Exception {
LockableResourcesManager.get().unlock(resourceHolder.required, context.get(Run.class), context, inversePrecedence);
context.get(TaskListener.class).getLogger().println("Lock released on resource [" + resourceHolder.required.get(0) + "]");
LOGGER.finest("Lock released on [" + resourceHolder.required.get(0) + "]");
LockableResourcesManager.get().unlockNames(this.resourceNames, context.get(Run.class), this.inversePrecedence);
context.get(TaskListener.class).getLogger().println("Lock released on resource [" + resourceDescription + "]");
LOGGER.finest("Lock released on [" + resourceDescription + "]");
}

private static final long serialVersionUID = 1L;
Expand All @@ -83,7 +92,7 @@ protected void finished(StepContext context) throws Exception {

@Override
public void stop(Throwable cause) throws Exception {
boolean cleaned = LockableResourcesManager.get().cleanWaitingContext(LockableResourcesManager.get().fromName(step.resource), getContext());
boolean cleaned = LockableResourcesManager.get().unqueueContext(getContext());
if (!cleaned) {
LOGGER.log(Level.WARNING, "Cannot remove context from lockable resource witing list. The context is not in the waiting list.");
}
Expand Down
Expand Up @@ -66,9 +66,11 @@ public class LockableResource extends AbstractDescribableImpl<LockableResource>
private long queuingStarted = 0;

/**
* Only used when this lockable resource is tried to be locked by {@link LockStep},
* otherwise (freestyle builds) regular Jenkins queue is used.
* Was used within the initial implementation of Pipeline functionality
* using {@link LockStep}, but became deprecated once several resources
* could be locked at once. See queuedContexts in {@link LockableResourcesManager}.
*/
@Deprecated
private List<StepContext> queuedContexts = new ArrayList<StepContext>();

@Deprecated
Expand All @@ -92,6 +94,11 @@ private Object readResolve() {
return this;
}

@Deprecated
public List<StepContext> getQueuedContexts() {
return this.queuedContexts;
}

@DataBoundSetter
public void setDescription(String description) {
this.description = description;
Expand All @@ -117,11 +124,6 @@ public String getLabels() {
return labels;
}


public Integer getContextsInQueue() {
return queuedContexts.size();
}

public boolean isValidLabel(String candidate, Map<String, Object> params) {
return candidate.startsWith(GROOVY_LABEL_MARKER) ? expressionMatches(
candidate, params) : labelsContain(candidate);
Expand Down Expand Up @@ -296,49 +298,6 @@ private void validateQueuingTimeout() {
}
}

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

/**
* Returns the next context (if exists) waiting to get thye lock.
* It removes the returned context from the queue.
*/
@CheckForNull
/* package */ StepContext getNextQueuedContext(boolean inversePrecedence) {
if (queuedContexts.size() > 0) {
if (!inversePrecedence) {
return queuedContexts.remove(0);
} else {
long newest = 0;
int index = 0;
int newestIndex = 0;
for(Iterator<StepContext> iterator = queuedContexts.iterator(); iterator.hasNext();) {
StepContext c = iterator.next();
try {
Run<?, ?> run = c.get(Run.class);
if (run.getStartTimeInMillis() > newest) {
newest = run.getStartTimeInMillis();
newestIndex = index;
}
} catch (Exception e) {
// skip this one and remove from queue
iterator.remove();
LOGGER.log(Level.FINE, "Skipping queued context as it does not hold a Run object", e);
}
index++;
}

return queuedContexts.remove(newestIndex);
}
}
return null;
}

/* package */ boolean remove(StepContext context) {
return queuedContexts.remove(context);
}

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

0 comments on commit 974572d

Please sign in to comment.