Skip to content

Commit 6feac60

Browse files
committed
Fix race in closing child watches
1 parent e53569a commit 6feac60

File tree

2 files changed

+33
-3
lines changed

2 files changed

+33
-3
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ public JDKDirectoryWatch(Path directory, Executor exec,
6868
this.nativeRecursive = nativeRecursive;
6969
}
7070

71+
public boolean isClosed() {
72+
return closed;
73+
}
74+
7175
private void handleJDKEvents(List<java.nio.file.WatchEvent<?>> events) {
7276
exec.execute(() -> {
7377
for (var ev : events) {

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

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public class JDKFileTreeWatch extends JDKBaseWatch {
5353
private final Path rootPath;
5454
private final Path relativePathParent;
5555
private final Map<Path, JDKFileTreeWatch> childWatches = new ConcurrentHashMap<>();
56-
private final JDKBaseWatch internal;
56+
private final JDKDirectoryWatch internal;
5757

5858
public JDKFileTreeWatch(Path fullPath, Executor exec,
5959
BiConsumer<EventHandlingWatch, WatchEvent> eventHandler,
@@ -156,7 +156,9 @@ private void acceptCreated(Path child) {
156156
// Events in the newly created directory might have been missed
157157
// between its creation and setting up its watch. So, generate
158158
// an `OVERFLOW` event for the watch.
159-
reportOverflowTo(childWatch);
159+
if (childWatch != null) {
160+
reportOverflowTo(childWatch);
161+
}
160162
}
161163
}
162164

@@ -193,12 +195,36 @@ private void syncChildWatchesWithFileSystem() {
193195
}
194196
}
195197

196-
private JDKFileTreeWatch openChildWatch(Path child) {
198+
/**
199+
* @return A child watch for {@code child} when the parent watch is still
200+
* open, or {@code null} when it is already closed.
201+
*/
202+
private @Nullable JDKFileTreeWatch openChildWatch(Path child) {
197203
assert !child.isAbsolute();
198204

199205
Function<Path, JDKFileTreeWatch> newChildWatch = p -> new JDKFileTreeWatch(
200206
rootPath, relativePathParent.resolve(child), exec, eventHandler, eventFilter);
201207
var childWatch = childWatches.computeIfAbsent(child, newChildWatch);
208+
209+
// The following may have happened at this point:
210+
// 1. Thread A: Reads `closed` at the beginning of an event handler,
211+
// sees it's `false`, runs the event handler, and reaches the
212+
// beginning of this method (but doesn't execute it yet).
213+
// 2. Thread B: Writes `true` to `closed`, gets all child watches from
214+
// the map, and closes them.
215+
// 3. Thread A: Creates a new child watch and puts it into the map.
216+
//
217+
// Without additional synchronization, which is costly, there will
218+
// always be a small window between the end of (1) and the beginning of
219+
// (2) that causes a "dangling" child watch to remain open when the
220+
// parent watch is closed. To mitigate this, after optimistically
221+
// putting a child watch into the map, we immediately close it when
222+
// needed.
223+
if (internal.isClosed()) {
224+
tryClose(childWatch);
225+
return null;
226+
}
227+
202228
try {
203229
childWatch.startIfFirstTime();
204230
} catch (IOException e) {

0 commit comments

Comments
 (0)