-
Notifications
You must be signed in to change notification settings - Fork 0
Improved overflow support: Recursive directory watches #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
sungshik
merged 36 commits into
improved-overflow-support-main
from
improved-overflow-support/jdk-file-tree-watch
Mar 28, 2025
Merged
Changes from 10 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 671d5eb
Add `JDKFileTreeWatch`
sungshik 62f4b1c
Move `updateChildWatches` to inner class
sungshik 36c4299
Merge branch 'improved-overflow-support-main' into improved-overflow-…
sungshik 47b8a10
Use `computeIfAbsent` instead of `putIfAbsent`
sungshik 500e238
Update `JDKRecursiveDirectoryWatch`
sungshik 6a328bf
Add import
sungshik 4aed091
Improve code quality of `JDKFileTreeWatch`
sungshik 71ac833
Improve code quality of `JDKFileTreeWatch`
sungshik fa65b30
Add mechanism to avoid relativization in `JDKFileTreeWatch`
sungshik fdd24f8
Simplify relativization of paths in `JDKFileTreeWatch`
sungshik 3bdafe6
Change order of closing internal/child watches in `JDKFileTreeWatch`
sungshik 4582d23
Simplify relativization of paths in `JDKFileTreeWatch`
sungshik 6a7df86
Use file names to store child watches (instead of full paths)
sungshik e676b5f
Use `JDKFileTreeWatch`
sungshik 387e7c3
Add asynchronous bookkeeping of `CREATED` and `OVERFLOW` events
sungshik 748e8ac
Fix issue that `JDKFileTreeWatch` relied on overflow handling to pres…
sungshik 4a1423b
Add license
sungshik 1ab8f29
Make the child watches updater asynchronous
sungshik 385db76
Add code to close child watches when their directories no longer exis…
sungshik 58d9561
Remove `JDKRecursiveDirectoryWatch` (replaced by `JDKFileTreeWatch`)
sungshik c96c943
Add filtering mechanism to `Watcher` and `JDK...` classes
sungshik eca305b
Move method implementation from base class to subclass (was already o…
sungshik 408c9d7
Improve logic to close `JDK...Watch` classes (avoid event handling o…
sungshik e0f039d
Fix a few relativization issues in `JDKFileTreeWatch` and `IndexingRe…
sungshik b8adb45
Add event filter to test
sungshik bbd1d39
Add test to check if overflows are recoverd from
sungshik 02b10b7
Fix JavaDoc
sungshik 76ec380
Remove old test
sungshik 2cc3c66
Remove `trySleep` helpers
sungshik b760db9
Rename method to better convey intent
sungshik 9b58bc4
Revert change to `relativize` in `JDKFileTreeWatch` (and add comment …
sungshik 84b627b
Move closed check to `handleEvent`
sungshik e53569a
Add general `handleEvent` implementation back to the base watch
sungshik 6feac60
Fix race in closing child watches
sungshik 3f80e77
Merge branch 'improved-overflow-support-main' into improved-overflow-…
sungshik File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
221 changes: 221 additions & 0 deletions
221
src/main/java/engineering/swat/watch/impl/jdk/JDKFileTreeWatch.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| /* | ||
| * BSD 2-Clause License | ||
| * | ||
| * Copyright (c) 2023, Swat.engineering | ||
| * | ||
| * Redistribution and use in source and binary forms, with or without | ||
| * modification, are permitted provided that the following conditions are met: | ||
| * | ||
| * 1. Redistributions of source code must retain the above copyright notice, this | ||
| * list of conditions and the following disclaimer. | ||
| * | ||
| * 2. Redistributions in binary form must reproduce the above copyright notice, | ||
| * this list of conditions and the following disclaimer in the documentation | ||
| * and/or other materials provided with the distribution. | ||
| * | ||
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
| * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||
| * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||
| * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||
| * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||
| * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||
| * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| */ | ||
| package engineering.swat.watch.impl.jdk; | ||
|
|
||
| import java.io.IOException; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| import java.util.Map; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.Executor; | ||
| import java.util.function.BiConsumer; | ||
| import java.util.function.Function; | ||
|
|
||
| import org.apache.logging.log4j.LogManager; | ||
| import org.apache.logging.log4j.Logger; | ||
|
|
||
| import engineering.swat.watch.WatchEvent; | ||
| import engineering.swat.watch.WatchScope; | ||
| import engineering.swat.watch.impl.EventHandlingWatch; | ||
|
|
||
| public class JDKFileTreeWatch extends JDKBaseWatch { | ||
| private final Logger logger = LogManager.getLogger(); | ||
| private final Path rootPath; | ||
| private final Path relativePathParent; | ||
| private final Map<Path, JDKFileTreeWatch> childWatches = new ConcurrentHashMap<>(); | ||
| private final JDKBaseWatch internal; | ||
|
|
||
| public JDKFileTreeWatch(Path fullPath, Executor exec, | ||
| BiConsumer<EventHandlingWatch, WatchEvent> eventHandler) { | ||
| this(fullPath, Path.of(""), exec, eventHandler); | ||
| } | ||
|
|
||
| public JDKFileTreeWatch(Path rootPath, Path relativePathParent, Executor exec, | ||
| BiConsumer<EventHandlingWatch, WatchEvent> eventHandler) { | ||
|
|
||
| super(rootPath.resolve(relativePathParent), exec, eventHandler); | ||
| this.rootPath = rootPath; | ||
| this.relativePathParent = relativePathParent; | ||
|
|
||
| var internalEventHandler = eventHandler.andThen(new ChildWatchesUpdater()); | ||
| this.internal = new JDKDirectoryWatch(path, exec, internalEventHandler) { | ||
|
|
||
| // Override to ensure that this watch relativizes events wrt | ||
| // `rootPath` (instead of `path`, as is the default behavior) | ||
| @Override | ||
| public WatchEvent relativize(WatchEvent event) { | ||
| return new WatchEvent(event.getKind(), rootPath, | ||
| rootPath.relativize(event.calculateFullPath())); | ||
| } | ||
|
|
||
| // Override to ensure that this watch translates JDK events using | ||
| // `rootPath` (instead of `path`, as is the default behavior). | ||
| // Events returned by this method do not need to be relativized. | ||
| @Override | ||
| protected WatchEvent translate(java.nio.file.WatchEvent<?> jdkEvent) { | ||
| var kind = translate(jdkEvent.kind()); | ||
|
|
||
| Path relativePath = null; | ||
| if (kind != WatchEvent.Kind.OVERFLOW) { | ||
| var child = (Path) jdkEvent.context(); | ||
| if (child != null) { | ||
| relativePath = relativePathParent.resolve(child); | ||
| } | ||
| } | ||
|
|
||
| var event = new WatchEvent(kind, rootPath, relativePath); | ||
| logger.trace("Translated: {} to {}", jdkEvent, event); | ||
| return event; | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Event handler that updates the child watches according to the following | ||
| * rules: (a) when an overflow happens, it's propagated to each existing | ||
| * child watch; (b) when a subdirectory creation happens, a new child watch | ||
| * is opened for that subdirectory; (c) when a subdirectory deletion | ||
| * happens, an existing child watch is closed for that subdirectory. | ||
| */ | ||
| private class ChildWatchesUpdater implements BiConsumer<EventHandlingWatch, WatchEvent> { | ||
| @Override | ||
| public void accept(EventHandlingWatch watch, WatchEvent event) { | ||
| switch (event.getKind()) { | ||
| case OVERFLOW: acceptOverflow(); break; | ||
sungshik marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| case CREATED: acceptCreated(event.calculateFullPath()); break; | ||
sungshik marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| case DELETED: acceptDeleted(event.calculateFullPath()); break; | ||
| case MODIFIED: break; | ||
| } | ||
| } | ||
|
|
||
| private void acceptOverflow() { | ||
| for (var childWatch : childWatches.values()) { | ||
| reportOverflowTo(childWatch); | ||
| } | ||
| } | ||
|
|
||
| private void acceptCreated(Path fullPath) { | ||
| if (Files.isDirectory(fullPath)) { | ||
| var childWatch = openChildWatch(fullPath); | ||
| // Events in the newly created directory might have been missed | ||
| // between its creation and setting up its watch. So, generate | ||
| // an `OVERFLOW` event for the watch. | ||
| reportOverflowTo(childWatch); | ||
sungshik marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| private void acceptDeleted(Path fullPath) { | ||
| try { | ||
| closeChildWatch(fullPath); | ||
| } catch (IOException e) { | ||
| logger.error("Could not close (nested) file tree watch for: {} ({})", fullPath, e); | ||
| } | ||
| } | ||
|
|
||
| private void reportOverflowTo(JDKFileTreeWatch childWatch) { | ||
| var overflow = new WatchEvent(WatchEvent.Kind.OVERFLOW, | ||
| childWatch.rootPath, childWatch.relativePathParent); | ||
| childWatch.handleEvent(overflow); | ||
| } | ||
| } | ||
|
|
||
| private JDKFileTreeWatch openChildWatch(Path child) { | ||
| Function<Path, JDKFileTreeWatch> newChildWatch = p -> new JDKFileTreeWatch( | ||
| rootPath, rootPath.relativize(child), exec, eventHandler); | ||
|
|
||
| var childWatch = childWatches.computeIfAbsent(child, newChildWatch); | ||
| try { | ||
| childWatch.startIfFirstTime(); | ||
| } catch (IOException e) { | ||
| logger.error("Could not open (nested) file tree watch for: {} ({})", child, e); | ||
| } | ||
| return childWatch; | ||
| } | ||
|
|
||
| private void closeChildWatch(Path child) throws IOException { | ||
| var childWatch = childWatches.remove(child); | ||
| if (childWatch != null) { | ||
| childWatch.close(); | ||
| } | ||
| } | ||
|
|
||
| // -- JDKBaseWatch -- | ||
|
|
||
| @Override | ||
| public WatchScope getScope() { | ||
| return WatchScope.PATH_AND_ALL_DESCENDANTS; | ||
| } | ||
|
|
||
| @Override | ||
| public void handleEvent(WatchEvent event) { | ||
| internal.handleEvent(event); | ||
| } | ||
|
|
||
| @Override | ||
| public synchronized void close() throws IOException { | ||
| IOException firstFail = null; | ||
| var children = childWatches.keySet().iterator(); | ||
| while (true) { | ||
| try { | ||
| // First, close all child watches | ||
| if (children.hasNext()) { | ||
| closeChildWatch(children.next()); | ||
| } | ||
| // Last, close the internal watch | ||
| else { | ||
| internal.close(); | ||
sungshik marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| break; | ||
| } | ||
| } catch (IOException ex) { | ||
| logger.error("Could not close watch", ex); | ||
| firstFail = firstFail == null ? ex : firstFail; | ||
| } catch (Exception ex) { | ||
| logger.error("Could not close watch", ex); | ||
| firstFail = firstFail == null ? new IOException("Unexpected exception when closing", ex) : firstFail; | ||
| } | ||
| } | ||
| if (firstFail != null) { | ||
| throw firstFail; | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| protected synchronized void start() throws IOException { | ||
| internal.open(); | ||
| try (var children = Files.find(path, 1, (p, attrs) -> p != path && attrs.isDirectory())) { | ||
| children.forEach(this::openChildWatch); | ||
| } catch (IOException e) { | ||
| logger.error("File tree watch (for: {}) could not iterate over its children ({})", path, e); | ||
| } | ||
| // There's no need to report an overflow event, because `internal` was | ||
| // opened *before* the file system was accessed to fetch children. Thus, | ||
| // if a new directory is created while this method is running, then at | ||
| // least one of the following is true: (a) the new directory is already | ||
| // visible by the time the file system is accessed; (b) its `CREATED` | ||
| // event is handled later, which starts a new child watch if needed. | ||
sungshik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.