@@ -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