4
* Copyright (c) 2012, CloudBees, Inc.
6
* Permission is hereby granted, free of charge, to any person obtaining a copy
7
* of this software and associated documentation files (the "Software"), to deal
8
* in the Software without restriction, including without limitation the rights
9
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
* copies of the Software, and to permit persons to whom the Software is
11
* furnished to do so, subject to the following conditions:
13
* The above copyright notice and this permission notice shall be included in
14
* all copies or substantial portions of the Software.
16
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
package jenkins.model.lazy;
26
import hudson.model.Job;
27
import hudson.model.Run;
28
import hudson.model.RunMap;
29
import org.apache.commons.collections.keyvalue.DefaultMapEntry;
30
import org.kohsuke.accmod.Restricted;
31
import org.kohsuke.accmod.restrictions.NoExternalUse;
34
import java.io.FilenameFilter;
35
import java.io.IOException;
36
import java.lang.ref.Reference;
37
import java.util.AbstractMap;
38
import java.util.ArrayList;
39
import java.util.Arrays;
40
import java.util.Collections;
41
import java.util.Comparator;
43
import java.util.NoSuchElementException;
45
import java.util.SortedMap;
46
import java.util.TreeMap;
47
import java.util.logging.Level;
48
import java.util.logging.Logger;
50
import static jenkins.model.lazy.AbstractLazyLoadRunMap.Direction.*;
51
import static jenkins.model.lazy.Boundary.*;
54
* {@link SortedMap} that keeps build records by their build numbers, in the descending order
58
* The main thing about this class is that it encapsulates the lazy loading logic.
59
* That is, while this class looks and feels like a normal {@link SortedMap} from outside,
60
* it actually doesn't have every item in the map instantiated yet. As items in the map get
61
* requested, this class {@link #retrieve(File) retrieves them} on demand, one by one.
64
* The lookup is primarily done by using the build number as the key (hence the key type is {@link Integer}),
65
* but this class also provides look up based on {@linkplain #getIdOf(Object) the build ID}.
68
* This class makes the following assumption about the on-disk layout of the data:
71
* <li>Every build is stored in a directory, named after its ID.
72
* <li>ID and build number are in the consistent order. That is,
73
* if there are two builds #M and #N, {@code M>N <=> M.id > N.id}.
77
* On certain platforms, there are symbolic links named after build numbers that link to the build ID.
78
* If these are available, they are used as a hint to speed up the lookup. Otherwise
79
* we rely on the assumption above and perform a binary search to locate the build.
80
* (notice that we'll have to do linear search if we don't have the consistent ordering assumption,
81
* which robs the whole point of doing lazy loading.)
84
* Some of the {@link SortedMap} operations are weakly implemented. For example,
85
* {@link #size()} may be inaccurate because we only count the number of directories that look like
86
* build records, without checking if they are loadable. But these weaknesses aren't distinguishable
87
* from concurrent modifications, where another thread deletes a build while one thread iterates them.
90
* Some of the {@link SortedMap} operations are inefficiently implemented, by
91
* {@linkplain #all() loading all the build records eagerly}. We hope to replace
92
* these implementations by more efficient lazy-loading ones as we go.
95
* Object lock of {@code this} is used to make sure mutation occurs sequentially.
96
* That is, ensure that only one thread is actually calling {@link #retrieve(File)} and
97
* updating {@link Index#byNumber} and {@link Index#byId}.
99
* @author Kohsuke Kawaguchi
102
public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> implements SortedMap<Integer,R> {
104
* Used in {@link #all()} to quickly determine if we've already loaded everything.
106
private boolean fullyLoaded;
109
* Currently visible index.
110
* Updated atomically. Once set to this field, the index object may not be modified.
112
private volatile Index index = new Index();
115
* Pair of two maps into a single class, so that the changes can be made visible atomically,
116
* and updates can happen concurrently to read.
118
* The idiom is that you put yourself in a synchronized block, {@linkplain #copy() make a copy of this},
119
* update the copy, then set it to {@link #index}.
121
private class Index {
123
* Stores the mapping from build number to build, for builds that are already loaded.
125
private final TreeMap<Integer,BuildReference<R>> byNumber;
128
* Stores the build ID to build number for builds that we already know.
130
* If we have known load failure of the given ID, we record that in the map
131
* by using the null value (not to be confused with a non-null {@link BuildReference}
132
* with null referent, which just means the record was GCed.)
134
private final TreeMap<String,BuildReference<R>> byId;
137
byId = new TreeMap<String,BuildReference<R>>();
138
byNumber = new TreeMap<Integer,BuildReference<R>>(COMPARATOR);
141
private Index(Index rhs) {
142
byId = new TreeMap<String, BuildReference<R>>(rhs.byId);
143
byNumber = new TreeMap<Integer,BuildReference<R>>(rhs.byNumber);
147
* Returns the build record #M (<=n)
149
private Map.Entry<Integer,BuildReference<R>> ceilingEntry(int n) {
150
// switch to this once we depend on JDK6
151
// return byNumber.ceilingEntry(n);
153
Set<Entry<Integer, BuildReference<R>>> s = byNumber.tailMap(n).entrySet();
154
if (s.isEmpty()) return null;
155
else return s.iterator().next();
159
* Returns the build record #M (>=n)
161
// >= and not <= because byNumber is in the descending order
162
private Map.Entry<Integer,BuildReference<R>> floorEntry(int n) {
163
// switch to this once we depend on JDK6
164
// return byNumber.floorEntry(n);
166
SortedMap<Integer, BuildReference<R>> sub = byNumber.headMap(n);
167
if (sub.isEmpty()) return null;
168
Integer k = sub.lastKey();
169
return new DefaultMapEntry(k,sub.get(k));
174
* Build IDs found as directories, in the ascending order.
177
private volatile SortedList<String> idOnDisk = new SortedList<String>(Collections.<String>emptyList());
180
* Build number shortcuts found on disk, in the ascending order.
183
private volatile SortedIntList numberOnDisk = new SortedIntList(0);
186
* Base directory for data.
187
* In effect this is treated as a final field, but can't mark it final
188
* because the compatibility requires that we make it settable
189
* in the first call after the constructor.
193
protected AbstractLazyLoadRunMap(File dir) {
197
@Restricted(NoExternalUse.class)
198
protected void initBaseDir(File dir) {
199
assert this.dir==null;
206
* @return true if {@link AbstractLazyLoadRunMap#AbstractLazyLoadRunMap} was called with a non-null param, or {@link RunMap#load(Job, RunMap.Constructor)} was called
208
@Restricted(NoExternalUse.class)
209
public final boolean baseDirInitialized() {
214
* Let go of all the loaded references.
216
* This is a bit more sophisticated version of forcing GC.
217
* Primarily for debugging and testing lazy loading behaviour.
219
public void purgeCache() {
223
private void loadIdOnDisk() {
224
String[] buildDirs = dir.list(createDirectoryFilter());
225
if (buildDirs==null) {
226
// the job may have just been created
227
buildDirs=EMPTY_STRING_ARRAY;
229
// wrap into ArrayList to enable mutation
230
Arrays.sort(buildDirs);
231
idOnDisk = new SortedList(new ArrayList<String>(Arrays.asList(buildDirs)));
233
// TODO: should we check that shortcuts is a symlink?
234
String[] shortcuts = dir.list();
235
if (shortcuts==null) shortcuts=EMPTY_STRING_ARRAY;
236
SortedIntList list = new SortedIntList(shortcuts.length/2);
237
for (String s : shortcuts) {
239
list.add(Integer.parseInt(s));
240
} catch (NumberFormatException e) {
241
// this isn't a shortcut
248
public Comparator<? super Integer> comparator() {
253
* If we have non-zero R in memory, we can return false right away.
254
* If we have zero R in memory, try loading one and see if we can find something.
257
public boolean isEmpty() {
258
return index.byId.isEmpty() && search(Integer.MAX_VALUE, DESC)==null;
262
public Set<Entry<Integer, R>> entrySet() {
263
assert baseDirInitialized();
264
return Collections.unmodifiableSet(new BuildReferenceMapAdapter<R>(this,all()).entrySet());
268
* Returns a read-only view of records that has already been loaded.
270
public SortedMap<Integer,R> getLoadedBuilds() {
271
return Collections.unmodifiableSortedMap(new BuildReferenceMapAdapter<R>(this, index.byNumber));
276
* Biggest build number to be in the returned set.
278
* Smallest build number-1 to be in the returned set (-1 because this is exclusive)
280
public SortedMap<Integer, R> subMap(Integer fromKey, Integer toKey) {
281
// TODO: if this method can produce a lazy map, that'd be wonderful
282
// because due to the lack of floor/ceil/higher/lower kind of methods
283
// to look up keys in SortedMap, various places of Jenkins rely on
284
// subMap+firstKey/lastKey combo.
286
R start = search(fromKey, DESC);
287
if (start==null) return EMPTY_SORTED_MAP;
289
R end = search(toKey, ASC);
290
if (end==null) return EMPTY_SORTED_MAP;
292
for (R i=start; i!=end; ) {
293
i = search(getNumberOf(i)-1,DESC);
297
return Collections.unmodifiableSortedMap(new BuildReferenceMapAdapter<R>(this, index.byNumber.subMap(fromKey, toKey)));
300
public SortedMap<Integer, R> headMap(Integer toKey) {
301
return subMap(Integer.MAX_VALUE, toKey);
304
public SortedMap<Integer, R> tailMap(Integer fromKey) {
305
return subMap(fromKey, Integer.MIN_VALUE);
308
public Integer firstKey() {
310
if (r==null) throw new NoSuchElementException();
311
return getNumberOf(r);
314
public Integer lastKey() {
316
if (r==null) throw new NoSuchElementException();
317
return getNumberOf(r);
320
public R newestBuild() {
321
return search(Integer.MAX_VALUE, DESC);
324
public R oldestBuild() {
325
return search(Integer.MIN_VALUE, ASC);
329
public R get(Object key) {
330
if (key instanceof Integer) {
331
int n = (Integer) key;
334
return super.get(key);
337
public R get(int n) {
338
return search(n,Direction.EXACT);
342
* Finds the build #M where M is nearby the given 'n'.
348
* the index to start the search from
350
* defines what we mean by "nearby" above.
351
* If EXACT, find #N or return null.
352
* If ASC, finds the closest #M that satisfies M>=N.
353
* If DESC, finds the closest #M that satisfies M<=N.
355
public R search(final int n, final Direction d) {
356
Entry<Integer, BuildReference<R>> c = index.ceilingEntry(n);
357
if (c!=null && c.getKey()== n) {
358
R r = c.getValue().get();
360
return r; // found the exact #n
363
// at this point we know that we don't have #n loaded yet
365
{// check numberOnDisk as a cache to see if we can find it there
366
int npos = numberOnDisk.find(n);
367
if (npos>=0) {// found exact match
368
R r = load(numberOnDisk.get(npos), null);
376
// didn't find the exact match, but what's the nearest ascending value in the cache?
377
int neighbor = (d==ASC?HIGHER:LOWER).apply(npos);
378
if (numberOnDisk.isInRange(neighbor)) {
379
R r = getByNumber(numberOnDisk.get(neighbor));
381
// make sure that the cache is accurate by looking at the previous ID
382
// and it actually satisfies the constraint
383
int prev = (d==ASC?LOWER:HIGHER).apply(idOnDisk.find(getIdOf(r)));
384
if (idOnDisk.isInRange(prev)) {
385
R pr = getById(idOnDisk.get(prev));
386
// sign*sign is making sure that #pr and #r sandwiches #n.
387
if (pr!=null && signOfCompare(getNumberOf(pr),n)*signOfCompare(n,getNumberOf(r))>0)
390
// cache is lying. there's something fishy.
391
// ignore the cache and do the slow search
394
// r is the build with youngest ID
398
// cache says we should have a build but we didn't.
399
// ignore the cache and do the slow search
407
// didn't find it in the cache, but don't give up yet
408
// maybe the cache just doesn't exist.
409
// so fall back to the slow search
412
// capture the snapshot and work off with it since it can be overwritten by other threads
413
SortedList<String> idOnDisk = this.idOnDisk;
414
boolean clonedIdOnDisk = false; // if we modify idOnDisk we need to write it back. this flag is set to true when we overwrit idOnDisk local var
416
// slow path: we have to find the build from idOnDisk by guessing ID of the build.
417
// first, narrow down the candidate IDs to try by using two known number-to-ID mapping
418
if (idOnDisk.isEmpty()) return null;
420
Entry<Integer, BuildReference<R>> f = index.floorEntry(n);
422
// if bound is null, use a sentinel value
423
String cid = c==null ? "\u0000" : c.getValue().id;
424
String fid = f==null ? "\uFFFF" : f.getValue().id;
425
// at this point, #n must be in (cid,fid)
427
// We know that the build we are looking for exists in [lo,hi) --- it's "hi)" and not "hi]" because we do +1.
428
// we will narrow this down via binary search
429
final int initialSize = idOnDisk.size();
430
int lo = idOnDisk.higher(cid);
431
int hi = idOnDisk.lower(fid)+1;
433
final int initialLo = lo, initialHi = hi;
435
if (!(0<=lo && lo<=hi && hi<=idOnDisk.size())) {
436
// assertion error, but we are so far unable to get to the bottom of this bug.
437
// but don't let this kill the loading the hard way
438
String msg = String.format(
439
"JENKINS-15652 Assertion error #1: failing to load %s #%d %s: lo=%d,hi=%d,size=%d,size2=%d",
440
dir, n, d, lo, hi, idOnDisk.size(), initialSize);
441
LOGGER.log(Level.WARNING, msg);
446
final int pivot = (lo+hi)/2;
447
if (!(0<=lo && lo<=pivot && pivot<hi && hi<=idOnDisk.size())) {
448
// assertion error, but we are so far unable to get to the bottom of this bug.
449
// but don't let this kill the loading the hard way
450
String msg = String.format(
451
"JENKINS-15652 Assertion error #2: failing to load %s #%d %s: lo=%d,hi=%d,pivot=%d,size=%d (initial:lo=%d,hi=%d,size=%d)",
452
dir, n, d, lo, hi, pivot, idOnDisk.size(), initialLo, initialHi, initialSize);
453
LOGGER.log(Level.WARNING, msg);
456
R r = load(idOnDisk.get(pivot), null);
458
// this ID isn't valid. get rid of that and retry pivot
460
if (!clonedIdOnDisk) {// if we are making an edit, we need to own a copy
461
idOnDisk = new SortedList<String>(idOnDisk);
462
clonedIdOnDisk = true;
464
idOnDisk.remove(pivot);
468
int found = getNumberOf(r);
470
return r; // exact match
472
if (found<n) lo = pivot+1; // the pivot was too small. look in the upper half
473
else hi = pivot; // the pivot was too big. look in the lower half
477
this.idOnDisk = idOnDisk; // feedback the modified result atomically
480
// didn't find the exact match
481
// both lo and hi point to the insertion point on idOnDisk
484
if (hi==idOnDisk.size()) return null;
485
return getById(idOnDisk.get(hi));
487
if (lo<=0) return null;
488
if (lo-1>=idOnDisk.size()) {
489
// assertion error, but we are so far unable to get to the bottom of this bug.
490
// but don't let this kill the loading the hard way
491
LOGGER.log(Level.WARNING, String.format(
492
"JENKINS-15652 Assertion error #3: failing to load %s #%d %s: lo=%d,hi=%d,size=%d (initial:lo=%d,hi=%d,size=%d)",
493
dir, n,d,lo,hi,idOnDisk.size(), initialLo,initialHi,initialSize));
496
return getById(idOnDisk.get(lo-1));
500
throw new AssertionError();
507
private static int signOfCompare(int a, int b) {
513
public R getById(String id) {
514
Index snapshot = index;
515
if (snapshot.byId.containsKey(id)) {
516
BuildReference<R> ref = snapshot.byId.get(id);
517
if (ref==null) return null; // known failure
519
if (v!=null) return v; // already in memory
520
// otherwise fall through to load
522
return load(id,null);
525
public R getByNumber(int n) {
526
return search(n,Direction.EXACT);
529
public R put(R value) {
533
protected R _put(R value) {
534
return put(getNumberOf(value),value);
538
public synchronized R put(Integer key, R r) {
539
String id = getIdOf(r);
540
int n = getNumberOf(r);
543
BuildReference<R> ref = createReference(r);
544
BuildReference<R> old = copy.byId.put(id,ref);
545
copy.byNumber.put(n,ref);
549
search relies on the fact that every object added via
550
put() method be available in the xyzOnDisk index, so I'm adding them here
551
however, this is awfully inefficient. I wonder if there's any better way to do this?
553
if (!idOnDisk.contains(id)) {
554
ArrayList<String> a = new ArrayList<String>(idOnDisk);
557
idOnDisk = new SortedList<String>(a);
560
if (!numberOnDisk.contains(n)) {
561
SortedIntList a = new SortedIntList(numberOnDisk);
570
private R unwrap(Reference<R> ref) {
571
return ref!=null ? ref.get() : null;
575
public synchronized void putAll(Map<? extends Integer,? extends R> rhs) {
577
for (R r : rhs.values()) {
578
String id = getIdOf(r);
579
BuildReference<R> ref = createReference(r);
580
copy.byId.put(id,ref);
581
copy.byNumber.put(getNumberOf(r),ref);
587
* Loads all the build records to fully populate the map.
588
* Calling this method results in eager loading everything,
589
* so the whole point of this class is to avoid this call as much as possible
590
* for typical code path.
593
* fully populated map.
595
private TreeMap<Integer,BuildReference<R>> all() {
597
synchronized (this) {
600
for (String id : idOnDisk) {
601
if (!copy.byId.containsKey(id))
609
return index.byNumber;
613
* Creates a duplicate for the COW data structure in preparation for mutation.
615
private Index copy() {
616
return new Index(index);
620
* Tries to load the record #N by using the shortcut.
622
* @return null if the data failed to load.
624
protected R load(int n, Index editInPlace) {
626
File shortcut = new File(dir,String.valueOf(n));
627
if (shortcut.isDirectory()) {
628
synchronized (this) {
629
r = load(shortcut,editInPlace);
631
// make sure what we actually loaded is #n,
632
// because the shortcuts can lie.
633
if (r!=null && getNumberOf(r)!=n)
637
// if failed to locate, record that fact
638
SortedIntList update = new SortedIntList(numberOnDisk);
639
update.removeValue(n);
640
numberOnDisk = update;
648
protected R load(String id, Index editInPlace) {
650
R v = load(new File(dir, id), editInPlace);
651
if (v==null && editInPlace!=null) {
652
// remember the failure.
653
// if editInPlace==null, we can create a new copy for this, but not sure if it's worth doing,
654
// given that we also update idOnDisk anyway.
655
editInPlace.byId.put(id,null);
662
* If non-null, update this data structure.
663
* Otherwise do a copy-on-write of {@link #index}
665
protected synchronized R load(File dataDir, Index editInPlace) {
667
R r = retrieve(dataDir);
668
if (r==null) return null;
670
Index copy = editInPlace!=null ? editInPlace : new Index(index);
672
String id = getIdOf(r);
673
BuildReference<R> ref = createReference(r);
674
copy.byId.put(id,ref);
675
copy.byNumber.put(getNumberOf(r),ref);
677
if (editInPlace==null) index = copy;
680
} catch (IOException e) {
681
LOGGER.log(Level.WARNING, "Failed to load "+dataDir,e);
687
* Subtype to provide {@link Run#getNumber()} so that this class doesn't have to depend on it.
689
protected abstract int getNumberOf(R r);
691
* Subtype to provide {@link Run#getId()} so that this class doesn't have to depend on it.
693
protected abstract String getIdOf(R r);
696
* Allow subtype to capture a reference.
698
protected BuildReference<R> createReference(R r) {
699
return new BuildReference<R>(getIdOf(r),r);
704
* Parses {@code R} instance from data in the specified directory.
707
* null if the parsing failed.
708
* @throws IOException
709
* if the parsing failed. This is just like returning null
710
* except the caller will catch the exception and report it.
712
protected abstract R retrieve(File dir) throws IOException;
714
public synchronized boolean removeValue(R run) {
716
copy.byNumber.remove(getNumberOf(run));
717
BuildReference<R> old = copy.byId.remove(getIdOf(run));
720
return unwrap(old)!=null;
724
* Replaces all the current loaded Rs with the given ones.
726
public synchronized void reset(TreeMap<Integer,R> builds) {
727
Index index = new Index();
728
for (R r : builds.values()) {
729
String id = getIdOf(r);
730
BuildReference<R> ref = createReference(r);
731
index.byId.put(id,ref);
732
index.byNumber.put(getNumberOf(r),ref);
739
public int hashCode() {
740
return System.identityHashCode(this);
744
public boolean equals(Object o) {
749
* Lists the actual data directory
751
protected abstract FilenameFilter createDirectoryFilter();
753
private static final Comparator<Comparable> COMPARATOR = new Comparator<Comparable>() {
754
public int compare(Comparable o1, Comparable o2) {
755
return -o1.compareTo(o2);
759
public enum Direction {
763
private static final String[] EMPTY_STRING_ARRAY = new String[0];
765
private static final SortedMap EMPTY_SORTED_MAP = Collections.unmodifiableSortedMap(new TreeMap());
767
static final Logger LOGGER = Logger.getLogger(AbstractLazyLoadRunMap.class.getName());