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