Java tutorial
/* * The MIT License * * Copyright 2014 Smartmatic International Corporation. All rights reserved. * * 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 com.sonyericsson.hudson.plugins.gerrit.trigger.dependency; import hudson.Extension; import hudson.ExtensionList; import hudson.model.Cause; import hudson.model.Item; import hudson.model.Job; import hudson.model.Queue; import hudson.model.Run; import hudson.model.queue.QueueTaskDispatcher; import hudson.model.queue.CauseOfBlockage; import jenkins.model.Jenkins; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.Collections; import java.util.StringTokenizer; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.sonymobile.tools.gerrit.gerritevents.GerritDefaultValues; import com.sonymobile.tools.gerrit.gerritevents.GerritEventListener; import com.sonymobile.tools.gerrit.gerritevents.dto.events.GerritTriggeredEvent; import com.sonymobile.tools.gerrit.gerritevents.dto.GerritEvent; import com.sonymobile.tools.gerrit.gerritevents.GerritHandler; import com.sonyericsson.hudson.plugins.gerrit.trigger.events.lifecycle.GerritEventLifecycleListener; import com.sonyericsson.hudson.plugins.gerrit.trigger.events.lifecycle.GerritEventLifecycle; import com.sonyericsson.hudson.plugins.gerrit.trigger.hudsontrigger.GerritCause; import com.sonyericsson.hudson.plugins.gerrit.trigger.hudsontrigger.GerritTrigger; import com.sonyericsson.hudson.plugins.gerrit.trigger.gerritnotifier.ToGerritRunListener; import com.sonyericsson.hudson.plugins.gerrit.trigger.PluginImpl; /** * Blocks builds from running until the projects on which they depend have finished building. * This applies on a per-event basis, so for each event, the plugin will wait for * dependency projects (i.e., projects on which it depends) which also trigger for the same * event, to finish building before building a dependent project. * * @author Yannick Brhon <yannick.brehon@smartmatic.com> */ @Extension public final class DependencyQueueTaskDispatcher extends QueueTaskDispatcher implements GerritEventLifecycleListener, GerritEventListener { private static final Logger logger = LoggerFactory.getLogger(DependencyQueueTaskDispatcher.class); private Set<GerritTriggeredEvent> currentlyTriggeringEvents; /** * Default constructor. */ public DependencyQueueTaskDispatcher() { this(PluginImpl.getHandler_()); } /** * Constructor use by default constructor and for unit tests. * * @param gerritHandler the handler */ DependencyQueueTaskDispatcher(GerritHandler gerritHandler) { this.currentlyTriggeringEvents = Collections .newSetFromMap(new ConcurrentHashMap<GerritTriggeredEvent, Boolean>()); if (gerritHandler == null) { logger.error("Gerrit Handler was not available to construct DependencyQueueTaskDispatcher"); } else { gerritHandler.addListener(this); } logger.debug("Registered to gerrit events"); } /** * Returns the registered instance of this class from the list of all listeners. * * @return the instance. */ public static DependencyQueueTaskDispatcher getInstance() { Jenkins jenkins = Jenkins.getInstance(); if (jenkins == null) { logger.error("INITIALIZATION ERROR? Could not find the Jenkins instance."); return null; } ExtensionList<DependencyQueueTaskDispatcher> dispatchers = jenkins .getExtensionList(DependencyQueueTaskDispatcher.class); if (dispatchers == null || dispatchers.isEmpty()) { logger.error("INITIALIZATION ERROR? Could not find the registered instance."); return null; } return dispatchers.get(0); } @Override public CauseOfBlockage canRun(Queue.Item item) { //Job check if (!(item.task instanceof Job)) { logger.debug("Not an Job instance: {}", item.task); return null; } GerritCause cause = getGerritCause(item); //Not gerrit-triggered if (cause == null) { logger.debug("Not Gerrit cause for {}", item); return null; } GerritTriggeredEvent event = cause.getEvent(); //The GerritCause should contain an event, but just in case. if (event == null) { logger.debug("{} does not contain an event", item); return null; } //we do not block an item when it reached the buildable state: a buildable item is //an item for which it has already been determined it canRun, and it is only //waiting for an executor. Once the executor is avail, an extra check is done, but //we already determined in the previous canRun checks that its dependencies were done. if (item.isBuildable()) { logger.debug("{} is already buildable for {}", item, event); return null; } Job p = (Job) item.task; GerritTrigger trigger = GerritTrigger.getTrigger(p); //The project being checked has no Gerrit Trigger if (trigger == null) { logger.debug("Project {} does not contain a trigger", p); return null; } //Dependency projects in the build queue List<Job> dependencies = getProjectsFromString(trigger.getDependencyJobsNames(), p); if (dependencies == null || dependencies.isEmpty()) { logger.debug("No dependencies on project: {} for event {}", p, event); return null; } //logger.debug("We have dependencies on project {} : {}", p, trigger.getDependencyJobsNames()); // We ensure that we wait until other jobs have been put into queue. // We use the default Gerrit Build Schedule Delay value long inQueueSince = item.getInQueueSince(); if (System.currentTimeMillis() - inQueueSince < TimeUnit.SECONDS .toMillis(GerritDefaultValues.DEFAULT_BUILD_SCHEDULE_DELAY)) { logger.debug("We need to wait to ensure dependent jobs {} are in queue for {}", event, p); return new BecauseWaitingToEnsureOtherJobsAreInQueue(); } /* we really should first check for other projects which will be triggered * for the same event, and haven't yet. Unfortunately, this requires some kind * of event notification - GerritEventLifecycle like - which is not available * at this time. Fortunately, triggering is near instant, and projects are kept * long enough in the queue for us to not worry too much about this at this time. * We can do this check for the retrigger.all action however, which does not have a quiet * time, because specific code exists for it in GerritTrigger, fortunately. */ if (currentlyTriggeringEvents.contains(event)) { logger.debug("We need to wait while {} is being triggered for {}", event, p); return new BecauseWaitingForOtherProjectsToTrigger(); } CauseOfBlockage causeOfBlockage = getCauseOfBlockage(dependencies, event); if (causeOfBlockage != null) { return causeOfBlockage; } else { ToGerritRunListener toGerritRunListener = ToGerritRunListener.getInstance(); if (toGerritRunListener != null) { List<Run> parentRuns = toGerritRunListener.getRuns(event); if (parentRuns == null) { logger.info("All dependencies for event: {} on project: {}, are triggered in silent mode. " + "Can not get list of actual dependencies", event, p); return null; } if (parentRuns.isEmpty()) { logger.info("Project {} has dependencies, but does not have known runs for {}", p, event); return null; } List<Run> actualDependencies = new ArrayList<Run>(dependencies.size()); for (Run run : parentRuns) { if (dependencies.contains(run.getParent())) { actualDependencies.add(run); } } if (actualDependencies.isEmpty()) { logger.info("Project {} has dependencies, but all of them are not interested in event {}", p, event); return null; } // TODO: returning `null` from a QueueTaskDispatcher does not mean the build will start immediately. // So, this line can be called multiple times. In case of performance/stability issues it makes sense to // mode to TransientActionFactory or maybe a QueueListener would be better. item.replaceAction(new GerritDependencyAction(actualDependencies)); } logger.info("No active dependencies on project: {} for event: {}, it will now build", p, event); return null; } } /** * Gets the cause of blockage if one of dependant project was not triggered or was not finished yet. * @param dependencies The list of projects which need to be checked * @param event The event should have also caused the blocking builds. * @return the cause of blockage. */ private CauseOfBlockage getCauseOfBlockage(List<Job> dependencies, GerritTriggeredEvent event) { ToGerritRunListener toGerritRunListener = ToGerritRunListener.getInstance(); if (toGerritRunListener != null) { for (Job dependency : dependencies) { if (toGerritRunListener.isTriggered(dependency, event)) { if (toGerritRunListener.isBuilding(dependency, event)) { return new BecauseDependentBuildIsBuilding(dependency); } } else { GerritTrigger gerritTrigger = GerritTrigger.getTrigger(dependency); if (gerritTrigger != null && gerritTrigger.isInteresting(event)) { return new BecauseWaitingForOtherProjectsToTrigger(); } } } } return null; } /** * Return the GerritCause of the specific item if any, otherwise return null. * @param item The item * @return the GerritCause */ private GerritCause getGerritCause(Queue.Item item) { for (Cause cause : item.getCauses()) { if (cause instanceof GerritCause) { return (GerritCause) cause; } } return null; } /** * Return a list of Abstract Projects from their string names. * @param projects The string containing the projects, comma-separated. * @param context The context in which to read the string * @return the list of projects */ public static List<Job> getProjectsFromString(String projects, Item context) { List<Job> dependencyJobs = new ArrayList<Job>(); if (StringUtils.isEmpty(projects)) { return null; } else { Jenkins jenkins = Jenkins.getInstance(); assert jenkins != null; StringTokenizer tokens = new StringTokenizer(projects, ","); while (tokens.hasMoreTokens()) { String projectName = tokens.nextToken().trim(); if (!projectName.isEmpty()) { Item item = jenkins.getItem(projectName, context, Item.class); if ((item != null) && (item instanceof Job)) { dependencyJobs.add((Job) item); logger.debug("project dependency job added : {}", item); } } } } return dependencyJobs; } /** * Signals this event started retriggering all its projects. * In the meantime, no builds with dependencies should be allowed to start. * @param event the event triggering */ public void onTriggeringAll(GerritTriggeredEvent event) { currentlyTriggeringEvents.add(event); logger.debug("Triggering all projects for {}", event); } /** * Signals this event is done retriggering all its projects. * Builds with dependencies may be allowed to start once their dependencies are built.. * @param event the event done triggering */ public void onDoneTriggeringAll(GerritTriggeredEvent event) { currentlyTriggeringEvents.remove(event); logger.debug("Done triggering all projects for {}", event); } /* * GerritEventListener interface */ /** * Process lifecycle events. We register to these events * so we can get notified of the beginning of the scanning and end of * scanning. * @param event the event to which we subscribe. */ @Override public void gerritEvent(GerritEvent event) { //we are only interested in the ManualPatchsetCreated events for now //as they are the only ones which have event scanning information. if (event instanceof GerritEventLifecycle) { logger.debug("registering to lifecycle"); // Registering to get the ScanDone event. ((GerritEventLifecycle) event).addListener(this); // while this is most likely a ManualPatchSetCreated, which is // a GerritTriggeredEvent, we don't have a guarantee that this // will necessarily be the case in the future. if (event instanceof GerritTriggeredEvent) { //We only can setup this "barrier" if we are certain to be able to //lift it, which only happens for Lifecycle events for which we //will get the ScanDone. onTriggeringAll((GerritTriggeredEvent) event); } } } /* * GerritEventLifecycleListener interface */ @Override public void triggerScanStarting(GerritEvent event) { // While it would make sense to call the onTriggeringAll, this event (ScanStarting) // is fired before the event even makes it to the gerritEvent above, meaning // nothing here will execute because we aren't registered yet. } @Override public void triggerScanDone(GerritEvent event) { // while this is most likely a ManualPatchSetCreated, which is // a GerritTriggeredEvent, we don't have a guarantee that this // will necessarily be the case in the future. logger.debug("trigger scan done"); if (event instanceof GerritTriggeredEvent) { onDoneTriggeringAll((GerritTriggeredEvent) event); } // But, we do know this is a Lifecycle because this method is for lifecyle events ((GerritEventLifecycle) event).removeListener(this); } @Override public void projectTriggered(GerritEvent event, Job project) { } @Override public void buildStarted(GerritEvent event, Run build) { } @Override public void buildCompleted(GerritEvent event, Run build) { } @Override public void allBuildsCompleted(GerritEvent event) { } }