@Override public synchronized R put(Integer key, R r) { String id = getIdOf(r); int n = getNumberOf(r); Index copy = copy(); BuildReference<R> ref = createReference(r); BuildReference<R> old = copy.byId.put(id, ref); copy.byNumber.put(n, ref); index = copy; /* search relies on the fact that every object added via put() method be available in the xyzOnDisk index, so I'm adding them here however, this is awfully inefficient. I wonder if there's any better way to do this? */ if (!idOnDisk.contains(id)) { ArrayList<String> a = new ArrayList<String>(idOnDisk); a.add(id); Collections.sort(a); idOnDisk = new SortedList<String>(a); } if (!numberOnDisk.contains(n)) { SortedIntList a = new SortedIntList(numberOnDisk); a.add(n); a.sort(); numberOnDisk = a; } return unwrap(old); }
/** * Tries to load the record #N by using the shortcut. * * @return null if the data failed to load. */ protected R load(int n, Index editInPlace) { R r = null; File shortcut = new File(dir, String.valueOf(n)); if (shortcut.isDirectory()) { synchronized (this) { r = load(shortcut, editInPlace); // make sure what we actually loaded is #n, // because the shortcuts can lie. if (r != null && getNumberOf(r) != n) r = null; if (r == null) { // if failed to locate, record that fact SortedIntList update = new SortedIntList(numberOnDisk); update.removeValue(n); numberOnDisk = update; } } } return r; }
private void loadIdOnDisk() { String[] buildDirs = dir.list(createDirectoryFilter()); if (buildDirs == null) { // the job may have just been created buildDirs = EMPTY_STRING_ARRAY; } // wrap into ArrayList to enable mutation Arrays.sort(buildDirs); idOnDisk = new SortedList(new ArrayList<String>(Arrays.asList(buildDirs))); // TODO: should we check that shortcuts is a symlink? String[] shortcuts = dir.list(); if (shortcuts == null) shortcuts = EMPTY_STRING_ARRAY; SortedIntList list = new SortedIntList(shortcuts.length / 2); for (String s : shortcuts) { try { list.add(Integer.parseInt(s)); } catch (NumberFormatException e) { // this isn't a shortcut } } list.sort(); numberOnDisk = list; }
/** * Finds the build #M where M is nearby the given 'n'. * * <p> * * @param n the index to start the search from * @param d defines what we mean by "nearby" above. If EXACT, find #N or return null. If ASC, * finds the closest #M that satisfies M>=N. If DESC, finds the closest #M that satisfies * M<=N. */ public R search(final int n, final Direction d) { Entry<Integer, BuildReference<R>> c = index.ceilingEntry(n); if (c != null && c.getKey() == n) { R r = c.getValue().get(); if (r != null) return r; // found the exact #n } // at this point we know that we don't have #n loaded yet { // check numberOnDisk as a cache to see if we can find it there int npos = numberOnDisk.find(n); if (npos >= 0) { // found exact match R r = load(numberOnDisk.get(npos), null); if (r != null) return r; } switch (d) { case ASC: case DESC: // didn't find the exact match, but what's the nearest ascending value in the cache? int neighbor = (d == ASC ? HIGHER : LOWER).apply(npos); if (numberOnDisk.isInRange(neighbor)) { R r = getByNumber(numberOnDisk.get(neighbor)); if (r != null) { // make sure that the cache is accurate by looking at the previous ID // and it actually satisfies the constraint int prev = (d == ASC ? LOWER : HIGHER).apply(idOnDisk.find(getIdOf(r))); if (idOnDisk.isInRange(prev)) { R pr = getById(idOnDisk.get(prev)); // sign*sign is making sure that #pr and #r sandwiches #n. if (pr != null && signOfCompare(getNumberOf(pr), n) * signOfCompare(n, getNumberOf(r)) > 0) return r; else { // cache is lying. there's something fishy. // ignore the cache and do the slow search } } else { // r is the build with youngest ID return r; } } else { // cache says we should have a build but we didn't. // ignore the cache and do the slow search } } break; case EXACT: // fall through } // didn't find it in the cache, but don't give up yet // maybe the cache just doesn't exist. // so fall back to the slow search } // capture the snapshot and work off with it since it can be overwritten by other threads SortedList<String> idOnDisk = this.idOnDisk; 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 // slow path: we have to find the build from idOnDisk by guessing ID of the build. // first, narrow down the candidate IDs to try by using two known number-to-ID mapping if (idOnDisk.isEmpty()) return null; Entry<Integer, BuildReference<R>> f = index.floorEntry(n); // if bound is null, use a sentinel value String cid = c == null ? "\u0000" : c.getValue().id; String fid = f == null ? "\uFFFF" : f.getValue().id; // at this point, #n must be in (cid,fid) // We know that the build we are looking for exists in [lo,hi) --- it's "hi)" and not "hi]" // because we do +1. // we will narrow this down via binary search final int initialSize = idOnDisk.size(); int lo = idOnDisk.higher(cid); int hi = idOnDisk.lower(fid) + 1; final int initialLo = lo, initialHi = hi; if (!(0 <= lo && lo <= hi && hi <= idOnDisk.size())) { // assertion error, but we are so far unable to get to the bottom of this bug. // but don't let this kill the loading the hard way String msg = String.format( "Assertion error: failing to load #%d %s: lo=%d,hi=%d,size=%d,size2=%d", n, d, lo, hi, idOnDisk.size(), initialSize); LOGGER.log(Level.WARNING, msg, new Exception()); throw new ArrayIndexOutOfBoundsException(msg); } while (lo < hi) { final int pivot = (lo + hi) / 2; if (!(0 <= lo && lo <= pivot && pivot < hi && hi <= idOnDisk.size())) { // assertion error, but we are so far unable to get to the bottom of this bug. // but don't let this kill the loading the hard way String msg = String.format( "Assertion error: failing to load #%d %s: lo=%d,hi=%d,pivot=%d,size=%d (initial:lo=%d,hi=%d,size=%d)", n, d, lo, hi, pivot, idOnDisk.size(), initialLo, initialHi, initialSize); LOGGER.log(Level.WARNING, msg, new Exception()); throw new ArrayIndexOutOfBoundsException(msg); } R r = load(idOnDisk.get(pivot), null); if (r == null) { // this ID isn't valid. get rid of that and retry pivot hi--; if (!clonedIdOnDisk) { // if we are making an edit, we need to own a copy idOnDisk = new SortedList<String>(idOnDisk); clonedIdOnDisk = true; } idOnDisk.remove(pivot); continue; } int found = getNumberOf(r); if (found == n) return r; // exact match if (found < n) lo = pivot + 1; // the pivot was too small. look in the upper half else hi = pivot; // the pivot was too big. look in the lower half } if (clonedIdOnDisk) this.idOnDisk = idOnDisk; // feedback the modified result atomically assert lo == hi; // didn't find the exact match // both lo and hi point to the insertion point on idOnDisk switch (d) { case ASC: if (hi == idOnDisk.size()) return null; return getById(idOnDisk.get(hi)); case DESC: if (lo <= 0) return null; if (lo - 1 >= idOnDisk.size()) { // assertion error, but we are so far unable to get to the bottom of this bug. // but don't let this kill the loading the hard way LOGGER.log( Level.WARNING, String.format( "Assertion error: failing to load #%d %s: lo=%d,hi=%d,size=%d (initial:lo=%d,hi=%d,size=%d)", n, d, lo, hi, idOnDisk.size(), initialLo, initialHi, initialSize), new Exception()); return null; } return getById(idOnDisk.get(lo - 1)); case EXACT: return null; default: throw new AssertionError(); } }