Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d51d88f
Update torture test to ignore overflows (they're auto-handled)
sungshik Mar 10, 2025
671d5eb
Add `JDKFileTreeWatch`
sungshik Mar 10, 2025
62f4b1c
Move `updateChildWatches` to inner class
sungshik Mar 10, 2025
36c4299
Merge branch 'improved-overflow-support-main' into improved-overflow-…
sungshik Mar 11, 2025
47b8a10
Use `computeIfAbsent` instead of `putIfAbsent`
sungshik Mar 11, 2025
500e238
Update `JDKRecursiveDirectoryWatch`
sungshik Mar 11, 2025
6a328bf
Add import
sungshik Mar 11, 2025
4aed091
Improve code quality of `JDKFileTreeWatch`
sungshik Mar 12, 2025
71ac833
Improve code quality of `JDKFileTreeWatch`
sungshik Mar 12, 2025
fa65b30
Add mechanism to avoid relativization in `JDKFileTreeWatch`
sungshik Mar 12, 2025
fdd24f8
Simplify relativization of paths in `JDKFileTreeWatch`
sungshik Mar 14, 2025
3bdafe6
Change order of closing internal/child watches in `JDKFileTreeWatch`
sungshik Mar 14, 2025
4582d23
Simplify relativization of paths in `JDKFileTreeWatch`
sungshik Mar 14, 2025
6a7df86
Use file names to store child watches (instead of full paths)
sungshik Mar 14, 2025
e676b5f
Use `JDKFileTreeWatch`
sungshik Mar 14, 2025
387e7c3
Add asynchronous bookkeeping of `CREATED` and `OVERFLOW` events
sungshik Mar 14, 2025
748e8ac
Fix issue that `JDKFileTreeWatch` relied on overflow handling to pres…
sungshik Mar 18, 2025
4a1423b
Add license
sungshik Mar 18, 2025
1ab8f29
Make the child watches updater asynchronous
sungshik Mar 18, 2025
385db76
Add code to close child watches when their directories no longer exis…
sungshik Mar 18, 2025
58d9561
Remove `JDKRecursiveDirectoryWatch` (replaced by `JDKFileTreeWatch`)
sungshik Mar 25, 2025
c96c943
Add filtering mechanism to `Watcher` and `JDK...` classes
sungshik Mar 25, 2025
eca305b
Move method implementation from base class to subclass (was already o…
sungshik Mar 25, 2025
408c9d7
Improve logic to close `JDK...Watch` classes (avoid event handling o…
sungshik Mar 25, 2025
e0f039d
Fix a few relativization issues in `JDKFileTreeWatch` and `IndexingRe…
sungshik Mar 25, 2025
b8adb45
Add event filter to test
sungshik Mar 25, 2025
bbd1d39
Add test to check if overflows are recoverd from
sungshik Mar 25, 2025
02b10b7
Fix JavaDoc
sungshik Mar 25, 2025
76ec380
Remove old test
sungshik Mar 25, 2025
2cc3c66
Remove `trySleep` helpers
sungshik Mar 26, 2025
b760db9
Rename method to better convey intent
sungshik Mar 26, 2025
9b58bc4
Revert change to `relativize` in `JDKFileTreeWatch` (and add comment …
sungshik Mar 26, 2025
84b627b
Move closed check to `handleEvent`
sungshik Mar 26, 2025
e53569a
Add general `handleEvent` implementation back to the base watch
sungshik Mar 26, 2025
6feac60
Fix race in closing child watches
sungshik Mar 26, 2025
3f80e77
Merge branch 'improved-overflow-support-main' into improved-overflow-…
sungshik Mar 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/main/java/engineering/swat/watch/WatchEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,16 @@
private final Path rootPath;
private final Path relativePath;

private static final Path EMPTY_PATH = Path.of("");

public WatchEvent(Kind kind, Path rootPath) {
this(kind, rootPath, null);
}

public WatchEvent(Kind kind, Path rootPath, @Nullable Path relativePath) {
this.kind = kind;
this.rootPath = rootPath;
this.relativePath = relativePath == null ? Path.of("") : relativePath;
this.relativePath = relativePath == null ? EMPTY_PATH : relativePath;
}

public Kind getKind() {
Expand Down Expand Up @@ -105,6 +107,20 @@
return rootPath.resolve(relativePath);
}

/**
* @return The file name of the full path of this event, or {@code null} if
* it has zero elements (cf. {@link Path#getFileName()}), but without
* calculating the full path. This method is equivalent to, but more
* efficient than, {@code calculateFullPath().getFileName()}.
*/
public @Nullable Path getFileName() {
var fileName = relativePath.getFileName();
if (fileName == null || fileName.equals(EMPTY_PATH)) {
fileName = rootPath.getFileName();

Check warning on line 119 in src/main/java/engineering/swat/watch/WatchEvent.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/WatchEvent.java#L119

Added line #L119 was not covered by tests
}
return fileName;
}

@Override
public String toString() {
return String.format("WatchEvent[%s, %s, %s]", this.rootPath, this.kind, this.relativePath);
Expand Down
29 changes: 24 additions & 5 deletions src/main/java/engineering/swat/watch/Watcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,15 @@
import java.util.concurrent.Executor;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import engineering.swat.watch.impl.EventHandlingWatch;
import engineering.swat.watch.impl.jdk.JDKDirectoryWatch;
import engineering.swat.watch.impl.jdk.JDKFileTreeWatch;
import engineering.swat.watch.impl.jdk.JDKFileWatch;
import engineering.swat.watch.impl.jdk.JDKRecursiveDirectoryWatch;
import engineering.swat.watch.impl.overflows.IndexingRescanner;
import engineering.swat.watch.impl.overflows.MemorylessRescanner;

Expand All @@ -61,6 +62,8 @@

private static final BiConsumer<EventHandlingWatch, WatchEvent> EMPTY_HANDLER = (w, e) -> {};
private volatile BiConsumer<EventHandlingWatch, WatchEvent> eventHandler = EMPTY_HANDLER;
private static final Predicate<WatchEvent> TRUE_FILTER = e -> true;
private volatile Predicate<WatchEvent> eventFilter = TRUE_FILTER;

private Watcher(Path path, WatchScope scope) {
this.path = path;
Expand Down Expand Up @@ -138,6 +141,22 @@
return this;
}

/**
* Configures the event filter to determine which events should be passed to
* the event handler. By default (without calling this method), all events
* are passed. This method must be called at most once.
* @param predicate The predicate to determine an event should be kept
* ({@code true}) or dropped ({@code false})
* @return {@code this} (to support method chaining)
*/
Watcher filter(Predicate<WatchEvent> predicate) {
if (this.eventFilter != TRUE_FILTER) {
throw new IllegalArgumentException("filter cannot be set more than once");

Check warning on line 154 in src/main/java/engineering/swat/watch/Watcher.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L154 was not covered by tests
}
this.eventFilter = predicate;
return this;
}

/**
* Optionally configure the executor in which the {@link #on(Consumer)} callbacks are scheduled.
* If not defined, every task will be scheduled on the {@link java.util.concurrent.ForkJoinPool#commonPool()}.
Expand Down Expand Up @@ -180,26 +199,26 @@

switch (scope) {
case PATH_AND_CHILDREN: {
var result = new JDKDirectoryWatch(path, executor, h);
var result = new JDKDirectoryWatch(path, executor, h, eventFilter);
result.open();
return result;
}
case PATH_AND_ALL_DESCENDANTS: {
try {
var result = new JDKDirectoryWatch(path, executor, eventHandler, true);
var result = new JDKDirectoryWatch(path, executor, h, eventFilter, true);
result.open();
return result;
} catch (Throwable ex) {
// no native support, use the simulation
logger.debug("Not possible to register the native watcher, using fallback for {}", path);
logger.trace(ex);
var result = new JDKRecursiveDirectoryWatch(path, executor, eventHandler);
var result = new JDKFileTreeWatch(path, executor, h, eventFilter);
result.open();
return result;
}
}
case PATH_ONLY: {
var result = new JDKFileWatch(path, executor, h);
var result = new JDKFileWatch(path, executor, h, eventFilter);
result.open();
return result;
}
Expand Down
15 changes: 8 additions & 7 deletions src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.Predicate;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand All @@ -46,12 +47,17 @@ public abstract class JDKBaseWatch implements EventHandlingWatch {
protected final Path path;
protected final Executor exec;
protected final BiConsumer<EventHandlingWatch, WatchEvent> eventHandler;
protected final Predicate<WatchEvent> eventFilter;
protected final AtomicBoolean started = new AtomicBoolean();

protected JDKBaseWatch(Path path, Executor exec, BiConsumer<EventHandlingWatch, WatchEvent> eventHandler) {
protected JDKBaseWatch(Path path, Executor exec,
BiConsumer<EventHandlingWatch, WatchEvent> eventHandler,
Predicate<WatchEvent> eventFilter) {

this.path = path;
this.exec = exec;
this.eventHandler = eventHandler;
this.eventFilter = eventFilter;
}

public void open() throws IOException {
Expand Down Expand Up @@ -99,7 +105,7 @@ protected WatchEvent translate(java.nio.file.WatchEvent<?> jdkEvent) {
return event;
}

private WatchEvent.Kind translate(java.nio.file.WatchEvent.Kind<?> jdkKind) {
protected WatchEvent.Kind translate(java.nio.file.WatchEvent.Kind<?> jdkKind) {
if (jdkKind == StandardWatchEventKinds.ENTRY_CREATE) {
return WatchEvent.Kind.CREATED;
}
Expand All @@ -118,11 +124,6 @@ private WatchEvent.Kind translate(java.nio.file.WatchEvent.Kind<?> jdkKind) {

// -- EventHandlingWatch --

@Override
public void handleEvent(WatchEvent e) {
eventHandler.accept(this, e);
}

@Override
public Path getPath() {
return path;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import java.util.List;
import java.util.concurrent.Executor;
import java.util.function.BiConsumer;
import java.util.function.Predicate;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand All @@ -47,27 +48,36 @@
private final Logger logger = LogManager.getLogger();
private final boolean nativeRecursive;
private volatile @MonotonicNonNull Closeable bundledJDKWatcher;
private volatile boolean closed = false;

private static final BundledSubscription<SubscriptionKey, List<java.nio.file.WatchEvent<?>>>
BUNDLED_JDK_WATCHERS = new BundledSubscription<>(JDKPoller::register);

public JDKDirectoryWatch(Path directory, Executor exec, BiConsumer<EventHandlingWatch, WatchEvent> eventHandler) {
this(directory, exec, eventHandler, false);
public JDKDirectoryWatch(Path directory, Executor exec,
BiConsumer<EventHandlingWatch, WatchEvent> eventHandler,
Predicate<WatchEvent> eventFilter) {

this(directory, exec, eventHandler, eventFilter, false);
}

public JDKDirectoryWatch(Path directory, Executor exec, BiConsumer<EventHandlingWatch, WatchEvent> eventHandler, boolean nativeRecursive) {
super(directory, exec, eventHandler);
public JDKDirectoryWatch(Path directory, Executor exec,
BiConsumer<EventHandlingWatch, WatchEvent> eventHandler,
Predicate<WatchEvent> eventFilter, boolean nativeRecursive) {

super(directory, exec, eventHandler, eventFilter);
this.nativeRecursive = nativeRecursive;
}

private void handleJDKEvents(List<java.nio.file.WatchEvent<?>> events) {
exec.execute(() -> {
for (var ev : events) {
try {
handleEvent(translate(ev));
}
catch (Throwable ignored) {
logger.error("Ignoring downstream exception:", ignored);
if (!closed) {
for (var ev : events) {
try {
handleEvent(translate(ev));
}
catch (Throwable ignored) {
logger.error("Ignoring downstream exception:", ignored);

Check warning on line 79 in src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java#L78-L79

Added lines #L78 - L79 were not covered by tests
}
}
}
});
Expand All @@ -80,10 +90,18 @@
return nativeRecursive ? WatchScope.PATH_AND_ALL_DESCENDANTS : WatchScope.PATH_AND_CHILDREN;
}

@Override
public void handleEvent(WatchEvent e) {
if (eventFilter.test(e)) {
eventHandler.accept(this, e);
}
}

@Override
public synchronized void close() throws IOException {
if (bundledJDKWatcher != null) {
if (!closed && bundledJDKWatcher != null) {
logger.trace("Closing watch for: {}", this.path);
closed = true;
bundledJDKWatcher.close();
}
}
Expand Down
Loading
Loading