diff options
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.java | 244 |
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; } } |