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