package org.terracotta.jenkins.plugins.acceleratedbuildnow; import hudson.matrix.MatrixConfiguration; import hudson.matrix.MatrixProject; import hudson.maven.MavenBuild; import hudson.model.Action; import hudson.model.Item; import hudson.model.Result; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Cause; import hudson.model.Computer; import hudson.model.Executor; import hudson.model.Label; import hudson.model.Node; import hudson.model.queue.QueueSorter; import hudson.model.queue.QueueTaskFuture; import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.logging.Logger; import javax.servlet.ServletException; import jenkins.model.Jenkins; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; /** * This class contains the main logic of the plugin * * @author : Anthony Dahanne */ public class AcceleratedBuildNowAction implements Action { private static final Logger LOG = Logger.getLogger(AcceleratedBuildNowAction.class.getName()); private final AbstractProject project; public AcceleratedBuildNowAction(AbstractProject abstractProject) { this.project = abstractProject; } public String getDisplayName() { if (project.hasPermission(Item.BUILD)) { return "Accelerated Build Now !"; } return null; } public String getIconFileName() { if (project.hasPermission(Item.BUILD)) { return "/plugin/accelerated-build-now-plugin/images/icon-64x64.jpg"; } return null; } public String getUrlName() { if (project.hasPermission(Item.BUILD)) { return "accelerated"; } return null; } public void doBuild(final StaplerRequest request, final StaplerResponse response) throws ServletException, IOException, InterruptedException, ExecutionException { if (!project.hasPermission(Item.BUILD)) { // Jenkins is secured AND the user is not supposed to build this job response.sendRedirect(request.getContextPath() + '/' + project.getUrl()); } LOG.info("project : " + project.getName() + " needs to be built NOW !"); Label assignedLabel = project.getAssignedLabel(); // what if the user clicks repeatedly on the link ? boolean alreadyTakenCareOf = project.getLastBuild() != null && project.getLastBuild().isBuilding() || queueSorterPriorityOn(project); // what if the queue is empty and all executors are already busy ? boolean jenkinsIsFreeToBuild = Jenkins.getInstance().getQueue().isEmpty() && atLeastOneExecutorIsIdle(assignedLabel); LOG.info("project : " + project.getName() + " is scheduled to build now !"); QueueTaskFuture queueTaskFuture = project.scheduleBuild2(0, new Cause.UserIdCause(), new Action[0]); if (alreadyTakenCareOf || jenkinsIsFreeToBuild) { LOG.info("No need for AcceleratedBuildNow plugin (already building or empty queue with idle executors"); if((Jenkins.getInstance().getQueue().getSorter() !=null) && (Jenkins.getInstance().getQueue().getBuildableItems()!=null)) { Jenkins.getInstance().getQueue().getSorter().sortBuildableItems(Jenkins.getInstance().getQueue().getBuildableItems()); } response.sendRedirect(request.getContextPath() + '/' + project.getUrl()); return; } //replace the original queue sorter with one that will place our project build first in the queue QueueSorter originalQueueSorter = Jenkins.getInstance().getQueue().getSorter(); AcceleratedBuildNowSorter acceleratedBuildNowSorter = new AcceleratedBuildNowSorter(project, originalQueueSorter); Jenkins.getInstance().getQueue().setSorter(acceleratedBuildNowSorter); // we sort the queue so that our project is next to be built on the list Jenkins.getInstance().getQueue().getSorter().sortBuildableItems(Jenkins.getInstance().getQueue().getBuildableItems()); AbstractBuild killedBuild = null; List<AbstractProject> allItems = Jenkins.getInstance().getAllItems(AbstractProject.class); for (AbstractProject projectConsidered : allItems) { AbstractBuild lastBuild = getLastBuild(projectConsidered); if (lastBuild != null && lastBuild.isBuilding()) { if (isBuildNotTriggeredByHuman(lastBuild) && slaveRunningBuildCompatible(lastBuild, assignedLabel) && !(projectConsidered instanceof MatrixProject) && !(projectConsidered instanceof MatrixConfiguration)) { LOG.info("project : " + lastBuild.getProject().getName() + " #" + lastBuild.getNumber() + " was not scheduled by a human, killing it right now to re schedule it later !"); Executor executor = getExecutor(lastBuild); executor.interrupt(Result.ABORTED); killedBuild = lastBuild; break; } } } if (killedBuild == null) { LOG.info("project : " + project.getName() + " could not be acceleratedly built (no builds could be aborted) : 'normal' build was triggered though !"); } else { AbstractBuild projectBuild = ((Future<AbstractBuild>) queueTaskFuture.getStartCondition()).get(); LOG.info("build #" + projectBuild.getNumber() + " for " + project.getName() + " was launched successfully !"); // we add a nice badge to the killer build projectBuild.getActions().add(new AcceleratedBuildNowBadgeAction(killedBuild)); // we add a nice badge to the killeD build killedBuild.getActions().add(new AcceleratedBuildNowVictimBadgeAction(projectBuild)); // we re schedule the build that got killed rescheduleKilledBuild(killedBuild, new AcceleratedBuildNowKilledCause(), originalQueueSorter); } Jenkins.getInstance().getQueue().setSorter(originalQueueSorter); response.sendRedirect(request.getContextPath() + '/' + project.getUrl()); } private Executor getExecutor(AbstractBuild lastBuild) { Executor executorConsidered = null; for (Executor executor : lastBuild.getBuiltOn().toComputer().getExecutors()) { if (lastBuild.equals(executor.getCurrentExecutable())) { executorConsidered = executor; } } return executorConsidered; } private AbstractBuild getLastBuild(AbstractProject projectConsidered) { AbstractBuild lastBuild = projectConsidered.getLastBuild(); // be careful that lastBuild can be a maven module building ! if (lastBuild instanceof MavenBuild) { lastBuild = ((MavenBuild) lastBuild).getParentBuild(); } return lastBuild; } private boolean atLeastOneExecutorIsIdle(Label assignedLabel) { int idleExecutors = 0; if (assignedLabel == null) { // no assignedLabel ? that's fine, let's find any idle executor // code based on Label.class code Set<Node> nodes = new HashSet<Node>(); Jenkins h = Jenkins.getInstance(); nodes.add(h); for (Node n : h.getNodes()) { nodes.add(n); } for (Node n : nodes) { Computer c = n.toComputer(); if (c != null && (c.isOnline() || c.isConnecting()) && c.isAcceptingTasks()) { idleExecutors += c.countIdle(); } } } else { idleExecutors = assignedLabel.getIdleExecutors(); } return idleExecutors > 0; } private boolean queueSorterPriorityOn(AbstractProject project) { QueueSorter originalQueueSorter = Jenkins.getInstance().getQueue().getSorter(); if (originalQueueSorter instanceof AcceleratedBuildNowSorter) { if (((AcceleratedBuildNowSorter) originalQueueSorter).getProject().equals(project)) { return true; } } return false; } private boolean isBuildNotTriggeredByHuman(AbstractBuild lastBuild) { return lastBuild.getCause(Cause.UserIdCause.class) == null && lastBuild.getCause(Cause.UserCause.class) == null; } private boolean slaveRunningBuildCompatible(AbstractBuild lastBuild, Label assignedLabel) { boolean contains = assignedLabel == null ? true : lastBuild.getBuiltOn().getAssignedLabels().contains(assignedLabel); if (!contains) { LOG.info("build : " + lastBuild.getNumber() + " of project " + lastBuild.getProject().getName() + " is not running on node compatible with " + assignedLabel.getName()); } return contains; } private void rescheduleKilledBuild(AbstractBuild killedBuild, Cause cause, QueueSorter originalQueueSorter) throws ExecutionException, InterruptedException { //replace the original queue sorter with one that will place our project build first in the queue AcceleratedBuildNowSorter acceleratedBuildNowSorter = new AcceleratedBuildNowSorter(project, originalQueueSorter); Jenkins.getInstance().getQueue().setSorter(acceleratedBuildNowSorter); QueueTaskFuture queueTaskFuture = killedBuild.getProject().scheduleBuild2(0, cause, new Action[0]); LOG.info("build that was killed : " + killedBuild.getProject().getName() + " #" + killedBuild.getNumber() + " is scheduled to build next !"); // we sort the queue so that our project is next to be built on the list Jenkins.getInstance().getQueue().getSorter().sortBuildableItems(Jenkins.getInstance().getQueue().getBuildableItems()); } public static class AcceleratedBuildNowKilledCause extends Cause { public AcceleratedBuildNowKilledCause() { } public String getShortDescription() { return "Started by AcceleratedBuildNow plugin"; } @Override public boolean equals(Object o) { return o instanceof AcceleratedBuildNowKilledCause; } @Override public int hashCode() { return 42; } } }