diff --git a/changelog.html b/changelog.html index 1d34749c3d9036b59a7bf4eff026a47c22990cb8..a2d1ff157728a7ec635a6d6bf630417253abb05d 100644 --- a/changelog.html +++ b/changelog.html @@ -61,6 +61,8 @@ Upcoming changes
  • Fingerprint action deserialization problem fixed. (issue 17125) +
  • + 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.