提交 ecac963e 编写于 作者: S Stephen Connolly

Merge pull request #1596 from stephenc/threadsafe-node-queue

[JENKINS-27565] Fix threading issues with Nodes and Queue
......@@ -28,6 +28,7 @@ package hudson;
import hudson.cli.CLICommand;
import hudson.console.ConsoleAnnotationDescriptor;
import hudson.console.ConsoleAnnotatorFactory;
import hudson.init.InitMilestone;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.Describable;
......@@ -201,7 +202,25 @@ public class Functions {
public static String rfc822Date(Calendar cal) {
return Util.RFC822_DATETIME_FORMATTER.format(cal.getTime());
}
/**
* During Jenkins start-up, before {@link InitMilestone#PLUGINS_STARTED} the extensions lists will be empty
* and they are not guaranteed to be fully populated until after {@link InitMilestone#EXTENSIONS_AUGMENTED}.
* If you attempt to access the extensions list from a UI thread while the extensions are being loaded you will
* hit a big honking great monitor lock that will block until the effective extension list has been determined
* (as if a plugin fails to start, all of the failed plugin's extensions and any dependent plugins' extensions
* will have to be evicted from the list of extensions. In practical terms this only affects the
* "Jenkins is loading" screen, but as that screen uses the generic layouts we provide this utility method
* so that the generic layouts can avoid iterating extension lists while Jenkins is starting up.
*
* @return {@code true} if the extensions lists have been populated.
* @since 1.FIXME
*/
public static boolean isExtensionsAvailable() {
final Jenkins jenkins = Jenkins.getInstance();
return jenkins != null && jenkins.getInitLevel().compareTo(InitMilestone.EXTENSIONS_AUGMENTED) >= 0;
}
public static void initPageVariables(JellyContext context) {
StaplerRequest currentRequest = Stapler.getCurrentRequest();
String rootURL = currentRequest.getContextPath();
......
......@@ -34,7 +34,6 @@ import jenkins.model.Jenkins;
import org.kohsuke.stapler.StaplerFallback;
import org.kohsuke.stapler.StaplerProxy;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Logger;
......@@ -48,8 +47,6 @@ public abstract class AbstractCIBase extends Node implements ItemGroup<TopLevelI
private static final Logger LOGGER = Logger.getLogger(AbstractCIBase.class.getName());
private final transient Object updateComputerLock = new Object();
/**
* If you are calling this on Hudson something is wrong.
*
......@@ -137,15 +134,20 @@ public abstract class AbstractCIBase extends Node implements ItemGroup<TopLevelI
used.add(c);
}
/*package*/ void removeComputer(Computer computer) {
Map<Node,Computer> computers = getComputerMap();
for (Map.Entry<Node, Computer> e : computers.entrySet()) {
if (e.getValue() == computer) {
computers.remove(e.getKey());
computer.onRemoved();
return;
/*package*/ void removeComputer(final Computer computer) {
Queue.withLock(new Runnable() {
@Override
public void run() {
Map<Node,Computer> computers = getComputerMap();
for (Map.Entry<Node, Computer> e : computers.entrySet()) {
if (e.getValue() == computer) {
computers.remove(e.getKey());
computer.onRemoved();
return;
}
}
}
}
});
}
/*package*/ @CheckForNull Computer getComputer(Node n) {
......@@ -160,36 +162,46 @@ public abstract class AbstractCIBase extends Node implements ItemGroup<TopLevelI
* This method tries to reuse existing {@link Computer} objects
* so that we won't upset {@link Executor}s running in it.
*/
protected void updateComputerList(boolean automaticSlaveLaunch) throws IOException {
Map<Node,Computer> computers = getComputerMap();
synchronized(updateComputerLock) {// just so that we don't have two code updating computer list at the same time
Map<String,Computer> byName = new HashMap<String,Computer>();
for (Computer c : computers.values()) {
Node node = c.getNode();
if (node == null)
continue; // this computer is gone
byName.put(node.getNodeName(),c);
}
protected void updateComputerList(final boolean automaticSlaveLaunch) {
final Map<Node,Computer> computers = getComputerMap();
final Set<Computer> old = new HashSet<Computer>(computers.size());
Queue.withLock(new Runnable() {
@Override
public void run() {
Map<String,Computer> byName = new HashMap<String,Computer>();
for (Computer c : computers.values()) {
old.add(c);
Node node = c.getNode();
if (node == null)
continue; // this computer is gone
byName.put(node.getNodeName(),c);
}
final Set<Computer> old = new HashSet<Computer>(computers.values());
Set<Computer> used = new HashSet<Computer>();
Set<Computer> used = new HashSet<Computer>(old.size());
updateComputer(this, byName, used, automaticSlaveLaunch);
for (Node s : getNodes()) {
long start = System.currentTimeMillis();
updateComputer(s, byName, used, automaticSlaveLaunch);
if(LOG_STARTUP_PERFORMANCE)
LOGGER.info(String.format("Took %dms to update node %s",
System.currentTimeMillis()-start, s.getNodeName()));
}
updateComputer(AbstractCIBase.this, byName, used, automaticSlaveLaunch);
for (Node s : getNodes()) {
long start = System.currentTimeMillis();
updateComputer(s, byName, used, automaticSlaveLaunch);
if(LOG_STARTUP_PERFORMANCE)
LOGGER.info(String.format("Took %dms to update node %s",
System.currentTimeMillis()-start, s.getNodeName()));
}
// find out what computers are removed, and kill off all executors.
// when all executors exit, it will be removed from the computers map.
// so don't remove too quickly
old.removeAll(used);
for (Computer c : old) {
killComputer(c);
// find out what computers are removed, and kill off all executors.
// when all executors exit, it will be removed from the computers map.
// so don't remove too quickly
old.removeAll(used);
// we need to start the process of reducing the executors on all computers as distinct
// from the killing action which should not excessively use the Queue lock.
for (Computer c : old) {
c.inflictMortalWound();
}
}
});
for (Computer c : old) {
// when we get to here, the number of executors should be zero so this call should not need the Queue.lock
killComputer(c);
}
getQueue().scheduleMaintenance();
for (ComputerListener cl : ComputerListener.all())
......
......@@ -45,6 +45,7 @@ import hudson.security.AccessControlled;
import hudson.security.Permission;
import hudson.security.PermissionGroup;
import hudson.security.PermissionScope;
import hudson.slaves.AbstractCloudSlave;
import hudson.slaves.ComputerLauncher;
import hudson.slaves.ComputerListener;
import hudson.slaves.NodeProperty;
......@@ -63,8 +64,11 @@ import hudson.util.NamingThreadFactory;
import jenkins.model.Jenkins;
import jenkins.util.ContextResettingExecutorService;
import jenkins.security.MasterToSlaveCallable;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.QueryParameter;
......@@ -77,6 +81,7 @@ import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.args4j.Option;
import org.kohsuke.stapler.interceptor.RequirePOST;
import javax.annotation.concurrent.GuardedBy;
import javax.servlet.ServletException;
import java.io.File;
import java.io.FilenameFilter;
......@@ -176,6 +181,58 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
protected final Object statusChangeLock = new Object();
/**
* Keeps track of stack traces to track the tremination requests for this computer.
*
* @since 1.FIXME
* @see Executor#resetWorkUnit(String)
*/
private transient final List<TerminationRequest> terminatedBy = Collections.synchronizedList(new ArrayList
<TerminationRequest>());
/**
* This method captures the information of a request to terminate a computer instance. Method is public as
* it needs to be called from {@link AbstractCloudSlave} and {@link jenkins.model.Nodes}. In general you should
* not need to call this method directly, however if implementing a custom node type or a different path
* for removing nodes, it may make sense to call this method in order to capture the originating request.
*
* @since 1.FIXME
*/
public void recordTermination() {
StaplerRequest request = Stapler.getCurrentRequest();
if (request != null) {
terminatedBy.add(new TerminationRequest(
String.format("Termination requested at %s by %s [id=%d] from HTTP request for %s",
new Date(),
Thread.currentThread(),
Thread.currentThread().getId(),
request.getRequestURL()
)
));
} else {
terminatedBy.add(new TerminationRequest(
String.format("Termination requested at %s by %s [id=%d]",
new Date(),
Thread.currentThread(),
Thread.currentThread().getId()
)
));
}
}
/**
* Returns the list of captured termination requests for this Computer. This method is used by {@link Executor}
* to provide details on why a Computer was removed in-between work being scheduled against the {@link Executor}
* and the {@link Executor} starting to execute the task.
*
* @return the (possibly empty) list of termination requests.
* @see Executor#resetWorkUnit(String)
* @since 1.FIXME
*/
public List<TerminationRequest> getTerminatedBy() {
return new ArrayList<TerminationRequest>(terminatedBy);
}
public Computer(Node node) {
setNode(node);
}
......@@ -404,6 +461,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
* @since 1.320
*/
public Future<?> disconnect(OfflineCause cause) {
recordTermination();
offlineCause = cause;
if (Util.isOverridden(Computer.class,getClass(),"disconnect"))
return disconnect(); // legacy subtypes that extend disconnect().
......@@ -419,6 +477,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
* Use {@link #disconnect(OfflineCause)} and specify the cause.
*/
public Future<?> disconnect() {
recordTermination();
if (Util.isOverridden(Computer.class,getClass(),"disconnect",OfflineCause.class))
// if the subtype already derives disconnect(OfflineCause), delegate to it
return disconnect(null);
......@@ -442,7 +501,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
@CLIMethod(name="offline-node")
public void cliOffline(@Option(name="-m",usage="Record the note about why you are disconnecting this node") String cause) throws ExecutionException, InterruptedException {
checkPermission(DISCONNECT);
setTemporarilyOffline(true,new ByCLI(cause));
setTemporarilyOffline(true, new ByCLI(cause));
}
@CLIMethod(name="online-node")
......@@ -698,6 +757,26 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
* @see #onRemoved()
*/
protected void kill() {
// On most code paths, this should already be zero, and thus this next call becomes a no-op... and more
// importantly it will not acquire a lock on the Queue... not that the lock is bad, more that the lock
// may delay unnecessarily
setNumExecutors(0);
}
/**
* Called by {@link Jenkins#updateComputerList()} to notify {@link Computer} that it will be discarded.
*
* <p>
* Note that at this point {@link #getNode()} returns null.
*
* <p>
* Note that the Queue lock is already held when this method is called.
*
* @see #onRemoved()
*/
@Restricted(NoExternalUse.class)
@GuardedBy("hudson.model.Queue.lock")
/*package*/ void inflictMortalWound() {
setNumExecutors(0);
}
......@@ -808,6 +887,28 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
return new ArrayList<OneOffExecutor>(oneOffExecutors);
}
/**
* Used to render the list of executors.
* @return a snapshot of the executor display information
* @since 1.FIXME
*/
@Restricted(NoExternalUse.class)
public List<DisplayExecutor> getDisplayExecutors() {
// The size may change while we are populating, but let's start with a reasonable guess to minimize resizing
List<DisplayExecutor> result = new ArrayList<DisplayExecutor>(executors.size()+oneOffExecutors.size());
int index = 0;
for (Executor e: executors) {
result.add(new DisplayExecutor(Integer.toString(index+1), String.format("executors/%d", index), e));
index++;
}
index = 0;
for (OneOffExecutor e: oneOffExecutors) {
result.add(new DisplayExecutor("", String.format("oneOffExecutors/%d", index), e));
index++;
}
return result;
}
/**
* Returns true if all the executors of this computer are idle.
*/
......@@ -867,14 +968,21 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
/**
* Called by {@link Executor} to kill excessive executors from this computer.
*/
/*package*/ synchronized void removeExecutor(Executor e) {
executors.remove(e);
addNewExecutorIfNecessary();
if(!isAlive()) // TODO except from interrupt/doYank this is called while the executor still isActive(), so how could !this.isAlive()?
{
AbstractCIBase ciBase = Jenkins.getInstance();
ciBase.removeComputer(this);
}
/*package*/ void removeExecutor(final Executor e) {
Queue.withLock(new Runnable() {
@Override
public void run() {
synchronized (Computer.this) {
executors.remove(e);
addNewExecutorIfNecessary();
if (!isAlive()) // TODO except from interrupt/doYank this is called while the executor still isActive(), so how could !this.isAlive()?
{
AbstractCIBase ciBase = Jenkins.getInstance();
ciBase.removeComputer(Computer.this);
}
}
}
});
}
/**
......@@ -1122,12 +1230,13 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
public void doRssAll( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
rss(req, rsp, " all builds", getBuilds());
}
public void doRssFailed( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
public void doRssFailed(StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
rss(req, rsp, " failed builds", getBuilds().failureOnly());
}
private void rss(StaplerRequest req, StaplerResponse rsp, String suffix, RunList runs) throws IOException, ServletException {
RSS.forwardToRss(getDisplayName()+ suffix, getUrl(),
runs.newBuilds(), Run.FEED_ADAPTER, req, rsp );
RSS.forwardToRss(getDisplayName() + suffix, getUrl(),
runs.newBuilds(), Run.FEED_ADAPTER, req, rsp);
}
@RequirePOST
......@@ -1205,7 +1314,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
_doScript(req, rsp, "_scriptText.jelly");
}
protected void _doScript( StaplerRequest req, StaplerResponse rsp, String view) throws IOException, ServletException {
protected void _doScript(StaplerRequest req, StaplerResponse rsp, String view) throws IOException, ServletException {
Jenkins._doScript(req, rsp, req.getView(this, view), getChannel(), getACL());
}
......@@ -1329,7 +1438,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
* Handles incremental log.
*/
public void doProgressiveLog( StaplerRequest req, StaplerResponse rsp) throws IOException {
getLogText().doProgressText(req,rsp);
getLogText().doProgressText(req, rsp);
}
/**
......@@ -1414,6 +1523,101 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces
}
}
/**
* A value class to provide a consistent snapshot view of the state of an executor to avoid race conditions
* during rendering of the executors list.
*
* @since 1.FIXME
*/
@Restricted(NoExternalUse.class)
public static class DisplayExecutor implements ModelObject {
@Nonnull
private final String displayName;
@Nonnull
private final String url;
@Nonnull
private final Executor executor;
public DisplayExecutor(@Nonnull String displayName, @Nonnull String url, @Nonnull Executor executor) {
this.displayName = displayName;
this.url = url;
this.executor = executor;
}
@Override
@Nonnull
public String getDisplayName() {
return displayName;
}
@Nonnull
public String getUrl() {
return url;
}
@Nonnull
public Executor getExecutor() {
return executor;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("DisplayExecutor{");
sb.append("displayName='").append(displayName).append('\'');
sb.append(", url='").append(url).append('\'');
sb.append(", executor=").append(executor);
sb.append('}');
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DisplayExecutor that = (DisplayExecutor) o;
if (!executor.equals(that.executor)) {
return false;
}
return true;
}
@Override
public int hashCode() {
return executor.hashCode();
}
}
/**
* Used to trace requests to terminate a computer.
*
* @since 1.FIXME
*/
public static class TerminationRequest extends RuntimeException {
private final long when;
public TerminationRequest(String message) {
super(message);
this.when = System.currentTimeMillis();
}
/**
* Returns the when the termination request was created.
*
* @return the difference, measured in milliseconds, between
* the time of the termination request and midnight, January 1, 1970 UTC.
*/
public long getWhen() {
return when;
}
}
public static final PermissionGroup PERMISSIONS = new PermissionGroup(Computer.class,Messages._Computer_Permissions_Title());
public static final Permission CONFIGURE = new Permission(PERMISSIONS,"Configure", Messages._Computer_ConfigurePermission_Description(), Permission.CONFIGURE, PermissionScope.COMPUTER);
/**
......
......@@ -46,13 +46,18 @@ import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.interceptor.RequirePOST;
import javax.annotation.concurrent.GuardedBy;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
......@@ -75,7 +80,9 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
public class Executor extends Thread implements ModelObject {
protected final @Nonnull Computer owner;
private final Queue queue;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
@GuardedBy("lock")
private long startTime;
/**
* Used to track when a job was last executed.
......@@ -89,35 +96,41 @@ public class Executor extends Thread implements ModelObject {
/**
* {@link hudson.model.Queue.Executable} being executed right now, or null if the executor is idle.
*/
private volatile Queue.Executable executable;
@GuardedBy("lock")
private Queue.Executable executable;
/**
* Used to mark that the execution is continuing asynchronously even though {@link Executor} as {@link Thread}
* has finished.
*/
@GuardedBy("lock")
private AsynchronousExecution asynchronousExecution;
/**
* When {@link Queue} allocates a work for this executor, this field is set
* and the executor is {@linkplain Thread#start() started}.
*/
private volatile WorkUnit workUnit;
@GuardedBy("lock")
private WorkUnit workUnit;
private Throwable causeOfDeath;
private boolean induceDeath;
private volatile boolean started;
@GuardedBy("lock")
private boolean started;
/**
* When the executor is interrupted, we allow the code that interrupted the thread to override the
* result code it prefers.
*/
@GuardedBy("lock")
private Result interruptStatus;
/**
* Cause of interruption. Access needs to be synchronized.
*/
@GuardedBy("lock")
private final List<CauseOfInterruption> causes = new Vector<CauseOfInterruption>();
public Executor(@Nonnull Computer owner, int n) {
......@@ -167,32 +180,41 @@ public class Executor extends Thread implements ModelObject {
if (LOGGER.isLoggable(FINE))
LOGGER.log(FINE, String.format("%s is interrupted(%s): %s", getDisplayName(), result, Util.join(Arrays.asList(causes),",")), new InterruptedException());
synchronized (this) {
lock.writeLock().lock();
try {
if (!started) {
// not yet started, so simply dispose this
owner.removeExecutor(this);
return;
}
}
interruptStatus = result;
synchronized (this.causes) {
interruptStatus = result;
for (CauseOfInterruption c : causes) {
if (!this.causes.contains(c))
this.causes.add(c);
}
}
if (asynchronousExecution != null) {
asynchronousExecution.interrupt(forShutdown);
} else {
super.interrupt();
if (asynchronousExecution != null) {
asynchronousExecution.interrupt(forShutdown);
} else {
super.interrupt();
}
} finally {
lock.writeLock().unlock();
}
}
public Result abortResult() {
Result r = interruptStatus;
if (r==null) r = Result.ABORTED; // this is when we programmatically throw InterruptedException instead of calling the interrupt method.
return r;
lock.readLock().lock();
try {
Result r = interruptStatus;
if (r == null) r =
Result.ABORTED; // this is when we programmatically throw InterruptedException instead of calling the interrupt method.
return r;
} finally {
lock.readLock().unlock();
}
}
/**
......@@ -203,11 +225,14 @@ public class Executor extends Thread implements ModelObject {
public void recordCauseOfInterruption(Run<?,?> build, TaskListener listener) {
List<CauseOfInterruption> r;
// atomically get&clear causes, and minimize the lock.
synchronized (causes) {
// atomically get&clear causes.
lock.writeLock().lock();
try {
if (causes.isEmpty()) return;
r = new ArrayList<CauseOfInterruption>(causes);
causes.clear();
} finally {
lock.writeLock().unlock();
}
build.addAction(new InterruptedBuildAction(r));
......@@ -215,9 +240,64 @@ public class Executor extends Thread implements ModelObject {
c.print(listener);
}
/**
* There are some cases where an executor is started but the node is removed or goes off-line before we are ready
* to start executing the assigned work unit. This method is called to clear the assigned work unit so that
* the {@link Queue#maintain()} method can return the item to the buildable state.
*
* Note: once we create the {@link Executable} we cannot unwind the state and the build will have to end up being
* marked as a failure.
*/
private void resetWorkUnit(String reason) {
StringWriter writer = new StringWriter();
PrintWriter pw = new PrintWriter(writer);
try {
pw.printf("%s grabbed %s from queue but %s %s. ", getName(), workUnit, owner.getDisplayName(), reason);
if (owner.getTerminatedBy().isEmpty()) {
pw.print("No termination trace available.");
} else {
pw.println("Termination trace follows:");
for (Computer.TerminationRequest request: owner.getTerminatedBy()) {
request.printStackTrace(pw);
}
}
} finally {
pw.close();
}
LOGGER.log(WARNING, writer.toString());
lock.writeLock().lock();
try {
if (executable != null) {
throw new IllegalStateException("Cannot reset the work unit after the executable has been created");
}
workUnit = null;
} finally {
lock.writeLock().unlock();
}
}
@Override
public void run() {
startTime = System.currentTimeMillis();
if (!owner.isOnline()) {
resetWorkUnit("went off-line before the task's worker thread started");
owner.removeExecutor(this);
queue.scheduleMaintenance();
return;
}
if (owner.getNode() == null) {
resetWorkUnit("was removed before the task's worker thread started");
owner.removeExecutor(this);
queue.scheduleMaintenance();
return;
}
final WorkUnit workUnit;
lock.writeLock().lock();
try {
startTime = System.currentTimeMillis();
workUnit = this.workUnit;
} finally {
lock.writeLock().unlock();
}
ACL.impersonate(ACL.SYSTEM);
......@@ -227,14 +307,45 @@ public class Executor extends Thread implements ModelObject {
SubTask task;
// transition from idle to building.
// perform this state change as an atomic operation wrt other queue operations
synchronized (queue) {
workUnit.setExecutor(this);
queue.onStartExecuting(this);
if (LOGGER.isLoggable(FINE))
LOGGER.log(FINE, getName()+" grabbed "+workUnit+" from queue");
task = workUnit.work;
executable = task.createExecutable();
workUnit.setExecutable(executable);
task = Queue.withLock(new java.util.concurrent.Callable<SubTask>() {
@Override
public SubTask call() throws Exception {
if (!owner.isOnline()) {
resetWorkUnit("went off-line before the task's worker thread was ready to execute");
return null;
}
if (owner.getNode() == null) {
resetWorkUnit("was removed before the task's worker thread was ready to execute");
return null;
}
// after this point we cannot unwind the assignment of the work unit, if the owner
// is removed or goes off-line then the build will just have to fail.
workUnit.setExecutor(Executor.this);
queue.onStartExecuting(Executor.this);
if (LOGGER.isLoggable(FINE))
LOGGER.log(FINE, getName()+" grabbed "+workUnit+" from queue");
SubTask task = workUnit.work;
Executable executable = task.createExecutable();
lock.writeLock().lock();
try {
Executor.this.executable = executable;
} finally {
lock.writeLock().unlock();
}
workUnit.setExecutable(executable);
return task;
}
});
Executable executable;
lock.readLock().lock();
try {
if (this.workUnit == null) {
// we called resetWorkUnit, so bail. Outer finally will remove this and schedule queue maintenance
return;
}
executable = this.executable;
} finally {
lock.readLock().unlock();
}
if (LOGGER.isLoggable(FINE))
LOGGER.log(FINE, getName()+" is going to execute "+executable);
......@@ -262,12 +373,24 @@ public class Executor extends Thread implements ModelObject {
LOGGER.log(FINE, getName()+" is now executing "+executable);
queue.execute(executable, task);
} catch (AsynchronousExecution x) {
x.setExecutor(this);
this.asynchronousExecution = x;
lock.writeLock().lock();
try {
x.setExecutor(this);
this.asynchronousExecution = x;
} finally {
lock.writeLock().unlock();
}
} catch (Throwable e) {
problems = e;
} finally {
if (asynchronousExecution == null) {
boolean needFinish1;
lock.readLock().lock();
try {
needFinish1 = asynchronousExecution == null;
} finally {
lock.readLock().unlock();
}
if (needFinish1) {
finish1(problems);
}
}
......@@ -286,7 +409,7 @@ public class Executor extends Thread implements ModelObject {
}
}
}
private void finish1(@CheckForNull Throwable problems) {
if (problems != null) {
// for some reason the executor died. this is really
......@@ -296,7 +419,7 @@ public class Executor extends Thread implements ModelObject {
workUnit.context.abort(problems);
}
long time = System.currentTimeMillis() - startTime;
LOGGER.log(FINE, "{0} completed {1} in {2}ms", new Object[] {getName(), executable, time});
LOGGER.log(FINE, "{0} completed {1} in {2}ms", new Object[]{getName(), executable, time});
try {
workUnit.context.synchronizeEnd(this, executable, problems, time);
} catch (InterruptedException e) {
......@@ -307,6 +430,7 @@ public class Executor extends Thread implements ModelObject {
}
private void finish2() {
for (RuntimeException e1: owner.getTerminatedBy()) LOGGER.log(Level.WARNING, String.format("%s termination trace", getName()), e1);
if (causeOfDeath == null) {// let this thread die and be replaced by a fresh unstarted instance
owner.removeExecutor(this);
}
......@@ -341,7 +465,12 @@ public class Executor extends Thread implements ModelObject {
*/
@Exported
public @CheckForNull Queue.Executable getCurrentExecutable() {
return executable;
lock.readLock().lock();
try {
return executable;
} finally {
lock.readLock().unlock();
}
}
/**
......@@ -353,7 +482,12 @@ public class Executor extends Thread implements ModelObject {
*/
@Exported
public WorkUnit getCurrentWorkUnit() {
return workUnit;
lock.readLock().lock();
try {
return workUnit;
} finally {
lock.readLock().unlock();
}
}
/**
......@@ -362,13 +496,19 @@ public class Executor extends Thread implements ModelObject {
* to that point yet.
*/
public FilePath getCurrentWorkspace() {
Executable e = executable;
if(e==null) return null;
if (e instanceof AbstractBuild) {
AbstractBuild ab = (AbstractBuild) e;
return ab.getWorkspace();
lock.readLock().lock();
try {
if (executable == null) {
return null;
}
if (executable instanceof AbstractBuild) {
AbstractBuild ab = (AbstractBuild) executable;
return ab.getWorkspace();
}
return null;
} finally {
lock.readLock().unlock();
}
return null;
}
/**
......@@ -395,14 +535,24 @@ public class Executor extends Thread implements ModelObject {
*/
@Exported
public boolean isIdle() {
return executable==null;
lock.readLock().lock();
try {
return workUnit == null && executable == null;
} finally {
lock.readLock().unlock();
}
}
/**
* The opposite of {@link #isIdle()} &mdash; the executor is doing some work.
*/
public boolean isBusy() {
return executable!=null;
lock.readLock().lock();
try {
return workUnit != null || executable != null;
} finally {
lock.readLock().unlock();
}
}
/**
......@@ -415,7 +565,12 @@ public class Executor extends Thread implements ModelObject {
* @since 1.536
*/
public boolean isActive() {
return !started || asynchronousExecution != null || isAlive();
lock.readLock().lock();
try {
return !started || asynchronousExecution != null || isAlive();
} finally {
lock.readLock().unlock();
}
}
/**
......@@ -423,14 +578,24 @@ public class Executor extends Thread implements ModelObject {
* @since TODO
*/
public @CheckForNull AsynchronousExecution getAsynchronousExecution() {
return asynchronousExecution;
lock.readLock().lock();
try {
return asynchronousExecution;
} finally {
lock.readLock().unlock();
}
}
/**
* Returns true if this executor is waiting for a task to execute.
*/
public boolean isParking() {
return !started;
lock.readLock().lock();
try {
return !started;
} finally {
lock.readLock().unlock();
}
}
/**
......@@ -451,13 +616,24 @@ public class Executor extends Thread implements ModelObject {
*/
@Exported
public int getProgress() {
Queue.Executable e = executable;
if(e==null) return -1;
long d = Executables.getEstimatedDurationFor(e);
if(d<0) return -1;
long d;
lock.readLock().lock();
try {
if (executable == null) {
return -1;
}
d = Executables.getEstimatedDurationFor(executable);
} finally {
lock.readLock().unlock();
}
if (d < 0) {
return -1;
}
int num = (int)(getElapsedTime()*100/d);
if(num>=100) num=99;
int num = (int) (getElapsedTime() * 100 / d);
if (num >= 100) {
num = 99;
}
return num;
}
......@@ -470,22 +646,35 @@ public class Executor extends Thread implements ModelObject {
*/
@Exported
public boolean isLikelyStuck() {
Queue.Executable e = executable;
if(e==null) return false;
long d;
long elapsed;
lock.readLock().lock();
try {
if (executable == null) {
return false;
}
long elapsed = getElapsedTime();
long d = Executables.getEstimatedDurationFor(e);
if(d>=0) {
elapsed = getElapsedTime();
d = Executables.getEstimatedDurationFor(executable);
} finally {
lock.readLock().unlock();
}
if (d >= 0) {
// if it's taking 10 times longer than ETA, consider it stuck
return d*10 < elapsed;
return d * 10 < elapsed;
} else {
// if no ETA is available, a build taking longer than a day is considered stuck
return TimeUnit2.MILLISECONDS.toHours(elapsed)>24;
return TimeUnit2.MILLISECONDS.toHours(elapsed) > 24;
}
}
public long getElapsedTime() {
return System.currentTimeMillis() - startTime;
lock.readLock().lock();
try {
return System.currentTimeMillis() - startTime;
} finally {
lock.readLock().unlock();
}
}
/**
......@@ -494,7 +683,12 @@ public class Executor extends Thread implements ModelObject {
* @since 1.440
*/
public long getTimeSpentInQueue() {
return startTime - workUnit.context.item.buildableStartMilliseconds;
lock.readLock().lock();
try {
return startTime - workUnit.context.item.buildableStartMilliseconds;
} finally {
lock.readLock().unlock();
}
}
/**
......@@ -512,14 +706,25 @@ public class Executor extends Thread implements ModelObject {
* until the build completes.
*/
public String getEstimatedRemainingTime() {
Queue.Executable e = executable;
if(e==null) return Messages.Executor_NotAvailable();
long d;
lock.readLock().lock();
try {
if (executable == null) {
return Messages.Executor_NotAvailable();
}
long d = Executables.getEstimatedDurationFor(e);
if(d<0) return Messages.Executor_NotAvailable();
d = Executables.getEstimatedDurationFor(executable);
} finally {
lock.readLock().unlock();
}
if (d < 0) {
return Messages.Executor_NotAvailable();
}
long eta = d-getElapsedTime();
if(eta<=0) return Messages.Executor_NotAvailable();
long eta = d - getElapsedTime();
if (eta <= 0) {
return Messages.Executor_NotAvailable();
}
return Util.getTimeSpanString(eta);
}
......@@ -529,14 +734,25 @@ public class Executor extends Thread implements ModelObject {
* it as a number of milli-seconds.
*/
public long getEstimatedRemainingTimeMillis() {
Queue.Executable e = executable;
if(e==null) return -1;
long d;
lock.readLock().lock();
try {
if (executable == null) {
return -1;
}
long d = Executables.getEstimatedDurationFor(e);
if(d<0) return -1;
d = Executables.getEstimatedDurationFor(executable);
} finally {
lock.readLock().unlock();
}
if (d < 0) {
return -1;
}
long eta = d-getElapsedTime();
if(eta<=0) return -1;
long eta = d - getElapsedTime();
if (eta <= 0) {
return -1;
}
return eta;
}
......@@ -547,14 +763,19 @@ public class Executor extends Thread implements ModelObject {
* @see #start(WorkUnit)
*/
@Override
public synchronized void start() {
public void start() {
throw new UnsupportedOperationException();
}
/*protected*/ synchronized void start(WorkUnit task) {
this.workUnit = task;
super.start();
started = true;
/*protected*/ void start(WorkUnit task) {
lock.writeLock().lock();
try {
this.workUnit = task;
super.start();
started = true;
} finally {
lock.writeLock().unlock();
}
}
......@@ -574,10 +795,14 @@ public class Executor extends Thread implements ModelObject {
*/
@RequirePOST
public HttpResponse doStop() {
Queue.Executable e = executable;
if(e!=null) {
Tasks.getOwnerTaskOf(getParentOf(e)).checkAbortPermission();
interrupt();
lock.writeLock().lock(); // need write lock as interrupt will change the field
try {
if (executable != null) {
Tasks.getOwnerTaskOf(getParentOf(executable)).checkAbortPermission();
interrupt();
}
} finally {
lock.writeLock().unlock();
}
return HttpResponses.forwardToPreviousPage();
}
......@@ -598,8 +823,12 @@ public class Executor extends Thread implements ModelObject {
* Checks if the current user has a permission to stop this build.
*/
public boolean hasStopPermission() {
Queue.Executable e = executable;
return e!=null && Tasks.getOwnerTaskOf(getParentOf(e)).hasAbortPermission();
lock.readLock().lock();
try {
return executable != null && Tasks.getOwnerTaskOf(getParentOf(executable)).hasAbortPermission();
} finally {
lock.readLock().unlock();
}
}
public @Nonnull Computer getOwner() {
......@@ -610,11 +839,16 @@ public class Executor extends Thread implements ModelObject {
* Returns when this executor started or should start being idle.
*/
public long getIdleStartMilliseconds() {
if (isIdle())
return Math.max(creationTime, owner.getConnectTime());
else {
return Math.max(startTime + Math.max(0, Executables.getEstimatedDurationFor(executable)),
System.currentTimeMillis() + 15000);
lock.readLock().lock();
try {
if (isIdle())
return Math.max(creationTime, owner.getConnectTime());
else {
return Math.max(startTime + Math.max(0, Executables.getEstimatedDurationFor(executable)),
System.currentTimeMillis() + 15000);
}
} finally {
lock.readLock().unlock();
}
}
......
......@@ -121,7 +121,7 @@ public class Hudson extends Jenkins {
* Use {@link #getNodes()}. Since 1.252.
*/
public List<Slave> getSlaves() {
return (List)slaves;
return (List)getNodes();
}
/**
......
......@@ -373,6 +373,10 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable
if (c!=null) return c;
}
if (!isAcceptingTasks()) {
return CauseOfBlockage.fromMessage(Messages._Node_BecauseNodeIsNotAcceptingTasks(getNodeName()));
}
// Looks like we can take the task
return null;
}
......
......@@ -88,6 +88,7 @@ import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
......@@ -97,6 +98,9 @@ import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
......@@ -140,9 +144,20 @@ import org.kohsuke.stapler.interceptor.RequirePOST;
* | |
* | v
* +--> buildables ---> pending ---> left
* ^ |
* | |
* +---(rarely)---+
* </pre>
*
* <p>
* Note: In the normal case of events pending items only move to left. However they can move back
* if the node they are assigned to execute on disappears before their {@link Executor} thread
* starts, where the node is removed before the {@link Executable} has been instantiated it
* is safe to move the pending item back to buildable. Once the {@link Executable} has been
* instantiated the only option is to let the {@link Executable} bomb out as soon as it starts
* to try an execute on the node that no longer exists.
*
* <p>
* In addition, at any stage, an item can be removed from the queue (for example, when the user
* cancels a job in the queue.) See the corresponding field for their exact meanings.
*
......@@ -191,6 +206,8 @@ public class Queue extends ResourceController implements Saveable {
*/
private final ItemList<BuildableItem> pendings = new ItemList<BuildableItem>();
private transient volatile Snapshot snapshot = new Snapshot(waitingList, blockedProjects, buildables, pendings);
/**
* Items that left queue would stay here for a while to enable tracking via {@link Item#getId()}.
*
......@@ -319,6 +336,10 @@ public class Queue extends ResourceController implements Saveable {
}
});
private transient final Lock lock = new ReentrantLock();
private transient final Condition condition = lock.newCondition();
public Queue(@Nonnull LoadBalancer loadBalancer) {
this.loadBalancer = loadBalancer.sanitize();
// if all the executors are busy doing something, then the queue won't be maintained in
......@@ -354,8 +375,9 @@ public class Queue extends ResourceController implements Saveable {
/**
* Loads the queue contents that was {@link #save() saved}.
*/
public synchronized void load() {
try {
public void load() {
lock.lock();
try { try {
// first try the old format
File queueFile = getQueueFile();
if (queueFile.exists()) {
......@@ -430,45 +452,58 @@ public class Queue extends ResourceController implements Saveable {
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to load the queue file " + getXMLQueueFile(), e);
} finally { updateSnapshot(); } } finally {
lock.unlock();
}
}
/**
* Persists the queue contents to the disk.
*/
public synchronized void save() {
public void save() {
if(BulkChange.contains(this)) return;
// write out the queue state we want to save
State state = new State();
state.counter = WaitingItem.COUNTER.longValue();
// write out the tasks on the queue
for (Item item: getItems()) {
if(item.task instanceof TransientTask) continue;
state.items.add(item);
}
XmlFile queueFile = new XmlFile(XSTREAM, getXMLQueueFile());
lock.lock();
try {
XmlFile queueFile = new XmlFile(XSTREAM, getXMLQueueFile());
queueFile.write(state);
SaveableListener.fireOnChange(this, queueFile);
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to write out the queue file " + getXMLQueueFile(), e);
// write out the queue state we want to save
State state = new State();
state.counter = WaitingItem.COUNTER.longValue();
// write out the tasks on the queue
for (Item item: getItems()) {
if(item.task instanceof TransientTask) continue;
state.items.add(item);
}
try {
queueFile.write(state);
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to write out the queue file " + getXMLQueueFile(), e);
}
} finally {
lock.unlock();
}
SaveableListener.fireOnChange(this, queueFile);
}
/**
* Wipes out all the items currently in the queue, as if all of them are cancelled at once.
*/
@CLIMethod(name="clear-queue")
public synchronized void clear() {
public void clear() {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
for (WaitingItem i : new ArrayList<WaitingItem>(waitingList)) // copy the list as we'll modify it in the loop
i.cancel(this);
blockedProjects.cancelAll();
pendings.cancelAll();
buildables.cancelAll();
lock.lock();
try { try {
for (WaitingItem i : new ArrayList<WaitingItem>(
waitingList)) // copy the list as we'll modify it in the loop
i.cancel(this);
blockedProjects.cancelAll();
pendings.cancelAll();
buildables.cancelAll();
} finally { updateSnapshot(); } } finally {
lock.unlock();
}
scheduleMaintenance();
}
......@@ -514,7 +549,7 @@ public class Queue extends ResourceController implements Saveable {
* @deprecated as of 1.521
* Use {@link #schedule2(Task, int, List)}
*/
public synchronized WaitingItem schedule(Task p, int quietPeriod, List<Action> actions) {
public WaitingItem schedule(Task p, int quietPeriod, List<Action> actions) {
return schedule2(p, quietPeriod, actions).getCreateItem();
}
......@@ -539,7 +574,7 @@ public class Queue extends ResourceController implements Saveable {
*
* That said, one can still look at {@link Queue.Item#future}, {@link Queue.Item#getId()}, etc.
*/
public synchronized @Nonnull ScheduleResult schedule2(Task p, int quietPeriod, List<Action> actions) {
public @Nonnull ScheduleResult schedule2(Task p, int quietPeriod, List<Action> actions) {
// remove nulls
actions = new ArrayList<Action>(actions);
for (Iterator<Action> itr = actions.iterator(); itr.hasNext();) {
......@@ -547,11 +582,16 @@ public class Queue extends ResourceController implements Saveable {
if (a==null) itr.remove();
}
for(QueueDecisionHandler h : QueueDecisionHandler.all())
if (!h.shouldSchedule(p, actions))
return ScheduleResult.refused(); // veto
lock.lock();
try { try {
for (QueueDecisionHandler h : QueueDecisionHandler.all())
if (!h.shouldSchedule(p, actions))
return ScheduleResult.refused(); // veto
return scheduleInternal(p, quietPeriod, actions);
return scheduleInternal(p, quietPeriod, actions);
} finally { updateSnapshot(); } } finally {
lock.unlock();
}
}
/**
......@@ -567,71 +607,76 @@ public class Queue extends ResourceController implements Saveable {
*
* That said, one can still look at {@link WaitingItem#future}, {@link WaitingItem#getId()}, etc.
*/
private synchronized @Nonnull ScheduleResult scheduleInternal(Task p, int quietPeriod, List<Action> actions) {
Calendar due = new GregorianCalendar();
due.add(Calendar.SECOND, quietPeriod);
// Do we already have this task in the queue? Because if so, we won't schedule a new one.
List<Item> duplicatesInQueue = new ArrayList<Item>();
for(Item item : getItems(p)) {
boolean shouldScheduleItem = false;
for (QueueAction action: item.getActions(QueueAction.class)) {
shouldScheduleItem |= action.shouldSchedule(actions);
}
for (QueueAction action: Util.filter(actions,QueueAction.class)) {
shouldScheduleItem |= action.shouldSchedule((new ArrayList<Action>(item.getAllActions())));
}
if(!shouldScheduleItem) {
duplicatesInQueue.add(item);
}
}
if (duplicatesInQueue.isEmpty()) {
LOGGER.log(Level.FINE, "{0} added to queue", p);
// put the item in the queue
WaitingItem added = new WaitingItem(due,p,actions);
added.enter(this);
scheduleMaintenance(); // let an executor know that a new item is in the queue.
return ScheduleResult.created(added);
}
private @Nonnull ScheduleResult scheduleInternal(Task p, int quietPeriod, List<Action> actions) {
lock.lock();
try { try {
Calendar due = new GregorianCalendar();
due.add(Calendar.SECOND, quietPeriod);
// Do we already have this task in the queue? Because if so, we won't schedule a new one.
List<Item> duplicatesInQueue = new ArrayList<Item>();
for (Item item : liveGetItems(p)) {
boolean shouldScheduleItem = false;
for (QueueAction action : item.getActions(QueueAction.class)) {
shouldScheduleItem |= action.shouldSchedule(actions);
}
for (QueueAction action : Util.filter(actions, QueueAction.class)) {
shouldScheduleItem |= action.shouldSchedule((new ArrayList<Action>(item.getAllActions())));
}
if (!shouldScheduleItem) {
duplicatesInQueue.add(item);
}
}
if (duplicatesInQueue.isEmpty()) {
LOGGER.log(Level.FINE, "{0} added to queue", p);
// put the item in the queue
WaitingItem added = new WaitingItem(due, p, actions);
added.enter(this);
scheduleMaintenance(); // let an executor know that a new item is in the queue.
return ScheduleResult.created(added);
}
LOGGER.log(Level.FINE, "{0} is already in the queue", p);
LOGGER.log(Level.FINE, "{0} is already in the queue", p);
// but let the actions affect the existing stuff.
for(Item item : duplicatesInQueue) {
for(FoldableAction a : Util.filter(actions,FoldableAction.class)) {
a.foldIntoExisting(item, p, actions);
// but let the actions affect the existing stuff.
for (Item item : duplicatesInQueue) {
for (FoldableAction a : Util.filter(actions, FoldableAction.class)) {
a.foldIntoExisting(item, p, actions);
}
}
}
boolean queueUpdated = false;
for(WaitingItem wi : Util.filter(duplicatesInQueue,WaitingItem.class)) {
if(quietPeriod<=0) {
// the user really wants to build now, and they mean NOW.
// so let's pull in the timestamp if we can.
if (wi.timestamp.before(due))
continue;
} else {
// otherwise we do the normal quiet period implementation
if (wi.timestamp.after(due))
continue;
// quiet period timer reset. start the period over again
}
boolean queueUpdated = false;
for (WaitingItem wi : Util.filter(duplicatesInQueue, WaitingItem.class)) {
if (quietPeriod <= 0) {
// the user really wants to build now, and they mean NOW.
// so let's pull in the timestamp if we can.
if (wi.timestamp.before(due))
continue;
} else {
// otherwise we do the normal quiet period implementation
if (wi.timestamp.after(due))
continue;
// quiet period timer reset. start the period over again
}
// waitingList is sorted, so when we change a timestamp we need to maintain order
wi.leave(this);
wi.timestamp = due;
wi.enter(this);
queueUpdated=true;
}
// waitingList is sorted, so when we change a timestamp we need to maintain order
wi.leave(this);
wi.timestamp = due;
wi.enter(this);
queueUpdated = true;
}
if (queueUpdated) scheduleMaintenance();
if (queueUpdated) scheduleMaintenance();
// REVISIT: when there are multiple existing items in the queue that matches the incoming one,
// whether the new one should affect all existing ones or not is debatable. I for myself
// thought this would only affect one, so the code was bit of surprise, but I'm keeping the current
// behaviour.
return ScheduleResult.existing(duplicatesInQueue.get(0));
// REVISIT: when there are multiple existing items in the queue that matches the incoming one,
// whether the new one should affect all existing ones or not is debatable. I for myself
// thought this would only affect one, so the code was bit of surprise, but I'm keeping the current
// behaviour.
return ScheduleResult.existing(duplicatesInQueue.get(0));
} finally { updateSnapshot(); } } finally {
lock.unlock();
}
}
......@@ -639,11 +684,11 @@ public class Queue extends ResourceController implements Saveable {
* @deprecated as of 1.311
* Use {@link #schedule(Task, int)}
*/
public synchronized boolean add(Task p, int quietPeriod) {
public boolean add(Task p, int quietPeriod) {
return schedule(p, quietPeriod)!=null;
}
public synchronized @CheckForNull WaitingItem schedule(Task p, int quietPeriod) {
public @CheckForNull WaitingItem schedule(Task p, int quietPeriod) {
return schedule(p, quietPeriod, new Action[0]);
}
......@@ -651,21 +696,21 @@ public class Queue extends ResourceController implements Saveable {
* @deprecated as of 1.311
* Use {@link #schedule(Task, int, Action...)}
*/
public synchronized boolean add(Task p, int quietPeriod, Action... actions) {
public boolean add(Task p, int quietPeriod, Action... actions) {
return schedule(p, quietPeriod, actions)!=null;
}
/**
* Convenience wrapper method around {@link #schedule(Task, int, List)}
*/
public synchronized @CheckForNull WaitingItem schedule(Task p, int quietPeriod, Action... actions) {
public @CheckForNull WaitingItem schedule(Task p, int quietPeriod, Action... actions) {
return schedule2(p, quietPeriod, actions).getCreateItem();
}
/**
* Convenience wrapper method around {@link #schedule2(Task, int, List)}
*/
public synchronized @Nonnull ScheduleResult schedule2(Task p, int quietPeriod, Action... actions) {
public @Nonnull ScheduleResult schedule2(Task p, int quietPeriod, Action... actions) {
return schedule2(p, quietPeriod, Arrays.asList(actions));
}
......@@ -675,20 +720,34 @@ public class Queue extends ResourceController implements Saveable {
* @return true if the project was indeed in the queue and was removed.
* false if this was no-op.
*/
public synchronized boolean cancel(Task p) {
LOGGER.log(Level.FINE, "Cancelling {0}", p);
for (WaitingItem item : waitingList) {
if (item.task.equals(p)) {
return item.cancel(this);
public boolean cancel(Task p) {
lock.lock();
try { try {
LOGGER.log(Level.FINE, "Cancelling {0}", p);
for (WaitingItem item : waitingList) {
if (item.task.equals(p)) {
return item.cancel(this);
}
}
// use bitwise-OR to make sure that both branches get evaluated all the time
return blockedProjects.cancel(p) != null | buildables.cancel(p) != null;
} finally { updateSnapshot(); } } finally {
lock.unlock();
}
// use bitwise-OR to make sure that both branches get evaluated all the time
return blockedProjects.cancel(p)!=null | buildables.cancel(p)!=null;
}
public synchronized boolean cancel(Item item) {
private void updateSnapshot() {
snapshot = new Snapshot(waitingList, blockedProjects, buildables, pendings);
}
public boolean cancel(Item item) {
LOGGER.log(Level.FINE, "Cancelling {0} item#{1}", new Object[] {item.task, item.id});
return item.cancel(this);
lock.lock();
try { try {
return item.cancel(this);
} finally { updateSnapshot(); } } finally {
lock.unlock();
}
}
/**
......@@ -703,11 +762,13 @@ public class Queue extends ResourceController implements Saveable {
return HttpResponses.forwardToPreviousPage();
}
public synchronized boolean isEmpty() {
return waitingList.isEmpty() && blockedProjects.isEmpty() && buildables.isEmpty() && pendings.isEmpty();
public boolean isEmpty() {
Snapshot snapshot = this.snapshot;
return snapshot.waitingList.isEmpty() && snapshot.blockedProjects.isEmpty() && snapshot.buildables.isEmpty()
&& snapshot.pendings.isEmpty();
}
private synchronized WaitingItem peek() {
private WaitingItem peek() {
return waitingList.iterator().next();
}
......@@ -718,16 +779,21 @@ public class Queue extends ResourceController implements Saveable {
* at the end.
*/
@Exported(inline=true)
public synchronized Item[] getItems() {
Item[] r = new Item[waitingList.size() + blockedProjects.size() + buildables.size() + pendings.size()];
waitingList.toArray(r);
int idx = waitingList.size();
for (BlockedItem p : blockedProjects.values())
public Item[] getItems() {
Snapshot s = this.snapshot;
Item[] r = new Item[s.waitingList.size() + s.blockedProjects.size() + s.buildables.size() +
s.pendings.size()];
s.waitingList.toArray(r);
int idx = s.waitingList.size();
for (BlockedItem p : s.blockedProjects) {
r[idx++] = p;
for (BuildableItem p : reverse(buildables.values()))
}
for (BuildableItem p : reverse(s.buildables)) {
r[idx++] = p;
for (BuildableItem p : reverse(pendings.values()))
}
for (BuildableItem p : reverse(s.pendings)) {
r[idx++] = p;
}
return r;
}
......@@ -752,30 +818,44 @@ public class Queue extends ResourceController implements Saveable {
return itemsView.get();
}
public synchronized Item getItem(long id) {
for (Item item: waitingList) if (item.id == id) return item;
for (Item item: blockedProjects) if (item.id == id) return item;
for (Item item: buildables) if (item.id == id) return item;
for (Item item: pendings) if (item.id == id) return item;
public Item getItem(long id) {
Snapshot snapshot = this.snapshot;
for (Item item : snapshot.blockedProjects) {
if (item.id == id)
return item;
}
for (Item item : snapshot.buildables) {
if (item.id == id)
return item;
}
for (Item item : snapshot.pendings) {
if (item.id == id)
return item;
}
for (Item item : snapshot.waitingList) {
if (item.id == id) {
return item;
}
}
return leftItems.getIfPresent(id);
}
/**
* Gets all the {@link BuildableItem}s that are waiting for an executor in the given {@link Computer}.
*/
public synchronized List<BuildableItem> getBuildableItems(Computer c) {
public List<BuildableItem> getBuildableItems(Computer c) {
Snapshot snapshot = this.snapshot;
List<BuildableItem> result = new ArrayList<BuildableItem>();
_getBuildableItems(c, buildables, result);
_getBuildableItems(c, pendings, result);
_getBuildableItems(c, snapshot.buildables, result);
_getBuildableItems(c, snapshot.pendings, result);
return result;
}
private void _getBuildableItems(Computer c, ItemList<BuildableItem> col, List<BuildableItem> result) {
private void _getBuildableItems(Computer c, List<BuildableItem> col, List<BuildableItem> result) {
Node node = c.getNode();
if (node == null) // Deleted computers cannot take build items...
return;
for (BuildableItem p : col.values()) {
for (BuildableItem p : col) {
if (node.canTake(p) == null)
result.add(p);
}
......@@ -784,17 +864,18 @@ public class Queue extends ResourceController implements Saveable {
/**
* Gets the snapshot of all {@link BuildableItem}s.
*/
public synchronized List<BuildableItem> getBuildableItems() {
ArrayList<BuildableItem> r = new ArrayList<BuildableItem>(buildables.values());
r.addAll(pendings.values());
public List<BuildableItem> getBuildableItems() {
Snapshot snapshot = this.snapshot;
ArrayList<BuildableItem> r = new ArrayList<BuildableItem>(snapshot.buildables);
r.addAll(snapshot.pendings);
return r;
}
/**
* Gets the snapshot of all {@link BuildableItem}s.
*/
public synchronized List<BuildableItem> getPendingItems() {
return new ArrayList<BuildableItem>(pendings.values());
public List<BuildableItem> getPendingItems() {
return new ArrayList<BuildableItem>(snapshot.pendings);
}
/**
......@@ -820,11 +901,12 @@ public class Queue extends ResourceController implements Saveable {
*
* @since 1.402
*/
public synchronized List<Item> getUnblockedItems() {
List<Item> queuedNotBlocked = new ArrayList<Item>();
queuedNotBlocked.addAll(waitingList);
queuedNotBlocked.addAll(buildables);
queuedNotBlocked.addAll(pendings);
public List<Item> getUnblockedItems() {
Snapshot snapshot = this.snapshot;
List<Item> queuedNotBlocked = new ArrayList<Item>();
queuedNotBlocked.addAll(snapshot.waitingList);
queuedNotBlocked.addAll(snapshot.buildables);
queuedNotBlocked.addAll(snapshot.pendings);
// but not 'blockedProjects'
return queuedNotBlocked;
}
......@@ -834,7 +916,7 @@ public class Queue extends ResourceController implements Saveable {
*
* @since 1.402
*/
public synchronized Set<Task> getUnblockedTasks() {
public Set<Task> getUnblockedTasks() {
List<Item> items = getUnblockedItems();
Set<Task> unblockedTasks = new HashSet<Task>(items.size());
for (Queue.Item t : items)
......@@ -845,8 +927,9 @@ public class Queue extends ResourceController implements Saveable {
/**
* Is the given task currently pending execution?
*/
public synchronized boolean isPending(Task t) {
for (BuildableItem i : pendings)
public boolean isPending(Task t) {
Snapshot snapshot = this.snapshot;
for (BuildableItem i : snapshot.pendings)
if (i.task.equals(t))
return true;
return false;
......@@ -855,15 +938,16 @@ public class Queue extends ResourceController implements Saveable {
/**
* How many {@link BuildableItem}s are assigned for the given label?
*/
public synchronized int countBuildableItemsFor(Label l) {
public int countBuildableItemsFor(Label l) {
Snapshot snapshot = this.snapshot;
int r = 0;
for (BuildableItem bi : buildables.values())
for (BuildableItem bi : snapshot.buildables)
for (SubTask st : bi.task.getSubTasks())
if (null==l || bi.getAssignedLabelFor(st)==l)
if (null == l || bi.getAssignedLabelFor(st) == l)
r++;
for (BuildableItem bi : pendings.values())
for (BuildableItem bi : snapshot.pendings)
for (SubTask st : bi.task.getSubTasks())
if (null==l || bi.getAssignedLabelFor(st)==l)
if (null == l || bi.getAssignedLabelFor(st) == l)
r++;
return r;
}
......@@ -871,7 +955,7 @@ public class Queue extends ResourceController implements Saveable {
/**
* Counts all the {@link BuildableItem}s currently in the queue.
*/
public synchronized int countBuildableItems() {
public int countBuildableItems() {
return countBuildableItemsFor(null);
}
......@@ -880,18 +964,21 @@ public class Queue extends ResourceController implements Saveable {
*
* @return null if the project is not in the queue.
*/
public synchronized Item getItem(Task t) {
BlockedItem bp = blockedProjects.get(t);
if (bp!=null)
return bp;
BuildableItem bi = buildables.get(t);
if(bi!=null)
return bi;
bi = pendings.get(t);
if(bi!=null)
return bi;
for (Item item : waitingList) {
public Item getItem(Task t) {
Snapshot snapshot = this.snapshot;
for (Item item : snapshot.blockedProjects) {
if (item.task.equals(t))
return item;
}
for (Item item : snapshot.buildables) {
if (item.task.equals(t))
return item;
}
for (Item item : snapshot.pendings) {
if (item.task.equals(t))
return item;
}
for (Item item : snapshot.waitingList) {
if (item.task.equals(t)) {
return item;
}
......@@ -903,36 +990,75 @@ public class Queue extends ResourceController implements Saveable {
* Gets the information about the queue item for the given project.
*
* @return null if the project is not in the queue.
* @since 1.FIXME
*/
public synchronized List<Item> getItems(Task t) {
List<Item> result =new ArrayList<Item>();
result.addAll(blockedProjects.getAll(t));
result.addAll(buildables.getAll(t));
result.addAll(pendings.getAll(t));
for (Item item : waitingList) {
if (item.task.equals(t)) {
result.add(item);
private List<Item> liveGetItems(Task t) {
lock.lock();
try {
List<Item> result = new ArrayList<Item>();
result.addAll(blockedProjects.getAll(t));
result.addAll(buildables.getAll(t));
result.addAll(pendings.getAll(t));
for (Item item : waitingList) {
if (item.task.equals(t)) {
result.add(item);
}
}
return result;
} finally {
lock.unlock();
}
return result;
}
/**
* Left for backward compatibility.
* Gets the information about the queue item for the given project.
*
* @see #getItem(Task)
public synchronized Item getItem(AbstractProject p) {
return getItem((Task) p);
}
* @return null if the project is not in the queue.
*/
public List<Item> getItems(Task t) {
Snapshot snapshot = this.snapshot;
List<Item> result = new ArrayList<Item>();
for (Item item : snapshot.blockedProjects) {
if (item.task.equals(t)) {
result.add(item);
}
}
for (Item item : snapshot.buildables) {
if (item.task.equals(t)) {
result.add(item);
}
}
for (Item item : snapshot.pendings) {
if (item.task.equals(t)) {
result.add(item);
}
}
for (Item item : snapshot.waitingList) {
if (item.task.equals(t)) {
result.add(item);
}
}
return result;
}
/**
* Returns true if this queue contains the said project.
*/
public synchronized boolean contains(Task t) {
if (blockedProjects.containsKey(t) || buildables.containsKey(t) || pendings.containsKey(t))
return true;
for (Item item : waitingList) {
public boolean contains(Task t) {
final Snapshot snapshot = this.snapshot;
for (Item item : snapshot.blockedProjects) {
if (item.task.equals(t))
return true;
}
for (Item item : snapshot.buildables) {
if (item.task.equals(t))
return true;
}
for (Item item : snapshot.pendings) {
if (item.task.equals(t))
return true;
}
for (Item item : snapshot.waitingList) {
if (item.task.equals(t)) {
return true;
}
......@@ -945,12 +1071,17 @@ public class Queue extends ResourceController implements Saveable {
*
* This moves the task from the pending state to the "left the queue" state.
*/
/*package*/ synchronized void onStartExecuting(Executor exec) throws InterruptedException {
final WorkUnit wu = exec.getCurrentWorkUnit();
pendings.remove(wu.context.item);
/*package*/ void onStartExecuting(Executor exec) throws InterruptedException {
lock.lock();
try { try {
final WorkUnit wu = exec.getCurrentWorkUnit();
pendings.remove(wu.context.item);
LeftItem li = new LeftItem(wu.context);
li.enter(this);
LeftItem li = new LeftItem(wu.context);
li.enter(this);
} finally { updateSnapshot(); } } finally {
lock.unlock();
}
}
/**
......@@ -1054,14 +1185,29 @@ public class Queue extends ResourceController implements Saveable {
}
}
@Override
protected void _await() throws InterruptedException {
condition.await();
}
@Override
protected void _signalAll() {
condition.signalAll();
}
/**
* Some operations require to be performed with the {@link Queue} lock held. Use one of these methods rather
* than locking directly on Queue in order to allow for future refactoring.
* @param runnable the operation to perform.
* @since 1.592
*/
protected synchronized void _withLock(Runnable runnable) {
runnable.run();
protected void _withLock(Runnable runnable) {
lock.lock();
try {
runnable.run();
} finally {
lock.unlock();
}
}
/**
......@@ -1075,8 +1221,13 @@ public class Queue extends ResourceController implements Saveable {
* @throws T the exception of the callable
* @since 1.592
*/
protected synchronized <V, T extends Throwable> V _withLock(hudson.remoting.Callable<V, T> callable) throws T {
return callable.call();
protected <V, T extends Throwable> V _withLock(hudson.remoting.Callable<V, T> callable) throws T {
lock.lock();
try {
return callable.call();
} finally {
lock.unlock();
}
}
/**
......@@ -1089,8 +1240,13 @@ public class Queue extends ResourceController implements Saveable {
* @throws Exception if the callable throws an exception.
* @since 1.592
*/
protected synchronized <V> V _withLock(java.util.concurrent.Callable<V> callable) throws Exception {
return callable.call();
protected <V> V _withLock(java.util.concurrent.Callable<V> callable) throws Exception {
lock.lock();
try {
return callable.call();
} finally {
lock.unlock();
}
}
/**
......@@ -1105,98 +1261,120 @@ public class Queue extends ResourceController implements Saveable {
* the scheduling (such as new node becoming online, # of executors change, a task completes execution, etc.),
* and it also gets invoked periodically (see {@link Queue.MaintainTask}.)
*/
public synchronized void maintain() {
LOGGER.log(Level.FINE, "Queue maintenance started {0}", this);
public void maintain() {
lock.lock();
try { try {
LOGGER.log(Level.FINE, "Queue maintenance started {0}", this);
// The executors that are currently waiting for a job to run.
Map<Executor,JobOffer> parked = new HashMap<Executor,JobOffer>();
// The executors that are currently waiting for a job to run.
Map<Executor, JobOffer> parked = new HashMap<Executor, JobOffer>();
{// update parked
for (Computer c : Jenkins.getInstance().getComputers()) {
for (Executor e : c.getExecutors()) {
if (e.isParking()) {
parked.put(e,new JobOffer(e));
{// update parked (and identify any pending items whose executor has disappeared)
List<BuildableItem> lostPendings = new ArrayList<BuildableItem>(pendings);
for (Computer c : Jenkins.getInstance().getComputers()) {
for (Executor e : c.getExecutors()) {
if (e.isParking()) {
parked.put(e, new JobOffer(e));
}
final WorkUnit workUnit = e.getCurrentWorkUnit();
if (workUnit != null) {
lostPendings.remove(workUnit.context.item);
}
}
}
// pending -> buildable
for (BuildableItem p: lostPendings) {
LOGGER.log(Level.INFO,
"BuildableItem {0}: pending -> buildable as the assigned executor disappeared",
p.task.getFullDisplayName());
p.isPending = false;
pendings.remove(p);
makeBuildable(p);
}
}
}
{// blocked -> buildable
for (BlockedItem p : new ArrayList<BlockedItem>(blockedProjects.values())) {// copy as we'll mutate the list
if (!isBuildBlocked(p) && allowNewBuildableTask(p.task)) {
// ready to be executed
Runnable r = makeBuildable(new BuildableItem(p));
if (r != null) {
p.leave(this);
r.run();
{// blocked -> buildable
for (BlockedItem p : new ArrayList<BlockedItem>(blockedProjects.values())) {// copy as we'll mutate the list
if (!isBuildBlocked(p) && allowNewBuildableTask(p.task)) {
// ready to be executed
Runnable r = makeBuildable(new BuildableItem(p));
if (r != null) {
p.leave(this);
r.run();
}
}
}
}
}
// waitingList -> buildable/blocked
while (!waitingList.isEmpty()) {
WaitingItem top = peek();
// waitingList -> buildable/blocked
while (!waitingList.isEmpty()) {
WaitingItem top = peek();
if (top.timestamp.compareTo(new GregorianCalendar())>0)
break; // finished moving all ready items from queue
if (top.timestamp.compareTo(new GregorianCalendar()) > 0)
break; // finished moving all ready items from queue
top.leave(this);
Task p = top.task;
if (!isBuildBlocked(top) && allowNewBuildableTask(p)) {
// ready to be executed immediately
Runnable r = makeBuildable(new BuildableItem(top));
if (r != null) {
r.run();
top.leave(this);
Task p = top.task;
if (!isBuildBlocked(top) && allowNewBuildableTask(p)) {
// ready to be executed immediately
Runnable r = makeBuildable(new BuildableItem(top));
if (r != null) {
r.run();
} else {
new BlockedItem(top).enter(this);
}
} else {
// this can't be built now because another build is in progress
// set this project aside.
new BlockedItem(top).enter(this);
}
} else {
// this can't be built now because another build is in progress
// set this project aside.
new BlockedItem(top).enter(this);
}
}
final QueueSorter s = sorter;
if (s != null)
s.sortBuildableItems(buildables);
// allocate buildable jobs to executors
for (BuildableItem p : new ArrayList<BuildableItem>(buildables)) {// copy as we'll mutate the list in the loop
// one last check to make sure this build is not blocked.
if (isBuildBlocked(p)) {
p.leave(this);
new BlockedItem(p).enter(this);
LOGGER.log(Level.FINE, "Catching that {0} is blocked in the last minute", p);
continue;
}
final QueueSorter s = sorter;
if (s != null)
s.sortBuildableItems(buildables);
// allocate buildable jobs to executors
for (BuildableItem p : new ArrayList<BuildableItem>(
buildables)) {// copy as we'll mutate the list in the loop
// one last check to make sure this build is not blocked.
if (isBuildBlocked(p)) {
p.leave(this);
new BlockedItem(p).enter(this);
LOGGER.log(Level.FINE, "Catching that {0} is blocked in the last minute", p);
continue;
}
List<JobOffer> candidates = new ArrayList<JobOffer>(parked.size());
for (JobOffer j : parked.values())
if (j.canTake(p))
candidates.add(j);
MappingWorksheet ws = new MappingWorksheet(p, candidates);
Mapping m = loadBalancer.map(p.task, ws);
if (m == null) {
// if we couldn't find the executor that fits,
// just leave it in the buildables list and
// check if we can execute other projects
LOGGER.log(Level.FINER, "Failed to map {0} to executors. candidates={1} parked={2}", new Object[]{p, candidates, parked.values()});
continue;
}
List<JobOffer> candidates = new ArrayList<JobOffer>(parked.size());
for (JobOffer j : parked.values())
if (j.canTake(p))
candidates.add(j);
MappingWorksheet ws = new MappingWorksheet(p, candidates);
Mapping m = loadBalancer.map(p.task, ws);
if (m == null) {
// if we couldn't find the executor that fits,
// just leave it in the buildables list and
// check if we can execute other projects
LOGGER.log(Level.FINER, "Failed to map {0} to executors. candidates={1} parked={2}",
new Object[]{p, candidates, parked.values()});
continue;
}
// found a matching executor. use it.
WorkUnitContext wuc = new WorkUnitContext(p);
m.execute(wuc);
// found a matching executor. use it.
WorkUnitContext wuc = new WorkUnitContext(p);
m.execute(wuc);
p.leave(this);
if (!wuc.getWorkUnits().isEmpty())
makePending(p);
else
LOGGER.log(Level.FINE, "BuildableItem {0} with empty work units!?", p);
p.leave(this);
if (!wuc.getWorkUnits().isEmpty())
makePending(p);
else
LOGGER.log(Level.FINE, "BuildableItem {0} with empty work units!?", p);
}
} finally { updateSnapshot(); } } finally {
lock.unlock();
}
}
......@@ -2325,6 +2503,21 @@ public class Queue extends ResourceController implements Saveable {
}
}
private static class Snapshot {
private final Set<WaitingItem> waitingList;
private final List<BlockedItem> blockedProjects;
private final List<BuildableItem> buildables;
private final List<BuildableItem> pendings;
public Snapshot(Set<WaitingItem> waitingList, List<BlockedItem> blockedProjects, List<BuildableItem> buildables,
List<BuildableItem> pendings) {
this.waitingList = new LinkedHashSet<WaitingItem>(waitingList);
this.blockedProjects = new ArrayList<BlockedItem>(blockedProjects);
this.buildables = new ArrayList<BuildableItem>(buildables);
this.pendings = new ArrayList<BuildableItem>(pendings);
}
}
@CLIResolver
public static Queue getInstance() {
return Jenkins.getInstance().getQueue();
......
/*
* The MIT License
*
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
*
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
......@@ -29,6 +29,7 @@ import java.util.Set;
import java.util.Collection;
import java.util.AbstractCollection;
import java.util.Iterator;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.Nonnull;
......@@ -61,7 +62,7 @@ public class ResourceController {
/**
* Union of all {@link Resource}s that are currently in use.
* Updated as a task starts/completes executing.
* Updated as a task starts/completes executing.
*/
private ResourceList inUse = ResourceList.EMPTY;
......@@ -74,26 +75,37 @@ public class ResourceController {
* @throws InterruptedException
* the thread can be interrupted while waiting for the available resources.
*/
public void execute(@Nonnull Runnable task, ResourceActivity activity ) throws InterruptedException {
ResourceList resources = activity.getResourceList();
synchronized(this) {
while(inUse.isCollidingWith(resources))
wait();
// we have a go
inProgress.add(activity);
inUse = ResourceList.union(inUse,resources);
}
public void execute(@Nonnull Runnable task, final ResourceActivity activity ) throws InterruptedException {
final ResourceList resources = activity.getResourceList();
_withLock(new Runnable() {
@Override
public void run() {
while(inUse.isCollidingWith(resources))
try {
// TODO revalidate the resource list after re-acquiring lock, for now we just let the build fail
_await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// we have a go
inProgress.add(activity);
inUse = ResourceList.union(inUse,resources);
}
});
try {
task.run();
} finally {
// TODO if AsynchronousExecution, do that later
synchronized(this) {
inProgress.remove(activity);
inUse = ResourceList.union(resourceView);
notifyAll();
}
// TODO if AsynchronousExecution, do that later
_withLock(new Runnable() {
@Override
public void run() {
inProgress.remove(activity);
inUse = ResourceList.union(resourceView);
_signalAll();
}
});
}
}
......@@ -106,8 +118,17 @@ public class ResourceController {
* another activity might acquire resources before the caller
* gets to call {@link #execute(Runnable, ResourceActivity)}.
*/
public synchronized boolean canRun(ResourceList resources) {
return !inUse.isCollidingWith(resources);
public boolean canRun(final ResourceList resources) {
try {
return _withLock(new Callable<Boolean>() {
@Override
public Boolean call() {
return !inUse.isCollidingWith(resources);
}
});
} catch (Exception e) {
throw new IllegalStateException("Inner callable does not throw exception");
}
}
/**
......@@ -118,8 +139,17 @@ public class ResourceController {
* If more than one such resource exists, one is chosen and returned.
* This method is used for reporting what's causing the blockage.
*/
public synchronized Resource getMissingResource(ResourceList resources) {
return resources.getConflict(inUse);
public Resource getMissingResource(final ResourceList resources) {
try {
return _withLock(new Callable<Resource>() {
@Override
public Resource call() {
return resources.getConflict(inUse);
}
});
} catch (Exception e) {
throw new IllegalStateException("Inner callable does not throw exception");
}
}
/**
......@@ -134,5 +164,25 @@ public class ResourceController {
return a;
return null;
}
protected void _await() throws InterruptedException {
wait();
}
protected void _signalAll() {
notifyAll();
}
protected void _withLock(Runnable runnable) {
synchronized (this) {
runnable.run();
}
}
protected <V> V _withLock(java.util.concurrent.Callable<V> callable) throws Exception {
synchronized (this) {
return callable.call();
}
}
}
......@@ -23,6 +23,7 @@
*/
package hudson.slaves;
import hudson.model.Computer;
import hudson.model.Descriptor.FormException;
import jenkins.model.Jenkins;
import hudson.model.Slave;
......@@ -57,6 +58,10 @@ public abstract class AbstractCloudSlave extends Slave {
* Releases and removes this slave.
*/
public void terminate() throws InterruptedException, IOException {
final Computer computer = toComputer();
if (computer != null) {
computer.recordTermination();
}
try {
// TODO: send the output to somewhere real
_terminate(new StreamTaskListener(System.out, Charset.defaultCharset()));
......
......@@ -28,6 +28,7 @@ import hudson.model.Node;
import hudson.model.Queue;
import jenkins.model.Jenkins;
import javax.annotation.concurrent.GuardedBy;
import java.io.IOException;
import java.util.logging.Logger;
......@@ -50,30 +51,20 @@ public class CloudRetentionStrategy extends RetentionStrategy<AbstractCloudCompu
}
@Override
public synchronized long check(final AbstractCloudComputer c) {
@GuardedBy("hudson.model.Queue.lock")
public long check(final AbstractCloudComputer c) {
final AbstractCloudSlave computerNode = c.getNode();
if (c.isIdle() && !disabled && computerNode != null) {
final long idleMilliseconds = System.currentTimeMillis() - c.getIdleStartMilliseconds();
if (idleMilliseconds > MINUTES.toMillis(idleMinutes)) {
Queue.withLock(new Runnable() {
@Override
public void run() {
// re-check idle now that we are within the Queue lock
if (c.isIdle()) {
final long idleMilliseconds = System.currentTimeMillis() - c.getIdleStartMilliseconds();
if (idleMilliseconds > MINUTES.toMillis(idleMinutes)) {
LOGGER.log(Level.INFO, "Disconnecting {0}", c.getName());
try {
computerNode.terminate();
} catch (InterruptedException e) {
LOGGER.log(WARNING, "Failed to terminate " + c.getName(), e);
} catch (IOException e) {
LOGGER.log(WARNING, "Failed to terminate " + c.getName(), e);
}
}
}
}
});
LOGGER.log(Level.INFO, "Disconnecting {0}", c.getName());
try {
computerNode.terminate();
} catch (InterruptedException e) {
LOGGER.log(WARNING, "Failed to terminate " + c.getName(), e);
} catch (IOException e) {
LOGGER.log(WARNING, "Failed to terminate " + c.getName(), e);
}
}
}
return 1;
......
......@@ -5,6 +5,7 @@ import hudson.model.Node;
import hudson.util.TimeUnit2;
import jenkins.model.Jenkins;
import javax.annotation.concurrent.GuardedBy;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
......@@ -22,6 +23,7 @@ import java.util.logging.Logger;
public class CloudSlaveRetentionStrategy<T extends Computer> extends RetentionStrategy<T> {
@Override
@GuardedBy("hudson.model.Queue.lock")
public long check(T c) {
if (!c.isConnecting() && c.isAcceptingTasks()) {
if (isIdleForTooLong(c)) {
......
/*
* The MIT License
*
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Stephen Connolly
*
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
......@@ -27,6 +27,7 @@ import java.util.Map;
import java.util.WeakHashMap;
import hudson.model.Computer;
import hudson.model.Queue;
import jenkins.model.Jenkins;
import hudson.model.Node;
import hudson.model.PeriodicWork;
......@@ -56,16 +57,21 @@ public class ComputerRetentionWork extends PeriodicWork {
@SuppressWarnings("unchecked")
protected void doRun() {
final long startRun = System.currentTimeMillis();
for (Computer c : Jenkins.getInstance().getComputers()) {
Node n = c.getNode();
if (n!=null && n.isHoldOffLaunchUntilSave())
continue;
if (!nextCheck.containsKey(c) || startRun > nextCheck.get(c)) {
// at the moment I don't trust strategies to wait more than 60 minutes
// strategies need to wait at least one minute
final long waitInMins = Math.max(1, Math.min(60, c.getRetentionStrategy().check(c)));
nextCheck.put(c, startRun + waitInMins*1000*60 /*MINS->MILLIS*/);
}
for (final Computer c : Jenkins.getInstance().getComputers()) {
Queue.withLock(new Runnable() {
@Override
public void run() {
Node n = c.getNode();
if (n!=null && n.isHoldOffLaunchUntilSave())
return;
if (!nextCheck.containsKey(c) || startRun > nextCheck.get(c)) {
// at the moment I don't trust strategies to wait more than 60 minutes
// strategies need to wait at least one minute
final long waitInMins = Math.max(1, Math.min(60, c.getRetentionStrategy().check(c)));
nextCheck.put(c, startRun + waitInMins*1000*60 /*MINS->MILLIS*/);
}
}
});
}
}
}
......@@ -30,9 +30,9 @@ import jenkins.model.Jenkins;
import static hudson.model.LoadStatistics.DECAY;
import hudson.model.MultiStageTimeSeries.TimeScale;
import hudson.Extension;
import net.jcip.annotations.GuardedBy;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.GuardedBy;
import java.awt.Color;
import java.util.Arrays;
import java.util.concurrent.Future;
......@@ -42,6 +42,8 @@ import java.util.Collection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.io.IOException;
......@@ -120,9 +122,14 @@ public class NodeProvisioner {
*/
private final Label label;
@GuardedBy("self")
@GuardedBy("provisioningLock")
private final List<PlannedNode> pendingLaunches = new ArrayList<PlannedNode>();
private final Lock provisioningLock = new ReentrantLock();
@GuardedBy("provisioningLock")
private StrategyState provisioningState = null;
private transient volatile long lastSuggestedReview;
/**
......@@ -148,8 +155,11 @@ public class NodeProvisioner {
* @since 1.401
*/
public List<PlannedNode> getPendingLaunches() {
synchronized (pendingLaunches) {
provisioningLock.lock();
try {
return new ArrayList<PlannedNode>(pendingLaunches);
} finally {
provisioningLock.unlock();
}
}
......@@ -173,81 +183,114 @@ public class NodeProvisioner {
/**
* Periodically invoked to keep track of the load.
* Launches additional nodes if necessary.
*
* Note: This method will obtain a lock on {@link #provisioningLock} first (to ensure that one and only one
* instance of this provisioner is running at a time) and then a lock on {@link Queue#lock}
*/
private synchronized void update() {
Jenkins jenkins = Jenkins.getInstance();
lastSuggestedReview = System.currentTimeMillis();
private void update() {
provisioningLock.lock();
try {
lastSuggestedReview = System.currentTimeMillis();
// clean up the cancelled launch activity, then count the # of executors that we are about to bring up.
int plannedCapacitySnapshot = 0;
List<PlannedNode> completedLaunches = new ArrayList<PlannedNode>();
// We need to get the lock on Queue for two reasons:
// 1. We will potentially adding a lot of nodes and we don't want to fight with Queue#maintain to acquire
// the Queue#lock in order to add each node. Much better is to hold the Queue#lock until all nodes
// that were provisioned since last we checked have been added.
// 2. We want to know the idle executors count, which can only be measured if you hold the Queue#lock
// Strictly speaking we don't need an accurate measure for this, but as we had to get the Queue#lock
// anyway, we might as well get an accurate measure.
//
// We do not need the Queue#lock to get the count of items in the queue as that is a lock-free call
// Since adding a node should not (in principle) confuse Queue#maintain (it is only removal of nodes
// that causes issues in Queue#maintain) we should be able to remove the need for Queue#lock
//
// TODO once Nodes#addNode is made lock free, we should be able to remove the requirement for Queue#lock
Queue.withLock(new Runnable() {
@Override
public void run() {
Jenkins jenkins = Jenkins.getInstance();
// clean up the cancelled launch activity, then count the # of executors that we are about to
// bring up.
int plannedCapacitySnapshot = 0;
List<PlannedNode> completedLaunches = new ArrayList<PlannedNode>();
for (Iterator<PlannedNode> itr = pendingLaunches.iterator(); itr.hasNext(); ) {
PlannedNode f = itr.next();
if (f.future.isDone()) {
completedLaunches.add(f);
itr.remove();
} else {
plannedCapacitySnapshot += f.numExecutors;
}
}
synchronized (pendingLaunches) {
for (Iterator<PlannedNode> itr = pendingLaunches.iterator(); itr.hasNext(); ) {
PlannedNode f = itr.next();
if (f.future.isDone()) {
completedLaunches.add(f);
itr.remove();
} else {
plannedCapacitySnapshot += f.numExecutors;
}
}
}
for (PlannedNode f : completedLaunches) {
try {
Node node = f.future.get();
for (CloudProvisioningListener cl : CloudProvisioningListener.all()) {
cl.onComplete(f, node);
}
for (PlannedNode f : completedLaunches) {
try {
Node node = f.future.get();
for (CloudProvisioningListener cl : CloudProvisioningListener.all()) {
cl.onComplete(f, node);
}
jenkins.addNode(node);
LOGGER.log(Level.INFO,
"{0} provisioning successfully completed. We have now {1,number,integer} computer"
+ "(s)",
new Object[]{f.displayName, jenkins.getComputers().length});
} catch (InterruptedException e) {
throw new AssertionError(e); // since we confirmed that the future is already done
} catch (ExecutionException e) {
LOGGER.log(Level.WARNING, "Provisioned slave " + f.displayName + " failed to launch",
e.getCause());
for (CloudProvisioningListener cl : CloudProvisioningListener.all()) {
cl.onFailure(f, e.getCause());
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Provisioned slave " + f.displayName + " failed to launch", e);
for (CloudProvisioningListener cl : CloudProvisioningListener.all()) {
cl.onFailure(f, e);
}
}
jenkins.addNode(node);
LOGGER.log(Level.INFO,
"{0} provisioning successfully completed. We have now {1,number,integer} computer(s)",
new Object[]{f.displayName, jenkins.getComputers().length});
} catch (InterruptedException e) {
throw new AssertionError(e); // since we confirmed that the future is already done
} catch (ExecutionException e) {
LOGGER.log(Level.WARNING, "Provisioned slave " + f.displayName + " failed to launch", e.getCause());
for (CloudProvisioningListener cl : CloudProvisioningListener.all()) {
cl.onFailure(f, e.getCause());
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Provisioned slave " + f.displayName + " failed to launch", e);
for (CloudProvisioningListener cl : CloudProvisioningListener.all()) {
cl.onFailure(f, e);
}
}
f.spent();
}
f.spent();
}
float plannedCapacity = plannedCapacitySnapshot;
plannedCapacitiesEMA.update(plannedCapacity);
int idleSnapshot = stat.computeIdleExecutors();
int queueLengthSnapshot = stat.computeQueueLength();
if (queueLengthSnapshot <= idleSnapshot) {
LOGGER.log(Level.FINE,
"Queue length {0} is less than the idle capacity {1}. No provisioning strategy "
+ "required",
new Object[]{queueLengthSnapshot, idleSnapshot});
provisioningState = null;
} else {
provisioningState = new StrategyState(queueLengthSnapshot, label, idleSnapshot,
stat.computeTotalExecutors(),
plannedCapacitySnapshot);
}
}
});
float plannedCapacity = plannedCapacitySnapshot;
plannedCapacitiesEMA.update(plannedCapacity);
int idleSnapshot = stat.computeIdleExecutors();
int queueLengthSnapshot = stat.computeQueueLength();
if (queueLengthSnapshot <= idleSnapshot) {
LOGGER.log(Level.FINE,
"Queue length {0} is less than the idle capacity {1}. No provisioning strategy required",
new Object[]{queueLengthSnapshot, idleSnapshot});
} else {
StrategyState state =
new StrategyState(queueLengthSnapshot, label, idleSnapshot, stat.computeTotalExecutors(),
plannedCapacitySnapshot);
List<Strategy> strategies = Jenkins.getInstance().getExtensionList(Strategy.class);
for (Strategy strategy : strategies.isEmpty()
? Arrays.<Strategy>asList(new StandardStrategyImpl())
: strategies) {
LOGGER.log(Level.FINER, "Consulting {0} provisioning strategy with state {1}",
new Object[]{strategy, state});
if (StrategyDecision.PROVISIONING_COMPLETED == strategy.apply(state)) {
LOGGER.log(Level.FINER, "Provisioning strategy {0} declared provisioning complete",
strategy);
break;
if (provisioningState != null) {
List<Strategy> strategies = Jenkins.getInstance().getExtensionList(Strategy.class);
for (Strategy strategy : strategies.isEmpty()
? Arrays.<Strategy>asList(new StandardStrategyImpl())
: strategies) {
LOGGER.log(Level.FINER, "Consulting {0} provisioning strategy with state {1}",
new Object[]{strategy, provisioningState});
if (StrategyDecision.PROVISIONING_COMPLETED == strategy.apply(provisioningState)) {
LOGGER.log(Level.FINER, "Provisioning strategy {0} declared provisioning complete",
strategy);
break;
}
}
}
} finally {
provisioningLock.unlock();
}
}
......@@ -283,7 +326,7 @@ public class NodeProvisioner {
* Called by {@link NodeProvisioner#update()} to apply this strategy against the specified state.
* Any provisioning activities should be recorded by calling
* {@link hudson.slaves.NodeProvisioner.StrategyState#recordPendingLaunches(java.util.Collection)}
* This method will be called by a thread that is holding a lock on {@link hudson.slaves.NodeProvisioner}
* This method will be called by a thread that is holding {@link hudson.slaves.NodeProvisioner#provisioningLock}
* @param state the current state.
* @return the decision.
*/
......@@ -384,8 +427,13 @@ public class NodeProvisioner {
* The additional planned capacity for this {@link #getLabel()} and provisioned by previous strategies during
* the current updating of the {@link NodeProvisioner}.
*/
public synchronized int getAdditionalPlannedCapacity() {
return additionalPlannedCapacity;
public int getAdditionalPlannedCapacity() {
provisioningLock.lock();
try {
return additionalPlannedCapacity;
} finally {
provisioningLock.unlock();
}
}
/**
......@@ -451,13 +499,14 @@ public class NodeProvisioner {
additionalPlannedCapacity += f.numExecutors;
}
}
synchronized (pendingLaunches) {
provisioningLock.lock();
try {
pendingLaunches.addAll(plannedNodes);
}
if (additionalPlannedCapacity > 0) {
synchronized (this) {
this.additionalPlannedCapacity += additionalPlannedCapacity;
if (additionalPlannedCapacity > 0) {
this.additionalPlannedCapacity += additionalPlannedCapacity;
}
} finally {
provisioningLock.unlock();
}
}
......
......@@ -35,6 +35,8 @@ import java.util.HashMap;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.DataBoundConstructor;
import javax.annotation.concurrent.GuardedBy;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
......@@ -54,6 +56,7 @@ public abstract class RetentionStrategy<T extends Computer> extends AbstractDesc
* @return The number of minutes after which the strategy would like to be checked again. The strategy may be
* rechecked earlier or later that this!
*/
@GuardedBy("hudson.model.Queue.lock")
public abstract long check(T c);
/**
......@@ -91,8 +94,13 @@ public abstract class RetentionStrategy<T extends Computer> extends AbstractDesc
*
* @since 1.275
*/
public void start(T c) {
check(c);
public void start(final T c) {
Queue.withLock(new Runnable() {
@Override
public void run() {
check(c);
}
});
}
/**
......@@ -113,6 +121,7 @@ public abstract class RetentionStrategy<T extends Computer> extends AbstractDesc
* Dummy instance that doesn't do any attempt to retention.
*/
public static final RetentionStrategy<Computer> NOOP = new RetentionStrategy<Computer>() {
@GuardedBy("hudson.model.Queue.lock")
public long check(Computer c) {
return 60;
}
......@@ -152,6 +161,7 @@ public abstract class RetentionStrategy<T extends Computer> extends AbstractDesc
public Always() {
}
@GuardedBy("hudson.model.Queue.lock")
public long check(SlaveComputer c) {
if (c.isOffline() && !c.isConnecting() && c.isLaunchSupported())
c.tryReconnect();
......@@ -208,7 +218,8 @@ public abstract class RetentionStrategy<T extends Computer> extends AbstractDesc
}
@Override
public synchronized long check(final SlaveComputer c) {
@GuardedBy("hudson.model.Queue.lock")
public long check(final SlaveComputer c) {
if (c.isOffline() && c.isLaunchSupported()) {
final HashMap<Computer, Integer> availableComputers = new HashMap<Computer, Integer>();
for (Computer o : Jenkins.getInstance().getComputers()) {
......@@ -257,21 +268,13 @@ public abstract class RetentionStrategy<T extends Computer> extends AbstractDesc
} else if (c.isIdle()) {
final long idleMilliseconds = System.currentTimeMillis() - c.getIdleStartMilliseconds();
if (idleMilliseconds > idleDelay * 1000 * 60 /*MINS->MILLIS*/) {
Queue.withLock(new Runnable() {
@Override
public void run() {
// re-check idle now that we are within the Queue lock
if (c.isIdle()) {
final long idleMilliseconds = System.currentTimeMillis() - c.getIdleStartMilliseconds();
if (idleMilliseconds > idleDelay * 1000 * 60 /*MINS->MILLIS*/) {
// we've been idle for long enough
logger.log(Level.INFO, "Disconnecting computer {0} as it has been idle for {1}",
new Object[]{c.getName(), Util.getTimeSpanString(idleMilliseconds)});
c.disconnect(OfflineCause.create(Messages._RetentionStrategy_Demand_OfflineIdle()));
}
}
}
});
// we've been idle for long enough
logger.log(Level.INFO, "Disconnecting computer {0} as it has been idle for {1}",
new Object[]{c.getName(), Util.getTimeSpanString(idleMilliseconds)});
c.disconnect(OfflineCause.create(Messages._RetentionStrategy_Demand_OfflineIdle()));
} else {
// no point revisiting until we can be confident we will be idle
return TimeUnit.MILLISECONDS.toMinutes(TimeUnit.MINUTES.toMillis(idleDelay) - idleMilliseconds);
}
}
return 1;
......
......@@ -34,6 +34,7 @@ import hudson.util.FormValidation;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import javax.annotation.concurrent.GuardedBy;
import java.io.InvalidObjectException;
import java.io.ObjectStreamException;
import java.util.Calendar;
......@@ -163,6 +164,7 @@ public class SimpleScheduledRetentionStrategy extends RetentionStrategy<SlaveCom
return isOnlineScheduled();
}
@GuardedBy("hudson.model.Queue.lock")
public synchronized long check(final SlaveComputer c) {
boolean shouldBeOnline = isOnlineScheduled();
LOGGER.log(Level.FINE, "Checking computer {0} against schedule. online = {1}, shouldBeOnline = {2}",
......
......@@ -678,7 +678,7 @@ public class SlaveComputer extends Computer {
}
@Override
protected void setNode(Node node) {
protected void setNode(final Node node) {
super.setNode(node);
launcher = grabLauncher(node);
......@@ -686,10 +686,16 @@ public class SlaveComputer extends Computer {
// "constructed==null" test is an ugly hack to avoid launching before the object is fully
// constructed.
if(constructed!=null) {
if (node instanceof Slave)
((Slave)node).getRetentionStrategy().check(this);
else
if (node instanceof Slave) {
Queue.withLock(new Runnable() {
@Override
public void run() {
((Slave)node).getRetentionStrategy().check(SlaveComputer.this);
}
});
} else {
connect(false);
}
}
}
......
......@@ -288,6 +288,7 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
......@@ -497,17 +498,18 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
}
/**
* Set of installed cluster nodes.
* <p>
* We use this field with copy-on-write semantics.
* This field has mutable list (to keep the serialization look clean),
* but it shall never be modified. Only new completely populated slave
* list can be set here.
* <p>
* The field name should be really {@code nodes}, but again the backward compatibility
* prevents us from renaming.
* Legacy store of the set of installed cluster nodes.
* @deprecated in favour of {@link Nodes}
*/
@Deprecated
protected transient volatile NodeList slaves;
/**
* The holder of the set of installed cluster nodes.
*
* @since 1.FIXME
*/
protected volatile NodeList slaves;
private transient final Nodes nodes = new Nodes(this);
/**
* Quiet period.
......@@ -1213,7 +1215,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
return null;
}
protected void updateComputerList() throws IOException {
protected void updateComputerList() {
updateComputerList(AUTOMATIC_SLAVE_LAUNCH);
}
......@@ -1662,7 +1664,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
* Gets the slave node of the give name, hooked under this Hudson.
*/
public @CheckForNull Node getNode(String name) {
return slaves.getNode(name);
return nodes.getNode(name);
}
/**
......@@ -1681,38 +1683,25 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
* represents the master.
*/
public List<Node> getNodes() {
return slaves;
return nodes.getNodes();
}
/**
* Adds one more {@link Node} to Hudson.
*/
public synchronized void addNode(Node n) throws IOException {
if(n==null) throw new IllegalArgumentException();
ArrayList<Node> nl = new ArrayList<Node>(this.slaves);
if(!nl.contains(n)) // defensive check
nl.add(n);
setNodes(nl);
public void addNode(Node n) throws IOException {
nodes.addNode(n);
}
/**
* Removes a {@link Node} from Hudson.
*/
public synchronized void removeNode(@Nonnull Node n) throws IOException {
Computer c = n.toComputer();
if (c!=null)
c.disconnect(OfflineCause.create(Messages._Hudson_NodeBeingRemoved()));
ArrayList<Node> nl = new ArrayList<Node>(this.slaves);
nl.remove(n);
setNodes(nl);
public void removeNode(@Nonnull Node n) throws IOException {
nodes.removeNode(n);
}
public void setNodes(final List<? extends Node> nodes) throws IOException {
Jenkins.this.slaves = new NodeList(nodes);
updateComputerList();
trimLabels();
save();
public void setNodes(final List<? extends Node> n) throws IOException {
nodes.setNodes(n);
}
public DescribableList<NodeProperty<?>, NodePropertyDescriptor> getNodeProperties() {
......@@ -1729,7 +1718,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
* This should be called when the assumptions behind label cache computation changes,
* but we also call this periodically to self-heal any data out-of-sync issue.
*/
private void trimLabels() {
/*package*/ void trimLabels() {
for (Iterator<Label> itr = labels.values().iterator(); itr.hasNext();) {
Label l = itr.next();
resetLabel(l);
......@@ -2318,8 +2307,11 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
}
public void setNumExecutors(int n) throws IOException {
this.numExecutors = n;
save();
if (this.numExecutors != n) {
this.numExecutors = n;
updateComputerList();
save();
}
}
......@@ -2626,10 +2618,6 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
TaskGraphBuilder g = new TaskGraphBuilder();
Handle loadHudson = g.requires(EXTENSIONS_AUGMENTED).attains(JOB_LOADED).add("Loading global config", new Executable() {
public void run(Reactor session) throws Exception {
// JENKINS-8043: some slaves (eg. swarm slaves) are not saved into the config file
// and will get overwritten when reloading. Make a backup copy now, and re-add them later
NodeList oldSlaves = slaves;
XmlFile cfg = getConfigFile();
if (cfg.exists()) {
// reset some data that may not exist in the disk file
......@@ -2642,23 +2630,14 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
}
// if we are loading old data that doesn't have this field
if (slaves == null) slaves = new NodeList();
if (slaves != null && !slaves.isEmpty() && nodes.isLegacy()) {
nodes.setNodes(slaves);
slaves = null;
} else {
nodes.load();
}
clouds.setOwner(Jenkins.this);
// JENKINS-8043: re-add the slaves which were not saved into the config file
// and are now missing, but still connected.
if (oldSlaves != null) {
ArrayList<Node> newSlaves = new ArrayList<Node>(slaves);
for (Node n: oldSlaves) {
if (n instanceof EphemeralNode) {
if(!newSlaves.contains(n)) {
newSlaves.add(n);
}
}
}
setNodes(newSlaves);
}
}
});
......@@ -2696,7 +2675,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve
rebuildDependencyGraph();
{// recompute label objects - populates the labels mapping.
for (Node slave : slaves)
for (Node slave : nodes.getNodes())
// Note that not all labels are visible until the slaves have connected.
slave.getAssignedLabels();
getAssignedLabels();
......
/*
* The MIT License
*
* Copyright (c) 2015, CloudBees, Inc., Stephen Connolly
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.model;
import hudson.BulkChange;
import hudson.Util;
import hudson.XmlFile;
import hudson.model.Computer;
import hudson.model.ItemGroupMixIn;
import hudson.model.Node;
import hudson.model.Queue;
import hudson.model.Saveable;
import hudson.model.listeners.SaveableListener;
import hudson.slaves.EphemeralNode;
import hudson.slaves.OfflineCause;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Manages all the nodes for Jenkins.
*
* @since 1.FIXME
*/
@Restricted(NoExternalUse.class) // for now, we may make it public later
public class Nodes implements Saveable {
/**
* The {@link Jenkins} instance that we are tracking nodes for.
*/
@Nonnull
private final Jenkins jenkins;
/**
* The map of nodes.
*/
private final ConcurrentMap<String, Node> nodes = new ConcurrentSkipListMap<String, Node>();
/**
* Constructor, intended to be called only from {@link Jenkins}.
*
* @param jenkins A reference to the {@link Jenkins} that this instance is tracking nodes for, beware not to
* let this reference escape from a partially constructed {@link Nodes} as when we are passed the
* reference the {@link Jenkins} instance has not completed instantiation.
*/
/*package*/ Nodes(@Nonnull Jenkins jenkins) {
this.jenkins = jenkins;
}
/**
* Returns the list of nodes.
*
* @return the list of nodes.
*/
@Nonnull
public List<Node> getNodes() {
return new ArrayList<Node>(nodes.values());
}
/**
* Sets the list of nodes.
*
* @param nodes the new list of nodes.
* @throws IOException if the new list of nodes could not be persisted.
*/
public void setNodes(final @Nonnull Collection<? extends Node> nodes) throws IOException {
Queue.withLock(new Runnable() {
@Override
public void run() {
Set<String> toRemove = new HashSet<String>(Nodes.this.nodes.keySet());
for (Node n : nodes) {
final String name = n.getNodeName();
toRemove.remove(name);
Nodes.this.nodes.put(name, n);
}
Nodes.this.nodes.keySet().removeAll(toRemove); // directory clean up will be handled by save
jenkins.updateComputerList();
jenkins.trimLabels();
}
});
save();
}
/**
* Adds a node. If a node of the same name already exists then that node will be replaced.
*
* @param node the new node.
* @throws IOException if the list of nodes could not be persisted.
*/
public void addNode(final @Nonnull Node node) throws IOException {
if (node != nodes.get(node.getNodeName())) {
// TODO we should not need to lock the queue for adding nodes but until we have a way to update the
// computer list for just the new node
Queue.withLock(new Runnable() {
@Override
public void run() {
nodes.put(node.getNodeName(), node);
jenkins.updateComputerList();
jenkins.trimLabels();
}
});
// no need for a full save() so we just do the minimum
if (node instanceof EphemeralNode) {
Util.deleteRecursive(new File(getNodesDir(), node.getNodeName()));
} else {
XmlFile xmlFile = new XmlFile(Jenkins.XSTREAM,
new File(new File(getNodesDir(), node.getNodeName()), "config.xml"));
xmlFile.write(node);
}
}
}
/**
* Removes a node. If the node instance is not in the list of nodes, then this will be a no-op, even if
* there is another instance with the same {@link Node#getNodeName()}.
*
* @param node the node instance to remove.
* @throws IOException if the list of nodes could not be persisted.
*/
public void removeNode(final @Nonnull Node node) throws IOException {
if (node == nodes.get(node.getNodeName())) {
Queue.withLock(new Runnable() {
@Override
public void run() {
Computer c = node.toComputer();
if (c != null) {
c.recordTermination();
c.disconnect(OfflineCause.create(hudson.model.Messages._Hudson_NodeBeingRemoved()));
}
if (node == nodes.remove(node.getNodeName())) {
jenkins.updateComputerList();
jenkins.trimLabels();
}
}
});
// no need for a full save() so we just do the minimum
Util.deleteRecursive(new File(getNodesDir(), node.getNodeName()));
}
}
/**
* {@inheritDoc}
*/
@Override
public void save() throws IOException {
if (BulkChange.contains(this)) {
return;
}
final File nodesDir = getNodesDir();
final Set<String> existing = new HashSet<String>();
for (Node n : nodes.values()) {
if (n instanceof EphemeralNode) {
continue;
}
existing.add(n.getNodeName());
XmlFile xmlFile = new XmlFile(Jenkins.XSTREAM, new File(new File(nodesDir, n.getNodeName()), "config.xml"));
xmlFile.write(n);
SaveableListener.fireOnChange(this, xmlFile);
}
for (File forDeletion : nodesDir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isDirectory() && !existing.contains(pathname.getName());
}
})) {
Util.deleteRecursive(forDeletion);
}
}
/**
* Returns the named node.
*
* @param name the {@link Node#getNodeName()} of the node to retrieve.
* @return the {@link Node} or {@code null} if the node could not be found.
*/
@CheckForNull
public Node getNode(String name) {
return nodes.get(name);
}
/**
* Loads the nodes from disk.
*
* @throws IOException if the nodes could not be deserialized.
*/
public void load() throws IOException {
final File nodesDir = getNodesDir();
final File[] subdirs = nodesDir.listFiles(new FileFilter() {
public boolean accept(File child) {
return child.isDirectory();
}
});
final Map<String, Node> newNodes = new TreeMap<String, Node>();
if (subdirs != null) {
for (File subdir : subdirs) {
try {
XmlFile xmlFile = new XmlFile(Jenkins.XSTREAM, new File(subdir, "config.xml"));
if (xmlFile.exists()) {
Node node = (Node) xmlFile.read();
newNodes.put(node.getNodeName(), node);
}
} catch (IOException e) {
Logger.getLogger(ItemGroupMixIn.class.getName()).log(Level.WARNING, "could not load " + subdir, e);
}
}
}
Queue.withLock(new Runnable() {
@Override
public void run() {
for (Iterator<Map.Entry<String, Node>> i = nodes.entrySet().iterator(); i.hasNext(); ) {
if (!(i.next().getValue() instanceof EphemeralNode)) {
i.remove();
}
}
nodes.putAll(newNodes);
jenkins.updateComputerList();
jenkins.trimLabels();
}
});
}
/**
* Returns the directory that the nodes are stored in.
*
* @return the directory that the nodes are stored in.
* @throws IOException
*/
private File getNodesDir() throws IOException {
final File nodesDir = new File(jenkins.getRootDir(), "nodes");
if (!nodesDir.isDirectory() && !nodesDir.mkdirs()) {
throw new IOException(String.format("Could not mkdirs %s", nodesDir));
}
return nodesDir;
}
/**
* Returns {@code true} if and only if the list of nodes is stored in the legacy location.
*
* @return {@code true} if and only if the list of nodes is stored in the legacy location.
*/
public boolean isLegacy() {
return !new File(jenkins.getRootDir(), "nodes").isDirectory();
}
}
......@@ -2,6 +2,8 @@ package jenkins.util;
import com.google.common.util.concurrent.SettableFuture;
import hudson.remoting.AtmostOneThreadExecutor;
import hudson.util.DaemonThreadFactory;
import hudson.util.NamingThreadFactory;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
......@@ -63,7 +65,12 @@ public class AtmostOneTaskExecutor<V> {
}
public AtmostOneTaskExecutor(Callable<V> task) {
this(new AtmostOneThreadExecutor(),task);
this(new AtmostOneThreadExecutor(new NamingThreadFactory(
new DaemonThreadFactory(),
String.format("AtmostOneTaskExecutor[%s]", task)
)),
task
);
}
public synchronized Future<V> submit() {
......
......@@ -315,6 +315,7 @@ RunParameterDefinition.DisplayName=Run Parameter
PasswordParameterDefinition.DisplayName=Password Parameter
Node.BecauseNodeIsReserved={0} is reserved for jobs with matching label expression
Node.BecauseNodeIsNotAcceptingTasks={0} is not accepting tasks
Node.LabelMissing={0} doesn\u2019t have label {1}
Node.LackingBuildPermission={0} doesn\u2019t have a permission to run on {1}
Node.Mode.NORMAL=Utilize this node as much as possible
......
<!--
The MIT License
Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
Copyright (c) 2004-2015, Sun Microsystems, Inc., Kohsuke Kawaguchi,
Stephen Connolly, id:cactusman, Yahoo! Inc., Alan Harder
Permission is hereby granted, free of charge, to any person obtaining a copy
......@@ -40,8 +40,7 @@ THE SOFTWARE.
</d:tag>
<d:tag name="executor">
<j:set var="ae" value="${e.asynchronousExecution}"/>
<j:choose>
<j:when test="${(!c.offline or (c.offline and !e.idle)) and (ae == null || ae.displayCell())}">
<j:if test="${(!c.offline or (c.offline and !e.idle)) and (ae == null || ae.displayCell())}">
<tr>
<td class="pane" align="right" style="vertical-align: top">
${name}
......@@ -77,6 +76,25 @@ THE SOFTWARE.
<td class="pane">
<div style="white-space: normal">
<j:set var="exe" value="${e.currentExecutable}" />
<j:set var="wu" value="${e.currentWorkUnit}" />
<j:choose>
<j:when test="${exe == null and wu != null}">
<j:set var="exeparent" value="${wu.work}"/>
<j:choose>
<j:when test="${h.hasPermission(exeparent,exeparent.READ)}">
<a href="${rootURL}/${exeparent.url}"><l:breakable value="${exeparent.fullDisplayName}"/></a>
<t:progressBar tooltip="${%Pending}" pos="-1" href="${rootURL}/${exeparent.url}"/>
</j:when>
<j:otherwise>
<span>${%Unknown Task}</span>
</j:otherwise>
</j:choose>
</j:when>
<j:when test="${exe == null and wu == null}">
<!-- went idle concurrent with testing for idle -->
${%Idle}
</j:when>
<j:otherwise>
<j:invokeStatic var="exeparent"
className="hudson.model.queue.Executables" method="getParentOf">
<j:arg type="hudson.model.Queue$Executable" value="${exe}" />
......@@ -90,6 +108,8 @@ THE SOFTWARE.
<span>${%Unknown Task}</span>
</j:otherwise>
</j:choose>
</j:otherwise>
</j:choose>
</div>
</td>
<td class="pane">
......@@ -106,29 +126,28 @@ THE SOFTWARE.
</j:otherwise>
</j:choose>
</tr>
</j:when>
</j:choose>
</j:if>
</d:tag>
</d:taglib>
<j:set var="computers" value="${attrs.computers?:app.computers}" />
<j:set var="computersSize" value="${computers.size()}"/>
<l:pane width="3" id="executors"
title="&lt;a href='${rootURL}/computer/'>${%Build Executor Status}&lt;/a>"
collapsedText="${%Computers(computers.size() - 1, app.unlabeledLoad.computeTotalExecutors() - app.unlabeledLoad.computeIdleExecutors(), app.unlabeledLoad.computeTotalExecutors())}">
collapsedText="${%Computers(computersSize - 1, app.unlabeledLoad.computeTotalExecutors() - app.unlabeledLoad.computeIdleExecutors(), app.unlabeledLoad.computeTotalExecutors())}">
<colgroup><col width="30"/><col width="200*"/><col width="24"/></colgroup>
<j:forEach var="c" items="${computers}">
<j:set var="cDisplayExecutors" value="${c.displayExecutors}"/>
<tr>
<j:if test="${computers.size() gt 1 and (c.executors.size()!=0 or c.oneOffExecutors.size()!=0)}">
<j:if test="${computersSize gt 1 and !cDisplayExecutors.isEmpty()}">
<th class="pane" colspan="3">
<local:computerCaption title="${c.displayName}" />
</th>
</j:if>
</tr>
<j:forEach var="e" items="${c.executors}" varStatus="eloop">
<local:executor name="${eloop.index+1}" url="executors/${eloop.index}" />
</j:forEach>
<j:forEach var="e" items="${c.oneOffExecutors}" varStatus="eloop">
<local:executor name="" url="oneOffExecutors/${eloop.index}" />
<j:forEach var="de" items="${cDisplayExecutors}" varStatus="eloop">
<j:set var="e" value="${de.executor}"/>
<local:executor name="${de.displayName}" url="${de.url}" />
</j:forEach>
</j:forEach>
</l:pane>
......
......@@ -69,11 +69,14 @@ ${h.initPageVariables(context)}
<j:set var="isMSIE" value="${userAgent.contains('MSIE')}"/>
<j:set var="_" value="${request.getSession()}"/>
<j:set var="_" value="${h.configureAutoRefresh(request, response, attrs.norefresh!=null and !attrs.norefresh.equals(false))}"/>
<j:set var="extensionsAvailable" value="${h.extensionsAvailable}"/>
<j:if test="${request.servletPath=='/' || request.servletPath==''}">
${h.advertiseHeaders(response)}
<j:forEach var="pd" items="${h.pageDecorators}">
<st:include it="${pd}" page="httpHeaders.jelly" optional="true"/>
</j:forEach>
<j:if test="${extensionsAvailable}">
<j:forEach var="pd" items="${h.pageDecorators}">
<st:include it="${pd}" page="httpHeaders.jelly" optional="true"/>
</j:forEach>
</j:if>
</j:if>
<x:doctype name="html" />
<html>
......@@ -126,9 +129,11 @@ ${h.initPageVariables(context)}
<script src="${resURL}/scripts/hudson-behavior.js" type="text/javascript"></script>
<script src="${resURL}/scripts/sortable.js" type="text/javascript"/>
<script>
crumb.init("${h.getCrumbRequestField()}", "${h.getCrumb(request)}");
</script>
<j:if test="${extensionsAvailable}">
<script>
crumb.init("${h.getCrumbRequestField()}", "${h.getCrumb(request)}");
</script>
</j:if>
<link rel="stylesheet" href="${resURL}/scripts/yui/container/assets/container.css" type="text/css"/>
<link rel="stylesheet" href="${resURL}/scripts/yui/assets/skins/sam/skin.css" type="text/css" />
......@@ -143,9 +148,11 @@ ${h.initPageVariables(context)}
<meta name="ROBOTS" content="INDEX,NOFOLLOW" />
<j:set var="mode" value="header" />
<d:invokeBody />
<j:forEach var="pd" items="${h.pageDecorators}">
<st:include it="${pd}" page="header.jelly" optional="true" />
</j:forEach>
<j:if test="${extensionsAvailable}">
<j:forEach var="pd" items="${h.pageDecorators}">
<st:include it="${pd}" page="header.jelly" optional="true" />
</j:forEach>
</j:if>
<j:if test="${isMSIE}">
<script src="${resURL}/scripts/msie.js" type="text/javascript"/>
......@@ -259,9 +266,11 @@ ${h.initPageVariables(context)}
<span class="jenkins_ver">
<a href="${h.getFooterURL()}">Jenkins ver. ${h.version}</a>
</span>
<j:if test="${extensionsAvailable}">
<j:forEach var="pd" items="${h.pageDecorators}">
<st:include it="${pd}" page="footer.jelly" optional="true" />
</j:forEach>
</j:if>
</div>
</div>
</body>
......
/*
* The MIT License
*
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
*
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
......@@ -400,7 +400,6 @@ public class AbstractProjectTest extends HudsonTestCase {
public void testQueueSuccessBehavior() {
// prevent any builds to test the behaviour
jenkins.numExecutors = 0;
jenkins.updateComputerList(false);
def p = createFreeStyleProject()
def f = p.scheduleBuild2(0)
......@@ -419,7 +418,6 @@ public class AbstractProjectTest extends HudsonTestCase {
public void testQueueSuccessBehaviorOverHTTP() {
// prevent any builds to test the behaviour
jenkins.numExecutors = 0;
jenkins.updateComputerList(false);
def p = createFreeStyleProject()
def wc = createWebClient();
......
......@@ -34,7 +34,6 @@ public class ExecutorTest {
@Test
public void yank() throws Exception {
j.jenkins.setNumExecutors(1);
j.jenkins.updateComputerList(true);
Computer c = j.jenkins.toComputer();
final Executor e = c.getExecutors().get(0);
......@@ -59,7 +58,6 @@ public class ExecutorTest {
@Issue("JENKINS-4756")
public void whenAnExecutorIsYankedANewExecutorTakesItsPlace() throws Exception {
j.jenkins.setNumExecutors(1);
j.jenkins.updateComputerList(true);
Computer c = j.jenkins.toComputer();
Executor e = getExecutorByNumber(c, 0);
......
......@@ -20,27 +20,27 @@ import org.jvnet.hudson.test.HudsonTestCase;
/**
* Tests that getEnvironment() calls outside of builds are safe.
*
*
* @author kutzi
*/
@Issue("JENKINS-11592")
public class GetEnvironmentOutsideBuildTest extends HudsonTestCase {
private int oldExecNum;
@Override
protected void runTest() throws Throwable {
// Disable tests
// It's unfortunately not working, yet, as whenJenkinsMasterHasNoExecutors is not working as expected
}
public void setUp() throws Exception {
super.setUp();
this.oldExecNum = Jenkins.getInstance().getNumExecutors();
}
public void tearDown() throws Exception {
restoreOldNumExecutors();
super.tearDown();
......@@ -48,10 +48,9 @@ public class GetEnvironmentOutsideBuildTest extends HudsonTestCase {
private void restoreOldNumExecutors() throws IOException {
Jenkins.getInstance().setNumExecutors(this.oldExecNum);
Jenkins.getInstance().setNodes(Jenkins.getInstance().getNodes());
assertNotNull(Jenkins.getInstance().toComputer());
}
private MavenModuleSet createSimpleMavenProject() throws Exception {
MavenModuleSet project = createMavenProject();
MavenInstallation mi = configureMaven3();
......@@ -61,36 +60,34 @@ public class GetEnvironmentOutsideBuildTest extends HudsonTestCase {
project.setGoals("validate");
return project;
}
private void whenJenkinsMasterHasNoExecutors() throws IOException {
Jenkins.getInstance().setNumExecutors(0);
// force update of nodes:
Jenkins.getInstance().setNodes(Jenkins.getInstance().getNodes());
assertNull(Jenkins.getInstance().toComputer());
}
public void testMaven() throws Exception {
MavenModuleSet m = createSimpleMavenProject();
assertGetEnvironmentCallOutsideBuildWorks(m);
}
public void testFreestyle() throws Exception {
FreeStyleProject project = createFreeStyleProject();
assertGetEnvironmentCallOutsideBuildWorks(project);
}
public void testMatrix() throws Exception {
MatrixProject createMatrixProject = createMatrixProject();
assertGetEnvironmentCallOutsideBuildWorks(createMatrixProject);
}
@SuppressWarnings({"rawtypes", "unchecked"})
private void assertGetEnvironmentCallOutsideBuildWorks(AbstractProject job) throws Exception {
AbstractBuild build = buildAndAssertSuccess(job);
assertGetEnvironmentWorks(build);
}
......
/*
* The MIT License
*
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
*
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
......@@ -113,7 +113,6 @@ public class QueueTest {
// prevent execution to push stuff into the queue
r.jenkins.setNumExecutors(0);
r.jenkins.setNodes(r.jenkins.getNodes());
FreeStyleProject testProject = r.createFreeStyleProject("test");
testProject.scheduleBuild(new UserIdCause());
......@@ -130,7 +129,7 @@ public class QueueTest {
assertEquals(1, q.getItems().length);
// did it bind back to the same object?
assertSame(q.getItems()[0].task,testProject);
assertSame(q.getItems()[0].task,testProject);
}
/**
......@@ -162,7 +161,6 @@ public class QueueTest {
// prevent execution to push stuff into the queue
r.jenkins.setNumExecutors(0);
r.jenkins.setNodes(r.jenkins.getNodes());
FreeStyleProject testProject = r.createFreeStyleProject("test");
testProject.scheduleBuild(new UserIdCause());
......@@ -393,7 +391,7 @@ public class QueueTest {
assertEquals(1, runs.size());
assertEquals("slave0", runs.get(0).getBuiltOnStr());
}
@Issue("JENKINS-10944")
@Test public void flyweightTasksBlockedByShutdown() throws Exception {
r.jenkins.doQuietDown(true, 0);
......@@ -607,8 +605,8 @@ public class QueueTest {
@Override
public Label getAssignedLabel(){
throw new IllegalArgumentException("Test exception"); //cause dead of executor
}
}
@Override
public void save(){
//do not need save
......@@ -635,7 +633,7 @@ public class QueueTest {
break; // executor is dead due to exception
}
if(e.isIdle()){
assertTrue("Node went to idle before project had" + project2.getDisplayName() + " been started", v.isDone());
assertTrue("Node went to idle before project had" + project2.getDisplayName() + " been started", v.isDone());
}
Thread.sleep(1000);
}
......@@ -644,37 +642,37 @@ public class QueueTest {
Queue.getInstance().cancel(projectError); // cancel job which cause dead of executor
e.doYank(); //restart executor
while(!e.isIdle()){ //executor should take project2 from queue
Thread.sleep(1000);
Thread.sleep(1000);
}
//project2 should not be in pendings
List<Queue.BuildableItem> items = Queue.getInstance().getPendingItems();
for(Queue.BuildableItem item : items){
assertFalse("Project " + project2.getDisplayName() + " stuck in pendings",item.task.getName().equals(project2.getName()));
assertFalse("Project " + project2.getDisplayName() + " stuck in pendings",item.task.getName().equals(project2.getName()));
}
}
@Test public void cancelInQueue() throws Exception {
// parepare an offline slave.
DumbSlave slave = r.createOnlineSlave();
assertFalse(slave.toComputer().isOffline());
slave.toComputer().disconnect(null).get();
assertTrue(slave.toComputer().isOffline());
FreeStyleProject p = r.createFreeStyleProject();
p.setAssignedNode(slave);
QueueTaskFuture<FreeStyleBuild> f = p.scheduleBuild2(0);
try {
f.get(3, TimeUnit.SECONDS);
fail("Should time out (as the slave is offline).");
} catch (TimeoutException e) {
}
Queue.Item item = Queue.getInstance().getItem(p);
assertNotNull(item);
Queue.getInstance().doCancelItem(item.getId());
assertNull(Queue.getInstance().getItem(p));
try {
f.get(10, TimeUnit.SECONDS);
fail("Should not get (as it is cancelled).");
......
......@@ -47,7 +47,7 @@ public class JenkinsReloadConfigurationTest {
}
private void modifyNode(Node node) throws Exception {
replace("config.xml", "oldLabel", "newLabel");
replace(node.getNodeName().equals("") ? "config.xml" : String.format("nodes/%s/config.xml",node.getNodeName()), "oldLabel", "newLabel");
assertEquals("oldLabel", node.getLabelString());
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册