You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

FileSnapshot.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. /*
  2. * Copyright (C) 2010, Google Inc.
  3. * and other copyright owners as documented in the project's IP log.
  4. *
  5. * This program and the accompanying materials are made available
  6. * under the terms of the Eclipse Distribution License v1.0 which
  7. * accompanies this distribution, is reproduced below, and is
  8. * available at http://www.eclipse.org/org/documents/edl-v10.php
  9. *
  10. * All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or
  13. * without modification, are permitted provided that the following
  14. * conditions are met:
  15. *
  16. * - Redistributions of source code must retain the above copyright
  17. * notice, this list of conditions and the following disclaimer.
  18. *
  19. * - Redistributions in binary form must reproduce the above
  20. * copyright notice, this list of conditions and the following
  21. * disclaimer in the documentation and/or other materials provided
  22. * with the distribution.
  23. *
  24. * - Neither the name of the Eclipse Foundation, Inc. nor the
  25. * names of its contributors may be used to endorse or promote
  26. * products derived from this software without specific prior
  27. * written permission.
  28. *
  29. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  30. * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  31. * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  32. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  33. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  34. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  35. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  36. * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  37. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  38. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  39. * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  40. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  41. * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  42. */
  43. package org.eclipse.jgit.internal.storage.file;
  44. import static org.eclipse.jgit.util.FS.FileStoreAttributes.FALLBACK_FILESTORE_ATTRIBUTES;
  45. import static org.eclipse.jgit.util.FS.FileStoreAttributes.FALLBACK_TIMESTAMP_RESOLUTION;
  46. import java.io.File;
  47. import java.io.IOException;
  48. import java.nio.file.attribute.BasicFileAttributes;
  49. import java.time.Duration;
  50. import java.time.Instant;
  51. import java.time.ZoneId;
  52. import java.time.format.DateTimeFormatter;
  53. import java.util.Locale;
  54. import java.util.Objects;
  55. import java.util.concurrent.TimeUnit;
  56. import org.eclipse.jgit.annotations.NonNull;
  57. import org.eclipse.jgit.util.FS;
  58. import org.eclipse.jgit.util.FS.FileStoreAttributes;
  59. import org.slf4j.Logger;
  60. import org.slf4j.LoggerFactory;
  61. /**
  62. * Caches when a file was last read, making it possible to detect future edits.
  63. * <p>
  64. * This object tracks the last modified time of a file. Later during an
  65. * invocation of {@link #isModified(File)} the object will return true if the
  66. * file may have been modified and should be re-read from disk.
  67. * <p>
  68. * A snapshot does not "live update" when the underlying filesystem changes.
  69. * Callers must poll for updates by periodically invoking
  70. * {@link #isModified(File)}.
  71. * <p>
  72. * To work around the "racy git" problem (where a file may be modified multiple
  73. * times within the granularity of the filesystem modification clock) this class
  74. * may return true from isModified(File) if the last modification time of the
  75. * file is less than 3 seconds ago.
  76. */
  77. public class FileSnapshot {
  78. private static final Logger LOG = LoggerFactory
  79. .getLogger(FileSnapshot.class);
  80. /**
  81. * An unknown file size.
  82. *
  83. * This value is used when a comparison needs to happen purely on the lastUpdate.
  84. */
  85. public static final long UNKNOWN_SIZE = -1;
  86. private static final Instant UNKNOWN_TIME = Instant.ofEpochMilli(-1);
  87. private static final Object MISSING_FILEKEY = new Object();
  88. private static final DateTimeFormatter dateFmt = DateTimeFormatter
  89. .ofPattern("yyyy-MM-dd HH:mm:ss.nnnnnnnnn") //$NON-NLS-1$
  90. .withLocale(Locale.getDefault()).withZone(ZoneId.systemDefault());
  91. /**
  92. * A FileSnapshot that is considered to always be modified.
  93. * <p>
  94. * This instance is useful for application code that wants to lazily read a
  95. * file, but only after {@link #isModified(File)} gets invoked. The returned
  96. * snapshot contains only invalid status information.
  97. */
  98. public static final FileSnapshot DIRTY = new FileSnapshot(UNKNOWN_TIME,
  99. UNKNOWN_TIME, UNKNOWN_SIZE, Duration.ZERO, MISSING_FILEKEY);
  100. /**
  101. * A FileSnapshot that is clean if the file does not exist.
  102. * <p>
  103. * This instance is useful if the application wants to consider a missing
  104. * file to be clean. {@link #isModified(File)} will return false if the file
  105. * path does not exist.
  106. */
  107. public static final FileSnapshot MISSING_FILE = new FileSnapshot(
  108. Instant.EPOCH, Instant.EPOCH, 0, Duration.ZERO, MISSING_FILEKEY) {
  109. @Override
  110. public boolean isModified(File path) {
  111. return FS.DETECTED.exists(path);
  112. }
  113. };
  114. /**
  115. * Record a snapshot for a specific file path.
  116. * <p>
  117. * This method should be invoked before the file is accessed.
  118. *
  119. * @param path
  120. * the path to later remember. The path's current status
  121. * information is saved.
  122. * @return the snapshot.
  123. */
  124. public static FileSnapshot save(File path) {
  125. return new FileSnapshot(path);
  126. }
  127. /**
  128. * Record a snapshot for a specific file path without using config file to
  129. * get filesystem timestamp resolution.
  130. * <p>
  131. * This method should be invoked before the file is accessed. It is used by
  132. * FileBasedConfig to avoid endless recursion.
  133. *
  134. * @param path
  135. * the path to later remember. The path's current status
  136. * information is saved.
  137. * @return the snapshot.
  138. */
  139. public static FileSnapshot saveNoConfig(File path) {
  140. return new FileSnapshot(path, false);
  141. }
  142. private static Object getFileKey(BasicFileAttributes fileAttributes) {
  143. Object fileKey = fileAttributes.fileKey();
  144. return fileKey == null ? MISSING_FILEKEY : fileKey;
  145. }
  146. /**
  147. * Record a snapshot for a file for which the last modification time is
  148. * already known.
  149. * <p>
  150. * This method should be invoked before the file is accessed.
  151. * <p>
  152. * Note that this method cannot rely on measuring file timestamp resolution
  153. * to avoid racy git issues caused by finite file timestamp resolution since
  154. * it's unknown in which filesystem the file is located. Hence the worst
  155. * case fallback for timestamp resolution is used.
  156. *
  157. * @param modified
  158. * the last modification time of the file
  159. * @return the snapshot.
  160. * @deprecated use {@link #save(Instant)} instead.
  161. */
  162. @Deprecated
  163. public static FileSnapshot save(long modified) {
  164. final Instant read = Instant.now();
  165. return new FileSnapshot(read, Instant.ofEpochMilli(modified),
  166. UNKNOWN_SIZE, FALLBACK_TIMESTAMP_RESOLUTION, MISSING_FILEKEY);
  167. }
  168. /**
  169. * Record a snapshot for a file for which the last modification time is
  170. * already known.
  171. * <p>
  172. * This method should be invoked before the file is accessed.
  173. * <p>
  174. * Note that this method cannot rely on measuring file timestamp resolution
  175. * to avoid racy git issues caused by finite file timestamp resolution since
  176. * it's unknown in which filesystem the file is located. Hence the worst
  177. * case fallback for timestamp resolution is used.
  178. *
  179. * @param modified
  180. * the last modification time of the file
  181. * @return the snapshot.
  182. */
  183. public static FileSnapshot save(Instant modified) {
  184. final Instant read = Instant.now();
  185. return new FileSnapshot(read, modified, UNKNOWN_SIZE,
  186. FALLBACK_TIMESTAMP_RESOLUTION, MISSING_FILEKEY);
  187. }
  188. /** Last observed modification time of the path. */
  189. private final Instant lastModified;
  190. /** Last wall-clock time the path was read. */
  191. private volatile Instant lastRead;
  192. /** True once {@link #lastRead} is far later than {@link #lastModified}. */
  193. private boolean cannotBeRacilyClean;
  194. /** Underlying file-system size in bytes.
  195. *
  196. * When set to {@link #UNKNOWN_SIZE} the size is not considered for modification checks. */
  197. private final long size;
  198. /** measured FileStore attributes */
  199. private FileStoreAttributes fileStoreAttributeCache;
  200. /**
  201. * Object that uniquely identifies the given file, or {@code
  202. * null} if a file key is not available
  203. */
  204. private final Object fileKey;
  205. private final File file;
  206. /**
  207. * Record a snapshot for a specific file path.
  208. * <p>
  209. * This method should be invoked before the file is accessed.
  210. *
  211. * @param file
  212. * the path to remember meta data for. The path's current status
  213. * information is saved.
  214. */
  215. protected FileSnapshot(File file) {
  216. this(file, true);
  217. }
  218. /**
  219. * Record a snapshot for a specific file path.
  220. * <p>
  221. * This method should be invoked before the file is accessed.
  222. *
  223. * @param file
  224. * the path to remember meta data for. The path's current status
  225. * information is saved.
  226. * @param useConfig
  227. * if {@code true} read filesystem time resolution from
  228. * configuration file otherwise use fallback resolution
  229. */
  230. protected FileSnapshot(File file, boolean useConfig) {
  231. this.file = file;
  232. this.lastRead = Instant.now();
  233. this.fileStoreAttributeCache = useConfig
  234. ? FS.getFileStoreAttributes(file.toPath().getParent())
  235. : FALLBACK_FILESTORE_ATTRIBUTES;
  236. BasicFileAttributes fileAttributes = null;
  237. try {
  238. fileAttributes = FS.DETECTED.fileAttributes(file);
  239. } catch (IOException e) {
  240. this.lastModified = Instant.ofEpochMilli(file.lastModified());
  241. this.size = file.length();
  242. this.fileKey = MISSING_FILEKEY;
  243. return;
  244. }
  245. this.lastModified = fileAttributes.lastModifiedTime().toInstant();
  246. this.size = fileAttributes.size();
  247. this.fileKey = getFileKey(fileAttributes);
  248. if (LOG.isDebugEnabled()) {
  249. LOG.debug("file={}, create new FileSnapshot: lastRead={}, lastModified={}, size={}, fileKey={}", //$NON-NLS-1$
  250. file, dateFmt.format(lastRead),
  251. dateFmt.format(lastModified), Long.valueOf(size),
  252. fileKey.toString());
  253. }
  254. }
  255. private boolean sizeChanged;
  256. private boolean fileKeyChanged;
  257. private boolean lastModifiedChanged;
  258. private boolean wasRacyClean;
  259. private long delta;
  260. private long racyThreshold;
  261. private FileSnapshot(Instant read, Instant modified, long size,
  262. @NonNull Duration fsTimestampResolution, @NonNull Object fileKey) {
  263. this.file = null;
  264. this.lastRead = read;
  265. this.lastModified = modified;
  266. this.fileStoreAttributeCache = new FileStoreAttributes(
  267. fsTimestampResolution);
  268. this.size = size;
  269. this.fileKey = fileKey;
  270. }
  271. /**
  272. * Get time of last snapshot update
  273. *
  274. * @return time of last snapshot update
  275. * @deprecated use {@link #lastModifiedInstant()} instead
  276. */
  277. @Deprecated
  278. public long lastModified() {
  279. return lastModified.toEpochMilli();
  280. }
  281. /**
  282. * Get time of last snapshot update
  283. *
  284. * @return time of last snapshot update
  285. */
  286. public Instant lastModifiedInstant() {
  287. return lastModified;
  288. }
  289. /**
  290. * @return file size in bytes of last snapshot update
  291. */
  292. public long size() {
  293. return size;
  294. }
  295. /**
  296. * Check if the path may have been modified since the snapshot was saved.
  297. *
  298. * @param path
  299. * the path the snapshot describes.
  300. * @return true if the path needs to be read again.
  301. */
  302. public boolean isModified(File path) {
  303. Instant currLastModified;
  304. long currSize;
  305. Object currFileKey;
  306. try {
  307. BasicFileAttributes fileAttributes = FS.DETECTED.fileAttributes(path);
  308. currLastModified = fileAttributes.lastModifiedTime().toInstant();
  309. currSize = fileAttributes.size();
  310. currFileKey = getFileKey(fileAttributes);
  311. } catch (IOException e) {
  312. currLastModified = Instant.ofEpochMilli(path.lastModified());
  313. currSize = path.length();
  314. currFileKey = MISSING_FILEKEY;
  315. }
  316. sizeChanged = isSizeChanged(currSize);
  317. if (sizeChanged) {
  318. return true;
  319. }
  320. fileKeyChanged = isFileKeyChanged(currFileKey);
  321. if (fileKeyChanged) {
  322. return true;
  323. }
  324. lastModifiedChanged = isModified(currLastModified);
  325. if (lastModifiedChanged) {
  326. return true;
  327. }
  328. return false;
  329. }
  330. /**
  331. * Update this snapshot when the content hasn't changed.
  332. * <p>
  333. * If the caller gets true from {@link #isModified(File)}, re-reads the
  334. * content, discovers the content is identical, and
  335. * {@link #equals(FileSnapshot)} is true, it can use
  336. * {@link #setClean(FileSnapshot)} to make a future
  337. * {@link #isModified(File)} return false. The logic goes something like
  338. * this:
  339. *
  340. * <pre>
  341. * if (snapshot.isModified(path)) {
  342. * FileSnapshot other = FileSnapshot.save(path);
  343. * Content newContent = ...;
  344. * if (oldContent.equals(newContent) &amp;&amp; snapshot.equals(other))
  345. * snapshot.setClean(other);
  346. * }
  347. * </pre>
  348. *
  349. * @param other
  350. * the other snapshot.
  351. */
  352. public void setClean(FileSnapshot other) {
  353. final Instant now = other.lastRead;
  354. if (!isRacyClean(now)) {
  355. cannotBeRacilyClean = true;
  356. }
  357. lastRead = now;
  358. }
  359. /**
  360. * Wait until this snapshot's file can't be racy anymore
  361. *
  362. * @throws InterruptedException
  363. * if sleep was interrupted
  364. */
  365. public void waitUntilNotRacy() throws InterruptedException {
  366. long timestampResolution = fileStoreAttributeCache
  367. .getFsTimestampResolution().toNanos();
  368. while (isRacyClean(Instant.now())) {
  369. TimeUnit.NANOSECONDS.sleep(timestampResolution);
  370. }
  371. }
  372. /**
  373. * Compare two snapshots to see if they cache the same information.
  374. *
  375. * @param other
  376. * the other snapshot.
  377. * @return true if the two snapshots share the same information.
  378. */
  379. @SuppressWarnings("NonOverridingEquals")
  380. public boolean equals(FileSnapshot other) {
  381. boolean sizeEq = size == UNKNOWN_SIZE || other.size == UNKNOWN_SIZE || size == other.size;
  382. return lastModified.equals(other.lastModified) && sizeEq
  383. && Objects.equals(fileKey, other.fileKey);
  384. }
  385. /** {@inheritDoc} */
  386. @Override
  387. public boolean equals(Object obj) {
  388. if (this == obj) {
  389. return true;
  390. }
  391. if (obj == null) {
  392. return false;
  393. }
  394. if (!(obj instanceof FileSnapshot)) {
  395. return false;
  396. }
  397. FileSnapshot other = (FileSnapshot) obj;
  398. return equals(other);
  399. }
  400. /** {@inheritDoc} */
  401. @Override
  402. public int hashCode() {
  403. return Objects.hash(lastModified, Long.valueOf(size), fileKey);
  404. }
  405. /**
  406. * @return {@code true} if FileSnapshot.isModified(File) found the file size
  407. * changed
  408. */
  409. boolean wasSizeChanged() {
  410. return sizeChanged;
  411. }
  412. /**
  413. * @return {@code true} if FileSnapshot.isModified(File) found the file key
  414. * changed
  415. */
  416. boolean wasFileKeyChanged() {
  417. return fileKeyChanged;
  418. }
  419. /**
  420. * @return {@code true} if FileSnapshot.isModified(File) found the file's
  421. * lastModified changed
  422. */
  423. boolean wasLastModifiedChanged() {
  424. return lastModifiedChanged;
  425. }
  426. /**
  427. * @return {@code true} if FileSnapshot.isModified(File) detected that
  428. * lastModified is racily clean
  429. */
  430. boolean wasLastModifiedRacilyClean() {
  431. return wasRacyClean;
  432. }
  433. /**
  434. * @return the delta in nanoseconds between lastModified and lastRead during
  435. * last racy check
  436. */
  437. public long lastDelta() {
  438. return delta;
  439. }
  440. /**
  441. * @return the racyLimitNanos threshold in nanoseconds during last racy
  442. * check
  443. */
  444. public long lastRacyThreshold() {
  445. return racyThreshold;
  446. }
  447. /** {@inheritDoc} */
  448. @SuppressWarnings({ "nls", "ReferenceEquality" })
  449. @Override
  450. public String toString() {
  451. if (this == DIRTY) {
  452. return "DIRTY";
  453. }
  454. if (this == MISSING_FILE) {
  455. return "MISSING_FILE";
  456. }
  457. return "FileSnapshot[modified: " + dateFmt.format(lastModified)
  458. + ", read: " + dateFmt.format(lastRead) + ", size:" + size
  459. + ", fileKey: " + fileKey + "]";
  460. }
  461. private boolean isRacyClean(Instant read) {
  462. racyThreshold = getEffectiveRacyThreshold();
  463. delta = Duration.between(lastModified, read).toNanos();
  464. wasRacyClean = delta <= racyThreshold;
  465. if (LOG.isDebugEnabled()) {
  466. LOG.debug(
  467. "file={}, isRacyClean={}, read={}, lastModified={}, delta={} ns, racy<={} ns", //$NON-NLS-1$
  468. file, Boolean.valueOf(wasRacyClean), dateFmt.format(read),
  469. dateFmt.format(lastModified), Long.valueOf(delta),
  470. Long.valueOf(racyThreshold));
  471. }
  472. return wasRacyClean;
  473. }
  474. private long getEffectiveRacyThreshold() {
  475. long timestampResolution = fileStoreAttributeCache
  476. .getFsTimestampResolution().toNanos();
  477. long minRacyInterval = fileStoreAttributeCache.getMinimalRacyInterval()
  478. .toNanos();
  479. long max = Math.max(timestampResolution, minRacyInterval);
  480. // safety margin: factor 2.5 below 100ms otherwise 1.25
  481. return max < 100_000_000L ? max * 5 / 2 : max * 5 / 4;
  482. }
  483. private boolean isModified(Instant currLastModified) {
  484. // Any difference indicates the path was modified.
  485. lastModifiedChanged = !lastModified.equals(currLastModified);
  486. if (lastModifiedChanged) {
  487. if (LOG.isDebugEnabled()) {
  488. LOG.debug(
  489. "file={}, lastModified changed from {} to {}", //$NON-NLS-1$
  490. file, dateFmt.format(lastModified),
  491. dateFmt.format(currLastModified));
  492. }
  493. return true;
  494. }
  495. // We have already determined the last read was far enough
  496. // after the last modification that any new modifications
  497. // are certain to change the last modified time.
  498. if (cannotBeRacilyClean) {
  499. LOG.debug("file={}, cannot be racily clean", file); //$NON-NLS-1$
  500. return false;
  501. }
  502. if (!isRacyClean(lastRead)) {
  503. // Our last read should have marked cannotBeRacilyClean,
  504. // but this thread may not have seen the change. The read
  505. // of the volatile field lastRead should have fixed that.
  506. LOG.debug("file={}, is unmodified", file); //$NON-NLS-1$
  507. return false;
  508. }
  509. // We last read this path too close to its last observed
  510. // modification time. We may have missed a modification.
  511. // Scan again, to ensure we still see the same state.
  512. LOG.debug("file={}, is racily clean", file); //$NON-NLS-1$
  513. return true;
  514. }
  515. private boolean isFileKeyChanged(Object currFileKey) {
  516. boolean changed = currFileKey != MISSING_FILEKEY
  517. && !currFileKey.equals(fileKey);
  518. if (changed) {
  519. LOG.debug("file={}, FileKey changed from {} to {}", //$NON-NLS-1$
  520. file, fileKey, currFileKey);
  521. }
  522. return changed;
  523. }
  524. private boolean isSizeChanged(long currSize) {
  525. boolean changed = (currSize != UNKNOWN_SIZE) && (currSize != size);
  526. if (changed) {
  527. LOG.debug("file={}, size changed from {} to {} bytes", //$NON-NLS-1$
  528. file, Long.valueOf(size), Long.valueOf(currSize));
  529. }
  530. return changed;
  531. }
  532. }