Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java')
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java244
1 files changed, 196 insertions, 48 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java
index 1b867861b4..8650ebfe29 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileSnapshot.java
@@ -43,19 +43,24 @@
package org.eclipse.jgit.internal.storage.file;
+import static org.eclipse.jgit.util.FS.FileStoreAttributes.FALLBACK_FILESTORE_ATTRIBUTES;
+import static org.eclipse.jgit.util.FS.FileStoreAttributes.FALLBACK_TIMESTAMP_RESOLUTION;
import java.io.File;
import java.io.IOException;
import java.nio.file.attribute.BasicFileAttributes;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
import java.time.Duration;
-import java.util.Date;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.FileStoreAttributes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Caches when a file was last read, making it possible to detect future edits.
@@ -74,6 +79,8 @@ import org.eclipse.jgit.util.FS;
* file is less than 3 seconds ago.
*/
public class FileSnapshot {
+ private static final Logger LOG = LoggerFactory
+ .getLogger(FileSnapshot.class);
/**
* An unknown file size.
*
@@ -81,8 +88,14 @@ public class FileSnapshot {
*/
public static final long UNKNOWN_SIZE = -1;
+ private static final Instant UNKNOWN_TIME = Instant.ofEpochMilli(-1);
+
private static final Object MISSING_FILEKEY = new Object();
+ private static final DateTimeFormatter dateFmt = DateTimeFormatter
+ .ofPattern("yyyy-MM-dd HH:mm:ss.nnnnnnnnn") //$NON-NLS-1$
+ .withLocale(Locale.getDefault()).withZone(ZoneId.systemDefault());
+
/**
* A FileSnapshot that is considered to always be modified.
* <p>
@@ -90,8 +103,8 @@ public class FileSnapshot {
* 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,
- UNKNOWN_SIZE, Duration.ZERO, MISSING_FILEKEY);
+ public static final FileSnapshot DIRTY = new FileSnapshot(UNKNOWN_TIME,
+ UNKNOWN_TIME, UNKNOWN_SIZE, Duration.ZERO, MISSING_FILEKEY);
/**
* A FileSnapshot that is clean if the file does not exist.
@@ -100,8 +113,8 @@ public class FileSnapshot {
* file to be clean. {@link #isModified(File)} will return false if the file
* path does not exist.
*/
- public static final FileSnapshot MISSING_FILE = new FileSnapshot(0, 0, 0,
- Duration.ZERO, MISSING_FILEKEY) {
+ public static final FileSnapshot MISSING_FILE = new FileSnapshot(
+ Instant.EPOCH, Instant.EPOCH, 0, Duration.ZERO, MISSING_FILEKEY) {
@Override
public boolean isModified(File path) {
return FS.DETECTED.exists(path);
@@ -122,6 +135,22 @@ public class FileSnapshot {
return new FileSnapshot(path);
}
+ /**
+ * Record a snapshot for a specific file path without using config file to
+ * get filesystem timestamp resolution.
+ * <p>
+ * This method should be invoked before the file is accessed. It is used by
+ * FileBasedConfig to avoid endless recursion.
+ *
+ * @param path
+ * the path to later remember. The path's current status
+ * information is saved.
+ * @return the snapshot.
+ */
+ public static FileSnapshot saveNoConfig(File path) {
+ return new FileSnapshot(path, false);
+ }
+
private static Object getFileKey(BasicFileAttributes fileAttributes) {
Object fileKey = fileAttributes.fileKey();
return fileKey == null ? MISSING_FILEKEY : fileKey;
@@ -141,18 +170,41 @@ public class FileSnapshot {
* @param modified
* the last modification time of the file
* @return the snapshot.
+ * @deprecated use {@link #save(Instant)} instead.
*/
+ @Deprecated
public static FileSnapshot save(long modified) {
- final long read = System.currentTimeMillis();
- return new FileSnapshot(read, modified, -1, Duration.ZERO,
- MISSING_FILEKEY);
+ final Instant read = Instant.now();
+ return new FileSnapshot(read, Instant.ofEpochMilli(modified),
+ UNKNOWN_SIZE, FALLBACK_TIMESTAMP_RESOLUTION, MISSING_FILEKEY);
+ }
+
+ /**
+ * Record a snapshot for a file for which the last modification time is
+ * already known.
+ * <p>
+ * This method should be invoked before the file is accessed.
+ * <p>
+ * Note that this method cannot rely on measuring file timestamp resolution
+ * to avoid racy git issues caused by finite file timestamp resolution since
+ * it's unknown in which filesystem the file is located. Hence the worst
+ * case fallback for timestamp resolution is used.
+ *
+ * @param modified
+ * the last modification time of the file
+ * @return the snapshot.
+ */
+ public static FileSnapshot save(Instant modified) {
+ final Instant read = Instant.now();
+ return new FileSnapshot(read, modified, UNKNOWN_SIZE,
+ FALLBACK_TIMESTAMP_RESOLUTION, MISSING_FILEKEY);
}
/** Last observed modification time of the path. */
- private final long lastModified;
+ private final Instant lastModified;
/** Last wall-clock time the path was read. */
- private volatile long lastRead;
+ private volatile Instant lastRead;
/** True once {@link #lastRead} is far later than {@link #lastModified}. */
private boolean cannotBeRacilyClean;
@@ -162,8 +214,8 @@ public class FileSnapshot {
* When set to {@link #UNKNOWN_SIZE} the size is not considered for modification checks. */
private final long size;
- /** measured filesystem timestamp resolution */
- private Duration fsTimestampResolution;
+ /** measured FileStore attributes */
+ private FileStoreAttributes fileStoreAttributeCache;
/**
* Object that uniquely identifies the given file, or {@code
@@ -171,31 +223,57 @@ public class FileSnapshot {
*/
private final Object fileKey;
+ private final File file;
+
/**
* 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
+ * @param file
+ * the path to remember meta data for. The path's current status
* information is saved.
*/
- protected FileSnapshot(File path) {
- this.lastRead = System.currentTimeMillis();
- this.fsTimestampResolution = FS
- .getFsTimerResolution(path.toPath().getParent());
+ protected FileSnapshot(File file) {
+ this(file, true);
+ }
+
+ /**
+ * Record a snapshot for a specific file path.
+ * <p>
+ * This method should be invoked before the file is accessed.
+ *
+ * @param file
+ * the path to remember meta data for. The path's current status
+ * information is saved.
+ * @param useConfig
+ * if {@code true} read filesystem time resolution from
+ * configuration file otherwise use fallback resolution
+ */
+ protected FileSnapshot(File file, boolean useConfig) {
+ this.file = file;
+ this.lastRead = Instant.now();
+ this.fileStoreAttributeCache = useConfig
+ ? FS.getFileStoreAttributes(file.toPath().getParent())
+ : FALLBACK_FILESTORE_ATTRIBUTES;
BasicFileAttributes fileAttributes = null;
try {
- fileAttributes = FS.DETECTED.fileAttributes(path);
+ fileAttributes = FS.DETECTED.fileAttributes(file);
} catch (IOException e) {
- this.lastModified = path.lastModified();
- this.size = path.length();
+ this.lastModified = Instant.ofEpochMilli(file.lastModified());
+ this.size = file.length();
this.fileKey = MISSING_FILEKEY;
return;
}
- this.lastModified = fileAttributes.lastModifiedTime().toMillis();
+ this.lastModified = fileAttributes.lastModifiedTime().toInstant();
this.size = fileAttributes.size();
this.fileKey = getFileKey(fileAttributes);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("file={}, create new FileSnapshot: lastRead={}, lastModified={}, size={}, fileKey={}", //$NON-NLS-1$
+ file, dateFmt.format(lastRead),
+ dateFmt.format(lastModified), Long.valueOf(size),
+ fileKey.toString());
+ }
}
private boolean sizeChanged;
@@ -206,11 +284,17 @@ public class FileSnapshot {
private boolean wasRacyClean;
- private FileSnapshot(long read, long modified, long size,
+ private long delta;
+
+ private long racyThreshold;
+
+ private FileSnapshot(Instant read, Instant modified, long size,
@NonNull Duration fsTimestampResolution, @NonNull Object fileKey) {
+ this.file = null;
this.lastRead = read;
this.lastModified = modified;
- this.fsTimestampResolution = fsTimestampResolution;
+ this.fileStoreAttributeCache = new FileStoreAttributes(
+ fsTimestampResolution);
this.size = size;
this.fileKey = fileKey;
}
@@ -219,8 +303,19 @@ public class FileSnapshot {
* Get time of last snapshot update
*
* @return time of last snapshot update
+ * @deprecated use {@link #lastModifiedInstant()} instead
*/
+ @Deprecated
public long lastModified() {
+ return lastModified.toEpochMilli();
+ }
+
+ /**
+ * Get time of last snapshot update
+ *
+ * @return time of last snapshot update
+ */
+ public Instant lastModifiedInstant() {
return lastModified;
}
@@ -239,16 +334,16 @@ public class FileSnapshot {
* @return true if the path needs to be read again.
*/
public boolean isModified(File path) {
- long currLastModified;
+ Instant currLastModified;
long currSize;
Object currFileKey;
try {
BasicFileAttributes fileAttributes = FS.DETECTED.fileAttributes(path);
- currLastModified = fileAttributes.lastModifiedTime().toMillis();
+ currLastModified = fileAttributes.lastModifiedTime().toInstant();
currSize = fileAttributes.size();
currFileKey = getFileKey(fileAttributes);
} catch (IOException e) {
- currLastModified = path.lastModified();
+ currLastModified = Instant.ofEpochMilli(path.lastModified());
currSize = path.length();
currFileKey = MISSING_FILEKEY;
}
@@ -290,7 +385,7 @@ public class FileSnapshot {
* the other snapshot.
*/
public void setClean(FileSnapshot other) {
- final long now = other.lastRead;
+ final Instant now = other.lastRead;
if (!isRacyClean(now)) {
cannotBeRacilyClean = true;
}
@@ -304,9 +399,10 @@ public class FileSnapshot {
* if sleep was interrupted
*/
public void waitUntilNotRacy() throws InterruptedException {
- while (isRacyClean(System.currentTimeMillis())) {
- TimeUnit.NANOSECONDS
- .sleep((fsTimestampResolution.toNanos() + 1) * 11 / 10);
+ long timestampResolution = fileStoreAttributeCache
+ .getFsTimestampResolution().toNanos();
+ while (isRacyClean(Instant.now())) {
+ TimeUnit.NANOSECONDS.sleep(timestampResolution);
}
}
@@ -319,7 +415,8 @@ public class FileSnapshot {
*/
@SuppressWarnings("NonOverridingEquals")
public boolean equals(FileSnapshot other) {
- return lastModified == other.lastModified && size == other.size
+ boolean sizeEq = size == UNKNOWN_SIZE || other.size == UNKNOWN_SIZE || size == other.size;
+ return lastModified.equals(other.lastModified) && sizeEq
&& Objects.equals(fileKey, other.fileKey);
}
@@ -342,8 +439,7 @@ public class FileSnapshot {
/** {@inheritDoc} */
@Override
public int hashCode() {
- return Objects.hash(Long.valueOf(lastModified), Long.valueOf(size),
- fileKey);
+ return Objects.hash(lastModified, Long.valueOf(size), fileKey);
}
/**
@@ -378,6 +474,22 @@ public class FileSnapshot {
return wasRacyClean;
}
+ /**
+ * @return the delta in nanoseconds between lastModified and lastRead during
+ * last racy check
+ */
+ public long lastDelta() {
+ return delta;
+ }
+
+ /**
+ * @return the racyLimitNanos threshold in nanoseconds during last racy
+ * check
+ */
+ public long lastRacyThreshold() {
+ return racyThreshold;
+ }
+
/** {@inheritDoc} */
@SuppressWarnings({ "nls", "ReferenceEquality" })
@Override
@@ -388,24 +500,46 @@ public class FileSnapshot {
if (this == MISSING_FILE) {
return "MISSING_FILE";
}
- DateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS",
- Locale.US);
- return "FileSnapshot[modified: " + f.format(new Date(lastModified))
- + ", read: " + f.format(new Date(lastRead)) + ", size:" + size
+ return "FileSnapshot[modified: " + dateFmt.format(lastModified)
+ + ", read: " + dateFmt.format(lastRead) + ", size:" + size
+ ", fileKey: " + fileKey + "]";
}
- private boolean isRacyClean(long read) {
- // add a 10% safety margin
- long racyNanos = (fsTimestampResolution.toNanos() + 1) * 11 / 10;
- return wasRacyClean = (read - lastModified) * 1_000_000 <= racyNanos;
+ private boolean isRacyClean(Instant read) {
+ racyThreshold = getEffectiveRacyThreshold();
+ delta = Duration.between(lastModified, read).toNanos();
+ wasRacyClean = delta <= racyThreshold;
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(
+ "file={}, isRacyClean={}, read={}, lastModified={}, delta={} ns, racy<={} ns", //$NON-NLS-1$
+ file, Boolean.valueOf(wasRacyClean), dateFmt.format(read),
+ dateFmt.format(lastModified), Long.valueOf(delta),
+ Long.valueOf(racyThreshold));
+ }
+ return wasRacyClean;
+ }
+
+ private long getEffectiveRacyThreshold() {
+ long timestampResolution = fileStoreAttributeCache
+ .getFsTimestampResolution().toNanos();
+ long minRacyInterval = fileStoreAttributeCache.getMinimalRacyInterval()
+ .toNanos();
+ long max = Math.max(timestampResolution, minRacyInterval);
+ // safety margin: factor 2.5 below 100ms otherwise 1.25
+ return max < 100_000_000L ? max * 5 / 2 : max * 5 / 4;
}
- private boolean isModified(long currLastModified) {
+ private boolean isModified(Instant currLastModified) {
// Any difference indicates the path was modified.
- lastModifiedChanged = lastModified != currLastModified;
+ lastModifiedChanged = !lastModified.equals(currLastModified);
if (lastModifiedChanged) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(
+ "file={}, lastModified changed from {} to {}", //$NON-NLS-1$
+ file, dateFmt.format(lastModified),
+ dateFmt.format(currLastModified));
+ }
return true;
}
@@ -413,26 +547,40 @@ public class FileSnapshot {
// after the last modification that any new modifications
// are certain to change the last modified time.
if (cannotBeRacilyClean) {
+ LOG.debug("file={}, cannot be racily clean", file); //$NON-NLS-1$
return false;
}
if (!isRacyClean(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.
+ LOG.debug("file={}, is unmodified", file); //$NON-NLS-1$
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.
+ LOG.debug("file={}, is racily clean", file); //$NON-NLS-1$
return true;
}
private boolean isFileKeyChanged(Object currFileKey) {
- return currFileKey != MISSING_FILEKEY && !currFileKey.equals(fileKey);
+ boolean changed = currFileKey != MISSING_FILEKEY
+ && !currFileKey.equals(fileKey);
+ if (changed) {
+ LOG.debug("file={}, FileKey changed from {} to {}", //$NON-NLS-1$
+ file, fileKey, currFileKey);
+ }
+ return changed;
}
private boolean isSizeChanged(long currSize) {
- return currSize != UNKNOWN_SIZE && currSize != size;
+ boolean changed = (currSize != UNKNOWN_SIZE) && (currSize != size);
+ if (changed) {
+ LOG.debug("file={}, size changed from {} to {} bytes", //$NON-NLS-1$
+ file, Long.valueOf(size), Long.valueOf(currSize));
+ }
+ return changed;
}
}

Back to the top