diff --git a/core/src/main/java/hudson/model/Hudson.java b/core/src/main/java/hudson/model/Hudson.java index 048005f3b806196f95a627fce6180707a92ca067..f450c4fc7057d0e948169843cce49bb40c95cf64 100644 --- a/core/src/main/java/hudson/model/Hudson.java +++ b/core/src/main/java/hudson/model/Hudson.java @@ -16,6 +16,7 @@ import hudson.tasks.BuildStep; import hudson.tasks.Builder; import hudson.tasks.Publisher; import hudson.triggers.Trigger; +import hudson.triggers.Triggers; import hudson.util.FormFieldValidator; import hudson.util.XStream2; import org.apache.commons.fileupload.FileItem; @@ -721,6 +722,9 @@ public final class Hudson extends JobCollection implements Node { for( Descriptor scmd : SCMS.SCMS ) result &= scmd.configure(req); + for( Descriptor d : Triggers.TRIGGERS ) + result &= d.configure(req); + save(); if(result) rsp.sendRedirect("."); // go to the top page diff --git a/core/src/main/java/hudson/triggers/SCMTrigger.java b/core/src/main/java/hudson/triggers/SCMTrigger.java index 808f9be82920e4274870cf542e01c00e05184149..36957e8467ddb73f00f819f2fbb0d1f0f8284576 100644 --- a/core/src/main/java/hudson/triggers/SCMTrigger.java +++ b/core/src/main/java/hudson/triggers/SCMTrigger.java @@ -8,18 +8,23 @@ import hudson.model.Descriptor; import hudson.model.Project; import hudson.model.TaskListener; import hudson.util.StreamTaskListener; +import org.kohsuke.stapler.StaplerRequest; +import javax.servlet.http.HttpServletRequest; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.util.Date; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; -import org.kohsuke.stapler.StaplerRequest; - /** * {@link Trigger} that checks for SCM updates periodically. * @@ -27,21 +32,28 @@ import org.kohsuke.stapler.StaplerRequest; */ public class SCMTrigger extends Trigger { /** - * Non-null if the polling is in progress. + * If we'd like to run another polling run, this is set to true. + * + *

+ * To avoid submitting more than one polling jobs (which could flood the queue), + * we first use the boolean flag. + * * @guardedBy this */ private transient boolean pollingScheduled; /** - * Non-null if the polling is in progress. - * @guardedBy this + * Signal to the polling thread to abort now. */ - private transient Thread pollingThread; + private transient boolean abortNow; /** - * Signal to the polling thread to abort now. + * Pending polling activity in progress. + * There's at most one polling activity per project at any given point. + * + * @guardedBy this */ - private transient boolean abortNow; + private transient Future polling; public SCMTrigger(String cronTabSpec) throws ANTLRException { super(cronTabSpec); @@ -64,11 +76,18 @@ public class SCMTrigger extends Trigger { * Makes sure that the polling is aborted. */ public synchronized void abort() throws InterruptedException { - if(pollingThread!=null && pollingThread.isAlive()) { + if(polling!=null && !polling.isDone()) { System.out.println("killing polling"); + abortNow = true; - pollingThread.interrupt(); - pollingThread.join(); + polling.cancel(true); + try { + polling.get(); + } catch (ExecutionException e) { + LOGGER.log(Level.WARNING, "Failed to poll",e); + } catch (CancellationException e) { + // this is fine + } abortNow = false; } } @@ -82,61 +101,14 @@ public class SCMTrigger extends Trigger { if(b!=null && b.isBuilding()) return; // build in progress - if(pollingThread!=null && pollingThread.isAlive()) + if(polling!=null && !polling.isDone()) return; // polling already in progress if(!pollingScheduled) return; // not scheduled pollingScheduled = false; - pollingThread = new Thread() { - private boolean runPolling() { - try { - // to make sure that the log file contains up-to-date text, - // don't do buffering. - OutputStream fos = new FileOutputStream(getLogFile()); - TaskListener listener = new StreamTaskListener(fos); - - try { - LOGGER.info("Polling SCM changes of "+project.getName()); - - PrintStream logger = listener.getLogger(); - long start = System.currentTimeMillis(); - logger.println("Started on "+new Date().toLocaleString()); - boolean result = project.pollSCMChanges(listener); - logger.println("Done. Took "+Util.getTimeSpanString(System.currentTimeMillis()-start)); - if(result) - logger.println("Changes found"); - else - logger.println("No changes"); - return result; - } finally { - fos.close(); - } - } catch (IOException e) { - LOGGER.log(Level.SEVERE,"Failed to record SCM polling",e); - return false; - } - } - - public void run() { - boolean repeat; - do { - if(runPolling()) { - LOGGER.info("SCM changes detected in "+project.getName()); - project.scheduleBuild(); - } - if(abortNow) - return; // terminate now - - synchronized(SCMTrigger.this) { - repeat = pollingScheduled; - pollingScheduled = false; - } - } while(repeat); - } - }; - pollingThread.start(); + polling = DESCRIPTOR.getExecutor().submit(new Runner()); } /** @@ -150,7 +122,24 @@ public class SCMTrigger extends Trigger { return DESCRIPTOR; } - public static final Descriptor DESCRIPTOR = new Descriptor(SCMTrigger.class) { + public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); + + public static final class DescriptorImpl extends Descriptor { + /** + * Used to control the execution of the polling tasks. + */ + transient volatile ExecutorService executor; + + DescriptorImpl() { + super(SCMTrigger.class); + // create an executor + update(getPollingThreadCount()); + } + + public ExecutorService getExecutor() { + return executor; + } + public String getDisplayName() { return "Poll SCM"; } @@ -166,7 +155,51 @@ public class SCMTrigger extends Trigger { throw new FormException(e.toString(),e,"scmpoll_spec"); } } - }; + + /** + * Gets the number of concurrent threads used for polling. + * + * @return + * 0 if unlimited. + */ + public int getPollingThreadCount() { + String value = (String)getProperties().get("poll_scm_threads"); + if(value==null) + return 0; + return Integer.parseInt(value); + } + + public void setPollingThreadCount(int n) { + getProperties().put("poll_scm_threads",String.valueOf(n)); + save(); + update(n); + } + + /** + * Update the {@link ExecutorService} instance. + */ + /*package*/ synchronized void update(int n) { + // fool proof + if(n<0) n=0; + if(n>100) n=0; + + // swap to a new one, and shut down the old one gradually + ExecutorService newExec = n==0 ? Executors.newCachedThreadPool() : Executors.newFixedThreadPool(n); + ExecutorService old = executor; + executor = newExec; + if(old!=null) + old.shutdown(); + } + + public boolean configure(HttpServletRequest req) throws FormException { + String t = req.getParameter("poll_scm_threads"); + if(t==null || t.length()==0) + setPollingThreadCount(0); + else + setPollingThreadCount(Integer.parseInt(t)); + return super.configure(req); + } + } /** * Action object for {@link Project}. Used to display the polling log. @@ -194,4 +227,56 @@ public class SCMTrigger extends Trigger { } private static final Logger LOGGER = Logger.getLogger(SCMTrigger.class.getName()); + + /** + * {@link Runnable} that actually performs polling. + */ + private class Runner implements Runnable { + private boolean runPolling() { + try { + // to make sure that the log file contains up-to-date text, + // don't do buffering. + OutputStream fos = new FileOutputStream(getLogFile()); + TaskListener listener = new StreamTaskListener(fos); + + try { + LOGGER.info("Polling SCM changes of "+project.getName()); + + PrintStream logger = listener.getLogger(); + long start = System.currentTimeMillis(); + logger.println("Started on "+new Date().toLocaleString()); + boolean result = project.pollSCMChanges(listener); + logger.println("Done. Took "+ Util.getTimeSpanString(System.currentTimeMillis()-start)); + if(result) + logger.println("Changes found"); + else + logger.println("No changes"); + return result; + } finally { + fos.close(); + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE,"Failed to record SCM polling",e); + return false; + } + } + + public void run() { + if(runPolling()) { + LOGGER.info("SCM changes detected in "+project.getName()); + project.scheduleBuild(); + } + + synchronized(SCMTrigger.this) { + if(abortNow) + return; // terminate now without queueing the next one. + + if(pollingScheduled) { + // schedule a next run + polling = DESCRIPTOR.getExecutor().submit(new Runner()); + } + pollingScheduled = false; + } + } + } } diff --git a/core/src/main/resources/hudson/model/Hudson/configure.jelly b/core/src/main/resources/hudson/model/Hudson/configure.jelly index c6e1053793568d160975288ca3b515a6693d4435..90e633303dca61e4e14d7f0c487f58ccb815b05f 100644 --- a/core/src/main/resources/hudson/model/Hudson/configure.jelly +++ b/core/src/main/resources/hudson/model/Hudson/configure.jelly @@ -118,7 +118,14 @@ - + + + + + + + + @@ -132,7 +139,7 @@ - + diff --git a/core/src/main/resources/hudson/triggers/SCMTrigger/global.jelly b/core/src/main/resources/hudson/triggers/SCMTrigger/global.jelly new file mode 100644 index 0000000000000000000000000000000000000000..07404a6ec57de35ffa76667fb9108ba0d59b8458 --- /dev/null +++ b/core/src/main/resources/hudson/triggers/SCMTrigger/global.jelly @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/triggers/TimerTrigger/global.jelly b/core/src/main/resources/hudson/triggers/TimerTrigger/global.jelly new file mode 100644 index 0000000000000000000000000000000000000000..1c123d2ec05b8061f4b6be838a9e809599f999f0 --- /dev/null +++ b/core/src/main/resources/hudson/triggers/TimerTrigger/global.jelly @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/war/.cvsignore b/war/.cvsignore index 28f16c324d74f6b897fa99b713daea1449894f9a..aa0f7fc01d5948d5a1881b94365f79147fd1a400 100644 --- a/war/.cvsignore +++ b/war/.cvsignore @@ -1,4 +1,6 @@ target .classpath *.iml -.project \ No newline at end of file +.project +.settings +work diff --git a/war/resources/help/triggers/SCMTrigger/concurrency.html b/war/resources/help/triggers/SCMTrigger/concurrency.html new file mode 100644 index 0000000000000000000000000000000000000000..b4e1e8c45ea2826ab873a79eb398aa70a115fb30 --- /dev/null +++ b/war/resources/help/triggers/SCMTrigger/concurrency.html @@ -0,0 +1,9 @@ +

+ If you use SCM polling to trigger a new build for a large number of projects, + you may want to limit the number of concurrent polling activities to avoid + overloading the server. + +

+ Setting a positive number sets the upper bound to the number of concurrent polling. + Leaving the field empty will make it unbounded. +

\ No newline at end of file