diff --git a/core/.cvsignore b/core/.cvsignore index f87512f564..be22279ea1 100644 --- a/core/.cvsignore +++ b/core/.cvsignore @@ -10,3 +10,4 @@ target hudson.iml hudson-core.iml hudson.ipr +.settings diff --git a/core/src/main/java/hudson/DependencyRunner.java b/core/src/main/java/hudson/DependencyRunner.java new file mode 100644 index 0000000000..8093fc617c --- /dev/null +++ b/core/src/main/java/hudson/DependencyRunner.java @@ -0,0 +1,55 @@ +package hudson; + +import hudson.model.AbstractProject; +import hudson.model.Hudson; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Runs a job on all projects in the order of dependencies + */ +public class DependencyRunner implements Runnable { + AbstractProject currentProject; + + ProjectRunnable runnable; + + List polledProjects = new ArrayList(); + + @SuppressWarnings("unchecked") + public DependencyRunner(ProjectRunnable runnable) { + this.runnable = runnable; + } + + public void run() { + Set topLevelProjects = new HashSet(); + // Get all top-level projects + for (AbstractProject p : Hudson.getInstance().getAllItems( + AbstractProject.class)) + if (p.getUpstreamProjects().size() == 0) + topLevelProjects.add(p); + populate(topLevelProjects); + for (AbstractProject p : polledProjects) + runnable.run(p); + } + + private void populate(Set projectList) { + for (AbstractProject p : projectList) { + if (polledProjects.contains(p)) + // Project will be readded at the queue, so that we always use + // the longest path + polledProjects.remove(p); + + polledProjects.add(p); + + // Add all downstream dependencies + populate(new HashSet(p.getDownstreamProjects())); + } + } + + public interface ProjectRunnable { + void run(AbstractProject p); + } +} diff --git a/core/src/main/java/hudson/triggers/SCMTrigger.java b/core/src/main/java/hudson/triggers/SCMTrigger.java index 1cd8d666ef..3e9e4e9e5c 100644 --- a/core/src/main/java/hudson/triggers/SCMTrigger.java +++ b/core/src/main/java/hudson/triggers/SCMTrigger.java @@ -4,6 +4,7 @@ import antlr.ANTLRException; import hudson.Util; import hudson.model.AbstractProject; import hudson.model.Action; +import hudson.model.Hudson; import hudson.model.Item; import hudson.model.Project; import hudson.model.SCMedItem; @@ -63,15 +64,20 @@ public class SCMTrigger extends Trigger { return super.readResolve(); } - protected void run() { + public void run() { if(pollingScheduled) return; // noop pollingScheduled = true; - // schedule the polling. - // even if we end up submitting this too many times, that's OK. - // the real exclusion control happens inside Runner. - DESCRIPTOR.getExecutor().submit(new Runner()); + if (DESCRIPTOR.synchronousPolling) { + // Run the trigger directly without threading, as it's already taken care of by Trigger.Cron + new Runner().run(); + } else { + // schedule the polling. + // even if we end up submitting this too many times, that's OK. + // the real exclusion control happens inside Runner. + DESCRIPTOR.getExecutor().submit(new Runner()); + } } public Action getProjectAction() { @@ -97,6 +103,12 @@ public class SCMTrigger extends Trigger { */ transient volatile ExecutorService executor; + /** + * Whether the projects should be polled all in one go in the order of dependencies. The default behavior is + * that each project polls for changes independently. + */ + public boolean synchronousPolling = false; + /** * Jobs that are being polled. The value is useful for trouble-shooting. */ diff --git a/core/src/main/java/hudson/triggers/TimerTrigger.java b/core/src/main/java/hudson/triggers/TimerTrigger.java index 657da3609b..66191db256 100644 --- a/core/src/main/java/hudson/triggers/TimerTrigger.java +++ b/core/src/main/java/hudson/triggers/TimerTrigger.java @@ -1,17 +1,19 @@ package hudson.triggers; -import antlr.ANTLRException; import static hudson.Util.fixNull; -import hudson.model.Descriptor; import hudson.model.BuildableItem; import hudson.model.Item; import hudson.scheduler.CronTabList; import hudson.util.FormFieldValidator; + +import java.io.IOException; + +import javax.servlet.ServletException; + import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; -import javax.servlet.ServletException; -import java.io.IOException; +import antlr.ANTLRException; /** * {@link Trigger} that runs a job periodically. @@ -23,7 +25,7 @@ public class TimerTrigger extends Trigger { super(cronTabSpec); } - protected void run() { + public void run() { job.scheduleBuild(); } diff --git a/core/src/main/java/hudson/triggers/Trigger.java b/core/src/main/java/hudson/triggers/Trigger.java index 50defdde7c..849cabc4e4 100644 --- a/core/src/main/java/hudson/triggers/Trigger.java +++ b/core/src/main/java/hudson/triggers/Trigger.java @@ -1,7 +1,9 @@ package hudson.triggers; import antlr.ANTLRException; +import hudson.DependencyRunner; import hudson.ExtensionPoint; +import hudson.DependencyRunner.ProjectRunnable; import hudson.model.AbstractProject; import hudson.model.Action; import hudson.model.Build; @@ -17,6 +19,7 @@ import hudson.scheduler.CronTabList; import java.io.InvalidObjectException; import java.io.ObjectStreamException; import java.util.Calendar; +import java.util.Collection; import java.util.Collections; import java.util.GregorianCalendar; import java.util.Timer; @@ -53,7 +56,7 @@ public abstract class Trigger implements Describable> * This method is invoked when {@link #Trigger(String)} is used * to create an instance, and the crontab matches the current time. */ - protected void run() {} + public void run() {} /** * Called before a {@link Trigger} is removed. @@ -131,16 +134,7 @@ public abstract class Trigger implements Describable> LOGGER.fine("cron checking "+cal.getTime().toLocaleString()); try { - Hudson inst = Hudson.getInstance(); - for (AbstractProject p : inst.getAllItems(AbstractProject.class)) { - for (Trigger t : p.getTriggers().values()) { - LOGGER.fine("cron checking "+p.getName()); - if(t.tabs.check(cal)) { - LOGGER.fine("cron triggered "+p.getName()); - t.run(); - } - } - } + checkTriggers(cal); } catch (Throwable e) { LOGGER.log(Level.WARNING,"Cron thread throw an exception",e); // bug in the code. Don't let the thread die. @@ -151,6 +145,38 @@ public abstract class Trigger implements Describable> } } + public static void checkTriggers(final Calendar cal) { + Hudson inst = Hudson.getInstance(); + + if (SCMTrigger.DESCRIPTOR.synchronousPolling) { + // Process SCMTriggers in the order of dependencies. Note that the crontab spec expressed per-project is + // ignored, only the global setting is honored + // FIXME allow to set a global crontab spec + SCMTrigger.DESCRIPTOR.getExecutor().submit(new DependencyRunner(new ProjectRunnable() { + public void run(AbstractProject p) { + for (Trigger t : (Collection) p.getTriggers().values()) { + if (t instanceof SCMTrigger) + t.run(); + } + } + })); + } + + // Process all triggers, except SCMTriggers when synchronousPolling is set + for (AbstractProject p : inst.getAllItems(AbstractProject.class)) { + for (Trigger t : p.getTriggers().values()) { + if (! (t instanceof SCMTrigger && SCMTrigger.DESCRIPTOR.synchronousPolling)) { + LOGGER.fine("cron checking "+p.getName()); + + if (t.tabs.check(cal)) { + LOGGER.fine("cron triggered "+p.getName()); + t.run(); + } + } + } + } + } + private static final Logger LOGGER = Logger.getLogger(Trigger.class.getName()); /**