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.      * Object that uniquely identifies the given file, or {@code
  170.      * null} if a file key is not available
  171.      */
  172.     private final Object fileKey;

  173.     private final File file;

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

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

  229.     private boolean sizeChanged;

  230.     private boolean fileKeyChanged;

  231.     private boolean lastModifiedChanged;

  232.     private boolean wasRacyClean;

  233.     private long delta;

  234.     private long racyThreshold;

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

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

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

  263.     /**
  264.      * @return file size in bytes of last snapshot update
  265.      */
  266.     public long size() {
  267.         return size;
  268.     }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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