Skip to content

Commit 3f80e77

Browse files
committed
Merge branch 'improved-overflow-support-main' into improved-overflow-support/jdk-file-tree-watch
2 parents 6feac60 + 9b0ba39 commit 3f80e77

File tree

3 files changed

+138
-11
lines changed

3 files changed

+138
-11
lines changed

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

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import java.nio.file.Path;
3333
import java.nio.file.attribute.BasicFileAttributes;
3434
import java.nio.file.attribute.FileTime;
35+
import java.util.ArrayDeque;
36+
import java.util.Deque;
3537
import java.util.HashSet;
3638
import java.util.Map;
3739
import java.util.Set;
@@ -82,20 +84,29 @@ protected MemorylessRescanner.Generator newGenerator(Path path, WatchScope scope
8284
}
8385

8486
protected class Generator extends MemorylessRescanner.Generator {
85-
// Field to keep track of the paths that are visited during the current
86-
// rescan. After the visit, the `DELETED` events that happened since the
87-
// previous rescan can be approximated.
88-
private Set<Path> visited = new HashSet<>();
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.
92+
private final Deque<Set<Path>> visited = new ArrayDeque<>();
8993

9094
public Generator(Path path, WatchScope scope) {
9195
super(path, scope);
96+
this.visited.push(new HashSet<>()); // Initial set for content of `path`
97+
}
98+
99+
private <T> void addToPeeked(Deque<Set<T>> deque, T t) {
100+
var peeked = deque.peek();
101+
if (peeked != null) {
102+
peeked.add(t);
103+
}
92104
}
93105

94106
// -- MemorylessRescanner.Generator --
95107

96108
@Override
97109
protected void generateEvents(Path path, BasicFileAttributes attrs) {
98-
visited.add(path);
99110
var lastModifiedTimeOld = index.get(path);
100111
var lastModifiedTimeNew = attrs.lastModifiedTime();
101112

@@ -111,14 +122,26 @@ else if (lastModifiedTimeOld.compareTo(lastModifiedTimeNew) < 0) {
111122
}
112123
}
113124

125+
@Override
126+
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
127+
addToPeeked(visited, dir);
128+
visited.push(new HashSet<>());
129+
return super.preVisitDirectory(dir, attrs);
130+
}
131+
132+
@Override
133+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
134+
addToPeeked(visited, file);
135+
return super.visitFile(file, attrs);
136+
}
137+
114138
@Override
115139
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
116-
// If the visitor is back at the root of the rescan, then the time
117-
// is right to issue `DELETED` events based on the set of `visited`
118-
// paths.
119-
if (dir.equals(path)) {
140+
// Issue `DELETED` events based on the set of paths visited in `dir`
141+
var visitedInDir = visited.pop();
142+
if (visitedInDir != null) {
120143
for (var p : index.keySet()) {
121-
if (p.startsWith(path) && !visited.contains(p)) {
144+
if (dir.equals(p.getParent()) && !visitedInDir.contains(p)) {
122145
events.add(new WatchEvent(WatchEvent.Kind.DELETED, p));
123146
}
124147
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ public class TestDirectory implements Closeable {
4141
private final Path testDirectory;
4242
private final List<Path> testFiles;
4343

44-
4544
public TestDirectory() throws IOException {
4645
testDirectory = Files.createTempDirectory("java-watch-test");
4746
List<Path> testFiles = new ArrayList<>();
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* BSD 2-Clause License
3+
*
4+
* Copyright (c) 2023, Swat.engineering
5+
*
6+
* Redistribution and use in source and binary forms, with or without
7+
* modification, are permitted provided that the following conditions are met:
8+
*
9+
* 1. Redistributions of source code must retain the above copyright notice, this
10+
* list of conditions and the following disclaimer.
11+
*
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
package engineering.swat.watch.impl.overflows;
28+
29+
import static org.awaitility.Awaitility.await;
30+
31+
import java.io.IOException;
32+
import java.util.concurrent.ForkJoinPool;
33+
import java.util.concurrent.atomic.AtomicBoolean;
34+
35+
import org.awaitility.Awaitility;
36+
import org.junit.jupiter.api.AfterEach;
37+
import org.junit.jupiter.api.BeforeAll;
38+
import org.junit.jupiter.api.BeforeEach;
39+
import org.junit.jupiter.api.Test;
40+
41+
import engineering.swat.watch.OnOverflow;
42+
import engineering.swat.watch.TestDirectory;
43+
import engineering.swat.watch.TestHelper;
44+
import engineering.swat.watch.WatchEvent;
45+
import engineering.swat.watch.WatchScope;
46+
import engineering.swat.watch.Watcher;
47+
import engineering.swat.watch.impl.EventHandlingWatch;
48+
49+
class IndexingRescannerTests {
50+
51+
private TestDirectory testDir;
52+
53+
@BeforeEach
54+
void setup() throws IOException {
55+
testDir = new TestDirectory();
56+
}
57+
58+
@AfterEach
59+
void cleanup() {
60+
if (testDir != null) {
61+
testDir.close();
62+
}
63+
}
64+
65+
@BeforeAll
66+
static void setupEverything() {
67+
Awaitility.setDefaultTimeout(TestHelper.NORMAL_WAIT);
68+
}
69+
70+
@Test
71+
void onlyEventsForFilesInScopeAreIssued() throws IOException, InterruptedException {
72+
var path = testDir.getTestDirectory();
73+
74+
// Configure a non-recursive directory watch that monitors only the
75+
// children (not all descendants) of `path`
76+
var eventsOnlyForChildren = new AtomicBoolean(true);
77+
var watchConfig = Watcher.watch(path, WatchScope.PATH_AND_CHILDREN)
78+
.approximate(OnOverflow.NONE) // Disable the auto-handler here; we'll have an explicit one below
79+
.on(e -> {
80+
if (e.getRelativePath().getNameCount() > 1) {
81+
eventsOnlyForChildren.set(false);
82+
}
83+
});
84+
85+
try (var watch = (EventHandlingWatch) watchConfig.start()) {
86+
// Create a rescanner that initially indexes all descendants (not
87+
// only the children) of `path`. The resulting initial index is an
88+
// overestimation of the files monitored by the watch.
89+
var rescanner = new IndexingRescanner(
90+
ForkJoinPool.commonPool(), path,
91+
WatchScope.PATH_AND_ALL_DESCENDANTS);
92+
93+
// Trigger a rescan. Because only the children (not all descendants)
94+
// of `path` are watched, the rescan should issue events only for
95+
// those children (even though the initial index contains entries
96+
// for all descendants).
97+
var overflow = new WatchEvent(WatchEvent.Kind.OVERFLOW, path);
98+
rescanner.accept(watch, overflow);
99+
Thread.sleep(TestHelper.SHORT_WAIT.toMillis());
100+
101+
await("No events for non-children descendants should have been issued")
102+
.until(eventsOnlyForChildren::get);
103+
}
104+
}
105+
}

0 commit comments

Comments
 (0)