Extract pack directory last modified check code

Pulling the last modified checking logic out of ObjectDirectory
makes it possible to reuse this code for other files, such as
the $GIT_DIR/config or $GIT_DIR/packed-refs files.

Change-Id: If2f27a89fc3b7adde7e65ff40bbca5d55b98b772
Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
This commit is contained in:
Shawn O. Pearce 2010-12-13 12:23:07 -08:00
parent 6f3b4d5d04
commit c8db22f355
2 changed files with 224 additions and 61 deletions

View File

@ -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.
* <p>
* 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.
* <p>
* A snapshot does not "live update" when the underlying filesystem changes.
* Callers must poll for updates by periodically invoking
* {@link #isModified(File)}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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:
*
* <pre>
* if (snapshot.isModified(path)) {
* FileSnapshot other = FileSnapshot.save(path);
* Content newContent = ...;
* if (oldContent.equals(newContent) &amp;&amp; snapshot.equals(other))
* snapshot.setClean(other);
* }
* </pre>
*
* @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;
}
}

View File

@ -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<String, PackFile> forReuse = reuseMap(old);
final long lastRead = System.currentTimeMillis();
final long lastModified = packDirectory.lastModified();
final FileSnapshot snapshot = FileSnapshot.save(packDirectory);
final Set<String> names = listPackDirectory();
final List<PackFile> list = new ArrayList<PackFile>(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<String, PackFile> 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;
}
}