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