Skip to content

Commit 748e8ac

Browse files
committed
Fix issue that JDKFileTreeWatch relied on overflow handling to preserve the integrity of its internal child watchers
1 parent 387e7c3 commit 748e8ac

File tree

3 files changed

+229
-14
lines changed

3 files changed

+229
-14
lines changed

src/main/java/engineering/swat/watch/impl/jdk/JDKFileTreeWatch.java

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ private void getFileNameAndThen(WatchEvent event, Consumer<Path> consumer) {
124124
}
125125

126126
private void acceptOverflow() {
127+
openChildWatches();
127128
for (var childWatch : childWatches.values()) {
128129
reportOverflowTo(childWatch);
129130
}
@@ -154,6 +155,21 @@ private void reportOverflowTo(JDKFileTreeWatch childWatch) {
154155
}
155156
}
156157

158+
private void openChildWatches() {
159+
try (var children = Files.find(path, 1, (p, attrs) -> p != path && attrs.isDirectory())) {
160+
children.forEach(p -> {
161+
var child = p.getFileName();
162+
if (child != null) {
163+
openChildWatch(child);
164+
} else {
165+
logger.error("File tree watch (for: {}) could not open a child watch for: {}", path, p);
166+
}
167+
});
168+
} catch (IOException e) {
169+
logger.error("File tree watch (for: {}) could not iterate over its children ({})", path, e);
170+
}
171+
}
172+
157173
private JDKFileTreeWatch openChildWatch(Path child) {
158174
assert !child.isAbsolute();
159175

@@ -224,18 +240,7 @@ public synchronized void close() throws IOException {
224240
@Override
225241
protected synchronized void start() throws IOException {
226242
internal.open();
227-
try (var children = Files.find(path, 1, (p, attrs) -> p != path && attrs.isDirectory())) {
228-
children.forEach(p -> {
229-
var child = p.getFileName();
230-
if (child != null) {
231-
openChildWatch(child);
232-
} else {
233-
logger.error("File tree watch (for: {}) could not open a child watch for: {}", path, p);
234-
}
235-
});
236-
} catch (IOException e) {
237-
logger.error("File tree watch (for: {}) could not iterate over its children ({})", path, e);
238-
}
243+
openChildWatches();
239244
// There's no need to report an overflow event, because `internal` was
240245
// opened *before* the file system was accessed to fetch children. Thus,
241246
// if a new directory is created while this method is running, then at

src/test/java/engineering/swat/watch/TestDirectory.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@
3737
import java.util.Comparator;
3838
import java.util.List;
3939

40-
class TestDirectory implements Closeable {
40+
public class TestDirectory implements Closeable {
4141
private final Path testDirectory;
4242
private final List<Path> testFiles;
4343

4444

45-
TestDirectory() throws IOException {
45+
public TestDirectory() throws IOException {
4646
testDirectory = Files.createTempDirectory("java-watch-test");
4747
List<Path> testFiles = new ArrayList<>();
4848
add3Files(testFiles, testDirectory);
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package engineering.swat.watch.impl;
2+
3+
import static org.awaitility.Awaitility.await;
4+
5+
import java.io.IOException;
6+
import java.nio.file.Files;
7+
import java.nio.file.Path;
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.concurrent.ConcurrentHashMap;
11+
import java.util.concurrent.Executor;
12+
import java.util.concurrent.ForkJoinPool;
13+
import java.util.function.BiConsumer;
14+
import java.util.function.BiFunction;
15+
import java.util.function.BiPredicate;
16+
import java.util.function.Function;
17+
import java.util.stream.Collectors;
18+
import java.util.stream.Stream;
19+
20+
import org.awaitility.Awaitility;
21+
import org.checkerframework.checker.nullness.qual.Nullable;
22+
import org.junit.jupiter.api.AfterEach;
23+
import org.junit.jupiter.api.BeforeAll;
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.Test;
26+
27+
import engineering.swat.watch.TestDirectory;
28+
import engineering.swat.watch.TestHelper;
29+
import engineering.swat.watch.WatchEvent;
30+
import engineering.swat.watch.WatchEvent.Kind;
31+
import engineering.swat.watch.WatchScope;
32+
import engineering.swat.watch.impl.jdk.JDKFileTreeWatch;
33+
import engineering.swat.watch.impl.overflows.IndexingRescanner;
34+
import engineering.swat.watch.impl.overflows.MemorylessRescanner;
35+
36+
class JDKFileTreeWatchTests {
37+
private TestDirectory testDir;
38+
39+
@BeforeEach
40+
void setup() throws IOException {
41+
testDir = new TestDirectory();
42+
}
43+
44+
@AfterEach
45+
void cleanup() {
46+
if (testDir != null) {
47+
testDir.close();
48+
}
49+
}
50+
51+
@BeforeAll
52+
static void setupEverything() {
53+
Awaitility.setDefaultTimeout(TestHelper.NORMAL_WAIT);
54+
}
55+
56+
@Test
57+
void noRescannerPreservesIntegrityOfChildWatches() throws IOException {
58+
var checkCreatedFiles = false;
59+
// By setting `checkCreatedFiles` to `false`, this test *does* check
60+
// that the integrity of the internal tree of watches is preserved, but
61+
// it *doesn't* check if `CREATED` events for files are missed. Such
62+
// events could happen between the creation of a directory and the start
63+
// of the watch for that directory. Without an (auto-)handler for
64+
// `OVERFLOW` events, these aren't observed by the watch. The other
65+
// tests in this class, which do use auto-handling for `OVERFLOW`
66+
// events, set `checkCreatedFiles` to `true`.
67+
rescannerPreservesIntegrity(
68+
(path, exec) -> (w, e) -> {},
69+
checkCreatedFiles);
70+
}
71+
72+
@Test
73+
void memorylessRescannerPreservesIntegrity() throws IOException {
74+
rescannerPreservesIntegrity((path, exec) ->
75+
new MemorylessRescanner(exec));
76+
}
77+
78+
@Test
79+
void indexingRescannerPreservesIntegrity() throws IOException {
80+
rescannerPreservesIntegrity((path, exec) ->
81+
new IndexingRescanner(exec, path, WatchScope.PATH_AND_ALL_DESCENDANTS));
82+
}
83+
84+
private void rescannerPreservesIntegrity(BiFunction<Path, Executor, BiConsumer<EventHandlingWatch, WatchEvent>> newRescanner) throws IOException {
85+
rescannerPreservesIntegrity(newRescanner, true);
86+
}
87+
88+
private void rescannerPreservesIntegrity(BiFunction<Path, Executor, BiConsumer<EventHandlingWatch, WatchEvent>> newRescanner, boolean checkCreatedFiles) throws IOException {
89+
var root = testDir.getTestDirectory();
90+
var exec = ForkJoinPool.commonPool();
91+
92+
var events = ConcurrentHashMap.<WatchEvent> newKeySet(); // Stores all incoming events
93+
var rescanner = newRescanner.apply(root, exec);
94+
var watch = new JDKFileTreeWatch(root, exec, (w, e) -> {
95+
events.add(e);
96+
rescanner.accept(w, e);
97+
});
98+
99+
watch.open();
100+
101+
try {
102+
var parent = Path.of("");
103+
var child1 = Path.of("foo");
104+
var child2 = Path.of("bar");
105+
var grandGrandGrandChild = Path.of("bar", "x", "y", "z");
106+
107+
var family = new Path[] {
108+
parent, child1, child2, grandGrandGrandChild };
109+
110+
// Define helper function
111+
Function<String, List<Path>> createFiles = fileName -> {
112+
try {
113+
var files = Stream.of(family)
114+
.map(p -> p.resolve(fileName))
115+
.collect(Collectors.toList());
116+
for (var f : files) {
117+
Files.createFile(root.resolve(f));
118+
}
119+
return files; // Paths relative to `parent`
120+
} catch (Exception e) {
121+
throw new RuntimeException(e);
122+
}
123+
};
124+
125+
// Define helper predicate
126+
BiPredicate<WatchEvent.Kind, Path> eventsContains = (kind, relative) ->
127+
events.stream().anyMatch(e ->
128+
e.getKind().equals(kind) &&
129+
e.getRootPath().equals(root) &&
130+
e.getRelativePath().equals(relative));
131+
132+
// Part 1: Create subdirectories. Changes in both the root and in
133+
// the descendants should be observed by the watch.
134+
Files.createDirectories(root.resolve(child1));
135+
Files.createDirectories(root.resolve(child2));
136+
Files.createDirectories(root.resolve(grandGrandGrandChild));
137+
138+
for (var p : family) {
139+
await("Watch should exist for `" + p + "`")
140+
.until(() -> p == parent || getDescendantWatch(watch, p) != null);
141+
}
142+
for (var file : createFiles.apply("file1.txt")) {
143+
await("Creation of `" + file + "` should be observed (events: " + events + ")")
144+
.until(() -> !checkCreatedFiles || eventsContains.test(Kind.CREATED, file));
145+
}
146+
147+
// Part 2: Artificially remove child watches. Changes in the root
148+
// should still be observed by the watch, but changes in the
149+
// descendants shouldn't.
150+
getChildWatches(watch).remove(child1).close();
151+
getChildWatches(watch).remove(child2).close(); // Should also remove and close the watch for `grandGrandGrandChild`
152+
153+
for (var p : family) {
154+
await("Watch shouldn't exist for `" + p + "`")
155+
.until(() -> p == parent || getDescendantWatch(watch, p) == null);
156+
}
157+
for (var file : createFiles.apply("file2.txt")) {
158+
await("Creation of `" + file + "` shouldn't be observed")
159+
.until(() ->
160+
!checkCreatedFiles ||
161+
file.equals(Path.of("file2.txt")) ||
162+
!eventsContains.test(Kind.CREATED, file));
163+
}
164+
165+
// Part 3: Trigger overflow to restore child watches. Changes in
166+
// both the root and in the descendants should be observed by the
167+
// watch.
168+
var overflow = new WatchEvent(WatchEvent.Kind.OVERFLOW, root);
169+
watch.handleEvent(overflow);
170+
171+
for (var p : family) {
172+
await("Watch should exist for `" + p + "`")
173+
.until(() -> p == parent || getDescendantWatch(watch, p) != null);
174+
}
175+
for (var file : createFiles.apply("file3.txt")) {
176+
await("Creation of `" + file + "` should be observed")
177+
.until(() -> !checkCreatedFiles || eventsContains.test(Kind.CREATED, file));
178+
}
179+
}
180+
finally {
181+
watch.close();
182+
}
183+
}
184+
185+
private static @Nullable JDKFileTreeWatch getDescendantWatch(JDKFileTreeWatch rootWatch, Path descendant) {
186+
assert !descendant.equals(Path.of(""));
187+
var child = descendant.getFileName();
188+
var parent = descendant.getParent();
189+
if (parent == null) {
190+
return getChildWatches(rootWatch).get(child);
191+
} else {
192+
var parentWatch = getDescendantWatch(rootWatch, parent);
193+
if (parentWatch == null) {
194+
return null;
195+
}
196+
return getChildWatches(parentWatch).get(child);
197+
}
198+
}
199+
200+
@SuppressWarnings("unchecked")
201+
private static Map<Path, JDKFileTreeWatch> getChildWatches(JDKFileTreeWatch watch) {
202+
try {
203+
var field = JDKFileTreeWatch.class.getDeclaredField("childWatches");
204+
field.setAccessible(true);
205+
return (Map<Path, JDKFileTreeWatch>) field.get(watch);
206+
} catch (Exception e) {
207+
throw new RuntimeException(e);
208+
}
209+
}
210+
}

0 commit comments

Comments
 (0)