Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

FileSnapshot.java 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  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. }