diff options
author | Thomas Wolf <thomas.wolf@paranor.ch> | 2022-03-15 19:12:46 +0100 |
---|---|---|
committer | Matthias Sohn <matthias.sohn@sap.com> | 2022-03-19 21:33:51 +0100 |
commit | 7b1c8cf147b5ecd8f77bf6d3d64c40acd643af83 (patch) | |
tree | 24a60e396cfcd507d1c0e1ae3daceb777f04c1fd /org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java | |
parent | ac78c175231979c7c5ab361980a233edee7626df (diff) | |
download | jgit-7b1c8cf147b5ecd8f77bf6d3d64c40acd643af83.tar.gz jgit-7b1c8cf147b5ecd8f77bf6d3d64c40acd643af83.zip |
Re-try reading a file when there are concurrent writes
Git and JGit are very careful to replace git files atomically when
writing. The normal mechanism for this is to write to a temporary
file and then to rename it atomically to the final destination. This
works fine on POSIX-compliant systems, but on systems where renaming
may not be atomic, exceptions may be thrown if code tries to read
the file while the rename is still ongoing. This happens in particular
on Windows, where the typical symptom is that a FileNotFoundException
with message "The process cannot access the file because it is being
used by another process" is thrown, but file.isFile() == true at the
same time.
In FileBasedConfig, a re-try was already implemented for this case.
But the same problem can also occur in other places, for instance
in RefDirectory when reading loose or packed refs. Additionally,
JGit has similar re-tries when a stale NFS file handle is detected,
but that mechanism wasn't used consistently (only for git configs
and packed refs, but not for loose refs).
Factor out the general re-try mechanism for reading into a new method
FileUtils.readWithRetry() and use that in all three places. The
re-try parameters are hardcoded: at most 5 times for stale NFS handles,
and at most 5 times with increasing backoff delays (50, 100, 200, 400,
and 800ms) for the above concurrent write case.
Bug: 579116
Change-Id: If0c2ad367446d3c0f32b509274cf8e814aca12cf
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java')
-rw-r--r-- | org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java | 94 |
1 files changed, 94 insertions, 0 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java index b9dd9baa61..f013e7e095 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java @@ -17,6 +17,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InterruptedIOException; import java.nio.channels.FileChannel; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.CopyOption; @@ -655,6 +656,99 @@ public class FileUtils { } /** + * Like a {@link java.util.function.Function} but throwing an + * {@link Exception}. + * + * @param <A> + * input type + * @param <B> + * output type + * @since 6.2 + */ + @FunctionalInterface + public interface IOFunction<A, B> { + + /** + * Performs the function. + * + * @param t + * input to operate on + * @return the output + * @throws Exception + * if a problem occurs + */ + B apply(A t) throws Exception; + } + + private static void backOff(long delay, IOException cause) + throws IOException { + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + IOException interruption = new InterruptedIOException(); + interruption.initCause(e); + interruption.addSuppressed(cause); + Thread.currentThread().interrupt(); // Re-set flag + throw interruption; + } + } + + /** + * Invokes the given {@link IOFunction}, performing a limited number of + * re-tries if exceptions occur that indicate either a stale NFS file handle + * or that indicate that the file may be written concurrently. + * + * @param <T> + * result type + * @param file + * to read + * @param reader + * for reading the file and creating an instance of {@code T} + * @return the result of the {@code reader}, or {@code null} if the file + * does not exist + * @throws Exception + * if a problem occurs + * @since 6.2 + */ + public static <T> T readWithRetries(File file, + IOFunction<File, ? extends T> reader) + throws Exception { + int maxStaleRetries = 5; + int retries = 0; + long backoff = 50; + while (true) { + try { + try { + return reader.apply(file); + } catch (IOException e) { + if (FileUtils.isStaleFileHandleInCausalChain(e) + && retries < maxStaleRetries) { + if (LOG.isDebugEnabled()) { + LOG.debug(MessageFormat.format( + JGitText.get().packedRefsHandleIsStale, + Integer.valueOf(retries)), e); + } + retries++; + continue; + } + throw e; + } + } catch (FileNotFoundException noFile) { + if (!file.isFile()) { + return null; + } + // Probably Windows and some other thread is writing the file + // concurrently. + if (backoff > 1000) { + throw noFile; + } + backOff(backoff, noFile); + backoff *= 2; // 50, 100, 200, 400, 800 ms + } + } + } + + /** * @param file * @return {@code true} if the passed file is a symbolic link */ |