diff --git a/changelog.html b/changelog.html
index 1d34749c3d9036b59a7bf4eff026a47c22990cb8..a2d1ff157728a7ec635a6d6bf630417253abb05d 100644
--- a/changelog.html
+++ b/changelog.html
@@ -61,6 +61,8 @@ Upcoming changes
+ Improved the tracking of queued jobs and their eventual builds in the REST API.
diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java
index 239d6ffaed316fd4a7527ec5539ed42d304851f0..14571c350335e80ea2f3e91016aa7905faccea91 100644
--- a/core/src/main/java/hudson/model/AbstractProject.java
+++ b/core/src/main/java/hudson/model/AbstractProject.java
@@ -1798,8 +1798,11 @@ public abstract class AbstractProject,R extends A
if (!isBuildable())
throw HttpResponses.error(SC_INTERNAL_SERVER_ERROR,new IOException(getFullName()+" is not buildable"));
- Jenkins.getInstance().getQueue().schedule(this, (int)delay.getTime(), getBuildCause(req));
- rsp.sendRedirect(".");
+ WaitingItem item = Jenkins.getInstance().getQueue().schedule(this, (int) delay.getTime(), getBuildCause(req));
+ if (item!=null) {
+ rsp.sendRedirect(SC_CREATED,req.getContextPath()+'/'+item.getUrl());
+ } else
+ rsp.sendRedirect(".");
}
/**
diff --git a/core/src/main/java/hudson/model/Executor.java b/core/src/main/java/hudson/model/Executor.java
index 4fcb0968fd33e5bb9f5dd99d1750b0a376005ca6..b99b6fbb0296808ce7e4358b1f377ce7d7ff2198 100644
--- a/core/src/main/java/hudson/model/Executor.java
+++ b/core/src/main/java/hudson/model/Executor.java
@@ -214,6 +214,7 @@ public class Executor extends Thread implements ModelObject {
task = workUnit.work;
startTime = System.currentTimeMillis();
executable = task.createExecutable();
+ workUnit.setExecutable(executable);
}
if (LOGGER.isLoggable(FINE))
LOGGER.log(FINE, getName()+" is going to execute "+executable);
diff --git a/core/src/main/java/hudson/model/ParametersDefinitionProperty.java b/core/src/main/java/hudson/model/ParametersDefinitionProperty.java
index 0cc266ce430fcfb0498dbb574411bbc7ecfaba6f..e25116bb6f5245391efcc3121f5131b495fd88b1 100644
--- a/core/src/main/java/hudson/model/ParametersDefinitionProperty.java
+++ b/core/src/main/java/hudson/model/ParametersDefinitionProperty.java
@@ -34,6 +34,7 @@ import java.util.AbstractList;
import javax.servlet.ServletException;
+import hudson.model.Queue.WaitingItem;
import jenkins.model.Jenkins;
import jenkins.util.TimeDuration;
import net.sf.json.JSONArray;
@@ -48,6 +49,8 @@ import org.kohsuke.stapler.export.ExportedBean;
import hudson.Extension;
import org.kohsuke.stapler.export.Flavor;
+import static javax.servlet.http.HttpServletResponse.SC_CREATED;
+
/**
* Keeps a list of the parameters defined for a project.
*
@@ -132,11 +135,13 @@ public class ParametersDefinitionProperty extends JobProperty
* This class implements the core scheduling logic. {@link Task} represents the executable
* task that are placed in the queue. While in the queue, it's wrapped into {@link Item}
- * so that we can keep track of additional data used for deciding what to exeucte when.
+ * so that we can keep track of additional data used for deciding what to execute when.
*
*
* Items in queue goes through several stages, as depicted below:
@@ -125,7 +132,7 @@ import org.kohsuke.stapler.interceptor.RequirePOST;
* | ^
* | |
* | v
- * +--> buildables ---> pending ---> (executed)
+ * +--> buildables ---> pending ---> left
*
*
*
@@ -167,8 +174,36 @@ public class Queue extends ResourceController implements Saveable {
*/
private final ItemList pendings = new ItemList();
+ /**
+ * Items that left queue would stay here for a while to enable tracking via {@link Item#id}.
+ *
+ * This map is forgetful, since we can't remember everything that executed in the past.
+ */
+// private final FifoMap leftItems = new FifoMap(256);
+ private final Cache leftItems = CacheBuilder.newBuilder().expireAfterWrite(5*60, TimeUnit.SECONDS).build();
+
private final CachedItemList itemsView = new CachedItemList();
+// class FifoMap {
+// private final ConcurrentMap store = new ConcurrentHashMap();
+// private final BlockingQueue order = new LinkedBlockingQueue();
+// private final int size;
+//
+// FifoMap(int size) {
+// this.size = size;
+// }
+//
+// private void add(K k, V v) {
+// store.put(k,v);
+// order.add(k);
+// while (order.size()>size)
+// store.remove(order.remove());
+// }
+//
+// public V get(K k) {
+// return store.get(k);
+// }
+// }
/**
* Maintains a copy of {@link Queue#getItems()}
*
@@ -612,6 +647,10 @@ public class Queue extends ResourceController implements Saveable {
LOGGER.log(Level.FINE, "Cancelling {0} item#{1}", new Object[] {item.task, item.id});
// use bitwise-OR to make sure that all the branches get evaluated all the time
boolean r = (item instanceof WaitingItem && waitingList.remove(item)) | blockedProjects.remove(item) | buildables.remove(item);
+
+ LeftItem li = new LeftItem(item);
+ leftItems.put(li.id,li);
+
if(r)
item.onCancelled();
return r;
@@ -683,7 +722,8 @@ public class Queue extends ResourceController implements Saveable {
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;
- return null;
+
+ return leftItems.getIfPresent(id);
}
/**
@@ -720,6 +760,13 @@ public class Queue extends ResourceController implements Saveable {
return new ArrayList(pendings.values());
}
+ /**
+ * Returns the snapshot of all {@link LeftItem}s.
+ */
+ public Collection getLeftItems() {
+ return Collections.unmodifiableCollection(leftItems.asMap().values());
+ }
+
/**
* Gets all items that are in the queue but not blocked
*
@@ -852,6 +899,9 @@ public class Queue extends ResourceController implements Saveable {
OneOffExecutor ooe = (OneOffExecutor) exec;
final WorkUnit wu = ooe.getAssignedWorkUnit();
pendings.remove(wu.context.item);
+
+ LeftItem li = new LeftItem(wu.context);
+ leftItems.put(li.id,li);
return wu;
}
@@ -895,8 +945,11 @@ public class Queue extends ResourceController implements Saveable {
LOGGER.log(Level.FINE, "Pop returning {0} for {1}", new Object[] {offer.workUnit, exec.getName()});
// TODO: I think this has to be done by the last executor that leaves the pop(), not by main executor
- if (offer.workUnit.isMainWork())
+ if (offer.workUnit.isMainWork()) {
pendings.remove(offer.workUnit.context.item);
+ LeftItem li = new LeftItem(offer.workUnit.context);
+ leftItems.put(li.id,li);
+ }
return offer.workUnit;
}
@@ -1407,6 +1460,17 @@ public class Queue extends ResourceController implements Saveable {
this(item.task, item.getActions(), item.id, item.future, item.inQueueSince);
}
+ /**
+ * Returns the URL of this {@link Item} relative to the context path of Jenkins
+ *
+ * @return
+ * URL that ends with '/'.
+ */
+ @Exported
+ public String getUrl() {
+ return "queue/item/"+id+'/';
+ }
+
/**
* Gets a human-readable status message describing why it's in the queue.
*/
@@ -1468,6 +1532,10 @@ public class Queue extends ResourceController implements Saveable {
future.setAsCancelled();
}
+ public Api getApi() {
+ return new Api(this);
+ }
+
private Object readResolve() {
this.future = new FutureImpl(task);
return this;
@@ -1691,6 +1759,52 @@ public class Queue extends ResourceController implements Saveable {
}
}
+ /**
+ * {@link Item} in the {@link Queue#leftItems} stage. These are items that had left the queue
+ * by either began executing or by getting cancelled.
+ */
+ public final static class LeftItem extends Item {
+ public final WorkUnitContext outcome;
+
+ /**
+ * When item has left the queue and begin executing.
+ */
+ public LeftItem(WorkUnitContext wuc) {
+ super(wuc.item);
+ this.outcome = wuc;
+ }
+
+ /**
+ * When item is cancelled.
+ */
+ public LeftItem(Item cancelled) {
+ super(cancelled);
+ this.outcome = null;
+ }
+
+ @Override
+ public CauseOfBlockage getCauseOfBlockage() {
+ return null;
+ }
+
+ /**
+ * If this is representing an item that started executing, this property returns
+ * the primary executable (such as {@link AbstractBuild}) that created out of it.
+ */
+ @Exported
+ public Executable getExecutable() {
+ return outcome!=null ? outcome.getPrimaryWorkUnit().getExecutable() : null;
+ }
+
+ /**
+ * Is this representing a cancelled item?
+ */
+ @Exported
+ public boolean isCancelled() {
+ return outcome==null;
+ }
+ }
+
private static final Logger LOGGER = Logger.getLogger(Queue.class.getName());
/**
diff --git a/core/src/main/java/hudson/model/queue/WorkUnit.java b/core/src/main/java/hudson/model/queue/WorkUnit.java
index 739cdfe64a5a655fcc92f1964cd6c8c2e27c663f..000bc02ee6610ac9948f8373b8061d800b15aefd 100644
--- a/core/src/main/java/hudson/model/queue/WorkUnit.java
+++ b/core/src/main/java/hudson/model/queue/WorkUnit.java
@@ -48,6 +48,7 @@ public final class WorkUnit {
public final WorkUnitContext context;
private volatile Executor executor;
+ private Executable executable;
WorkUnit(WorkUnitContext context, SubTask work) {
this.context = context;
@@ -69,10 +70,14 @@ public final class WorkUnit {
}
/**
- * If the execution has already started, return the current executable.
+ * If the execution has already started, return the executable that was created.
*/
public Executable getExecutable() {
- return executor!=null ? executor.getCurrentExecutable() : null;
+ return executable;
+ }
+
+ public void setExecutable(Executable executable) {
+ this.executable = executable;
}
/**
diff --git a/core/src/main/resources/hudson/model/Job/_api.jelly b/core/src/main/resources/hudson/model/Job/_api.jelly
index 5d019fbb395fc8daefa772a75a07b7c596c4fee9..4d73d50d6215c609855b7e2aeb91839edb643f09 100644
--- a/core/src/main/resources/hudson/model/Job/_api.jelly
+++ b/core/src/main/resources/hudson/model/Job/_api.jelly
@@ -37,6 +37,11 @@ THE SOFTWARE.
To programmatically schedule a new build, post to this URL.
If the build has parameters, post to this URL and provide the parameters as form data.
+ Either way, the successful queueing will result in 201 status code with Location HTTP header
+ pointing the URL of the item in the queue. By polling the api/xml sub-URL of the queue item,
+ you can track the status of the queued task. Generally, the task will go through some state transitions,
+ then eventually it becomes either cancelled (look for the "cancelled" boolean property), or gets executed
+ (look for the "executable" property that typically points to the AbstractBuild object.)
To programmatically schedule SCM polling, post to this URL.