diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileSnapshot.java new file mode 100644 index 000000000..c1ce449dc --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileSnapshot.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2010, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.storage.file; + +import java.io.File; + +import org.eclipse.jgit.util.SystemReader; + +/** + * Caches when a file was last read, making it possible to detect future edits. + *

+ * This object tracks the last modified time of a file. Later during an + * invocation of {@link #isModified(File)} the object will return true if the + * file may have been modified and should be re-read from disk. + *

+ * A snapshot does not "live update" when the underlying filesystem changes. + * Callers must poll for updates by periodically invoking + * {@link #isModified(File)}. + *

+ * To work around the "racy git" problem (where a file may be modified multiple + * times within the granularity of the filesystem modification clock) this class + * may return true from isModified(File) if the last modification time of the + * file is less than 3 seconds ago. + */ +public class FileSnapshot { + /** + * A FileSnapshot that is considered to always be modified. + *

+ * This instance is useful for application code that wants to lazily read a + * file, but only after {@link #isModified(File)} gets invoked. The returned + * snapshot contains only invalid status information. + */ + public static final FileSnapshot DIRTY = new FileSnapshot(-1, -1); + + /** + * Record a snapshot for a specific file path. + *

+ * This method should be invoked before the file is accessed. + * + * @param path + * the path to later remember. The path's current status + * information is saved. + * @return the snapshot. + */ + public static FileSnapshot save(File path) { + final long read = SystemReader.getInstance().getCurrentTime(); + final long modified = path.lastModified(); + return new FileSnapshot(read, modified); + } + + /** Last observed modification time of the path. */ + private final long lastModified; + + /** Last wall-clock time the path was read. */ + private volatile long lastRead; + + /** True once {@link #lastRead} is far later than {@link #lastModified}. */ + private boolean cannotBeRacilyClean; + + private FileSnapshot(long read, long modified) { + this.lastRead = read; + this.lastModified = modified; + this.cannotBeRacilyClean = notRacyClean(read); + } + + /** + * Check if the path has been modified since the snapshot was saved. + * + * @param path + * the path the snapshot describes. + * @return true if the path needs to be read again. + */ + public boolean isModified(File path) { + return isModified(path.lastModified()); + } + + /** + * Update this snapshot when the content hasn't changed. + *

+ * If the caller gets true from {@link #isModified(File)}, re-reads the + * content, discovers the content is identical, and + * {@link #equals(FileSnapshot)} is true, it can use + * {@link #setClean(FileSnapshot)} to make a future + * {@link #isModified(File)} return false. The logic goes something like + * this: + * + *

+	 * if (snapshot.isModified(path)) {
+	 *  FileSnapshot other = FileSnapshot.save(path);
+	 *  Content newContent = ...;
+	 *  if (oldContent.equals(newContent) && snapshot.equals(other))
+	 *      snapshot.setClean(other);
+	 * }
+	 * 
+ * + * @param other + * the other snapshot. + */ + public void setClean(FileSnapshot other) { + final long now = other.lastRead; + if (notRacyClean(now)) + cannotBeRacilyClean = true; + lastRead = now; + } + + /** + * Compare two snapshots to see if they cache the same information. + * + * @param other + * the other snapshot. + * @return true if the two snapshots share the same information. + */ + public boolean equals(FileSnapshot other) { + return lastModified == other.lastModified; + } + + @Override + public boolean equals(Object other) { + if (other instanceof FileSnapshot) + return equals((FileSnapshot) other); + return false; + } + + @Override + public int hashCode() { + // This is pretty pointless, but override hashCode to ensure that + // x.hashCode() == y.hashCode() when x.equals(y) is true. + // + return (int) lastModified; + } + + private boolean notRacyClean(final long read) { + // The last modified time granularity of FAT filesystems is 2 seconds. + // Using 2.5 seconds here provides a reasonably high assurance that + // a modification was not missed. + // + return read - lastModified > 2500; + } + + private boolean isModified(final long currLastModified) { + // Any difference indicates the path was modified. + // + if (lastModified != currLastModified) + return true; + + // We have already determined the last read was far enough + // after the last modification that any new modifications + // are certain to change the last modified time. + // + if (cannotBeRacilyClean) + return false; + + if (notRacyClean(lastRead)) { + // Our last read should have marked cannotBeRacilyClean, + // but this thread may not have seen the change. The read + // of the volatile field lastRead should have fixed that. + // + return false; + } + + // We last read this path too close to its last observed + // modification time. We may have missed a modification. + // Scan again, to ensure we still see the same state. + // + return true; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/ObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/ObjectDirectory.java index c61c773fd..4620357fb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/ObjectDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/ObjectDirectory.java @@ -96,7 +96,8 @@ * considered. */ public class ObjectDirectory extends FileObjectDatabase { - private static final PackList NO_PACKS = new PackList(-1, -1, new PackFile[0]); + private static final PackList NO_PACKS = new PackList( + FileSnapshot.DIRTY, new PackFile[0]); /** Maximum number of candidates offered as resolutions of abbreviation. */ private static final int RESOLVE_ABBREV_LIMIT = 256; @@ -509,7 +510,7 @@ InsertLooseObjectResult insertUnpackedObject(File tmp, ObjectId id, boolean tryAgain1() { final PackList old = packList.get(); - if (old.tryAgain(packDirectory.lastModified())) + if (old.snapshot.isModified(packDirectory)) return old != scanPacks(old); return false; } @@ -539,7 +540,7 @@ private void insertPack(final PackFile pf) { final PackFile[] newList = new PackFile[1 + oldList.length]; newList[0] = pf; System.arraycopy(oldList, 0, newList, 1, oldList.length); - n = new PackList(o.lastRead, o.lastModified, newList); + n = new PackList(o.snapshot, newList); } while (!packList.compareAndSet(o, n)); } @@ -556,7 +557,7 @@ private void removePack(final PackFile deadPack) { final PackFile[] newList = new PackFile[oldList.length - 1]; System.arraycopy(oldList, 0, newList, 0, j); System.arraycopy(oldList, j + 1, newList, j, newList.length - j); - n = new PackList(o.lastRead, o.lastModified, newList); + n = new PackList(o.snapshot, newList); } while (!packList.compareAndSet(o, n)); deadPack.close(); } @@ -590,8 +591,7 @@ private PackList scanPacks(final PackList original) { private PackList scanPacksImpl(final PackList old) { final Map forReuse = reuseMap(old); - final long lastRead = System.currentTimeMillis(); - final long lastModified = packDirectory.lastModified(); + final FileSnapshot snapshot = FileSnapshot.save(packDirectory); final Set names = listPackDirectory(); final List list = new ArrayList(names.size() >> 2); boolean foundNew = false; @@ -628,19 +628,21 @@ private PackList scanPacksImpl(final PackList old) { // the same as the set we were given. Instead of building a new object // return the same collection. // - if (!foundNew && lastModified == old.lastModified && forReuse.isEmpty()) - return old.updateLastRead(lastRead); + if (!foundNew && forReuse.isEmpty() && snapshot.equals(old.snapshot)) { + old.snapshot.setClean(snapshot); + return old; + } for (final PackFile p : forReuse.values()) { p.close(); } if (list.isEmpty()) - return new PackList(lastRead, lastModified, NO_PACKS.packs); + return new PackList(snapshot, NO_PACKS.packs); final PackFile[] r = list.toArray(new PackFile[list.size()]); Arrays.sort(r, PackFile.SORT); - return new PackList(lastRead, lastModified, r); + return new PackList(snapshot, r); } private static Map reuseMap(final PackList old) { @@ -737,62 +739,15 @@ private AlternateHandle openAlternate(File objdir) throws IOException { } private static final class PackList { - /** Last wall-clock time the directory was read. */ - volatile long lastRead; - - /** Last modification time of {@link ObjectDirectory#packDirectory}. */ - final long lastModified; + /** State just before reading the pack directory. */ + final FileSnapshot snapshot; /** All known packs, sorted by {@link PackFile#SORT}. */ final PackFile[] packs; - private boolean cannotBeRacilyClean; - - PackList(final long lastRead, final long lastModified, - final PackFile[] packs) { - this.lastRead = lastRead; - this.lastModified = lastModified; + PackList(final FileSnapshot monitor, final PackFile[] packs) { + this.snapshot = monitor; this.packs = packs; - this.cannotBeRacilyClean = notRacyClean(lastRead); - } - - private boolean notRacyClean(final long read) { - return read - lastModified > 2 * 60 * 1000L; - } - - PackList updateLastRead(final long now) { - if (notRacyClean(now)) - cannotBeRacilyClean = true; - lastRead = now; - return this; - } - - boolean tryAgain(final long currLastModified) { - // Any difference indicates the directory was modified. - // - if (lastModified != currLastModified) - return true; - - // We have already determined the last read was far enough - // after the last modification that any new modifications - // are certain to change the last modified time. - // - if (cannotBeRacilyClean) - return false; - - if (notRacyClean(lastRead)) { - // Our last read should have marked cannotBeRacilyClean, - // but this thread may not have seen the change. The read - // of the volatile field lastRead should have fixed that. - // - return false; - } - - // We last read this directory too close to its last observed - // modification time. We may have missed a modification. Scan - // the directory again, to ensure we still see the same state. - // - return true; } }