/** 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";
 }
Exemple #4
0
/**
 * {@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));
}