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 16KB

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