FileSnapshot.java

  1. /*
  2.  * Copyright (C) 2010, Google Inc. and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.internal.storage.file;

  11. import static org.eclipse.jgit.util.FS.FileStoreAttributes.FALLBACK_FILESTORE_ATTRIBUTES;
  12. import static org.eclipse.jgit.util.FS.FileStoreAttributes.FALLBACK_TIMESTAMP_RESOLUTION;

  13. import java.io.File;
  14. import java.io.IOException;
  15. import java.nio.file.NoSuchFileException;
  16. import java.nio.file.attribute.BasicFileAttributes;
  17. import java.time.Duration;
  18. import java.time.Instant;
  19. import java.time.ZoneId;
  20. import java.time.format.DateTimeFormatter;
  21. import java.util.Locale;
  22. import java.util.Objects;
  23. import java.util.concurrent.TimeUnit;

  24. import org.eclipse.jgit.annotations.NonNull;
  25. import org.eclipse.jgit.util.FS;
  26. import org.eclipse.jgit.util.FS.FileStoreAttributes;
  27. import org.slf4j.Logger;
  28. import org.slf4j.LoggerFactory;

  29. /**
  30.  * Caches when a file was last read, making it possible to detect future edits.
  31.  * <p>
  32.  * This object tracks the last modified time of a file. Later during an
  33.  * invocation of {@link #isModified(File)} the object will return true if the
  34.  * file may have been modified and should be re-read from disk.
  35.  * <p>
  36.  * A snapshot does not "live update" when the underlying filesystem changes.
  37.  * Callers must poll for updates by periodically invoking
  38.  * {@link #isModified(File)}.
  39.  * <p>
  40.  * To work around the "racy git" problem (where a file may be modified multiple
  41.  * times within the granularity of the filesystem modification clock) this class
  42.  * may return true from isModified(File) if the last modification time of the
  43.  * file is less than 3 seconds ago.
  44.  */
  45. public class FileSnapshot {
  46.     private static final Logger LOG = LoggerFactory
  47.             .getLogger(FileSnapshot.class);
  48.     /**
  49.      * An unknown file size.
  50.      *
  51.      * This value is used when a comparison needs to happen purely on the lastUpdate.
  52.      */
  53.     public static final long UNKNOWN_SIZE = -1;

  54.     private static final Instant UNKNOWN_TIME = Instant.ofEpochMilli(-1);

  55.     private static final Object MISSING_FILEKEY = new Object();

  56.     private static final DateTimeFormatter dateFmt = DateTimeFormatter
  57.             .ofPattern("yyyy-MM-dd HH:mm:ss.nnnnnnnnn") //$NON-NLS-1$
  58.             .withLocale(Locale.getDefault()).withZone(ZoneId.systemDefault());

  59.     /**
  60.      * A FileSnapshot that is considered to always be modified.
  61.      * <p>
  62.      * This instance is useful for application code that wants to lazily read a
  63.      * file, but only after {@link #isModified(File)} gets invoked. The returned
  64.      * snapshot contains only invalid status information.
  65.      */
  66.     public static final FileSnapshot DIRTY = new FileSnapshot(UNKNOWN_TIME,
  67.             UNKNOWN_TIME, UNKNOWN_SIZE, Duration.ZERO, MISSING_FILEKEY);

  68.     /**
  69.      * A FileSnapshot that is clean if the file does not exist.
  70.      * <p>
  71.      * This instance is useful if the application wants to consider a missing
  72.      * file to be clean. {@link #isModified(File)} will return false if the file
  73.      * path does not exist.
  74.      */
  75.     public static final FileSnapshot MISSING_FILE = new FileSnapshot(
  76.             Instant.EPOCH, Instant.EPOCH, 0, Duration.ZERO, MISSING_FILEKEY) {
  77.         @Override
  78.         public boolean isModified(File path) {
  79.             return FS.DETECTED.exists(path);
  80.         }
  81.     };

  82.     /**
  83.      * Record a snapshot for a specific file path.
  84.      * <p>
  85.      * This method should be invoked before the file is accessed.
  86.      *
  87.      * @param path
  88.      *            the path to later remember. The path's current status
  89.      *            information is saved.
  90.      * @return the snapshot.
  91.      */
  92.     public static FileSnapshot save(File path) {
  93.         return new FileSnapshot(path);
  94.     }

  95.     /**
  96.      * Record a snapshot for a specific file path without using config file to
  97.      * get filesystem timestamp resolution.
  98.      * <p>
  99.      * This method should be invoked before the file is accessed. It is used by
  100.      * FileBasedConfig to avoid endless recursion.
  101.      *
  102.      * @param path
  103.      *            the path to later remember. The path's current status
  104.      *            information is saved.
  105.      * @return the snapshot.
  106.      */
  107.     public static FileSnapshot saveNoConfig(File path) {
  108.         return new FileSnapshot(path, false);
  109.     }

  110.     private static Object getFileKey(BasicFileAttributes fileAttributes) {
  111.         Object fileKey = fileAttributes.fileKey();
  112.         return fileKey == null ? MISSING_FILEKEY : fileKey;
  113.     }

  114.     /**
  115.      * Record a snapshot for a file for which the last modification time is
  116.      * already known.
  117.      * <p>
  118.      * This method should be invoked before the file is accessed.
  119.      * <p>
  120.      * Note that this method cannot rely on measuring file timestamp resolution
  121.      * to avoid racy git issues caused by finite file timestamp resolution since
  122.      * it's unknown in which filesystem the file is located. Hence the worst
  123.      * case fallback for timestamp resolution is used.
  124.      *
  125.      * @param modified
  126.      *            the last modification time of the file
  127.      * @return the snapshot.
  128.      * @deprecated use {@link #save(Instant)} instead.
  129.      */
  130.     @Deprecated
  131.     public static FileSnapshot save(long modified) {
  132.         final Instant read = Instant.now();
  133.         return new FileSnapshot(read, Instant.ofEpochMilli(modified),
  134.                 UNKNOWN_SIZE, FALLBACK_TIMESTAMP_RESOLUTION, MISSING_FILEKEY);
  135.     }

  136.     /**
  137.      * Record a snapshot for a file for which the last modification time is
  138.      * already known.
  139.      * <p>
  140.      * This method should be invoked before the file is accessed.
  141.      * <p>
  142.      * Note that this method cannot rely on measuring file timestamp resolution
  143.      * to avoid racy git issues caused by finite file timestamp resolution since
  144.      * it's unknown in which filesystem the file is located. Hence the worst
  145.      * case fallback for timestamp resolution is used.
  146.      *
  147.      * @param modified
  148.      *            the last modification time of the file
  149.      * @return the snapshot.
  150.      */
  151.     public static FileSnapshot save(Instant modified) {
  152.         final Instant read = Instant.now();
  153.         return new FileSnapshot(read, modified, UNKNOWN_SIZE,
  154.                 FALLBACK_TIMESTAMP_RESOLUTION, MISSING_FILEKEY);
  155.     }

  156.     /** Last observed modification time of the path. */
  157.     private final Instant lastModified;

  158.     /** Last wall-clock time the path was read. */
  159.     private volatile Instant lastRead;

  160.     /** True once {@link #lastRead} is far later than {@link #lastModified}. */
  161.     private boolean cannotBeRacilyClean;

  162.     /** Underlying file-system size in bytes.
  163.      *
  164.      * When set to {@link #UNKNOWN_SIZE} the size is not considered for modification checks. */
  165.     private final long size;

  166.     /** measured FileStore attributes */
  167.     private FileStoreAttributes fileStoreAttributeCache;

  168.     /**
  169.      * if {@code true} read filesystem time resolution from configuration file
  170.      * otherwise use fallback resolution
  171.      */
  172.     private boolean useConfig;

  173.     /**
  174.      * Object that uniquely identifies the given file, or {@code
  175.      * null} if a file key is not available
  176.      */
  177.     private final Object fileKey;

  178.     private final File file;

  179.     /**
  180.      * Record a snapshot for a specific file path.
  181.      * <p>
  182.      * This method should be invoked before the file is accessed.
  183.      *
  184.      * @param file
  185.      *            the path to remember meta data for. The path's current status
  186.      *            information is saved.
  187.      */
  188.     protected FileSnapshot(File file) {
  189.         this(file, true);
  190.     }

  191.     /**
  192.      * Record a snapshot for a specific file path.
  193.      * <p>
  194.      * This method should be invoked before the file is accessed.
  195.      *
  196.      * @param file
  197.      *            the path to remember meta data for. The path's current status
  198.      *            information is saved.
  199.      * @param useConfig
  200.      *            if {@code true} read filesystem time resolution from
  201.      *            configuration file otherwise use fallback resolution
  202.      */
  203.     protected FileSnapshot(File file, boolean useConfig) {
  204.         this.file = file;
  205.         this.lastRead = Instant.now();
  206.         this.useConfig = useConfig;
  207.         BasicFileAttributes fileAttributes = null;
  208.         try {
  209.             fileAttributes = FS.DETECTED.fileAttributes(file);
  210.         } catch (NoSuchFileException e) {
  211.             this.lastModified = Instant.EPOCH;
  212.             this.size = 0L;
  213.             this.fileKey = MISSING_FILEKEY;
  214.             return;
  215.         } catch (IOException e) {
  216.             LOG.error(e.getMessage(), e);
  217.             this.lastModified = Instant.EPOCH;
  218.             this.size = 0L;
  219.             this.fileKey = MISSING_FILEKEY;
  220.             return;
  221.         }
  222.         this.lastModified = fileAttributes.lastModifiedTime().toInstant();
  223.         this.size = fileAttributes.size();
  224.         this.fileKey = getFileKey(fileAttributes);
  225.         if (LOG.isDebugEnabled()) {
  226.             LOG.debug("file={}, create new FileSnapshot: lastRead={}, lastModified={}, size={}, fileKey={}", //$NON-NLS-1$
  227.                     file, dateFmt.format(lastRead),
  228.                     dateFmt.format(lastModified), Long.valueOf(size),
  229.                     fileKey.toString());
  230.         }
  231.     }

  232.     private boolean sizeChanged;

  233.     private boolean fileKeyChanged;

  234.     private boolean lastModifiedChanged;

  235.     private boolean wasRacyClean;

  236.     private long delta;

  237.     private long racyThreshold;

  238.     private FileSnapshot(Instant read, Instant modified, long size,
  239.             @NonNull Duration fsTimestampResolution, @NonNull Object fileKey) {
  240.         this.file = null;
  241.         this.lastRead = read;
  242.         this.lastModified = modified;
  243.         this.fileStoreAttributeCache = new FileStoreAttributes(
  244.                 fsTimestampResolution);
  245.         this.size = size;
  246.         this.fileKey = fileKey;
  247.     }

  248.     /**
  249.      * Get time of last snapshot update
  250.      *
  251.      * @return time of last snapshot update
  252.      * @deprecated use {@link #lastModifiedInstant()} instead
  253.      */
  254.     @Deprecated
  255.     public long lastModified() {
  256.         return lastModified.toEpochMilli();
  257.     }

  258.     /**
  259.      * Get time of last snapshot update
  260.      *
  261.      * @return time of last snapshot update
  262.      */
  263.     public Instant lastModifiedInstant() {
  264.         return lastModified;
  265.     }

  266.     /**
  267.      * @return file size in bytes of last snapshot update
  268.      */
  269.     public long size() {
  270.         return size;
  271.     }

  272.     /**
  273.      * Check if the path may have been modified since the snapshot was saved.
  274.      *
  275.      * @param path
  276.      *            the path the snapshot describes.
  277.      * @return true if the path needs to be read again.
  278.      */
  279.     public boolean isModified(File path) {
  280.         Instant currLastModified;
  281.         long currSize;
  282.         Object currFileKey;
  283.         try {
  284.             BasicFileAttributes fileAttributes = FS.DETECTED.fileAttributes(path);
  285.             currLastModified = fileAttributes.lastModifiedTime().toInstant();
  286.             currSize = fileAttributes.size();
  287.             currFileKey = getFileKey(fileAttributes);
  288.         } catch (NoSuchFileException e) {
  289.             currLastModified = Instant.EPOCH;
  290.             currSize = 0L;
  291.             currFileKey = MISSING_FILEKEY;
  292.         } catch (IOException e) {
  293.             LOG.error(e.getMessage(), e);
  294.             currLastModified = Instant.EPOCH;
  295.             currSize = 0L;
  296.             currFileKey = MISSING_FILEKEY;
  297.         }
  298.         sizeChanged = isSizeChanged(currSize);
  299.         if (sizeChanged) {
  300.             return true;
  301.         }
  302.         fileKeyChanged = isFileKeyChanged(currFileKey);
  303.         if (fileKeyChanged) {
  304.             return true;
  305.         }
  306.         lastModifiedChanged = isModified(currLastModified);
  307.         if (lastModifiedChanged) {
  308.             return true;
  309.         }
  310.         return false;
  311.     }

  312.     /**
  313.      * Update this snapshot when the content hasn't changed.
  314.      * <p>
  315.      * If the caller gets true from {@link #isModified(File)}, re-reads the
  316.      * content, discovers the content is identical, and
  317.      * {@link #equals(FileSnapshot)} is true, it can use
  318.      * {@link #setClean(FileSnapshot)} to make a future
  319.      * {@link #isModified(File)} return false. The logic goes something like
  320.      * this:
  321.      *
  322.      * <pre>
  323.      * if (snapshot.isModified(path)) {
  324.      *  FileSnapshot other = FileSnapshot.save(path);
  325.      *  Content newContent = ...;
  326.      *  if (oldContent.equals(newContent) &amp;&amp; snapshot.equals(other))
  327.      *      snapshot.setClean(other);
  328.      * }
  329.      * </pre>
  330.      *
  331.      * @param other
  332.      *            the other snapshot.
  333.      */
  334.     public void setClean(FileSnapshot other) {
  335.         final Instant now = other.lastRead;
  336.         if (!isRacyClean(now)) {
  337.             cannotBeRacilyClean = true;
  338.         }
  339.         lastRead = now;
  340.     }

  341.     /**
  342.      * Wait until this snapshot's file can't be racy anymore
  343.      *
  344.      * @throws InterruptedException
  345.      *             if sleep was interrupted
  346.      */
  347.     public void waitUntilNotRacy() throws InterruptedException {
  348.         long timestampResolution = fileStoreAttributeCache()
  349.                 .getFsTimestampResolution().toNanos();
  350.         while (isRacyClean(Instant.now())) {
  351.             TimeUnit.NANOSECONDS.sleep(timestampResolution);
  352.         }
  353.     }

  354.     /**
  355.      * Compare two snapshots to see if they cache the same information.
  356.      *
  357.      * @param other
  358.      *            the other snapshot.
  359.      * @return true if the two snapshots share the same information.
  360.      */
  361.     @SuppressWarnings("NonOverridingEquals")
  362.     public boolean equals(FileSnapshot other) {
  363.         boolean sizeEq = size == UNKNOWN_SIZE || other.size == UNKNOWN_SIZE || size == other.size;
  364.         return lastModified.equals(other.lastModified) && sizeEq
  365.                 && Objects.equals(fileKey, other.fileKey);
  366.     }

  367.     /** {@inheritDoc} */
  368.     @Override
  369.     public boolean equals(Object obj) {
  370.         if (this == obj) {
  371.             return true;
  372.         }
  373.         if (obj == null) {
  374.             return false;
  375.         }
  376.         if (!(obj instanceof FileSnapshot)) {
  377.             return false;
  378.         }
  379.         FileSnapshot other = (FileSnapshot) obj;
  380.         return equals(other);
  381.     }

  382.     /** {@inheritDoc} */
  383.     @Override
  384.     public int hashCode() {
  385.         return Objects.hash(lastModified, Long.valueOf(size), fileKey);
  386.     }

  387.     /**
  388.      * @return {@code true} if FileSnapshot.isModified(File) found the file size
  389.      *         changed
  390.      */
  391.     boolean wasSizeChanged() {
  392.         return sizeChanged;
  393.     }

  394.     /**
  395.      * @return {@code true} if FileSnapshot.isModified(File) found the file key
  396.      *         changed
  397.      */
  398.     boolean wasFileKeyChanged() {
  399.         return fileKeyChanged;
  400.     }

  401.     /**
  402.      * @return {@code true} if FileSnapshot.isModified(File) found the file's
  403.      *         lastModified changed
  404.      */
  405.     boolean wasLastModifiedChanged() {
  406.         return lastModifiedChanged;
  407.     }

  408.     /**
  409.      * @return {@code true} if FileSnapshot.isModified(File) detected that
  410.      *         lastModified is racily clean
  411.      */
  412.     boolean wasLastModifiedRacilyClean() {
  413.         return wasRacyClean;
  414.     }

  415.     /**
  416.      * @return the delta in nanoseconds between lastModified and lastRead during
  417.      *         last racy check
  418.      */
  419.     public long lastDelta() {
  420.         return delta;
  421.     }

  422.     /**
  423.      * @return the racyLimitNanos threshold in nanoseconds during last racy
  424.      *         check
  425.      */
  426.     public long lastRacyThreshold() {
  427.         return racyThreshold;
  428.     }

  429.     /** {@inheritDoc} */
  430.     @SuppressWarnings({ "nls", "ReferenceEquality" })
  431.     @Override
  432.     public String toString() {
  433.         if (this == DIRTY) {
  434.             return "DIRTY";
  435.         }
  436.         if (this == MISSING_FILE) {
  437.             return "MISSING_FILE";
  438.         }
  439.         return "FileSnapshot[modified: " + dateFmt.format(lastModified)
  440.                 + ", read: " + dateFmt.format(lastRead) + ", size:" + size
  441.                 + ", fileKey: " + fileKey + "]";
  442.     }

  443.     private boolean isRacyClean(Instant read) {
  444.         racyThreshold = getEffectiveRacyThreshold();
  445.         delta = Duration.between(lastModified, read).toNanos();
  446.         wasRacyClean = delta <= racyThreshold;
  447.         if (LOG.isDebugEnabled()) {
  448.             LOG.debug(
  449.                     "file={}, isRacyClean={}, read={}, lastModified={}, delta={} ns, racy<={} ns", //$NON-NLS-1$
  450.                     file, Boolean.valueOf(wasRacyClean), dateFmt.format(read),
  451.                     dateFmt.format(lastModified), Long.valueOf(delta),
  452.                     Long.valueOf(racyThreshold));
  453.         }
  454.         return wasRacyClean;
  455.     }

  456.     private long getEffectiveRacyThreshold() {
  457.         long timestampResolution = fileStoreAttributeCache()
  458.                 .getFsTimestampResolution().toNanos();
  459.         long minRacyInterval = fileStoreAttributeCache()
  460.                 .getMinimalRacyInterval().toNanos();
  461.         long max = Math.max(timestampResolution, minRacyInterval);
  462.         // safety margin: factor 2.5 below 100ms otherwise 1.25
  463.         return max < 100_000_000L ? max * 5 / 2 : max * 5 / 4;
  464.     }

  465.     private boolean isModified(Instant currLastModified) {
  466.         // Any difference indicates the path was modified.

  467.         lastModifiedChanged = !lastModified.equals(currLastModified);
  468.         if (lastModifiedChanged) {
  469.             if (LOG.isDebugEnabled()) {
  470.                 LOG.debug(
  471.                         "file={}, lastModified changed from {} to {}", //$NON-NLS-1$
  472.                         file, dateFmt.format(lastModified),
  473.                         dateFmt.format(currLastModified));
  474.             }
  475.             return true;
  476.         }

  477.         // We have already determined the last read was far enough
  478.         // after the last modification that any new modifications
  479.         // are certain to change the last modified time.
  480.         if (cannotBeRacilyClean) {
  481.             LOG.debug("file={}, cannot be racily clean", file); //$NON-NLS-1$
  482.             return false;
  483.         }
  484.         if (!isRacyClean(lastRead)) {
  485.             // Our last read should have marked cannotBeRacilyClean,
  486.             // but this thread may not have seen the change. The read
  487.             // of the volatile field lastRead should have fixed that.
  488.             LOG.debug("file={}, is unmodified", file); //$NON-NLS-1$
  489.             return false;
  490.         }

  491.         // We last read this path too close to its last observed
  492.         // modification time. We may have missed a modification.
  493.         // Scan again, to ensure we still see the same state.
  494.         LOG.debug("file={}, is racily clean", file); //$NON-NLS-1$
  495.         return true;
  496.     }

  497.     private boolean isFileKeyChanged(Object currFileKey) {
  498.         boolean changed = currFileKey != MISSING_FILEKEY
  499.                 && !currFileKey.equals(fileKey);
  500.         if (changed) {
  501.             LOG.debug("file={}, FileKey changed from {} to {}", //$NON-NLS-1$
  502.                     file, fileKey, currFileKey);
  503.         }
  504.         return changed;
  505.     }

  506.     private boolean isSizeChanged(long currSize) {
  507.         boolean changed = (currSize != UNKNOWN_SIZE) && (currSize != size);
  508.         if (changed) {
  509.             LOG.debug("file={}, size changed from {} to {} bytes", //$NON-NLS-1$
  510.                     file, Long.valueOf(size), Long.valueOf(currSize));
  511.         }
  512.         return changed;
  513.     }

  514.     private FileStoreAttributes fileStoreAttributeCache() {
  515.         if (fileStoreAttributeCache == null) {
  516.             fileStoreAttributeCache = useConfig
  517.                     ? FS.getFileStoreAttributes(file.toPath().getParent())
  518.                     : FALLBACK_FILESTORE_ATTRIBUTES;
  519.         }
  520.         return fileStoreAttributeCache;
  521.     }
  522. }