Skip to content

Commit 532ff11

Browse files
committed
Make the index generic and thread-safe
1 parent ba4b7dc commit 532ff11

File tree

1 file changed

+60
-49
lines changed

1 file changed

+60
-49
lines changed

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

Lines changed: 60 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@
4040
import java.util.Set;
4141
import java.util.concurrent.ConcurrentHashMap;
4242
import java.util.concurrent.Executor;
43+
import java.util.function.BiFunction;
4344

4445
import org.apache.logging.log4j.LogManager;
4546
import org.apache.logging.log4j.Logger;
46-
import org.checkerframework.checker.nullness.qual.KeyFor;
4747
import org.checkerframework.checker.nullness.qual.Nullable;
4848

4949
import engineering.swat.watch.WatchEvent;
@@ -52,78 +52,89 @@
5252

5353
public class IndexingRescanner extends MemorylessRescanner {
5454
private final Logger logger = LogManager.getLogger();
55-
private final Index index = new Index();
55+
private final PathMap<FileTime> index = new PathMap<>();
5656

5757
public IndexingRescanner(Executor exec, Path path, WatchScope scope) {
5858
super(exec);
5959
new Indexer(path, scope).walkFileTree(); // Make an initial scan to populate the index
6060
}
6161

62-
private static class Index {
63-
private final Map<Path, Map<Path, FileTime>> lastModifiedTimes = new ConcurrentHashMap<>();
62+
private static class PathMap<V> {
63+
private final Map<Path, Map<Path, V>> values = new ConcurrentHashMap<>();
6464
// ^^^^ ^^^^
6565
// Parent File name (possibly a directory itself)
6666

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-
}
67+
public @Nullable V put(Path p, V value) {
68+
return apply((parent, fileName) -> put(parent, fileName, value), p);
7569
}
7670

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);
71+
public @Nullable V get(Path p) {
72+
return apply(this::get, p);
8073
}
8174

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);
75+
public Set<Path> getParents() {
76+
return values.keySet();
9577
}
9678

9779
public Set<Path> getFileNames(Path parent) {
98-
var nested = lastModifiedTimes.get(parent);
80+
var nested = values.get(parent);
9981
return nested == null ? Collections.emptySet() : (Set<Path>) nested.keySet();
10082
}
10183

102-
public @Nullable FileTime remove(Path p) {
84+
public @Nullable V remove(Path p) {
85+
return apply(this::remove, p);
86+
}
87+
88+
private V apply(BiFunction<Path, Path, V> action, Path p) {
10389
var parent = p.getParent();
10490
var fileName = p.getFileName();
10591
if (parent != null && fileName != null) {
106-
return remove(parent, fileName);
92+
return action.apply(parent, fileName);
10793
} else {
108-
throw new IllegalArgumentException("A path key should have both a parent and a file name");
94+
throw new IllegalArgumentException("The path should have both a parent and a file name");
95+
}
96+
}
97+
98+
private @Nullable V put(Path parent, Path fileName, V value) {
99+
// Let "here" and "there" refer to threads that perform this method
100+
// (here) and a concurrent `remove` (there).
101+
var nested = values.computeIfAbsent(parent, x -> new ConcurrentHashMap<>());
102+
var previous = nested.put(fileName, value);
103+
// <-- At this point, if a concurrent `values.remove(...)` has
104+
// happened there, then `values.get(parent) != nested` is true
105+
// here, so the put is retried here.
106+
if (values.get(parent) != nested) {
107+
return put(parent, fileName, value);
109108
}
109+
// <-- At this point, if a concurrent `values.remove(...)` has
110+
// happened there, then `!nested.isEmpty()` is true there, so
111+
// the new entry is re-put there.
112+
return previous;
110113
}
111114

112-
public @Nullable FileTime remove(Path parent, Path fileName) {
113-
var nested = lastModifiedTimes.get(parent);
115+
private @Nullable V get(Path parent, Path fileName) {
116+
var nested = values.get(parent);
117+
return nested == null ? null : nested.get(fileName);
118+
}
119+
120+
private @Nullable V remove(Path parent, Path fileName) {
121+
// Let "here" and "there" refer to threads that perform this method
122+
// (here) and a concurrent `put` (there).
123+
var nested = values.get(parent);
114124
if (nested != null) {
115125
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.
126+
if (nested.isEmpty() && values.remove(parent, nested)) {
127+
// <-- At this point, if a concurrent `nested.put(...)` has
128+
// happened there, then `!nested.isEmpty()` is true
129+
// here, so the new entry is re-put here.
130+
if (!nested.isEmpty()) {
131+
for (var e : nested.entrySet()) {
132+
put(parent, e.getKey(), e.getValue());
133+
}
134+
}
135+
// <-- At this point, if a concurrent `nested.put(...)` has
136+
// happened there, then `values.get(parent) != nested`
137+
// is true there, so the put is retried.
127138
}
128139
return removed;
129140
} else {
@@ -140,14 +151,14 @@ public Indexer(Path path, WatchScope scope) {
140151
@Override
141152
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
142153
if (!path.equals(dir)) {
143-
index.putLastModifiedTime(dir, attrs.lastModifiedTime());
154+
index.put(dir, attrs.lastModifiedTime());
144155
}
145156
return FileVisitResult.CONTINUE;
146157
}
147158

148159
@Override
149160
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
150-
index.putLastModifiedTime(file, attrs.lastModifiedTime());
161+
index.put(file, attrs.lastModifiedTime());
151162
return FileVisitResult.CONTINUE;
152163
}
153164
}
@@ -185,7 +196,7 @@ private void addToPeeked(Deque<Set<Path>> deque, Path p) {
185196

186197
@Override
187198
protected void generateEvents(Path path, BasicFileAttributes attrs) {
188-
var lastModifiedTimeOld = index.getLastModifiedTime(path);
199+
var lastModifiedTimeOld = index.get(path);
189200
var lastModifiedTimeNew = attrs.lastModifiedTime();
190201

191202
// The path isn't indexed yet
@@ -250,7 +261,7 @@ public void accept(EventHandlingWatch watch, WatchEvent event) {
250261
case MODIFIED:
251262
try {
252263
var lastModifiedTimeNew = Files.getLastModifiedTime(fullPath);
253-
var lastModifiedTimeOld = index.putLastModifiedTime(fullPath, lastModifiedTimeNew);
264+
var lastModifiedTimeOld = index.put(fullPath, lastModifiedTimeNew);
254265

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

0 commit comments

Comments
 (0)