Browse Source

Optionally measure filesystem timestamp resolution asynchronously

In order to avoid blocking on the main thread during measurement
interactive applications like EGit may want to measure the filesystem
timestamp resolution asynchronously.

In order to enable measurement in the background call
FileStoreAttributeCache.setAsyncfileStoreAttrCache(true)
before the first access to cached FileStore attributes.

Bug: 548188
Change-Id: I8c9a2dbfc3f1d33441edea18b90e36b1dc0156c7
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
stable-5.1
Matthias Sohn 5 years ago
parent
commit
a950eac23b
  1. 6
      org.eclipse.jgit/.settings/.api_filters
  2. 181
      org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java

6
org.eclipse.jgit/.settings/.api_filters

@ -115,6 +115,12 @@
<message_argument value="fileAttributes(File)"/> <message_argument value="fileAttributes(File)"/>
</message_arguments> </message_arguments>
</filter> </filter>
<filter id="1142947843">
<message_arguments>
<message_argument value="5.1.9"/>
<message_argument value="setAsyncfileStoreAttrCache(boolean)"/>
</message_arguments>
</filter>
<filter id="1142947843"> <filter id="1142947843">
<message_arguments> <message_arguments>
<message_argument value="5.2.3"/> <message_argument value="5.2.3"/>

181
org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java

