/******************************************************************************* * * Copyright (c) 2004-2013 Oracle Corporation. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Kohsuke Kawaguchi, Tom Huybrechts, Roy Varghese * * *******************************************************************************/ package hudson.model; import com.google.common.base.Function; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterators; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.io.xml.XppReader; import hudson.Extension; import hudson.XmlFile; import hudson.model.listeners.RunListener; import hudson.model.listeners.SaveableListener; import hudson.tasks.test.AbstractTestResultAction; import hudson.util.AtomicFileWriter; import hudson.util.XStream2; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.lang.StringUtils; /** * {@link Map} from build number to {@link Run}. * * <p> This class is multi-thread safe by using copy-on-write technique, and it * also updates the bi-directional links within {@link Run} accordingly. * * @author Kohsuke Kawaguchi */ public final class RunMap<J extends Job<J, R>, R extends Run<J, R>> extends AbstractMap<Integer, R> implements SortedMap<Integer, R>, BuildHistory<J,R> { final public XStream2 xstream = new XStream2(); final private Function<RunValue<J,R>, Record<J,R>> RUNVALUE_TO_RECORD_TRANSFORMER = new Function<RunValue<J,R>, Record<J,R>>() { @Override public Record<J, R> apply(RunValue<J, R> input) { return input; } }; @Override public Iterator<Record<J, R>> iterator() { return Iterators.transform(builds.values().iterator(), RUNVALUE_TO_RECORD_TRANSFORMER); } @Override public List<Record<J,R>> allRecords() { return ImmutableList.copyOf(iterator()); } private enum BuildMarker { LAST_COMPLETED, LAST_SUCCESSFUL, LAST_UNSUCCESSFUL, LAST_FAILED, LAST_STABLE, LAST_UNSTABLE, } private transient HashMap<BuildMarker, RunValue<J,R>> buildMarkersCache = new HashMap<BuildMarker, RunValue<J,R>>(); // copy-on-write map private SortedMap<Integer, RunValue<J,R>> builds; // A cache of build objects private final transient LazyRunValueCache runValueCache = new LazyRunValueCache(); private volatile Map<Integer, Long> buildsTimeMap = new HashMap<Integer, Long>(); private transient File persistenceFile; // Marker to indicate if this objects need to be saved to disk private transient volatile boolean dirty; // Reference to parent job. final private J parent; public RunMap(J parent) { this.parent = parent; builds = new TreeMap<Integer, RunValue<J,R>>(BUILD_TIME_COMPARATOR); // Initialize xstream xstream.alias("buildHistory", RunMap.class); xstream.alias("builds", SortedMap.class); xstream.alias("build", LazyRunValue.class); xstream.alias("build", EagerRunValue.class); } public Set<Entry<Integer, R>> entrySet() { return Maps.transformValues(builds, RUNVALUE_TO_RUN_TRANSFORMER).entrySet(); } public synchronized R put(R value) { return put(value.getNumber(), value); } @Override public synchronized R put(Integer key, R value) { // copy-on-write update TreeMap<Integer, RunValue<J,R>> m = new TreeMap<Integer, RunValue<J,R>>(builds); buildsTimeMap.put(key, value.getTimeInMillis()); final EagerRunValue erv = new EagerRunValue(this, value); RunValue<J,R> r = update(m, key, erv); // Save the build, so that we can reload it later. // For now, the way to figure out if its saved is to check if the config file // exists. if ( !buildXmlExists(erv.buildDir())) { try { value.save(); } catch (IOException ex) { LOGGER.warning("Unable to save build.xml to " + erv.buildDir().getPath()); // Not fatal, unless build object reference is released by // Hudson, in which case it won't be loaded again unless // it has actually started running. } } setBuilds(Collections.unmodifiableSortedMap(m)); return r!= null? r.getBuild(): null; } private static boolean buildXmlExists(File buildDir) { return new File(buildDir, "build.xml").exists(); } @Override public synchronized void putAll(Map<? extends Integer, ? extends R> rhs) { throw new UnsupportedOperationException("Not implemented"); } private synchronized void setBuilds(SortedMap<Integer, RunValue<J,R>> map) { this.builds = map; recalcMarkers(); saveToRunMapXml(); } private synchronized void recalcMarkers() { recalcLastSuccessful(); recalcLastUnsuccessful(); recalcLastCompleted(); recalcLastFailed(); recalcLastStable(); recalcLastUnstable(); } private synchronized void putAllRunValues(SortedMap<Integer, RunValue<J,R>> lhs, SortedMap<Integer, RunValue<J,R>> rhs) { TreeMap<Integer, RunValue<J,R>> m = new TreeMap<Integer, RunValue<J,R>>(lhs); buildsTimeMap.clear(); for (Map.Entry<Integer, RunValue<J,R>> e : rhs.entrySet()) { RunValue<J,R> runValue = e.getValue(); buildsTimeMap.put(e.getKey(), runValue.timeInMillis); update(m, e.getKey(), runValue); } setBuilds(Collections.unmodifiableSortedMap(m)); } private RunValue<J,R> update(TreeMap<Integer, RunValue<J,R>> m, Integer key, RunValue<J,R> value) { assert value != null; // things are bit tricky because this map is order so that the newest one comes first, // yet 'nextBuild' refers to the newer build. RunValue<J,R> first = m.isEmpty() ? null : m.get(m.firstKey()); RunValue<J,R> runValue = m.put(key, value); // R r = runValue != null? runValue.get(): null; SortedMap<Integer, RunValue<J,R>> head = m.headMap(key); if (!head.isEmpty()) { if(m.containsKey(head.lastKey())) { RunValue<J,R> prev = m.get(head.lastKey()); value.setPrevious(prev.getPrevious()); value.setNext(prev); if (m.containsValue(value.getPrevious())) { value.getPrevious().setNext(value); } prev.setPrevious( value); } } else { value.setPrevious( first); value.setNext(null); if (m.containsValue(first)) { first.setNext( value); } } return runValue; } public synchronized boolean remove(R run) { Integer buildNumber = run.getNumber(); RunValue<J,R> runValue = builds.get(buildNumber); if ( runValue == null ) { return false; } final RunValue<J,R> next = runValue.getNext(); if ( next != null) { next.setPrevious( runValue.getPrevious()); } final RunValue<J,R> prev = runValue.getPrevious(); if ( prev != null) { prev.setNext( runValue.getNext()); } // copy-on-write update // This block is not thread safe TreeMap<Integer, RunValue<J,R>> m = new TreeMap<Integer, RunValue<J,R>>(builds); buildsTimeMap.remove(buildNumber); RunValue<J,R> r = m.remove(buildNumber); if ( r instanceof BuildNavigable) { ((BuildNavigable)run).setBuildNavigator(null); } setBuilds(Collections.unmodifiableSortedMap(m)); return r != null; } public synchronized void reset(TreeMap<Integer, RunValue<J,R>> map) { putAllRunValues(builds, map); } /** * Gets the read-only view of this map. */ public SortedMap<Integer, R> getView() { return Maps.transformValues(builds, RUNVALUE_TO_RUN_TRANSFORMER); } // // SortedMap delegation // public Comparator<? super Integer> comparator() { return builds.comparator(); } public SortedMap<Integer, R> subMap(Integer fromKey, Integer toKey) { return Maps.transformValues(builds.subMap(fromKey, toKey), RUNVALUE_TO_RUN_TRANSFORMER); // return new ReadonlySortedMap<J,R>(builds.subMap(fromKey, toKey)); } public SortedMap<Integer, R> headMap(Integer toKey) { return Maps.transformValues(builds.headMap(toKey), RUNVALUE_TO_RUN_TRANSFORMER); // return new ReadonlySortedMap<J,R>((builds.headMap(toKey))); } public SortedMap<Integer, R> tailMap(Integer fromKey) { return Maps.transformValues(builds.tailMap(fromKey), RUNVALUE_TO_RUN_TRANSFORMER); // return new ReadonlySortedMap<J,R>(builds.tailMap(fromKey)); } /** * Callers must first check that map is not empty. * @return * @throws NoSuchElementException if map is empty */ public Integer firstKey() { return builds.firstKey(); } /** * Callers must first check that map is not empty. * @return * @throws NoSuchElementException if map is empty */ public Integer lastKey() { return builds.lastKey(); } public static final Comparator<Comparable> COMPARATOR = new Comparator<Comparable>() { public int compare(Comparable o1, Comparable o2) { return -o1.compareTo(o2); } }; /** * Compare Build by timestamp */ private Comparator<Integer> BUILD_TIME_COMPARATOR = new Comparator<Integer>() { public int compare(Integer i1, Integer i2) { Long date1 = buildsTimeMap.get(i1); Long date2 = buildsTimeMap.get(i2); if (null == date1 || null == date2) { return COMPARATOR.compare(i1, i2); } return -date1.compareTo(date2); } }; @Override public RunValue<J,R> getFirst() { if (builds.isEmpty()) { return null; } return builds.get(builds.lastKey()); } @Override public RunValue<J,R> getLast() { if (builds.isEmpty()) { return null; } return builds.get(builds.firstKey()); } @Override public R getLastBuild() { final RunValue<J,R> runValue = getLast(); return runValue != null? runValue.getBuild(): null; } @Override public R getFirstBuild() { final RunValue<J,R> runValue = getFirst(); return runValue != null? runValue.getBuild(): null; } private void recalcLastSuccessful() { RunValue<J,R> r = getLast(); while (r != null && (r.isBuilding() || r.getResult() == null || r.getResult().isWorseThan(Result.UNSTABLE))) { r = r.getPrevious(); } RunValue<J,R> old = buildMarkersCache.put(BuildMarker.LAST_SUCCESSFUL, r); if ( r != old) { markDirty(true); } } @Override public RunValue<J,R> getLastSuccessful() { return buildMarkersCache.get(BuildMarker.LAST_SUCCESSFUL); } @Override public R getLastSuccessfulBuild() { RunValue<J,R> r = getLastSuccessful(); return r !=null? r.getBuild(): null; } private void recalcLastUnsuccessful() { RunValue<J,R> r = getLast(); while (r != null && (r.isBuilding() || r.getResult() == Result.SUCCESS)) { r = r.getPrevious(); } RunValue<J,R> old = buildMarkersCache.put(BuildMarker.LAST_UNSUCCESSFUL, r); if ( old != r) { markDirty(true); } } @Override public RunValue<J,R> getLastUnsuccessful() { return buildMarkersCache.get(BuildMarker.LAST_UNSUCCESSFUL); } @Override public R getLastUnsuccessfulBuild() { RunValue<J,R> r = getLastUnsuccessful(); return r!= null? r.getBuild(): null; } private void recalcLastUnstable() { RunValue<J,R> r = getLast(); while (r != null && (r.isBuilding() || r.getResult() != Result.UNSTABLE)) { r = r.getPrevious(); } RunValue<J,R> old = buildMarkersCache.put(BuildMarker.LAST_UNSTABLE,r); if ( old != r ) { markDirty(true); } } @Override public RunValue<J,R> getLastUnstable() { return buildMarkersCache.get(BuildMarker.LAST_UNSTABLE); } @Override public R getLastUnstableBuild() { RunValue<J,R> r = getLastUnstable(); return r != null? r.getBuild(): null; } private void recalcLastStable() { RunValue<J,R> r = getLast(); while (r != null && (r.isBuilding() || r.getResult().isWorseThan(Result.SUCCESS))) { r = r.getPrevious(); } RunValue<J,R> old = buildMarkersCache.put(BuildMarker.LAST_STABLE,r); if ( old != r ) { markDirty(true); } } @Override public RunValue<J,R> getLastStable() { return buildMarkersCache.get(BuildMarker.LAST_STABLE); } @Override public R getLastStableBuild() { RunValue<J,R> r = getLastStable(); return r != null? r.getBuild(): null; } private void recalcLastFailed() { RunValue<J,R> r = getLast(); while (r != null && (r.isBuilding() || r.getResult() != Result.FAILURE)) { r = r.getPrevious(); } RunValue<J,R> old = buildMarkersCache.put(BuildMarker.LAST_FAILED, r); if (old != r) { markDirty(true); } } @Override public RunValue<J,R> getLastFailed() { return buildMarkersCache.get(BuildMarker.LAST_FAILED); } @Override public R getLastFailedBuild() { RunValue<J,R> r = getLastFailed(); return r != null? r.getBuild(): null; } private void recalcLastCompleted() { RunValue<J,R> r = getLast(); while (r != null && r.isBuilding()) { r = r.getPrevious(); } RunValue<J,R> old = buildMarkersCache.put(BuildMarker.LAST_COMPLETED, r); if (old != r) { markDirty(true); } } @Override public RunValue<J,R> getLastCompleted() { return buildMarkersCache.get(BuildMarker.LAST_COMPLETED); } @Override public R getLastCompletedBuild() { RunValue<J,R> r = getLastCompleted(); return r != null? r.getBuild(): null; } @Override public List<R> getLastBuildsOverThreshold(int numberOfBuilds, Result threshold) { final List<Record<J,R>> records = getLastRecordsOverThreshold(numberOfBuilds, threshold); return Lists.transform(records, new Function<Record<J,R>, R>() { @Override public R apply(Record<J, R> input) { return input.getBuild(); } }); } @Override public List<Record<J, R>> getLastRecordsOverThreshold(int numberOfRecords, Result threshold) { List<Record<J,R>> result = new ArrayList<Record<J,R>>(numberOfRecords); RunValue<J,R> r = getLast(); while (r != null && result.size() < numberOfRecords) { if (!r.isBuilding() && (r.getResult() != null && r.getResult().isBetterOrEqualTo(threshold))) { result.add(r); } r = r.getPrevious(); } return result; } /** * {@link Run} factory. */ public interface Constructor<R extends Run<?, R>> { R create(File dir) throws IOException; } /** * Fills in {@link RunMap} by loading build records from the file system. * * @param job Job that owns this map. * @param cons Used to create new instance of {@link Run}. */ public synchronized void load(J job, Constructor<R> cons) { // If saved Runmap exists, load from that. File buildDir = job.getBuildDir(); persistenceFile = new java.io.File(buildDir, "_runmap.xml"); if ( !loadFromRunMapXml(job, cons)) { final Hudson.HudsonDateFormat formatter = Run.ID_FORMATTER; TreeMap<Integer, RunValue<J,R>> m = new TreeMap<Integer, RunValue<J,R>>(BUILD_TIME_COMPARATOR); buildDir.mkdirs(); String[] buildDirs = buildDir.list(new FilenameFilter() { public boolean accept(File dir, String name) { // HUDSON-1461 sometimes create bogus data directories with impossible dates, such as year 0, April 31st, // or August 0th. Date object doesn't roundtrip those, so we eventually fail to load this data. // Don't even bother trying. if (!isCorrectDate(name)) { LOGGER.fine("Skipping " + new File(dir, name)); return false; } return !name.startsWith("0000") && new File(dir, name).isDirectory(); } private boolean isCorrectDate(String name) { try { if (formatter.format(formatter.parse(name)).equals(name)) { return true; } } catch (ParseException e) { // fall through } return false; } }); for (String build : buildDirs) { if (buildXmlExists(new File(buildDir, build))) { // if the build result file isn't in the directory, ignore it. try { RunValue<J,R> lzRunValue = new LazyRunValue<J,R>(this, buildDir, build, cons); R b = lzRunValue.getBuild(); long timeInMillis = b.getTimeInMillis(); buildsTimeMap.put(b.getNumber(), timeInMillis); lzRunValue.timeInMillis = timeInMillis; m.put(b.getNumber(), lzRunValue); } catch (InstantiationError e) { e.printStackTrace(); } } } reset(m); } } private synchronized void markDirty(boolean value) { this.dirty = value; if ( !dirty ) { // mark down for (RunValue rv: builds.values()) { rv.markDirty(false); } } } private boolean isDirty() { return dirty; } private synchronized void saveToRunMapXml() { if (!isDirty() || persistenceFile == null) { return; } AtomicFileWriter w = null; try { w = new AtomicFileWriter(persistenceFile); w.write("<?xml version='1.0' encoding='UTF-8'?>\n"); xstream.toXML(this, w); w.commit(); markDirty(false); } catch (Exception ex) { LOGGER.log(Level.SEVERE, "Cannot write RunMap.xml", ex); } finally { if ( w != null) { try { w.abort();} catch (IOException ex) {}; } } } private synchronized boolean loadFromRunMapXml(J job, Constructor<R> cons) { assert persistenceFile != null; Reader r = null; if ( persistenceFile.exists()) { try { r = new BufferedReader(new InputStreamReader(new FileInputStream(persistenceFile), "UTF-8")); xstream.unmarshal(new XppReader(r), this); // Fix up all the parent and constructor references File buildDir = persistenceFile.getParentFile(); boolean wasBuilding = false; for (RunValue<J,R> rv: builds.values()) { assert rv instanceof LazyRunValue; LazyRunValue<J,R> lrv = (LazyRunValue<J,R>) rv; lrv.key.ctor = cons; if ( lrv.isBuilding()) { lrv.sync(); wasBuilding = true; } } // If any builds were still building when file was last persisted // update runMap with new status and save the file again. if ( wasBuilding ) { recalcMarkers(); saveToRunMapXml(); } return true; } catch (FileNotFoundException ex) { LOGGER.log(Level.SEVERE, "Cannot read _runmap.xml", ex); } catch (UnsupportedEncodingException ex) { LOGGER.log(Level.SEVERE, "Cannot read _runmap.xml", ex); persistenceFile.delete(); } finally { if ( r != null ) { try { r.close(); } catch (Exception e) {} } } } return false; } private LazyRunValueCache runValueCache() { return this.runValueCache; } private static class LazyRunValueCache { final private LoadingCache<LazyRunValue.Key, Run> cache; // Seconds after which items in cache are removed. Time is reset on access. private static int EVICT_IN_SECONDS; // Initial cache capacity private static int INITIAL_CAPACITY; // Maximum number of cached entries. private static int MAX_ENTRIES; // Initialize from system properties if available { Integer val; val = Integer.getInteger("hudson.job.builds.cache.evict_in_seconds"); EVICT_IN_SECONDS = val == null? 60: val; val = Integer.getInteger("hudson.job.builds.cache.initial_capacity"); INITIAL_CAPACITY = val == null ? 512: val; val = Integer.getInteger("hudson.job.builds.cache.max_entries"); MAX_ENTRIES = val == null ? 10 * 1024 : val; } private LazyRunValueCache() { cache = CacheBuilder.newBuilder() .expireAfterAccess(EVICT_IN_SECONDS, TimeUnit.SECONDS) .initialCapacity(INITIAL_CAPACITY) .maximumSize(MAX_ENTRIES) .softValues() .build( new CacheLoader<LazyRunValue.Key, Run>() { @Override public Run load(LazyRunValue.Key key) throws Exception { LazyRunValue.Key k = (LazyRunValue.Key)key; // Load the data from storage // This may not succeed, so set the cached values in a // thread context variable for Run to retrieve it. This way // we don't throw, but end up with a placehold build object for // unloadable builds. LazyRunValue.setCurrentKey(k); Run r = k.ctor.create(k.referenced.buildDir()); LazyRunValue.setCurrentKey(null); if ( r instanceof BuildNavigable) { ((BuildNavigable)r).setBuildNavigator(k.referenced); } // Cannot call onLoad() here, it will try // to query a mutating cache. So just mark it // for refresh. k.refreshed = true; return r; } }); } private Run get(LazyRunValue.Key key) { try { return cache.get(key); } catch (ExecutionException ex) { LOGGER.log(Level.SEVERE,"Unable to load build: " + ex.getMessage()); return null; } } } static abstract class RunValue<J extends Job<J,R>, R extends Run<J,R>> implements BuildHistory.Record<J,R> { private transient RunMap<J,R> runMap; long timeInMillis; long duration; String fullDisplayName; String displayName; String description; String url; String builtOnStr; private RunValue<J,R> previous; private RunValue<J,R> next; boolean isBuilding; boolean isLogUpdated; Result result; Run.State state; private transient boolean dirty; int buildNumber; RunValue() { } static void update(Run run) { // Updates a RunValue with the latest information from Run final Object job = run.getParent(); if (job instanceof AbstractProject) { final AbstractProject p = (AbstractProject) job; RunValue rv = (RunValue) p.builds.builds.get(run.getNumber()); if ( rv != null ) { // Can happen if build has not yet been added to project // or is in the process of being added. rv.sync(); p.builds.recalcMarkers(); p.builds.saveToRunMapXml(); } } } protected void sync() { R build = getBuild(); if ( build == null || build.hasLoadFailure() ) { return; } setBuildNumber( build.getNumber()); setResult( build.getResult()); setState( build.getState()); setBuilding( build.isBuilding()); setLogUpdated( build.isLogUpdated()); setTimeInMillis( build.getTimeInMillis()); setDisplayName( build.getDisplayName()); setDescription( build.getDescription()); setDuration( build.getDuration()); if ( build instanceof AbstractBuild) { setBuiltOnNodeName(((AbstractBuild)build).getBuiltOnStr()); setFullDisplayName( build.getFullDisplayName()); setUrl( build.getUrl()); } } abstract File buildDir(); String relativeBuildDir(File buildsDir) { return buildsDir.toURI().relativize(buildDir().toURI()).getPath(); } public void setBuildNumber(int number) { this.buildNumber = number; } public void setRunMap(RunMap runMap) { this.runMap = runMap; } protected RunMap runMap() { return runMap; } void setTimeInMillis(long millis) { if ( this.timeInMillis == millis) { return; } this.timeInMillis = millis; markDirty(true); } void setDuration(long duration) { if ( this.duration == duration) { return; } this.duration = duration; markDirty(true); } void setDisplayName(String name) { if ( StringUtils.equals(this.displayName, name)) { return; } this.displayName = name; markDirty(true); } void setDescription(String desc) { if ( StringUtils.equals(this.description, desc)) { return; } this.description = desc; markDirty(true); } void setFullDisplayName(String name) { if ( StringUtils.equals(this.fullDisplayName, name)) { return; } this.fullDisplayName = name; markDirty(true); } void setUrl(String url) { if ( StringUtils.equals(this.url, url)) { return; } this.url = url; markDirty(true); } void setBuiltOnNodeName(String builtOn) { if ( StringUtils.equals(this.builtOnStr, builtOn)) { return; } this.builtOnStr = builtOn; markDirty(true); } private void markDirty(boolean dirty) { this.dirty = dirty; if ( dirty ) { // Dirty up runMap.markDirty(true); } } private boolean isDirty() { return dirty; } void setResult(Result result) { if ( result == this.result) { return; } this.result = result; markDirty(true); } void setState(Run.State state) { if ( state == this.state) { return; } this.state = state; markDirty(true); } void setLogUpdated(boolean value) { if (this.isLogUpdated == value) { return; } this.isLogUpdated = value; markDirty(true); } void setBuilding(boolean value) { if (this.isBuilding == value) { return; } this.isBuilding = value; markDirty(true); } void setPrevious(RunValue<J,R> previousRunValue) { if (this.previous == previousRunValue) { return; } this.previous = previousRunValue; markDirty(true); } void setNext(RunValue<J,R> nextRunvalue) { if (this.next == nextRunvalue) { return; } this.next = nextRunvalue; markDirty(true); } @Override public int getNumber() { return buildNumber; } @Override public String getDisplayName() { return displayName; } @Override public long getTimeInMillis() { return timeInMillis; } @Override public Calendar getTimestamp() { GregorianCalendar c = new GregorianCalendar(); c.setTimeInMillis(getTimeInMillis()); return c; } @Override public String getTimestampString() { return Run.getTimestampString(getTimeInMillis()); } @Override public String getTimestampString2() { return Run.getTimestampString2(getTimeInMillis()); } @Override public Date getTime() { return new Date(getTimeInMillis()); } @Override public long getDuration() { return duration; } @Override public String getUrl() { return url; } @Override public String getDescription() { return description; } @Override public String getTruncatedDescription() { return Run.getTruncatedDescription(description); } @Override public String getFullDisplayName() { return fullDisplayName; } @Override public String getBuiltOnNodeName() { return builtOnStr; } @Override public Executor getExecutor() { return Run.getExecutor(getBuild()); } @Override public RunValue<J,R> getPrevious() { return previous; } @Override public List<BuildBadgeAction> getBadgeActions() { return Run.getBadgeActions(getBuild()); } @Override public RunValue<J,R> getNext() { return next; } @Override public Result getResult() { return result; } @Override public J getParent() { return runMap.parent; } @Override public R getPreviousBuild() { RunValue<J,R> v = getPrevious(); return v != null? v.getBuild(): null; } @Override public R getNextBuild() { RunValue<J,R> v = getNext(); return v != null? v.getBuild(): null; } public boolean isBuilding() { return isBuilding; } public boolean isLogUpdated() { return isLogUpdated; } @Override public R getPreviousCompletedBuild() { RunValue<J,R> v = getPreviousCompleted(); return v != null? v.getBuild(): null; } @Override public RunValue<J,R> getPreviousCompleted() { RunValue<J,R> v = getPrevious(); while (v != null && v.isBuilding()) { v = v.getPrevious(); } return v; } @Override public R getPreviousBuildInProgress() { RunValue<J,R> v = getPreviousInProgress(); return v != null? v.getBuild(): null; } @Override public RunValue<J,R> getPreviousInProgress() { RunValue<J,R> v = getPrevious(); while ( v != null && !v.isBuilding()) { v = v.getPrevious(); } return v; } @Override public R getPreviousBuiltBuild() { RunValue<J,R> v = getPreviousBuilt(); return v != null? v.getBuild(): null; } @Override public RunValue<J,R> getPreviousBuilt() { RunValue<J,R> v = getPrevious(); // in certain situations (aborted m2 builds) v.getResult() can still be null, although it should theoretically never happen while (v != null && (v.getResult() == null || v.getResult() == Result.NOT_BUILT)) { v = v.getPrevious(); } return v; } @Override public R getPreviousNotFailedBuild() { RunValue<J,R> v = getPreviousNotFailed(); return v!= null? v.getBuild(): null; } @Override public RunValue<J,R> getPreviousNotFailed() { RunValue<J,R> v = getPrevious(); while (v != null && v.getResult() == Result.FAILURE) { v = v.getPrevious(); } return v; } @Override public R getPreviousFailedBuild() { RunValue<J,R> v = getPreviousFailed(); return v != null? v.getBuild(): null; } @Override public RunValue<J,R> getPreviousFailed() { RunValue<J,R> v = getPrevious(); while (v != null && v.getResult() != Result.FAILURE) { v = v.getPrevious(); } return v; } @Override public R getPreviousSuccessfulBuild() { RunValue<J,R> v = getPreviousSuccessful(); return v != null? v.getBuild(): null; } @Override public RunValue<J,R> getPreviousSuccessful() { RunValue<J,R> v = getPrevious(); while (v != null && v.getResult() != Result.SUCCESS) { v = v.getPrevious(); } return v; } @Override public List<BuildHistory.Record<J,R>> getPreviousOverThreshold(int numberOfBuilds, Result threshold) { List<BuildHistory.Record<J,R>> builds = new ArrayList<BuildHistory.Record<J,R>>(numberOfBuilds); RunValue<J,R> r = getPrevious(); while (r != null && builds.size() < numberOfBuilds) { if (!r.isBuilding() && (r.getResult() != null && r.getResult().isBetterOrEqualTo(threshold))) { builds.add(r); } r = r.getPrevious(); } return builds; } @Override public List<R> getPreviousBuildsOverThreshold(int numberOfBuilds, Result threshold) { return Lists.transform(getPreviousOverThreshold(numberOfBuilds, threshold), new Function<BuildHistory.Record<J,R>, R>() { @Override public R apply(BuildHistory.Record<J,R> f) { return f != null? f.getBuild(): null; } }); } @Override public Run.State getState() { return state; } @Override public BallColor getIconColor() { if (!isBuilding()) { // already built return getResult().color; } // a new build is in progress BallColor baseColor; if (getPrevious() == null) { baseColor = BallColor.GREY; } else { baseColor = getPrevious().getIconColor(); } return baseColor.anime(); } @Override public String getBuildStatusUrl() { return getIconColor().getImage(); } @Override public Run.Summary getBuildStatusSummary() { Record<J,R> prev = getPrevious(); if (getResult() == Result.SUCCESS) { if (prev == null || prev.getResult() == Result.SUCCESS) { return new Run.Summary(false, Messages.Run_Summary_Stable()); } else { return new Run.Summary(false, Messages.Run_Summary_BackToNormal()); } } if (getResult() == Result.FAILURE) { Record<J,R> since = getPreviousNotFailed(); if (since == null) { return new Run.Summary(false, Messages.Run_Summary_BrokenForALongTime()); } if (since == prev) { return new Run.Summary(true, Messages.Run_Summary_BrokenSinceThisBuild()); } Record<J,R> failedBuild = since.getNext(); return new Run.Summary(false, Messages.Run_Summary_BrokenSince(failedBuild.getBuild().getDisplayName())); } if (getResult() == Result.ABORTED) { return new Run.Summary(false, Messages.Run_Summary_Aborted()); } if (getResult() == Result.UNSTABLE) { R run = this.getBuild(); AbstractTestResultAction trN = ((AbstractBuild) run).getTestResultAction(); AbstractTestResultAction trP = prev == null ? null : ((AbstractBuild) prev.getBuild()).getTestResultAction(); if (trP == null) { if (trN != null && trN.getFailCount() > 0) { return new Run.Summary(false, Messages.Run_Summary_TestFailures(trN.getFailCount())); } else // ??? { return new Run.Summary(false, Messages.Run_Summary_Unstable()); } } if (trP.getFailCount() == 0) { return new Run.Summary(true, Messages.Run_Summary_TestsStartedToFail(trN.getFailCount())); } if (trP.getFailCount() < trN.getFailCount()) { return new Run.Summary(true, Messages.Run_Summary_MoreTestsFailing(trN.getFailCount() - trP.getFailCount(), trN.getFailCount())); } if (trP.getFailCount() > trN.getFailCount()) { return new Run.Summary(false, Messages.Run_Summary_LessTestsFailing(trP.getFailCount() - trN.getFailCount(), trN.getFailCount())); } return new Run.Summary(false, Messages.Run_Summary_TestsStillFailing(trN.getFailCount())); } return new Run.Summary(false, Messages.Run_Summary_Unknown()); } @Override public String toString() { return String.format("RunValue(number=%d,displayName=%s,buildDir=%s,state=%s,result=%s)", buildNumber, displayName, buildDir(), state, result); } public static class ConverterImpl implements Converter { final File buildsDir; final RunMap runMap; ConverterImpl(RunMap runMap) { this.runMap = runMap; this.buildsDir = runMap.persistenceFile.getParentFile(); } @Override public void marshal(Object o, HierarchicalStreamWriter writer, MarshallingContext mc) { // TODO - turn element names sinto constants. RunValue current = (RunValue) o; writer.startNode("build"); writer.startNode("number"); writer.setValue(String.valueOf(current.buildNumber)); writer.endNode(); if ( StringUtils.isNotEmpty(current.displayName)) { writer.startNode("displayName"); writer.setValue(current.displayName); writer.endNode(); } if ( StringUtils.isNotEmpty(current.fullDisplayName)) { writer.startNode("fullDisplayName"); writer.setValue(current.fullDisplayName); writer.endNode(); } if ( StringUtils.isNotEmpty(current.description )) { writer.startNode("description"); writer.setValue(current.description); writer.endNode(); } writer.startNode("buildDir"); writer.setValue( current.relativeBuildDir(buildsDir)); writer.endNode(); writer.startNode("state"); writer.setValue( current.state.toString()); writer.endNode(); if ( current.result != null) { writer.startNode("result"); writer.setValue( current.result.toString()); writer.endNode(); } writer.startNode("building"); writer.setValue( Boolean.toString( current.isBuilding)); writer.endNode(); writer.startNode("logUpdated"); writer.setValue( Boolean.toString( current.isLogUpdated())); writer.endNode(); writer.startNode("timestamp"); writer.setValue( String.valueOf( current.timeInMillis)); writer.endNode(); writer.startNode("duration"); writer.setValue( String.valueOf( current.duration)); writer.endNode(); if ( current.url != null ) { writer.startNode("url"); writer.setValue( current.url); writer.endNode(); } if ( current.builtOnStr != null) { writer.startNode("builtOn"); writer.setValue( current.builtOnStr); writer.endNode(); } writer.endNode(); } @Override public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext uc) { LazyRunValue rv = new LazyRunValue(runMap); assert "build".equals(reader.getNodeName()); while (reader.hasMoreChildren()) { reader.moveDown(); String name = reader.getNodeName(); if ( "number".equals(name) ) { rv.buildNumber = Integer.parseInt(reader.getValue()); } else if ( "displayName".equals(name)) { rv.displayName = reader.getValue(); } else if ( "fullDisplayName".equals(name)) { rv.fullDisplayName = reader.getValue(); } else if ( "description".equals(name)) { rv.description = reader.getValue(); } else if ( "buildDir".equals(name)) { rv.key.buildsDir = this.buildsDir; rv.key.buildDir = reader.getValue(); } else if ( "state".equals(name)) { rv.state = Run.State.valueOf(reader.getValue()); } else if ( "result".equals(name)) { String resultValue = reader.getValue(); rv.result = resultValue.length() > 0? Result.fromString( resultValue ): null; } else if ( "building".equals(name)) { rv.isBuilding = Boolean.parseBoolean(reader.getValue()); } else if ( "logUpdated".equals(name)) { rv.isLogUpdated = Boolean.parseBoolean(reader.getValue()); } else if ( "timestamp".equals(name)) { rv.timeInMillis = Long.parseLong(reader.getValue()); } else if ( "duration".equals(name)) { rv.duration = Long.parseLong(reader.getValue()); } else if ("url".equals(name)) { rv.url = reader.getValue(); if ( rv.url.length() == 0) { rv = null; } } else if ("builtOn".equals(name)) { rv.builtOnStr = reader.getValue(); if ( rv.builtOnStr.length() == 0) { rv.builtOnStr = null; } } reader.moveUp(); } return rv; } @Override public boolean canConvert(Class type) { return RunValue.class.isAssignableFrom(type); } } } /** * Hold onto the Constructor and {@literal config} directory and re-instantiate on * demand. */ static class LazyRunValue<J extends Job<J,R>, R extends Run<J,R>> extends RunValue<J,R> { static class Key { private String buildDir; private File buildsDir; private transient RunMap.Constructor ctor; final LazyRunValue referenced; private volatile boolean refreshed; Key(File buildsDir, String buildDir, RunMap.Constructor ctor, LazyRunValue ref) { this.buildsDir = buildsDir; this.buildDir = buildDir; this.ctor = ctor; this.referenced = ref; } @Override public boolean equals(Object o) { boolean equal = false; if ( o instanceof Key) { Key other = (Key)o; equal = buildDir.equals(other.buildDir) && buildsDir.getPath().equals(other.buildsDir.getPath()) && ctor.equals(other.ctor); } return equal; } @Override public int hashCode() { return buildDir.hashCode(); } } private final Key key; static private ThreadLocal<Key> currentKey = new ThreadLocal<Key>(); private LazyRunValue(RunMap runMap) { // Used when loaded from file this.key = new Key(null, null, null, this); setRunMap(runMap); } private LazyRunValue( RunMap runMap, File buildsDir, String buildDir, RunMap.Constructor ctor) { this.key = new Key(buildsDir, buildDir, ctor, this); setRunMap(runMap); sync(); } @Override File buildDir() { return new File(key.buildsDir, key.buildDir); } @Override public R getBuild() { R v= (R) runMap().runValueCache().get(key); if ( key.refreshed ) { // key.refreshed is true if item has been loaded from disk // for the first time by the cache. key.refreshed = false; v.onLoad(); } return v; } static Key getCurrentKey() { return currentKey.get(); } static void setCurrentKey(Key key) { currentKey.set(key); } } /** * No Lazy stuff here, just hold onto the instance since we do not * know how to reconstruct it. */ private static class EagerRunValue<J extends Job<J,R>, R extends Run<J,R>> extends RunValue<J,R> { private R referenced; EagerRunValue(RunMap runMap, R r) { setRunMap(runMap); this.referenced = r; if ( r instanceof BuildNavigable) { ((BuildNavigable)r).setBuildNavigator(this); } sync(); } @Override public R getBuild() { return this.referenced; } @Override File buildDir() { return getBuild().getRootDir(); } } private static class RunEntry<J extends Job<J,R>, R extends Run<J,R> & BuildNavigable> implements Map.Entry<Integer, R> { private Integer key; private RunValue<J,R> value; private RunEntry(Integer key, RunValue<J,R> value) { this.key = key; this.value = value; } @Override public Integer getKey() { return key; } @Override public R getValue() { return value.getBuild(); } @Override public R setValue(R value) { throw new UnsupportedOperationException("Not implemented"); } } /** * This is a global listener that is called for all types of builds, so * does not have any type-arguments, but it only operates on AbstractProjects. */ @Extension public static class RunValueUpdater extends RunListener<Run> { @Override public void onCompleted(Run r, TaskListener listener) { RunValue.update(r); } @Override public void onFinalized(Run r) { RunValue.update(r); } @Override public void onStarted(Run r, TaskListener listener) { RunValue.update(r); } @Override public void onDeleted(Run r) { RunValue.update(r); } } /** * This detects changes to the build configuration, which may * happen after the build has completed. */ @Extension public static class RunValueUpdater2 extends SaveableListener { @Override public void onChange(Saveable saveable, XmlFile file) { if (saveable instanceof Run) { RunValue.update((Run)saveable); } } } public static class ConverterImpl implements Converter { @Override public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext mc) { final RunMap runMap = (RunMap) source; writer.startNode("builds"); RunValue rv = runMap.getFirst(); while ( rv != null) { mc.convertAnother(rv, new RunValue.ConverterImpl(runMap)); rv = rv.getNext(); } writer.endNode(); writer.startNode("markers"); for (Object bm: runMap.buildMarkersCache.keySet()) { RunValue mbrv = (RunValue) runMap.buildMarkersCache.get(bm); if ( mbrv != null) { writer.startNode(((BuildMarker)bm).name()); writer.setValue(String.valueOf(mbrv.buildNumber)); writer.endNode(); } } writer.endNode(); } @Override public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext uc) { final RunMap runMap = (RunMap)uc.currentObject(); runMap.builds.clear(); runMap.buildsTimeMap.clear(); runMap.buildMarkersCache.clear(); while (reader.hasMoreChildren()) { reader.moveDown(); if ( "builds".equals(reader.getNodeName())) { RunValue prev = null; while (reader.hasMoreChildren()) { reader.moveDown(); RunValue rv = (RunValue) uc.convertAnother(runMap, RunValue.class, new RunValue.ConverterImpl(runMap)); rv.setPrevious(prev); runMap.builds.put(rv.getNumber(), rv); runMap.buildsTimeMap.put(rv.getNumber(), rv.timeInMillis); if ( prev != null ) { prev.setNext(rv); } prev = rv; reader.moveUp(); } } else if ("markers".equals(reader.getNodeName())) { while (reader.hasMoreChildren()) { reader.moveDown(); BuildMarker bm = BuildMarker.valueOf(reader.getNodeName()); Integer buildNumber = Integer.parseInt(reader.getValue()); RunValue bmrv = (RunValue) runMap.builds.get(buildNumber); runMap.buildMarkersCache.put(bm, bmrv); reader.moveUp(); } } reader.moveUp(); } return runMap; } @Override public boolean canConvert(Class type) { return type == RunMap.class; } } private final Function<RunValue<J,R>, R> RUNVALUE_TO_RUN_TRANSFORMER = new Function<RunValue<J,R>,R>() { @Override public R apply(RunValue<J,R> input) { final R build = input.getBuild(); return build; } }; private static final Logger LOGGER = Logger.getLogger(RunMap.class.getName()); }