Measure minimum racy interval to auto-configure FileSnapshot
By running FileSnapshotTest#detectFileModified we found that the sum of measured filesystem timestamp resolution and measured clock resolution may yield a too small interval after a file has been modified which we need to consider racily clean. In our tests we didn't find this behavior on all systems we tested on, e.g. on MacOS using APFS and Java 8 and 11 this effect was not observed. On Linux (SLES 15, kernel 4.12.14-150.22-default) we collected the following test results using Java 8 and 11: In 23-98% of 10000 test runs (depending on filesystem type and Java version) the test failed, which means the effective interval which needs to be considered racily clean after a file was modified is larger than the measured file timestamp resolution. "delta" is the observed interval after a file has been modified but FileSnapshot did not yet detect the modification: "resolution" is the measured sum of file timestamp resolution and clock resolution seen in Java. Java version filesystem failures resolution min delta max delta 1.8.0_212-b04 btrfs 98.6% 1 ms 3.6 ms 6.6 ms 1.8.0_212-b04 ext4 82.6% 3 ms 1.1 ms 4.1 ms 1.8.0_212-b04 xfs 23.8% 4 ms 3.7 ms 3.9 ms 1.8.0_212-b04 zfs 23.1% 3 ms 4.8 ms 5.0 ms 11.0.3+7 btrfs 98.1% 3 us 0.7 ms 4.7 ms 11.0.3+7 ext4 98.1% 6 us 0.7 ms 4.7 ms 11.0.3+7 xfs 98.5% 7 us 0.1 ms 8.0 ms 11.0.3+7 zfs 98.4% 7 us 0.7 ms 5.2 ms Mac OS 1.8.0_212 APFS 0% 1 s 11.0.3+7 APFS 0% 6 us The observed delta is not distributed according to a normal gaussian distribution but rather random in the observed range between "min delta" and "max delta". Run this test after measuring file timestamp resolution in FS.FileAttributeCache to auto-configure JGit since it's unclear what mechanism is causing this effect. In FileSnapshot#isRacyClean use the maximum of the measured timestamp resolution and the measured "delta" as explained above to decide if a given FileSnapshot is to be considered racily clean. Add a 30% safety margin to ensure we are on the safe side. Change-Id: I1c8bb59f6486f174b7bbdc63072777ddbe06694d Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
This commit is contained in:
parent
902935c38c
commit
5911521ba6
|
@ -66,7 +66,7 @@ public class GitCloneTaskTest extends LocalDiskRepositoryTestCase {
|
|||
@Before
|
||||
public void before() throws IOException {
|
||||
dest = createTempFile();
|
||||
FS.getFsTimerResolution(dest.toPath().getParent());
|
||||
FS.getFileStoreAttributeCache(dest.toPath().getParent());
|
||||
project = new Project();
|
||||
project.init();
|
||||
enableLogging();
|
||||
|
|
|
@ -130,7 +130,7 @@ public void setUp() throws Exception {
|
|||
|
||||
// measure timer resolution before the test to avoid time critical tests
|
||||
// are affected by time needed for measurement
|
||||
FS.getFsTimerResolution(tmp.toPath().getParent());
|
||||
FS.getFileStoreAttributeCache(tmp.toPath().getParent());
|
||||
|
||||
mockSystemReader = new MockSystemReader();
|
||||
mockSystemReader.userGitConfig = new FileBasedConfig(new File(tmp,
|
||||
|
|
|
@ -378,7 +378,8 @@ public static Instant fsTick(File lastFile)
|
|||
tmp = File.createTempFile("fsTickTmpFile", null,
|
||||
lastFile.getParentFile());
|
||||
}
|
||||
long res = FS.getFsTimerResolution(tmp.toPath()).toNanos();
|
||||
long res = FS.getFileStoreAttributeCache(tmp.toPath())
|
||||
.getFsTimestampResolution().toNanos();
|
||||
long sleepTime = res / 10;
|
||||
try {
|
||||
Instant startTime = fs.lastModifiedInstant(lastFile);
|
||||
|
|
|
@ -123,7 +123,7 @@ public void setup() throws Exception {
|
|||
|
||||
// measure timer resolution before the test to avoid time critical tests
|
||||
// are affected by time needed for measurement
|
||||
FS.getFsTimerResolution(tmp.getParent());
|
||||
FS.getFileStoreAttributeCache(tmp.getParent());
|
||||
|
||||
server = new AppServer();
|
||||
ServletContextHandler app = server.addContext("/lfs");
|
||||
|
|
|
@ -8,4 +8,7 @@ log4j.appender.stdout.Target=System.out
|
|||
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
|
||||
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
|
||||
#log4j.appender.fileLogger.bufferedIO = true
|
||||
#log4j.appender.fileLogger.bufferSize = 1024
|
||||
#log4j.appender.fileLogger.bufferSize = 4096
|
||||
|
||||
#log4j.logger.org.eclipse.jgit.util.FS = DEBUG
|
||||
#log4j.logger.org.eclipse.jgit.internal.storage.file.FileSnapshot = DEBUG
|
||||
|
|
|
@ -62,6 +62,7 @@
|
|||
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;
|
||||
|
@ -78,14 +79,15 @@ public class FileSnapshotTest {
|
|||
|
||||
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
|
||||
|
@ -131,11 +133,13 @@ public void testNewFileWithWait() throws Exception {
|
|||
// 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()));
|
||||
}
|
||||
|
||||
|
@ -149,7 +153,8 @@ public void testNewFileNoWait() throws Exception {
|
|||
// 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()));
|
||||
|
@ -230,7 +235,7 @@ public void detectFileModified() throws IOException {
|
|||
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));
|
||||
|
@ -244,7 +249,7 @@ public void detectFileModified() throws IOException {
|
|||
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(
|
||||
|
@ -253,7 +258,14 @@ public void detectFileModified() throws IOException {
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ public class FileBasedConfigTest {
|
|||
@Before
|
||||
public void setUp() throws Exception {
|
||||
trash = Files.createTempDirectory("tmp_");
|
||||
FS.getFsTimerResolution(trash.getParent());
|
||||
FS.getFileStoreAttributeCache(trash.getParent());
|
||||
}
|
||||
|
||||
@After
|
||||
|
|
|
@ -203,7 +203,8 @@ public void testFsTimestampResolution() throws Exception {
|
|||
.ofPattern("uuuu-MMM-dd HH:mm:ss.nnnnnnnnn", Locale.ENGLISH)
|
||||
.withZone(ZoneId.systemDefault());
|
||||
Path dir = Files.createTempDirectory("probe-filesystem");
|
||||
Duration resolution = FS.getFsTimerResolution(dir);
|
||||
Duration resolution = FS.getFileStoreAttributeCache(dir)
|
||||
.getFsTimestampResolution();
|
||||
long resolutionNs = resolution.toNanos();
|
||||
assertTrue(resolutionNs > 0);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
|
|
|
@ -56,14 +56,6 @@
|
|||
</message_arguments>
|
||||
</filter>
|
||||
</resource>
|
||||
<resource path="src/org/eclipse/jgit/lib/Constants.java" type="org.eclipse.jgit.lib.Constants">
|
||||
<filter id="1142947843">
|
||||
<message_arguments>
|
||||
<message_argument value="5.1.9"/>
|
||||
<message_argument value="FALLBACK_TIMESTAMP_RESOLUTION"/>
|
||||
</message_arguments>
|
||||
</filter>
|
||||
</resource>
|
||||
<resource path="src/org/eclipse/jgit/lib/GitmoduleEntry.java" type="org.eclipse.jgit.lib.GitmoduleEntry">
|
||||
<filter id="1109393411">
|
||||
<message_arguments>
|
||||
|
@ -179,6 +171,12 @@
|
|||
<message_argument value="fileAttributes(File)"/>
|
||||
</message_arguments>
|
||||
</filter>
|
||||
<filter id="1142947843">
|
||||
<message_arguments>
|
||||
<message_argument value="5.1.9"/>
|
||||
<message_argument value="getFileStoreAttributeCache(Path)"/>
|
||||
</message_arguments>
|
||||
</filter>
|
||||
<filter id="1142947843">
|
||||
<message_arguments>
|
||||
<message_argument value="5.1.9"/>
|
||||
|
@ -203,12 +201,6 @@
|
|||
<message_argument value="setLastModified(Path, Instant)"/>
|
||||
</message_arguments>
|
||||
</filter>
|
||||
<filter id="1142947843">
|
||||
<message_arguments>
|
||||
<message_argument value="5.2.3"/>
|
||||
<message_argument value="getFsTimerResolution(Path)"/>
|
||||
</message_arguments>
|
||||
</filter>
|
||||
</resource>
|
||||
<resource path="src/org/eclipse/jgit/util/FS.java" type="org.eclipse.jgit.util.FS$Attributes">
|
||||
<filter id="1142947843">
|
||||
|
@ -218,6 +210,14 @@
|
|||
</message_arguments>
|
||||
</filter>
|
||||
</resource>
|
||||
<resource path="src/org/eclipse/jgit/util/FS.java" type="org.eclipse.jgit.util.FS$FileStoreAttributeCache">
|
||||
<filter id="1142947843">
|
||||
<message_arguments>
|
||||
<message_argument value="5.1.9"/>
|
||||
<message_argument value="FileStoreAttributeCache"/>
|
||||
</message_arguments>
|
||||
</filter>
|
||||
</resource>
|
||||
<resource path="src/org/eclipse/jgit/util/FileUtils.java" type="org.eclipse.jgit.util.FileUtils">
|
||||
<filter id="1142947843">
|
||||
<message_arguments>
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
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;
|
||||
|
@ -58,6 +58,7 @@
|
|||
|
||||
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;
|
||||
|
||||
|
@ -213,8 +214,8 @@ public static FileSnapshot save(Instant modified) {
|
|||
* 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
|
||||
|
@ -252,9 +253,9 @@ protected FileSnapshot(File file) {
|
|||
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);
|
||||
|
@ -285,14 +286,15 @@ protected FileSnapshot(File file, boolean useConfig) {
|
|||
|
||||
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;
|
||||
}
|
||||
|
@ -397,9 +399,10 @@ public void setClean(FileSnapshot other) {
|
|||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -474,15 +477,16 @@ boolean wasLastModifiedRacilyClean() {
|
|||
* @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} */
|
||||
|
@ -501,20 +505,28 @@ public String toString() {
|
|||
}
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
@ -52,7 +52,6 @@
|
|||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.MessageFormat;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.eclipse.jgit.errors.CorruptObjectException;
|
||||
import org.eclipse.jgit.internal.JGitText;
|
||||
|
@ -723,16 +722,6 @@ public static byte[] encode(String str) {
|
|||
*/
|
||||
public static final String LOCK_SUFFIX = ".lock"; //$NON-NLS-1$
|
||||
|
||||
/**
|
||||
* Fallback filesystem timestamp resolution used when we can't measure the
|
||||
* resolution. The last modified time granularity of FAT filesystems is 2
|
||||
* seconds.
|
||||
*
|
||||
* @since 5.1.9
|
||||
*/
|
||||
public static final Duration FALLBACK_TIMESTAMP_RESOLUTION = Duration
|
||||
.ofMillis(2000);
|
||||
|
||||
private Constants() {
|
||||
// Hide the default constructor
|
||||
}
|
||||
|
|
|
@ -45,7 +45,6 @@
|
|||
|
||||
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;
|
||||
|
@ -55,7 +54,9 @@
|
|||
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;
|
||||
|
@ -68,6 +69,7 @@
|
|||
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;
|
||||
|
@ -94,6 +96,7 @@
|
|||
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;
|
||||
|
@ -200,11 +203,24 @@ public int getRc() {
|
|||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
@ -216,36 +232,58 @@ private static void setBackground(boolean async) {
|
|||
}
|
||||
|
||||
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 Duration getFsTimestampResolution(Path file) {
|
||||
file = file.toAbsolutePath();
|
||||
Path dir = Files.isDirectory(file) ? file : file.getParent();
|
||||
private static final Duration FALLBACK_MIN_RACY_INTERVAL = Duration
|
||||
.ofMillis(10);
|
||||
|
||||
/**
|
||||
* @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
|
||||
|
@ -253,28 +291,36 @@ private static Duration getFsTimestampResolution(Path file) {
|
|||
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);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug(cache.toString());
|
||||
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(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();
|
||||
|
@ -286,11 +332,79 @@ private static Duration getFsTimestampResolution(Path file) {
|
|||
} 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);
|
||||
|
@ -310,6 +424,8 @@ private static Optional<Duration> measureFsTimestampResolution(
|
|||
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
|
||||
|
@ -424,21 +540,45 @@ private static void saveFileTimeResolution(FileStore s,
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -507,10 +647,11 @@ public static FS detect(Boolean cygwinUsed) {
|
|||
* 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;
|
||||
|
|
Loading…
Reference in New Issue