Skip to content

Commit d569c50

Browse files
authored
Merge pull request #26 from SWAT-engineering/improved-overflow-support/indexing-rescanner
Improved overflow support: Indexing rescanner
2 parents 2aa7db2 + 493165d commit d569c50

File tree

5 files changed

+400
-55
lines changed

5 files changed

+400
-55
lines changed

src/main/java/engineering/swat/watch/Watcher.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import engineering.swat.watch.impl.jdk.JDKDirectoryWatch;
4343
import engineering.swat.watch.impl.jdk.JDKFileWatch;
4444
import engineering.swat.watch.impl.jdk.JDKRecursiveDirectoryWatch;
45+
import engineering.swat.watch.impl.overflows.IndexingRescanner;
4546
import engineering.swat.watch.impl.overflows.MemorylessRescanner;
4647

4748
/**
@@ -213,6 +214,8 @@ private BiConsumer<EventHandlingWatch, WatchEvent> applyApproximateOnOverflow()
213214
return eventHandler;
214215
case ALL:
215216
return eventHandler.andThen(new MemorylessRescanner(executor));
217+
case DIRTY:
218+
return eventHandler.andThen(new IndexingRescanner(executor, path, scope));
216219
default:
217220
throw new UnsupportedOperationException("No event handler has been defined yet for this overflow policy");
218221
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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 java.io.IOException;
30+
import java.nio.file.FileVisitOption;
31+
import java.nio.file.FileVisitResult;
32+
import java.nio.file.Files;
33+
import java.nio.file.Path;
34+
import java.nio.file.SimpleFileVisitor;
35+
import java.util.EnumSet;
36+
37+
import org.apache.logging.log4j.LogManager;
38+
import org.apache.logging.log4j.Logger;
39+
40+
import engineering.swat.watch.WatchEvent;
41+
import engineering.swat.watch.WatchScope;
42+
43+
/**
44+
* Base extension of {@link SimpleFileVisitor}, intended to be further
45+
* specialized by subclasses to auto-handle {@link WatchEvent.Kind#OVERFLOW}
46+
* events. In particular, method {@link #walkFileTree} of this class internally
47+
* calls {@link Files#walkFileTree} to visit the file tree that starts at
48+
* {@link #path}, with a maximum depth inferred from {@link #scope}. Subclasses
49+
* can be specialized, for instance, to generate synthetic events or index a
50+
* file tree.
51+
*/
52+
public class BaseFileVisitor extends SimpleFileVisitor<Path> {
53+
private final Logger logger = LogManager.getLogger();
54+
protected final Path path;
55+
protected final WatchScope scope;
56+
57+
public BaseFileVisitor(Path path, WatchScope scope) {
58+
this.path = path;
59+
this.scope = scope;
60+
}
61+
62+
public void walkFileTree() {
63+
var options = EnumSet.noneOf(FileVisitOption.class);
64+
var maxDepth = scope == WatchScope.PATH_AND_ALL_DESCENDANTS ? Integer.MAX_VALUE : 1;
65+
try {
66+
Files.walkFileTree(path, options, maxDepth, this);
67+
} catch (IOException e) {
68+
logger.error("Could not walk: {} ({})", path, e);
69+
}
70+
}
71+
72+
// -- SimpleFileVisitor --
73+
74+
@Override
75+
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
76+
logger.error("Could not walk regular file: {} ({})", file, exc);
77+
return FileVisitResult.CONTINUE;
78+
}
79+
80+
@Override
81+
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
82+
if (exc != null) {
83+
logger.error("Could not walk directory: {} ({})", dir, exc);
84+
}
85+
return FileVisitResult.CONTINUE;
86+
}
87+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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 java.io.IOException;
30+
import java.nio.file.FileVisitResult;
31+
import java.nio.file.Files;
32+
import java.nio.file.Path;
33+
import java.nio.file.attribute.BasicFileAttributes;
34+
import java.nio.file.attribute.FileTime;
35+
import java.util.HashSet;
36+
import java.util.Map;
37+
import java.util.Set;
38+
import java.util.concurrent.ConcurrentHashMap;
39+
import java.util.concurrent.Executor;
40+
41+
import org.apache.logging.log4j.LogManager;
42+
import org.apache.logging.log4j.Logger;
43+
44+
import engineering.swat.watch.WatchEvent;
45+
import engineering.swat.watch.WatchScope;
46+
import engineering.swat.watch.impl.EventHandlingWatch;
47+
48+
public class IndexingRescanner extends MemorylessRescanner {
49+
private final Logger logger = LogManager.getLogger();
50+
private final Map<Path, FileTime> index = new ConcurrentHashMap<>();
51+
52+
public IndexingRescanner(Executor exec, Path path, WatchScope scope) {
53+
super(exec);
54+
new Indexer(path, scope).walkFileTree(); // Make an initial scan to populate the index
55+
}
56+
57+
private class Indexer extends BaseFileVisitor {
58+
public Indexer(Path path, WatchScope scope) {
59+
super(path, scope);
60+
}
61+
62+
@Override
63+
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
64+
if (!path.equals(dir)) {
65+
index.put(dir, attrs.lastModifiedTime());
66+
}
67+
return FileVisitResult.CONTINUE;
68+
}
69+
70+
@Override
71+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
72+
index.put(file, attrs.lastModifiedTime());
73+
return FileVisitResult.CONTINUE;
74+
}
75+
}
76+
77+
// -- MemorylessRescanner --
78+
79+
@Override
80+
protected MemorylessRescanner.Generator newGenerator(Path path, WatchScope scope) {
81+
return new Generator(path, scope);
82+
}
83+
84+
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<>();
89+
90+
public Generator(Path path, WatchScope scope) {
91+
super(path, scope);
92+
}
93+
94+
// -- MemorylessRescanner.Generator --
95+
96+
@Override
97+
protected void generateEvents(Path path, BasicFileAttributes attrs) {
98+
visited.add(path);
99+
var lastModifiedTimeOld = index.get(path);
100+
var lastModifiedTimeNew = attrs.lastModifiedTime();
101+
102+
// The path isn't indexed yet
103+
if (lastModifiedTimeOld == null) {
104+
super.generateEvents(path, attrs);
105+
}
106+
107+
// The path is already indexed, and the previous last-modified-time
108+
// is older than the current last-modified-time
109+
else if (lastModifiedTimeOld.compareTo(lastModifiedTimeNew) < 0) {
110+
events.add(new WatchEvent(WatchEvent.Kind.MODIFIED, path));
111+
}
112+
}
113+
114+
@Override
115+
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)) {
120+
for (var p : index.keySet()) {
121+
if (p.startsWith(path) && !visited.contains(p)) {
122+
events.add(new WatchEvent(WatchEvent.Kind.DELETED, p));
123+
}
124+
}
125+
}
126+
return super.postVisitDirectory(dir, exc);
127+
}
128+
}
129+
130+
// -- MemorylessRescanner --
131+
132+
@Override
133+
public void accept(EventHandlingWatch watch, WatchEvent event) {
134+
// Auto-handle `OVERFLOW` events
135+
super.accept(watch, event);
136+
137+
// Additional processing is needed to update the index when `CREATED`,
138+
// `MODIFIED`, and `DELETED` events happen.
139+
var kind = event.getKind();
140+
var fullPath = event.calculateFullPath();
141+
switch (kind) {
142+
case CREATED:
143+
case MODIFIED:
144+
try {
145+
var lastModifiedTimeNew = Files.getLastModifiedTime(fullPath);
146+
var lastModifiedTimeOld = index.put(fullPath, lastModifiedTimeNew);
147+
148+
// If a `MODIFIED` event happens for a path that wasn't in
149+
// the index yet, then a `CREATED` event has somehow been
150+
// missed. Just in case, it's issued synthetically here.
151+
if (lastModifiedTimeOld == null && kind == WatchEvent.Kind.MODIFIED) {
152+
var created = new WatchEvent(WatchEvent.Kind.CREATED, fullPath);
153+
watch.handleEvent(created);
154+
}
155+
} catch (IOException e) {
156+
logger.error("Could not get modification time of: {} ({})", fullPath, e);
157+
}
158+
break;
159+
case DELETED:
160+
index.remove(fullPath);
161+
break;
162+
case OVERFLOW: // Already auto-handled above
163+
break;
164+
}
165+
}
166+
}

0 commit comments

Comments
 (0)