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 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 public class FileSnapshot {
49 private static final Logger LOG = LoggerFactory
50 .getLogger(FileSnapshot.class);
51
52
53
54
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")
64 .withLocale(Locale.getDefault()).withZone(ZoneId.systemDefault());
65
66
67
68
69
70
71
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
78
79
80
81
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
93
94
95
96
97
98
99
100
101 public static FileSnapshot save(File path) {
102 return new FileSnapshot(path);
103 }
104
105
106
107
108
109
110
111
112
113
114
115
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
128
129
130
131
132
133
134
135
136
137
138
139
140
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
151
152
153
154
155
156
157
158
159
160
161
162
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
171 private final Instant lastModified;
172
173
174 private volatile Instant lastRead;
175
176
177 private boolean cannotBeRacilyClean;
178
179
180
181
182 private final long size;
183
184
185 private FileStoreAttributes fileStoreAttributeCache;
186
187
188
189
190
191 private final Object fileKey;
192
193 private final File file;
194
195
196
197
198
199
200
201
202
203
204 protected FileSnapshot(File file) {
205 this(file, true);
206 }
207
208
209
210
211
212
213
214
215
216
217
218
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={}",
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
271
272
273
274
275 @Deprecated
276 public long lastModified() {
277 return lastModified.toEpochMilli();
278 }
279
280
281
282
283
284
285 public Instant lastModifiedInstant() {
286 return lastModified;
287 }
288
289
290
291
292 public long size() {
293 return size;
294 }
295
296
297
298
299
300
301
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
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
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
364
365
366
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
378
379
380
381
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
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
407 @Override
408 public int hashCode() {
409 return Objects.hash(lastModified, Long.valueOf(size), fileKey);
410 }
411
412
413
414
415
416 boolean wasSizeChanged() {
417 return sizeChanged;
418 }
419
420
421
422
423
424 boolean wasFileKeyChanged() {
425 return fileKeyChanged;
426 }
427
428
429
430
431
432 boolean wasLastModifiedChanged() {
433 return lastModifiedChanged;
434 }
435
436
437
438
439
440 boolean wasLastModifiedRacilyClean() {
441 return wasRacyClean;
442 }
443
444
445
446
447
448 public long lastDelta() {
449 return delta;
450 }
451
452
453
454
455
456 public long lastRacyThreshold() {
457 return racyThreshold;
458 }
459
460
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",
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
496 return max < 100_000_000L ? max * 5 / 2 : max * 5 / 4;
497 }
498
499 private boolean isModified(Instant currLastModified) {
500
501
502 lastModifiedChanged = !lastModified.equals(currLastModified);
503 if (lastModifiedChanged) {
504 if (LOG.isDebugEnabled()) {
505 LOG.debug(
506 "file={}, lastModified changed from {} to {}",
507 file, dateFmt.format(lastModified),
508 dateFmt.format(currLastModified));
509 }
510 return true;
511 }
512
513
514
515
516 if (cannotBeRacilyClean) {
517 LOG.debug("file={}, cannot be racily clean", file);
518 return false;
519 }
520 if (!isRacyClean(lastRead)) {
521
522
523
524 LOG.debug("file={}, is unmodified", file);
525 return false;
526 }
527
528
529
530
531 LOG.debug("file={}, is racily clean", file);
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 {}",
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",
549 file, Long.valueOf(size), Long.valueOf(currSize));
550 }
551 return changed;
552 }
553 }