Skip to content

Commit ba4b7dc

Browse files
committed
Merge branch 'improved-overflow-support/parameterized-torture-tests' of https://github.com/SWAT-engineering/java-watch into improved-overflow-support/parameterized-torture-tests
2 parents f84f842 + 9eea3e0 commit ba4b7dc

File tree

1 file changed

+100
-19
lines changed

1 file changed

+100
-19
lines changed

src/main/java/engineering/swat/watch/impl/overflows/IndexingRescanner.java

Lines changed: 100 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.nio.file.attribute.BasicFileAttributes;
3434
import java.nio.file.attribute.FileTime;
3535
import java.util.ArrayDeque;
36+
import java.util.Collections;
3637
import java.util.Deque;
3738
import java.util.HashSet;
3839
import java.util.Map;
@@ -42,20 +43,95 @@
4243

4344
import org.apache.logging.log4j.LogManager;
4445
import org.apache.logging.log4j.Logger;
46+
import org.checkerframework.checker.nullness.qual.KeyFor;
47+
import org.checkerframework.checker.nullness.qual.Nullable;
4548

4649
import engineering.swat.watch.WatchEvent;
4750
import engineering.swat.watch.WatchScope;
4851
import engineering.swat.watch.impl.EventHandlingWatch;
4952

5053
public class IndexingRescanner extends MemorylessRescanner {
5154
private final Logger logger = LogManager.getLogger();
52-
private final Map<Path, FileTime> index = new ConcurrentHashMap<>();
55+
private final Index index = new Index();
5356

5457
public IndexingRescanner(Executor exec, Path path, WatchScope scope) {
5558
super(exec);
5659
new Indexer(path, scope).walkFileTree(); // Make an initial scan to populate the index
5760
}
5861

62+
private static class Index {
63+
private final Map<Path, Map<Path, FileTime>> lastModifiedTimes = new ConcurrentHashMap<>();
64+
// ^^^^ ^^^^
65+
// Parent File name (possibly a directory itself)
66+
67+
public @Nullable FileTime putLastModifiedTime(Path p, FileTime time) {
68+
var parent = p.getParent();
69+
var fileName = p.getFileName();
70+
if (parent != null && fileName != null) {
71+
return putLastModifiedTime(parent, fileName, time);
72+
} else {
73+
throw new IllegalArgumentException("A path key should have both a parent and a file name");
74+
}
75+
}
76+
77+
public @Nullable FileTime putLastModifiedTime(Path parent, Path fileName, FileTime time) {
78+
var nested = lastModifiedTimes.computeIfAbsent(parent, x -> new ConcurrentHashMap<>());
79+
return nested.put(fileName, time);
80+
}
81+
82+
public @Nullable FileTime getLastModifiedTime(Path p) {
83+
var parent = p.getParent();
84+
var fileName = p.getFileName();
85+
if (parent != null && fileName != null) {
86+
return getLastModifiedTime(parent, fileName);
87+
} else {
88+
throw new IllegalArgumentException("A path key should have both a parent and a file name");
89+
}
90+
}
91+
92+
public @Nullable FileTime getLastModifiedTime(Path parent, Path fileName) {
93+
var nested = lastModifiedTimes.get(parent);
94+
return nested == null ? null : nested.get(fileName);
95+
}
96+
97+
public Set<Path> getFileNames(Path parent) {
98+
var nested = lastModifiedTimes.get(parent);
99+
return nested == null ? Collections.emptySet() : (Set<Path>) nested.keySet();
100+
}
101+
102+
public @Nullable FileTime remove(Path p) {
103+
var parent = p.getParent();
104+
var fileName = p.getFileName();
105+
if (parent != null && fileName != null) {
106+
return remove(parent, fileName);
107+
} else {
108+
throw new IllegalArgumentException("A path key should have both a parent and a file name");
109+
}
110+
}
111+
112+
public @Nullable FileTime remove(Path parent, Path fileName) {
113+
var nested = lastModifiedTimes.get(parent);
114+
if (nested != null) {
115+
var removed = nested.remove(fileName);
116+
if (nested.isEmpty()) {
117+
lastModifiedTimes.remove(parent, nested);
118+
// Note: Between checking `nested` for non-emptiness and
119+
// removing it from `lastModifiedTimes`, other threads may
120+
// have put new entries in it. After the removal, these
121+
// entries are lost, so the index doesn't completely reflect
122+
// the file system anymore, and redundant events may be
123+
// issued. This doesn't break the contract with the client,
124+
// though (because this rescanner still provides an
125+
// over-approximation). Avoiding this race would be costly
126+
// in terms of synchronization.
127+
}
128+
return removed;
129+
} else {
130+
return null;
131+
}
132+
}
133+
}
134+
59135
private class Indexer extends BaseFileVisitor {
60136
public Indexer(Path path, WatchScope scope) {
61137
super(path, scope);
@@ -64,14 +140,14 @@ public Indexer(Path path, WatchScope scope) {
64140
@Override
65141
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
66142
if (!path.equals(dir)) {
67-
index.put(dir, attrs.lastModifiedTime());
143+
index.putLastModifiedTime(dir, attrs.lastModifiedTime());
68144
}
69145
return FileVisitResult.CONTINUE;
70146
}
71147

72148
@Override
73149
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
74-
index.put(file, attrs.lastModifiedTime());
150+
index.putLastModifiedTime(file, attrs.lastModifiedTime());
75151
return FileVisitResult.CONTINUE;
76152
}
77153
}
@@ -84,30 +160,32 @@ protected MemorylessRescanner.Generator newGenerator(Path path, WatchScope scope
84160
}
85161

86162
protected class Generator extends MemorylessRescanner.Generator {
87-
// Field to keep track of (a stack of) the paths that are visited during
88-
// the current rescan (one frame for each nested subdirectory), to
89-
// approximate `DELETED` events that happened since the previous rescan.
90-
// Instances of this class are supposed to be used non-concurrently, so
91-
// no synchronization to access this field is needed.
163+
// Field to keep track of (a stack of sets, of file names, of) the paths
164+
// that are visited during the current rescan (one frame for each nested
165+
// subdirectory), to approximate `DELETED` events that happened since
166+
// the previous rescan. Instances of this class are supposed to be used
167+
// non-concurrently, so no synchronization to access this field is
168+
// needed.
92169
private final Deque<Set<Path>> visited = new ArrayDeque<>();
93170

94171
public Generator(Path path, WatchScope scope) {
95172
super(path, scope);
96173
this.visited.push(new HashSet<>()); // Initial set for content of `path`
97174
}
98175

99-
private <T> void addToPeeked(Deque<Set<T>> deque, T t) {
176+
private void addToPeeked(Deque<Set<Path>> deque, Path p) {
100177
var peeked = deque.peek();
101-
if (peeked != null) {
102-
peeked.add(t);
178+
var fileName = p.getFileName();
179+
if (peeked != null && fileName != null) {
180+
peeked.add(fileName);
103181
}
104182
}
105183

106184
// -- MemorylessRescanner.Generator --
107185

108186
@Override
109187
protected void generateEvents(Path path, BasicFileAttributes attrs) {
110-
var lastModifiedTimeOld = index.get(path);
188+
var lastModifiedTimeOld = index.getLastModifiedTime(path);
111189
var lastModifiedTimeNew = attrs.lastModifiedTime();
112190

113191
// The path isn't indexed yet
@@ -140,12 +218,15 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx
140218
// Issue `DELETED` events based on the set of paths visited in `dir`
141219
var visitedInDir = visited.pop();
142220
if (visitedInDir != null) {
143-
for (var p : index.keySet()) {
144-
if (dir.equals(p.getParent()) && !visitedInDir.contains(p) && !Files.exists(p)) {
145-
// Note: The third subcondition is needed because the
146-
// index may have been updated during the visit. In that
147-
// case, `p` might not be in `visitedInDir`, but exist.
148-
events.add(new WatchEvent(WatchEvent.Kind.DELETED, p));
221+
for (var p : index.getFileNames(dir)) {
222+
if (!visitedInDir.contains(p)) {
223+
var fullPath = dir.resolve(p);
224+
// The index may have been updated during the visit, so
225+
// even if `p` isn't contained in `visitedInDir`, by
226+
// now, it might have come into existance.
227+
if (!Files.exists(fullPath)) {
228+
events.add(new WatchEvent(WatchEvent.Kind.DELETED, fullPath));
229+
}
149230
}
150231
}
151232
}
@@ -169,7 +250,7 @@ public void accept(EventHandlingWatch watch, WatchEvent event) {
169250
case MODIFIED:
170251
try {
171252
var lastModifiedTimeNew = Files.getLastModifiedTime(fullPath);
172-
var lastModifiedTimeOld = index.put(fullPath, lastModifiedTimeNew);
253+
var lastModifiedTimeOld = index.putLastModifiedTime(fullPath, lastModifiedTimeNew);
173254

174255
// If a `MODIFIED` event happens for a path that wasn't in
175256
// the index yet, then a `CREATED` event has somehow been

0 commit comments

Comments
 (0)