Skip to content

Commit 546fbe8

Browse files
authored
Merge pull request #24 from SWAT-engineering/improved-overflow-support/first-overflow-policy
Improved overflow support: Overflow auto-handling for directory watches (non-recursive)
2 parents a712a3a + 9e71ad7 commit 546fbe8

File tree

9 files changed

+361
-9
lines changed

9 files changed

+361
-9
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,9 @@ public interface ActiveWatch extends Closeable {
4040
* Gets the path watched by this watch.
4141
*/
4242
Path getPath();
43+
44+
/**
45+
* Gets the scope of this watch.
46+
*/
47+
WatchScope getScope();
4348
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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;
28+
29+
/**
30+
* Constants to indicate for which regular files/directories in the scope of the
31+
* watch an <i>approximation</i> of synthetic events (of kinds
32+
* {@link WatchEvent.Kind#CREATED}, {@link WatchEvent.Kind#MODIFIED}, and/or
33+
* {@link WatchEvent.Kind#DELETED}) should be issued when an overflow event
34+
* happens. These synthetic events, as well as the overflow event itself, are
35+
* subsequently passed to the user-defined event handler of the watch.
36+
* Typically, the user-defined event handler can ignore the original overflow
37+
* event (i.e., handling the synthetic events is sufficient to address the
38+
* overflow issue), but it doesn't have to (e.g., it may carry out additional
39+
* overflow bookkeeping).
40+
*/
41+
public enum OnOverflow {
42+
43+
/**
44+
* Synthetic events are issued for <b>no regular files/directories</b> in
45+
* the scope of the watch. Thus, the user-defined event handler is fully
46+
* responsible to handle overflow events.
47+
*/
48+
NONE,
49+
50+
/**
51+
* <p>
52+
* Synthetic events of kinds {@link WatchEvent.Kind#CREATED} and
53+
* {@link WatchEvent.Kind#MODIFIED}, but not
54+
* {@link WatchEvent.Kind#DELETED}, are issued for all regular
55+
* files/directories in the scope of the watch. Specifically, when an
56+
* overflow event happens:
57+
*
58+
* <ul>
59+
* <li>CREATED events are issued for all regular files/directories
60+
* (overapproximation).
61+
* <li>MODIFIED events are issued for all non-empty, regular files
62+
* (overapproximation) but for no directories (underapproximation).
63+
* <li>DELETED events are issued for no regular files/directories
64+
* (underapproximation).
65+
* </ul>
66+
*
67+
* <p>
68+
* This approach is relatively cheap in terms of memory usage (cf.
69+
* {@link #DIRTY}), but it results in a large over/underapproximation of the
70+
* actual events (cf. DIRTY).
71+
*/
72+
ALL,
73+
74+
75+
/**
76+
* <p>
77+
* Synthetic events of kinds {@link WatchEvent.Kind#CREATED},
78+
* {@link WatchEvent.Kind#MODIFIED}, and {@link WatchEvent.Kind#DELETED} are
79+
* issued for dirty regular files/directories in the scope of the watch, as
80+
* determined using <i>last-modified-times</i>. Specifically, when an
81+
* overflow event happens:
82+
*
83+
* <ul>
84+
* <li>CREATED events are issued for all regular files/directories when the
85+
* previous last-modified-time is unknown, but the current
86+
* last-modified-time is known (i.e., the file started existing).
87+
* <li>MODIFIED events are issued for all regular files/directories when the
88+
* previous last-modified-time is before the current last-modified-time.
89+
* <li>DELETED events are issued for all regular files/directories when the
90+
* previous last-modified-time is known, but the current
91+
* last-modified-time is unknown (i.e., the file stopped existing).
92+
* </ul>
93+
*
94+
* <p>
95+
* To keep track of last-modified-times, an internal <i>index</i> is
96+
* populated with last-modified-times of all regular files/directories in
97+
* the scope of the watch when the watch is started. Each time when any
98+
* event happens, the index is updated accordingly, so when an overflow
99+
* event happens, last-modified-times can be compared as described above.
100+
*
101+
* <p>
102+
* This approach results in a small overapproximation (cf. {@link #ALL}),
103+
* but it is relatively expensive in terms of memory usage (cf. ALL), as the
104+
* watch needs to keep track of last-modified-times.
105+
*/
106+
DIRTY
107+
}

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

Lines changed: 36 additions & 7 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.MemorylessRescanner;
4546

4647
/**
4748
* <p>Watch a path for changes.</p>
@@ -52,17 +53,17 @@
5253
*/
5354
public class Watcher {
5455
private final Logger logger = LogManager.getLogger();
55-
private final WatchScope scope;
5656
private final Path path;
57+
private final WatchScope scope;
58+
private volatile OnOverflow approximateOnOverflow = OnOverflow.ALL;
5759
private volatile Executor executor = CompletableFuture::runAsync;
5860

5961
private static final BiConsumer<EventHandlingWatch, WatchEvent> EMPTY_HANDLER = (w, e) -> {};
6062
private volatile BiConsumer<EventHandlingWatch, WatchEvent> eventHandler = EMPTY_HANDLER;
6163

62-
63-
private Watcher(WatchScope scope, Path path) {
64-
this.scope = scope;
64+
private Watcher(Path path, WatchScope scope) {
6565
this.path = path;
66+
this.scope = scope;
6667
}
6768

6869
/**
@@ -89,9 +90,8 @@ public static Watcher watch(Path path, WatchScope scope) {
8990
break;
9091
default:
9192
throw new IllegalArgumentException("Unsupported scope: " + scope);
92-
9393
}
94-
return new Watcher(scope, path);
94+
return new Watcher(path, scope);
9595
}
9696

9797
/**
@@ -148,6 +148,22 @@ public Watcher withExecutor(Executor callbackHandler) {
148148
return this;
149149
}
150150

151+
/**
152+
* Optionally configure which regular files/directories in the scope of the
153+
* watch an <i>approximation</i> of synthetic events (of kinds
154+
* {@link WatchEvent.Kind#CREATED}, {@link WatchEvent.Kind#MODIFIED}, and/or
155+
* {@link WatchEvent.Kind#DELETED}) should be issued when an overflow event
156+
* happens. If not defined before this watcher is started, the
157+
* {@link engineering.swat.watch.OnOverflow#ALL} approach will be used.
158+
* @param whichFiles Constant to indicate for which regular
159+
* files/directories to approximate
160+
* @return This watcher for optional method chaining
161+
*/
162+
public Watcher approximate(OnOverflow whichFiles) {
163+
this.approximateOnOverflow = whichFiles;
164+
return this;
165+
}
166+
151167
/**
152168
* Start watch the path for events.
153169
* @return a subscription for the watch, when closed, new events will stop being registered to the worker pool.
@@ -159,9 +175,11 @@ public ActiveWatch start() throws IOException {
159175
throw new IllegalStateException("There is no onEvent handler defined");
160176
}
161177

178+
var h = applyApproximateOnOverflow();
179+
162180
switch (scope) {
163181
case PATH_AND_CHILDREN: {
164-
var result = new JDKDirectoryWatch(path, executor, eventHandler, false);
182+
var result = new JDKDirectoryWatch(path, executor, h);
165183
result.open();
166184
return result;
167185
}
@@ -188,4 +206,15 @@ public ActiveWatch start() throws IOException {
188206
throw new IllegalStateException("Not supported yet");
189207
}
190208
}
209+
210+
private BiConsumer<EventHandlingWatch, WatchEvent> applyApproximateOnOverflow() {
211+
switch (approximateOnOverflow) {
212+
case NONE:
213+
return eventHandler;
214+
case ALL:
215+
return eventHandler.andThen(new MemorylessRescanner(executor));
216+
default:
217+
throw new UnsupportedOperationException("No event handler has been defined yet for this overflow policy");
218+
}
219+
}
191220
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
3939

4040
import engineering.swat.watch.WatchEvent;
41+
import engineering.swat.watch.WatchScope;
4142
import engineering.swat.watch.impl.EventHandlingWatch;
4243
import engineering.swat.watch.impl.util.BundledSubscription;
4344
import engineering.swat.watch.impl.util.SubscriptionKey;
@@ -74,6 +75,11 @@ private void handleJDKEvents(List<java.nio.file.WatchEvent<?>> events) {
7475

7576
// -- JDKBaseWatch --
7677

78+
@Override
79+
public WatchScope getScope() {
80+
return nativeRecursive ? WatchScope.PATH_AND_ALL_DESCENDANTS : WatchScope.PATH_AND_CHILDREN;
81+
}
82+
7783
@Override
7884
public synchronized void close() throws IOException {
7985
if (bundledJDKWatcher != null) {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.checkerframework.checker.nullness.qual.Nullable;
3737

3838
import engineering.swat.watch.WatchEvent;
39+
import engineering.swat.watch.WatchScope;
3940
import engineering.swat.watch.impl.EventHandlingWatch;
4041

4142
/**
@@ -73,6 +74,11 @@ private static Path requireNonNull(@Nullable Path p, String message) {
7374

7475
// -- JDKBaseWatch --
7576

77+
@Override
78+
public WatchScope getScope() {
79+
return WatchScope.PATH_ONLY;
80+
}
81+
7682
@Override
7783
public void handleEvent(WatchEvent event) {
7884
internal.handleEvent(event);

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.apache.logging.log4j.Logger;
4949

5050
import engineering.swat.watch.WatchEvent;
51+
import engineering.swat.watch.WatchScope;
5152
import engineering.swat.watch.impl.EventHandlingWatch;
5253

5354
public class JDKRecursiveDirectoryWatch extends JDKBaseWatch {
@@ -298,6 +299,11 @@ private void detectedMissingEntries(Path dir, ArrayList<WatchEvent> events, Hash
298299

299300
// -- JDKBaseWatch --
300301

302+
@Override
303+
public WatchScope getScope() {
304+
return WatchScope.PATH_AND_ALL_DESCENDANTS;
305+
}
306+
301307
@Override
302308
public void handleEvent(WatchEvent event) {
303309
processEvents(event);

0 commit comments

Comments
 (0)