1
2
3
4
5
6
7
8
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 public class FileSnapshot {
51 private static final Logger LOG = LoggerFactory
52 .getLogger(FileSnapshot.class);
53
54
55
56
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")
66 .withLocale(Locale.getDefault()).withZone(ZoneId.systemDefault());
67
68
69
70
71
72
73
74
75 public static final FileSnapshot DIRTY = new FileSnapshot(UNKNOWN_TIME,
76 UNKNOWN_TIME, UNKNOWN_SIZE, Duration.ZERO, MISSING_FILEKEY);
77
78
79
80
81
82
83
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
95
96
97
98
99
100
101
102
103 public static FileSnapshot save(File path) {
104 return new FileSnapshot(path);
105 }
106
107
108
109
110
111
112
113
114
115
116
117
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
130
131
132
133
134
135
136
137
138
139
140
141
142
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
153
154
155
156
157
158
159
160
161
162
163
164
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
173 private final Instant lastModified;
174
175
176 private volatile Instant lastRead;
177
178
179 private boolean cannotBeRacilyClean;
180
181
182
183
184 private final long size;
185
186
187 private FileStoreAttributes fileStoreAttributeCache;
188
189
190
191
192
193 private final Object fileKey;
194
195 private final File file;
196
197
198
199
200
201
202
203
204
205
206 protected FileSnapshot(File file) {
207 this(file, true);
208 }
209
210
211
212
213
214
215
216
217
218
219
220
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={}",
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
279
280
281
282
283 @Deprecated
284 public long lastModified() {
285 return lastModified.toEpochMilli();
286 }
287
288
289
290
291
292
293 public Instant lastModifiedInstant() {
294 return lastModified;
295 }
296
297
298
299
300 public long size() {
301 return size;
302 }
303
304
305
306
307
308
309
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
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
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
377
378
379
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
391
392
393
394
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
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
420 @Override
421 public int hashCode() {
422 return Objects.hash(lastModified, Long.valueOf(size), fileKey);
423 }
424
425
426
427
428
429 boolean wasSizeChanged() {
430 return sizeChanged;
431 }
432
433
434
435
436
437 boolean wasFileKeyChanged() {
438 return fileKeyChanged;
439 }
440
441
442
443
444
445 boolean wasLastModifiedChanged() {
446 return lastModifiedChanged;
447 }
448
449
450
451
452
453 boolean wasLastModifiedRacilyClean() {
454 return wasRacyClean;
455 }
456
457
458
459
460
461 public long lastDelta() {
462 return delta;
463 }
464
465
466
467
468
469 public long lastRacyThreshold() {
470 return racyThreshold;
471 }
472
473
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",
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
509 return max < 100_000_000L ? max * 5 / 2 : max * 5 / 4;
510 }
511
512 private boolean isModified(Instant currLastModified) {
513
514
515 lastModifiedChanged = !lastModified.equals(currLastModified);
516 if (lastModifiedChanged) {
517 if (LOG.isDebugEnabled()) {
518 LOG.debug(
519 "file={}, lastModified changed from {} to {}",
520 file, dateFmt.format(lastModified),
521 dateFmt.format(currLastModified));
522 }
523 return true;
524 }
525
526
527
528
529 if (cannotBeRacilyClean) {
530 LOG.debug("file={}, cannot be racily clean", file);
531 return false;
532 }
533 if (!isRacyClean(lastRead)) {
534
535
536
537 LOG.debug("file={}, is unmodified", file);
538 return false;
539 }
540
541
542
543
544 LOG.debug("file={}, is racily clean", file);
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 {}",
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",
562 file, Long.valueOf(size), Long.valueOf(currSize));
563 }
564 return changed;
565 }
566 }