/** Choose which datapoint to use. */ public enum TimeScale { SEC10(TimeUnit2.SECONDS.toMillis(10)), MIN(TimeUnit2.MINUTES.toMillis(1)), HOUR(TimeUnit2.HOURS.toMillis(1)); /** Number of milliseconds (10 secs, 1 min, and 1 hour) that this constant represents. */ public final long tick; TimeScale(long tick) { this.tick = tick; } /** Creates a new {@link DateFormat} suitable for processing this {@link TimeScale}. */ public DateFormat createDateFormat() { switch (this) { case HOUR: return new SimpleDateFormat("MMM/dd HH"); case MIN: return new SimpleDateFormat("HH:mm"); case SEC10: return new SimpleDateFormat("HH:mm:ss"); default: throw new AssertionError(); } } /** Parses the {@link TimeScale} from the query parameter. */ public static TimeScale parse(String type) { if (type == null) return TimeScale.MIN; return Enum.valueOf(TimeScale.class, type.toUpperCase(Locale.ENGLISH)); } }
/** * Turns an interval into a suitable crontab. * * @param interval the interval. * @return the crontab. */ private static String toCrontab(String interval) { long millis = toIntervalMillis(interval); if (millis < TimeUnit2.MINUTES.toMillis(5)) { return "* * * * *"; } if (millis < TimeUnit2.MINUTES.toMillis(10)) { return "*/12 * * * *"; } if (millis < TimeUnit2.MINUTES.toMillis(30)) { return "*/6 * * * *"; } if (millis < TimeUnit2.HOURS.toMillis(1)) { return "*/2 * * * *"; } if (millis < TimeUnit2.HOURS.toMillis(8)) { return "H * * * *"; } return "H H * * *"; }
/** * Returns the interval between indexing. * * @return the interval between indexing. */ @SuppressWarnings("unused") // used by Jelly EL public String getInterval() { if (interval < TimeUnit2.SECONDS.toMillis(1)) { return Long.toString(interval) + "ms"; } if (interval < TimeUnit2.MINUTES.toMillis(1)) { return Long.toString(TimeUnit2.MILLISECONDS.toSeconds(interval)) + "s"; } if (interval < TimeUnit2.HOURS.toMillis(1)) { return Long.toString(TimeUnit2.MILLISECONDS.toMinutes(interval)) + "m"; } if (interval < TimeUnit2.DAYS.toMillis(1)) { return Long.toString(TimeUnit2.MILLISECONDS.toHours(interval)) + "h"; } return Long.toString(TimeUnit2.MILLISECONDS.toDays(interval)) + "d"; }
/** * {@link Trigger} that checks for SCM updates periodically. * * @author Kohsuke Kawaguchi */ public class SCMTrigger extends Trigger<SCMedItem> { @DataBoundConstructor public SCMTrigger(String scmpoll_spec) throws ANTLRException { super(scmpoll_spec); } @Override public void run() { run(null); } /** * Run the SCM trigger with additional build actions. Used by SubversionRepositoryStatus to * trigger a build at a specific revisionn number. * * @param additionalActions * @since 1.375 */ public void run(Action[] additionalActions) { if (Hudson.getInstance().isQuietingDown()) return; // noop DescriptorImpl d = getDescriptor(); LOGGER.fine("Scheduling a polling for " + job); if (d.synchronousPolling) { LOGGER.fine( "Running the trigger directly without threading, " + "as it's already taken care of by Trigger.Cron"); new Runner(additionalActions).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. LOGGER.fine("scheduling the trigger to (asynchronously) run"); d.queue.execute(new Runner(additionalActions)); d.clogCheck(); } } @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } @Override public Collection<? extends Action> getProjectActions() { return Collections.singleton(new SCMAction()); } /** Returns the file that records the last/current polling activity. */ public File getLogFile() { return new File(job.getRootDir(), "scm-polling.log"); } @Extension public static class DescriptorImpl extends TriggerDescriptor { /** * Used to control the execution of the polling tasks. * * <p>This executor implementation has a semantics suitable for polling. Namely, no two threads * will try to poll the same project at once, and multiple polling requests to the same job will * be combined into one. Note that because executor isn't aware of a potential workspace lock * between a build and a polling, we may end up using executor threads unwisely --- they may * block. */ private final transient SequentialExecutionQueue queue = new SequentialExecutionQueue(Executors.newSingleThreadExecutor()); /** * 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; /** Max number of threads for SCM polling. 0 for unbounded. */ private int maximumThreads; public DescriptorImpl() { load(); resizeThreadPool(); } public boolean isApplicable(Item item) { return item instanceof SCMedItem; } public ExecutorService getExecutor() { return queue.getExecutors(); } /** Returns true if the SCM polling thread queue has too many jobs than it can handle. */ public boolean isClogged() { return queue.isStarving(STARVATION_THRESHOLD); } /** Checks if the queue is clogged, and if so, activate {@link AdministrativeMonitorImpl}. */ public void clogCheck() { AdministrativeMonitor.all().get(AdministrativeMonitorImpl.class).on = isClogged(); } /** Gets the snapshot of {@link Runner}s that are performing polling. */ public List<Runner> getRunners() { return Util.filter(queue.getInProgress(), Runner.class); } /** Gets the snapshot of {@link SCMedItem}s that are being polled at this very moment. */ public List<SCMedItem> getItemsBeingPolled() { List<SCMedItem> r = new ArrayList<SCMedItem>(); for (Runner i : getRunners()) r.add(i.getTarget()); return r; } public String getDisplayName() { return Messages.SCMTrigger_DisplayName(); } /** * Gets the number of concurrent threads used for polling. * * @return 0 if unlimited. */ public int getPollingThreadCount() { return maximumThreads; } /** * Sets the number of concurrent threads used for SCM polling and resizes the thread pool * accordingly * * @param n number of concurrent threads, zero or less means unlimited, maximum is 100 */ public void setPollingThreadCount(int n) { // fool proof if (n < 0) n = 0; if (n > 100) n = 100; maximumThreads = n; resizeThreadPool(); } /** Update the {@link ExecutorService} instance. */ /*package*/ synchronized void resizeThreadPool() { queue.setExecutors( (maximumThreads == 0 ? Executors.newCachedThreadPool() : Executors.newFixedThreadPool(maximumThreads))); } @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { String t = json.optString("pollingThreadCount", null); if (t == null || t.length() == 0) setPollingThreadCount(0); else setPollingThreadCount(Integer.parseInt(t)); // Save configuration save(); return true; } public FormValidation doCheckPollingThreadCount(@QueryParameter String value) { if (value != null && "".equals(value.trim())) return FormValidation.ok(); return FormValidation.validateNonNegativeInteger(value); } } @Extension public static final class AdministrativeMonitorImpl extends AdministrativeMonitor { private boolean on; public boolean isActivated() { return on; } } /** * Associated with {@link AbstractBuild} to show the polling log that triggered that build. * * @since 1.376 */ public static class BuildAction implements Action { public final AbstractBuild build; public BuildAction(AbstractBuild build) { this.build = build; } /** Polling log that triggered the build. */ public File getPollingLogFile() { return new File(build.getRootDir(), "polling.log"); } public String getIconFileName() { return "clipboard.gif"; } public String getDisplayName() { return Messages.SCMTrigger_BuildAction_DisplayName(); } public String getUrlName() { return "pollingLog"; } /** Sends out the raw polling log output. */ public void doPollingLog(StaplerRequest req, StaplerResponse rsp) throws IOException { rsp.setContentType("text/plain;charset=UTF-8"); // Prevent jelly from flushing stream so Content-Length header can be added afterwards FlushProofOutputStream out = new FlushProofOutputStream(rsp.getCompressedOutputStream(req)); getPollingLogText().writeLogTo(0, out); out.close(); } public AnnotatedLargeText getPollingLogText() { return new AnnotatedLargeText<BuildAction>( getPollingLogFile(), Charset.defaultCharset(), true, this); } /** Used from <tt>polling.jelly</tt> to write annotated polling log to the given output. */ public void writePollingLogTo(long offset, XMLOutput out) throws IOException { // TODO: resurrect compressed log file support getPollingLogText().writeHtmlTo(offset, out.asWriter()); } } /** Action object for {@link Project}. Used to display the last polling log. */ public final class SCMAction implements Action { public AbstractProject<?, ?> getOwner() { return job.asProject(); } public String getIconFileName() { return "clipboard.gif"; } public String getDisplayName() { return Messages.SCMTrigger_getDisplayName(job.getScm().getDescriptor().getDisplayName()); } public String getUrlName() { return "scmPollLog"; } public String getLog() throws IOException { return Util.loadFile(getLogFile()); } /** * Writes the annotated log to the given output. * * @since 1.350 */ public void writeLogTo(XMLOutput out) throws IOException { new AnnotatedLargeText<SCMAction>(getLogFile(), Charset.defaultCharset(), true, this) .writeHtmlTo(0, out.asWriter()); } } private static final Logger LOGGER = Logger.getLogger(SCMTrigger.class.getName()); /** {@link Runnable} that actually performs polling. */ public class Runner implements Runnable { /** When did the polling start? */ private volatile long startTime; private Action[] additionalActions; public Runner() { additionalActions = new Action[0]; } public Runner(Action[] actions) { if (actions == null) { additionalActions = new Action[0]; } else { additionalActions = actions; } } /** Where the log file is written. */ public File getLogFile() { return SCMTrigger.this.getLogFile(); } /** For which {@link Item} are we polling? */ public SCMedItem getTarget() { return job; } /** When was this polling started? */ public long getStartTime() { return startTime; } /** Human readable string of when this polling is started. */ public String getDuration() { return Util.getTimeSpanString(System.currentTimeMillis() - startTime); } private boolean runPolling() { try { // to make sure that the log file contains up-to-date text, // don't do buffering. StreamTaskListener listener = new StreamTaskListener(getLogFile()); try { PrintStream logger = listener.getLogger(); long start = System.currentTimeMillis(); logger.println("Started on " + DateFormat.getDateTimeInstance().format(new Date())); boolean result = job.poll(listener).hasChanges(); logger.println( "Done. Took " + Util.getTimeSpanString(System.currentTimeMillis() - start)); if (result) logger.println("Changes found"); else logger.println("No changes"); return result; } catch (Error e) { e.printStackTrace(listener.error("Failed to record SCM polling")); LOGGER.log(Level.SEVERE, "Failed to record SCM polling", e); throw e; } catch (RuntimeException e) { e.printStackTrace(listener.error("Failed to record SCM polling")); LOGGER.log(Level.SEVERE, "Failed to record SCM polling", e); throw e; } finally { listener.close(); } } catch (IOException e) { LOGGER.log(Level.SEVERE, "Failed to record SCM polling", e); return false; } } public void run() { String threadName = Thread.currentThread().getName(); Thread.currentThread().setName("SCM polling for " + job); try { startTime = System.currentTimeMillis(); if (runPolling()) { AbstractProject p = job.asProject(); String name = " #" + p.getNextBuildNumber(); SCMTriggerCause cause; try { cause = new SCMTriggerCause(getLogFile()); } catch (IOException e) { LOGGER.log(WARNING, "Failed to parse the polling log", e); cause = new SCMTriggerCause(); } if (p.scheduleBuild(p.getQuietPeriod(), cause, additionalActions)) { LOGGER.info("SCM changes detected in " + job.getName() + ". Triggering " + name); } else { LOGGER.info( "SCM changes detected in " + job.getName() + ". Job is already in the queue"); } } } finally { Thread.currentThread().setName(threadName); } } private SCMedItem job() { return job; } // as per the requirement of SequentialExecutionQueue, value equality is necessary @Override public boolean equals(Object that) { return that instanceof Runner && job() == ((Runner) that).job(); } @Override public int hashCode() { return job.hashCode(); } } public static class SCMTriggerCause extends Cause { /** * Only used while ths cause is in the queue. Once attached to the build, we'll move this into a * file to reduce the memory footprint. */ private String pollingLog; public SCMTriggerCause(File logFile) throws IOException { // TODO: charset of this log file? this(FileUtils.readFileToString(logFile)); } public SCMTriggerCause(String pollingLog) { this.pollingLog = pollingLog; } /** @deprecated Use {@link #SCMTriggerCause(String)}. */ public SCMTriggerCause() { this(""); } @Override public void onAddedTo(AbstractBuild build) { BuildAction oldAction = build.getAction(BuildAction.class); if (oldAction != null) { build.getActions().remove(oldAction); } try { BuildAction a = new BuildAction(build); FileUtils.writeStringToFile(a.getPollingLogFile(), pollingLog); build.addAction(a); } catch (IOException e) { LOGGER.log(WARNING, "Failed to persist the polling log", e); } pollingLog = null; } @Override public String getShortDescription() { return Messages.SCMTrigger_SCMTriggerCause_ShortDescription(); } @Override public boolean equals(Object o) { return o instanceof SCMTriggerCause; } @Override public int hashCode() { return 3; } } /** How long is too long for a polling activity to be in the queue? */ public static long STARVATION_THRESHOLD = Long.getLong( SCMTrigger.class.getName() + ".starvationThreshold", TimeUnit2.HOURS.toMillis(1)); }