import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.FileStoreAttributeCache;
import org.eclipse.jgit.util.FileUtils;
import org.eclipse.jgit.util.Stats;
import org.eclipse.jgit.util.SystemReader;
private Path trash;
- private Duration fsTimerResolution;
+ private FileStoreAttributeCache fsAttrCache;
@Before
public void setUp() throws Exception {
trash = Files.createTempDirectory("tmp_");
// measure timer resolution before the test to avoid time critical tests
// are affected by time needed for measurement
- fsTimerResolution = FS.getFsTimerResolution(trash.getParent());
+ fsAttrCache = FS
+ .getFileStoreAttributeCache(trash.getParent());
}
@Before
// if filesystem timestamp resolution is high the snapshot won't be
// racily clean
Assume.assumeTrue(
- fsTimerResolution.compareTo(Duration.ofMillis(10)) > 0);
+ fsAttrCache.getFsTimestampResolution()
+ .compareTo(Duration.ofMillis(10)) > 0);
Path f1 = createFile("newfile");
waitNextTick(f1);
FileSnapshot save = FileSnapshot.save(f1.toFile());
- TimeUnit.NANOSECONDS.sleep(fsTimerResolution.dividedBy(2).toNanos());
+ TimeUnit.NANOSECONDS.sleep(
+ fsAttrCache.getFsTimestampResolution().dividedBy(2).toNanos());
assertTrue(save.isModified(f1.toFile()));
}
// if filesystem timestamp resolution is high the snapshot won't be
// racily clean
Assume.assumeTrue(
- fsTimerResolution.compareTo(Duration.ofMillis(10)) > 0);
+ fsAttrCache.getFsTimestampResolution()
+ .compareTo(Duration.ofMillis(10)) > 0);
Path f1 = createFile("newfile");
FileSnapshot save = FileSnapshot.save(f1.toFile());
assertTrue(save.isModified(f1.toFile()));
write(f, "b");
if (!snapshot.isModified(f)) {
deltas.add(snapshot.lastDelta());
- racyNanos = snapshot.lastRacyNanos();
+ racyNanos = snapshot.lastRacyThreshold();
failures++;
}
assertEquals("file should contain 'b'", "b", read(f));
LOG.debug(String.format("%,d", d));
}
LOG.error(
- "count, failures, racy limit [ns], delta min [ns],"
+ "count, failures, eff. racy threshold [ns], delta min [ns],"
+ " delta max [ns], delta avg [ns],"
+ " delta stddev [ns]");
LOG.error(String.format(
stats.avg(), stats.stddev()));
}
assertTrue(
- "FileSnapshot: number of failures to detect file modifications should be 0",
+ String.format(
+ "FileSnapshot: failures to detect file modifications"
+ + " %d out of %d\n"
+ + "timestamp resolution %d µs"
+ + " min racy threshold %d µs"
+ , failures, COUNT,
+ fsAttrCache.getFsTimestampResolution().toNanos() / 1000,
+ fsAttrCache.getMinimalRacyInterval().toNanos() / 1000),
failures == 0);
}
package org.eclipse.jgit.internal.storage.file;
-import static org.eclipse.jgit.lib.Constants.FALLBACK_TIMESTAMP_RESOLUTION;
+import static org.eclipse.jgit.util.FS.FileStoreAttributeCache.FALLBACK_FILESTORE_ATTRIBUTES;
import java.io.File;
import java.io.IOException;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.FileStoreAttributeCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
* 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 FileStoreAttributeCache fileStoreAttributeCache;
/**
* Object that uniquely identifies the given file, or {@code
protected FileSnapshot(File file, boolean useConfig) {
this.file = file;
this.lastRead = Instant.now();
- this.fsTimestampResolution = useConfig
- ? FS.getFsTimerResolution(file.toPath().getParent())
- : FALLBACK_TIMESTAMP_RESOLUTION;
+ this.fileStoreAttributeCache = useConfig
+ ? FS.getFileStoreAttributeCache(file.toPath().getParent())
+ : FALLBACK_FILESTORE_ATTRIBUTES;
BasicFileAttributes fileAttributes = null;
try {
fileAttributes = FS.DETECTED.fileAttributes(file);
private long delta;
- private long racyNanos;
+ 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 FileStoreAttributeCache(
+ fsTimestampResolution);
this.size = size;
this.fileKey = fileKey;
}
* if sleep was interrupted
*/
public void waitUntilNotRacy() throws InterruptedException {
+ long timestampResolution = fileStoreAttributeCache
+ .getFsTimestampResolution().toNanos();
while (isRacyClean(Instant.now())) {
- TimeUnit.NANOSECONDS
- .sleep((fsTimestampResolution.toNanos() + 1) * 11 / 10);
+ TimeUnit.NANOSECONDS.sleep(timestampResolution);
}
}
* @return the delta in nanoseconds between lastModified and lastRead during
* last racy check
*/
- long lastDelta() {
+ public long lastDelta() {
return delta;
}
/**
- * @return the racyNanos threshold in nanoseconds during last racy check
+ * @return the racyLimitNanos threshold in nanoseconds during last racy
+ * check
*/
- long lastRacyNanos() {
- return racyNanos;
+ public long lastRacyThreshold() {
+ return racyThreshold;
}
/** {@inheritDoc} */
}
private boolean isRacyClean(Instant read) {
- // add a 10% safety margin
- racyNanos = (fsTimestampResolution.toNanos() + 1) * 11 / 10;
+ racyThreshold = getEffectiveRacyThreshold();
delta = Duration.between(lastModified, read).toNanos();
- wasRacyClean = delta <= racyNanos;
+ 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(racyNanos));
+ Long.valueOf(racyThreshold));
}
return wasRacyClean;
}
+ private long getEffectiveRacyThreshold() {
+ long timestampResolution = fileStoreAttributeCache
+ .getFsTimestampResolution().toNanos();
+ long minRacyInterval = fileStoreAttributeCache.getMinimalRacyInterval()
+ .toNanos();
+ // add a 30% safety margin
+ return Math.max(timestampResolution, minRacyInterval) * 13 / 10;
+ }
+
private boolean isModified(Instant currLastModified) {
// Any difference indicates the path was modified.
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.time.Instant.EPOCH;
-import static org.eclipse.jgit.lib.Constants.FALLBACK_TIMESTAMP_RESOLUTION;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
+import java.io.OutputStreamWriter;
import java.io.PrintStream;
+import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileStore;
import java.text.MessageFormat;
import java.time.Duration;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.LockFailedException;
import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.file.FileSnapshot;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
}
}
- private static final class FileStoreAttributeCache {
+ /**
+ * Attributes of FileStores on this system
+ *
+ * @since 5.1.9
+ */
+ public final static class FileStoreAttributeCache {
private static final Duration UNDEFINED_RESOLUTION = Duration
.ofNanos(Long.MAX_VALUE);
+ /**
+ * Fallback FileStore attributes used when we can't measure the
+ * filesystem timestamp resolution. The last modified time granularity
+ * of FAT filesystems is 2 seconds.
+ */
+ public static final FileStoreAttributeCache FALLBACK_FILESTORE_ATTRIBUTES = new FileStoreAttributeCache(
+ Duration.ofMillis(2000));
+
private static final Map<FileStore, FileStoreAttributeCache> attributeCache = new ConcurrentHashMap<>();
private static AtomicBoolean background = new AtomicBoolean();
}
private static final String javaVersionPrefix = System
- .getProperty("java.vm.vendor") + '|' //$NON-NLS-1$
- + System.getProperty("java.vm.version") + '|'; //$NON-NLS-1$
+ .getProperty("java.vendor") + '|' //$NON-NLS-1$
+ + System.getProperty("java.version") + '|'; //$NON-NLS-1$
+
+ private static final Duration FALLBACK_MIN_RACY_INTERVAL = Duration
+ .ofMillis(10);
- private static Duration getFsTimestampResolution(Path file) {
- file = file.toAbsolutePath();
- Path dir = Files.isDirectory(file) ? file : file.getParent();
+ /**
+ * @param path
+ * file residing in the FileStore to get attributes for
+ * @return FileStoreAttributeCache entry for the given path.
+ */
+ public static FileStoreAttributeCache get(Path path) {
+ path = path.toAbsolutePath();
+ Path dir = Files.isDirectory(path) ? path : path.getParent();
+ return getFileAttributeCache(dir);
+ }
+
+ private static FileStoreAttributeCache getFileAttributeCache(Path dir) {
FileStore s;
try {
if (Files.exists(dir)) {
s = Files.getFileStore(dir);
FileStoreAttributeCache c = attributeCache.get(s);
if (c != null) {
- return c.getFsTimestampResolution();
+ return c;
}
if (!Files.isWritable(dir)) {
// cannot measure resolution in a read-only directory
- return FALLBACK_TIMESTAMP_RESOLUTION;
+ LOG.debug(
+ "{}: cannot measure timestamp resolution in read-only directory {}", //$NON-NLS-1$
+ Thread.currentThread(), dir);
+ return FALLBACK_FILESTORE_ATTRIBUTES;
}
} else {
// cannot determine FileStore of an unborn directory
- return FALLBACK_TIMESTAMP_RESOLUTION;
+ LOG.debug(
+ "{}: cannot measure timestamp resolution of unborn directory {}", //$NON-NLS-1$
+ Thread.currentThread(), dir);
+ return FALLBACK_FILESTORE_ATTRIBUTES;
}
- CompletableFuture<Optional<Duration>> f = CompletableFuture
+ CompletableFuture<Optional<FileStoreAttributeCache>> f = CompletableFuture
.supplyAsync(() -> {
Lock lock = locks.computeIfAbsent(s,
l -> new ReentrantLock());
if (!lock.tryLock()) {
+ LOG.debug(
+ "{}: couldn't get lock to measure timestamp resolution in {}", //$NON-NLS-1$
+ Thread.currentThread(), dir);
return Optional.empty();
}
- Optional<Duration> resolution;
+ Optional<FileStoreAttributeCache> cache = Optional
+ .empty();
try {
// Some earlier future might have set the value
// and removed itself since we checked for the
FileStoreAttributeCache c = attributeCache
.get(s);
if (c != null) {
- return Optional
- .of(c.getFsTimestampResolution());
+ return Optional.of(c);
}
- resolution = measureFsTimestampResolution(s,
- dir);
+ Optional<Duration> resolution = measureFsTimestampResolution(
+ s, dir);
if (resolution.isPresent()) {
- FileStoreAttributeCache cache = new FileStoreAttributeCache(
+ c = new FileStoreAttributeCache(
resolution.get());
- attributeCache.put(s, cache);
+ attributeCache.put(s, c);
+ // for high timestamp resolution measure
+ // minimal racy interval
+ if (c.fsTimestampResolution
+ .toNanos() < 100_000_000L) {
+ c.minimalRacyInterval = measureMinimalRacyInterval(
+ dir);
+ }
if (LOG.isDebugEnabled()) {
- LOG.debug(cache.toString());
+ LOG.debug(c.toString());
}
+ cache = Optional.of(c);
}
} finally {
lock.unlock();
locks.remove(s);
}
- return resolution;
+ return cache;
});
// even if measuring in background wait a little - if the result
// arrives, it's better than returning the large fallback
- Optional<Duration> d = f.get(background.get() ? 50 : 2000,
+ Optional<FileStoreAttributeCache> d = f.get(
+ background.get() ? 100 : 5000,
TimeUnit.MILLISECONDS);
if (d.isPresent()) {
return d.get();
} catch (TimeoutException | SecurityException e) {
// use fallback
}
- return FALLBACK_TIMESTAMP_RESOLUTION;
+ LOG.debug("{}: use fallback timestamp resolution for directory {}", //$NON-NLS-1$
+ Thread.currentThread(), dir);
+ return FALLBACK_FILESTORE_ATTRIBUTES;
+ }
+
+ @SuppressWarnings("boxing")
+ private static Duration measureMinimalRacyInterval(Path dir) {
+ LOG.debug("{}: start measure minimal racy interval in {}", //$NON-NLS-1$
+ Thread.currentThread(), dir);
+ int failures = 0;
+ long racyNanos = 0;
+ final int COUNT = 1000;
+ ArrayList<Long> deltas = new ArrayList<>();
+ Path probe = dir.resolve(".probe-" + UUID.randomUUID()); //$NON-NLS-1$
+ try {
+ Files.createFile(probe);
+ for (int i = 0; i < COUNT; i++) {
+ write(probe, "a"); //$NON-NLS-1$
+ FileSnapshot snapshot = FileSnapshot.save(probe.toFile());
+ read(probe);
+ write(probe, "b"); //$NON-NLS-1$
+ if (!snapshot.isModified(probe.toFile())) {
+ deltas.add(Long.valueOf(snapshot.lastDelta()));
+ racyNanos = snapshot.lastRacyThreshold();
+ failures++;
+ }
+ }
+ } catch (IOException e) {
+ LOG.error(e.getMessage(), e);
+ return FALLBACK_MIN_RACY_INTERVAL;
+ } finally {
+ deleteProbe(probe);
+ }
+ if (failures > 0) {
+ Stats stats = new Stats();
+ for (Long d : deltas) {
+ stats.add(d);
+ }
+ LOG.debug(
+ "delta [ns] since modification FileSnapshot failed to detect\n" //$NON-NLS-1$
+ + "count, failures, racy limit [ns], delta min [ns]," //$NON-NLS-1$
+ + " delta max [ns], delta avg [ns]," //$NON-NLS-1$
+ + " delta stddev [ns]\n" //$NON-NLS-1$
+ + "{}, {}, {}, {}, {}, {}, {}", //$NON-NLS-1$
+ COUNT, failures, racyNanos, stats.min(), stats.max(),
+ stats.avg(), stats.stddev());
+ return Duration
+ .ofNanos(Double.valueOf(stats.max()).longValue());
+ }
+ // since no failures occurred using the measured filesystem
+ // timestamp resolution there is no need for minimal racy interval
+ LOG.debug("{}: no failures when measuring minimal racy interval", //$NON-NLS-1$
+ Thread.currentThread());
+ return Duration.ZERO;
+ }
+
+ private static void write(Path p, String body) throws IOException {
+ FileUtils.mkdirs(p.getParent().toFile(), true);
+ try (Writer w = new OutputStreamWriter(Files.newOutputStream(p),
+ UTF_8)) {
+ w.write(body);
+ }
+ }
+
+ private static String read(Path p) throws IOException {
+ final byte[] body = IO.readFully(p.toFile());
+ return new String(body, 0, body.length, UTF_8);
}
private static Optional<Duration> measureFsTimestampResolution(
FileStore s, Path dir) {
+ LOG.debug("{}: start measure timestamp resolution {} in {}", //$NON-NLS-1$
+ Thread.currentThread(), s, dir);
Duration configured = readFileTimeResolution(s);
if (!UNDEFINED_RESOLUTION.equals(configured)) {
return Optional.of(configured);
Duration clockResolution = measureClockResolution();
fsResolution = fsResolution.plus(clockResolution);
saveFileTimeResolution(s, fsResolution);
+ LOG.debug("{}: end measure timestamp resolution {} in {}", //$NON-NLS-1$
+ Thread.currentThread(), s, dir);
return Optional.of(fsResolution);
} catch (AccessDeniedException e) {
LOG.warn(e.getLocalizedMessage(), e); // see bug 548648
private final @NonNull Duration fsTimestampResolution;
+ private Duration minimalRacyInterval;
+
+ /**
+ * @return the measured minimal interval after a file has been modified
+ * in which we cannot rely on lastModified to detect
+ * modifications
+ */
+ public Duration getMinimalRacyInterval() {
+ return minimalRacyInterval;
+ }
+
+ /**
+ * @return the measured filesystem timestamp resolution
+ */
@NonNull
- Duration getFsTimestampResolution() {
+ public Duration getFsTimestampResolution() {
return fsTimestampResolution;
}
- private FileStoreAttributeCache(
+ /**
+ * Construct a FileStoreAttributeCache entry for the given filesystem
+ * timestamp resolution
+ *
+ * @param fsTimestampResolution
+ */
+ public FileStoreAttributeCache(
@NonNull Duration fsTimestampResolution) {
this.fsTimestampResolution = fsTimestampResolution;
+ this.minimalRacyInterval = Duration.ZERO;
}
- @SuppressWarnings("nls")
+ @SuppressWarnings({ "nls", "boxing" })
@Override
public String toString() {
- return "FileStoreAttributeCache [fsTimestampResolution="
- + fsTimestampResolution + "]";
+ return String.format(
+ "FileStoreAttributeCache[fsTimestampResolution=%,d µs, "
+ + "minimalRacyInterval=%,d µs]",
+ fsTimestampResolution.toNanos() / 1000,
+ minimalRacyInterval.toNanos() / 1000);
}
}
* the directory under which the probe file will be created to
* measure the timer resolution.
* @return measured filesystem timestamp resolution
- * @since 5.2.3
+ * @since 5.1.9
*/
- public static Duration getFsTimerResolution(@NonNull Path dir) {
- return FileStoreAttributeCache.getFsTimestampResolution(dir);
+ public static FileStoreAttributeCache getFileStoreAttributeCache(
+ @NonNull Path dir) {
+ return FileStoreAttributeCache.get(dir);
}
private volatile Holder<File> userHome;