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 boolean useConfig;
194
195
196
197
198
199 private final Object fileKey;
200
201 private final File file;
202
203
204
205
206
207
208
209
210
211
212 protected FileSnapshot(File file) {
213 this(file, true);
214 }
215
216
217
218
219
220
221
222
223
224
225
226
227
228 protected FileSnapshot(File file, boolean useConfig) {
229 this.file = file;
230 this.lastRead = Instant.now();
231 this.useConfig = useConfig;
232 BasicFileAttributes fileAttributes = null;
233 try {
234 fileAttributes = FS.DETECTED.fileAttributes(file);
235 } catch (NoSuchFileException e) {
236 this.lastModified = Instant.EPOCH;
237 this.size = 0L;
238 this.fileKey = MISSING_FILEKEY;
239 return;
240 } catch (IOException e) {
241 LOG.error(e.getMessage(), e);
242 this.lastModified = Instant.EPOCH;
243 this.size = 0L;
244 this.fileKey = MISSING_FILEKEY;
245 return;
246 }
247 this.lastModified = fileAttributes.lastModifiedTime().toInstant();
248 this.size = fileAttributes.size();
249 this.fileKey = getFileKey(fileAttributes);
250 if (LOG.isDebugEnabled()) {
251 LOG.debug("file={}, create new FileSnapshot: lastRead={}, lastModified={}, size={}, fileKey={}",
252 file, dateFmt.format(lastRead),
253 dateFmt.format(lastModified), Long.valueOf(size),
254 fileKey.toString());
255 }
256 }
257
258 private boolean sizeChanged;
259
260 private boolean fileKeyChanged;
261
262 private boolean lastModifiedChanged;
263
264 private boolean wasRacyClean;
265
266 private long delta;
267
268 private long racyThreshold;
269
270 private FileSnapshot(Instant read, Instant modified, long size,
271 @NonNull Duration fsTimestampResolution, @NonNull Object fileKey) {
272 this.file = null;
273 this.lastRead = read;
274 this.lastModified = modified;
275 this.fileStoreAttributeCache = new FileStoreAttributes(
276 fsTimestampResolution);
277 this.size = size;
278 this.fileKey = fileKey;
279 }
280
281
282
283
284
285
286
287 @Deprecated
288 public long lastModified() {
289 return lastModified.toEpochMilli();
290 }
291
292
293
294
295
296
297 public Instant lastModifiedInstant() {
298 return lastModified;
299 }
300
301
302
303
304 public long size() {
305 return size;
306 }
307
308
309
310
311
312
313
314
315 public boolean isModified(File path) {
316 Instant currLastModified;
317 long currSize;
318 Object currFileKey;
319 try {
320 BasicFileAttributes fileAttributes = FS.DETECTED.fileAttributes(path);
321 currLastModified = fileAttributes.lastModifiedTime().toInstant();
322 currSize = fileAttributes.size();
323 currFileKey = getFileKey(fileAttributes);
324 } catch (NoSuchFileException e) {
325 currLastModified = Instant.EPOCH;
326 currSize = 0L;
327 currFileKey = MISSING_FILEKEY;
328 } catch (IOException e) {
329 LOG.error(e.getMessage(), e);
330 currLastModified = Instant.EPOCH;
331 currSize = 0L;
332 currFileKey = MISSING_FILEKEY;
333 }
334 sizeChanged = isSizeChanged(currSize);
335 if (sizeChanged) {
336 return true;
337 }
338 fileKeyChanged = isFileKeyChanged(currFileKey);
339 if (fileKeyChanged) {
340 return true;
341 }
342 lastModifiedChanged = isModified(currLastModified);
343 if (lastModifiedChanged) {
344 return true;
345 }
346 return false;
347 }
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371 public void setClean(FileSnapshot other) {
372 final Instant now = other.lastRead;
373 if (!isRacyClean(now)) {
374 cannotBeRacilyClean = true;
375 }
376 lastRead = now;
377 }
378
379
380
381
382
383
384
385 public void waitUntilNotRacy() throws InterruptedException {
386 long timestampResolution = fileStoreAttributeCache()
387 .getFsTimestampResolution().toNanos();
388 while (isRacyClean(Instant.now())) {
389 TimeUnit.NANOSECONDS.sleep(timestampResolution);
390 }
391 }
392
393
394
395
396
397
398
399
400 @SuppressWarnings("NonOverridingEquals")
401 public boolean equals(FileSnapshot other) {
402 boolean sizeEq = size == UNKNOWN_SIZE || other.size == UNKNOWN_SIZE || size == other.size;
403 return lastModified.equals(other.lastModified) && sizeEq
404 && Objects.equals(fileKey, other.fileKey);
405 }
406
407
408 @Override
409 public boolean equals(Object obj) {
410 if (this == obj) {
411 return true;
412 }
413 if (obj == null) {
414 return false;
415 }
416 if (!(obj instanceof FileSnapshot)) {
417 return false;
418 }
419 FileSnapshot other = (FileSnapshot) obj;
420 return equals(other);
421 }
422
423
424 @Override
425 public int hashCode() {
426 return Objects.hash(lastModified, Long.valueOf(size), fileKey);
427 }
428
429
430
431
432
433 boolean wasSizeChanged() {
434 return sizeChanged;
435 }
436
437
438
439
440
441 boolean wasFileKeyChanged() {
442 return fileKeyChanged;
443 }
444
445
446
447
448
449 boolean wasLastModifiedChanged() {
450 return lastModifiedChanged;
451 }
452
453
454
455
456
457 boolean wasLastModifiedRacilyClean() {
458 return wasRacyClean;
459 }
460
461
462
463
464
465 public long lastDelta() {
466 return delta;
467 }
468
469
470
471
472
473 public long lastRacyThreshold() {
474 return racyThreshold;
475 }
476
477
478 @SuppressWarnings({ "nls", "ReferenceEquality" })
479 @Override
480 public String toString() {
481 if (this == DIRTY) {
482 return "DIRTY";
483 }
484 if (this == MISSING_FILE) {
485 return "MISSING_FILE";
486 }
487 return "FileSnapshot[modified: " + dateFmt.format(lastModified)
488 + ", read: " + dateFmt.format(lastRead) + ", size:" + size
489 + ", fileKey: " + fileKey + "]";
490 }
491
492 private boolean isRacyClean(Instant read) {
493 racyThreshold = getEffectiveRacyThreshold();
494 delta = Duration.between(lastModified, read).toNanos();
495 wasRacyClean = delta <= racyThreshold;
496 if (LOG.isDebugEnabled()) {
497 LOG.debug(
498 "file={}, isRacyClean={}, read={}, lastModified={}, delta={} ns, racy<={} ns",
499 file, Boolean.valueOf(wasRacyClean), dateFmt.format(read),
500 dateFmt.format(lastModified), Long.valueOf(delta),
501 Long.valueOf(racyThreshold));
502 }
503 return wasRacyClean;
504 }
505
506 private long getEffectiveRacyThreshold() {
507 long timestampResolution = fileStoreAttributeCache()
508 .getFsTimestampResolution().toNanos();
509 long minRacyInterval = fileStoreAttributeCache()
510 .getMinimalRacyInterval().toNanos();
511 long max = Math.max(timestampResolution, minRacyInterval);
512
513 return max < 100_000_000L ? max * 5 / 2 : max * 5 / 4;
514 }
515
516 private boolean isModified(Instant currLastModified) {
517
518
519 lastModifiedChanged = !lastModified.equals(currLastModified);
520 if (lastModifiedChanged) {
521 if (LOG.isDebugEnabled()) {
522 LOG.debug(
523 "file={}, lastModified changed from {} to {}",
524 file, dateFmt.format(lastModified),
525 dateFmt.format(currLastModified));
526 }
527 return true;
528 }
529
530
531
532
533 if (cannotBeRacilyClean) {
534 LOG.debug("file={}, cannot be racily clean", file);
535 return false;
536 }
537 if (!isRacyClean(lastRead)) {
538
539
540
541 LOG.debug("file={}, is unmodified", file);
542 return false;
543 }
544
545
546
547
548 LOG.debug("file={}, is racily clean", file);
549 return true;
550 }
551
552 private boolean isFileKeyChanged(Object currFileKey) {
553 boolean changed = currFileKey != MISSING_FILEKEY
554 && !currFileKey.equals(fileKey);
555 if (changed) {
556 LOG.debug("file={}, FileKey changed from {} to {}",
557 file, fileKey, currFileKey);
558 }
559 return changed;
560 }
561
562 private boolean isSizeChanged(long currSize) {
563 boolean changed = (currSize != UNKNOWN_SIZE) && (currSize != size);
564 if (changed) {
565 LOG.debug("file={}, size changed from {} to {} bytes",
566 file, Long.valueOf(size), Long.valueOf(currSize));
567 }
568 return changed;
569 }
570
571 private FileStoreAttributes fileStoreAttributeCache() {
572 if (fileStoreAttributeCache == null) {
573 fileStoreAttributeCache = useConfig
574 ? FS.getFileStoreAttributes(file.toPath().getParent())
575 : FALLBACK_FILESTORE_ATTRIBUTES;
576 }
577 return fileStoreAttributeCache;
578 }
579 }