@ -55,7 +55,6 @@ import java.io.InputStreamReader;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.PrintStream; import java.io.PrintStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileStore; import java.nio.file.FileStore;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@ -71,12 +70,17 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.NonNull;
@ -197,74 +201,143 @@ public abstract class FS {
private static final Map<FileStore, FileStoreAttributeCache> attributeCache = new ConcurrentHashMap<>(); private static final Map<FileStore, FileStoreAttributeCache> attributeCache = new ConcurrentHashMap<>();
static Duration getFsTimestampResolution(Path file) { private static AtomicBoolean background = new AtomicBoolean();
try {
private static Map<FileStore, Lock> locks = new ConcurrentHashMap<>();
private static void setBackground(boolean async) {
background.set(async);
}
private static Duration getFsTimestampResolution(Path file) {
Path dir = Files.isDirectory(file) ? file : file.getParent(); Path dir = Files.isDirectory(file) ? file : file.getParent();
if (!dir.toFile().canWrite()) { FileStore s;
// can not determine FileStore of an unborn directory or in try {
// a read-only directory if (Files.exists(dir)) {
s = Files.getFileStore(dir);
FileStoreAttributeCache c = attributeCache.get(s);
if (c != null) {
return c.getFsTimestampResolution();
}
if (!Files.isWritable(dir)) {
// cannot measure resolution in a read-only directory
return FALLBACK_TIMESTAMP_RESOLUTION; return FALLBACK_TIMESTAMP_RESOLUTION;
} }
FileStore s = Files.getFileStore(dir); } else {
FileStoreAttributeCache c = attributeCache.get(s); // cannot determine FileStore of an unborn directory
if (c == null) { return FALLBACK_TIMESTAMP_RESOLUTION;
c = new FileStoreAttributeCache(s, dir); }
attributeCache.put(s, c); CompletableFuture<Optional<Duration>> f = CompletableFuture
.supplyAsync(() -> {
Lock lock = locks.computeIfAbsent(s,
l -> new ReentrantLock());
if (!lock.tryLock()) {
return Optional.empty();
}
Optional<Duration> resolution;
try {
// Some earlier future might have set the value
// and removed itself since we checked for the
// value above. Hence check cache again.
FileStoreAttributeCache c = attributeCache
.get(s);
if (c != null) {
return Optional
.of(c.getFsTimestampResolution());
}
resolution = measureFsTimestampResolution(s,
dir);
if (resolution.isPresent()) {
FileStoreAttributeCache cache = new FileStoreAttributeCache(
resolution.get());
attributeCache.put(s, cache);
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug(c.toString()); LOG.debug(cache.toString());
} }
} }
return c.getFsTimestampResolution(); } finally {
lock.unlock();
} catch (IOException | InterruptedException e) { locks.remove(s);
LOG.warn(e.getMessage(), e);
return FALLBACK_TIMESTAMP_RESOLUTION;
} }
return resolution;
});
// 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,
TimeUnit.MILLISECONDS);
if (d.isPresent()) {
return d.get();
}
// return fallback until measurement is finished
} catch (IOException | InterruptedException
| ExecutionException e) {
LOG.error(e.getMessage(), e);
} catch (TimeoutException e) {
// use fallback
} }
return FALLBACK_TIMESTAMP_RESOLUTION;
private Duration fsTimestampResolution;
Duration getFsTimestampResolution() {
return fsTimestampResolution;
} }
private FileStoreAttributeCache(FileStore s, Path dir) private static Optional<Duration> measureFsTimestampResolution(
throws IOException, InterruptedException { FileStore s, Path dir) {
Path probe = dir.resolve(".probe-" + UUID.randomUUID()); //$NON-NLS-1$ Path probe = dir.resolve(".probe-" + UUID.randomUUID()); //$NON-NLS-1$
Files.createFile(probe);
try { try {
Files.createFile(probe);
long wait = 512;
long start = System.nanoTime(); long start = System.nanoTime();
FileTime startTime = Files.getLastModifiedTime(probe); FileTime t1 = Files.getLastModifiedTime(probe);
FileTime actTime = startTime; FileTime t2 = t1;
long sleepTime = 512; while (t2.compareTo(t1) <= 0) {
while (actTime.compareTo(startTime) <= 0) { TimeUnit.NANOSECONDS.sleep(wait);
TimeUnit.NANOSECONDS.sleep(sleepTime); checkTimeout(s, start);
if (timeout(start)) {
LOG.warn(MessageFormat.format(JGitText
.get().timeoutMeasureFsTimestampResolution,
s.toString()));
fsTimestampResolution = FALLBACK_TIMESTAMP_RESOLUTION;
return;
}
FileUtils.touch(probe); FileUtils.touch(probe);
actTime = Files.getLastModifiedTime(probe); t2 = Files.getLastModifiedTime(probe);
// limit sleep time to max. 100ms if (wait < 100_000_000L) {
if (sleepTime < 100_000_000L) { wait = wait * 2;
sleepTime = sleepTime * 2;
} }
} }
fsTimestampResolution = Duration.between(startTime.toInstant(), return Optional
actTime.toInstant()); .of(Duration.between(t1.toInstant(), t2.toInstant()));
} catch (AccessDeniedException e) { } catch (IOException | TimeoutException e) {
LOG.error(e.getLocalizedMessage(), e); LOG.error(e.getLocalizedMessage(), e);
} catch (InterruptedException e) {
LOG.error(e.getLocalizedMessage(), e);
Thread.currentThread().interrupt();
} finally { } finally {
deleteProbe(probe);
}
return Optional.empty();
}
private static void checkTimeout(FileStore s, long start)
throws TimeoutException {
if (System.nanoTime() - start >= FALLBACK_TIMESTAMP_RESOLUTION
.toNanos()) {
throw new TimeoutException(MessageFormat.format(JGitText
.get().timeoutMeasureFsTimestampResolution,
s.toString()));
}
}
private static void deleteProbe(Path probe) {
if (Files.exists(probe)) {
try {
Files.delete(probe); Files.delete(probe);
} catch (IOException e) {
LOG.error(e.getLocalizedMessage(), e);
}
} }
} }
private static boolean timeout(long start) { private final @NonNull Duration fsTimestampResolution;
return System.nanoTime() - start >= FALLBACK_TIMESTAMP_RESOLUTION
.toNanos(); @NonNull
Duration getFsTimestampResolution() {
return fsTimestampResolution;
}
private FileStoreAttributeCache(
@NonNull Duration fsTimestampResolution) {
this.fsTimestampResolution = fsTimestampResolution;
} }
@SuppressWarnings("nls") @SuppressWarnings("nls")
@ -292,6 +365,20 @@ public abstract class FS {
return detect(null); return detect(null);
} }
/**
* Whether FileStore attribute cache entries should be determined
* asynchronously
*
* @param asynch
* whether FileStore attribute cache entries should be determined
* asynchronously. If false access to cached attributes may block
* for some seconds for the first call per FileStore
* @since 5.1.9
*/
public static void setAsyncfileStoreAttrCache(boolean asynch) {
FileStoreAttributeCache.setBackground(asynch);
}
/** /**
* Auto-detect the appropriate file system abstraction, taking into account * Auto-detect the appropriate file system abstraction, taking into account
* the presence of a Cygwin installation on the system. Using jgit in * the presence of a Cygwin installation on the system. Using jgit in

Loading…
Cancel
Save