1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81 public class FileSnapshot {
82 private static final Logger LOG = LoggerFactory
83 .getLogger(FileSnapshot.class);
84
85
86
87
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")
97 .withLocale(Locale.getDefault()).withZone(ZoneId.systemDefault());
98
99
100
101
102
103
104
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
111
112
113
114
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
126
127
128
129
130
131
132
133
134 public static FileSnapshot save(File path) {
135 return new FileSnapshot(path);
136 }
137
138
139
140
141
142
143
144
145
146
147
148
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
161
162
163
164
165
166
167
168
169
170
171
172
173
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
184
185
186
187
188
189
190
191
192
193
194
195
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
204 private final Instant lastModified;
205
206
207 private volatile Instant lastRead;
208
209
210 private boolean cannotBeRacilyClean;
211
212
213
214
215 private final long size;
216
217
218 private FileStoreAttributes fileStoreAttributeCache;
219
220
221
222
223
224 private final Object fileKey;
225
226 private final File file;
227
228
229
230
231
232
233
234
235
236
237 protected FileSnapshot(File file) {
238 this(file, true);
239 }
240
241
242
243
244
245
246
247
248
249
250
251
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={}",
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
304
305
306
307
308 @Deprecated
309 public long lastModified() {
310 return lastModified.toEpochMilli();
311 }
312
313
314
315
316
317
318 public Instant lastModifiedInstant() {
319 return lastModified;
320 }
321
322
323
324
325 public long size() {
326 return size;
327 }
328
329
330
331
332
333
334
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
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
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
397
398
399
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
411
412
413
414
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
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
440 @Override
441 public int hashCode() {
442 return Objects.hash(lastModified, Long.valueOf(size), fileKey);
443 }
444
445
446
447
448
449 boolean wasSizeChanged() {
450 return sizeChanged;
451 }
452
453
454
455
456
457 boolean wasFileKeyChanged() {
458 return fileKeyChanged;
459 }
460
461
462
463
464
465 boolean wasLastModifiedChanged() {
466 return lastModifiedChanged;
467 }
468
469
470
471
472
473 boolean wasLastModifiedRacilyClean() {
474 return wasRacyClean;
475 }
476
477
478
479
480
481 public long lastDelta() {
482 return delta;
483 }
484
485
486
487
488
489 public long lastRacyThreshold() {
490 return racyThreshold;
491 }
492
493
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",
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
529 return max < 100_000_000L ? max * 5 / 2 : max * 5 / 4;
530 }
531
532 private boolean isModified(Instant currLastModified) {
533
534
535 lastModifiedChanged = !lastModified.equals(currLastModified);
536 if (lastModifiedChanged) {
537 if (LOG.isDebugEnabled()) {
538 LOG.debug(
539 "file={}, lastModified changed from {} to {}",
540 file, dateFmt.format(lastModified),
541 dateFmt.format(currLastModified));
542 }
543 return true;
544 }
545
546
547
548
549 if (cannotBeRacilyClean) {
550 LOG.debug("file={}, cannot be racily clean", file);
551 return false;
552 }
553 if (!isRacyClean(lastRead)) {
554
555
556
557 LOG.debug("file={}, is unmodified", file);
558 return false;
559 }
560
561
562
563
564 LOG.debug("file={}, is racily clean", file);
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 {}",
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",
582 file, Long.valueOf(size), Long.valueOf(currSize));
583 }
584 return changed;
585 }
586 }