@@ -62,10 +62,10 @@ public IndexingRescanner(Executor exec, Path path, WatchScope scope) {
6262 private static class PathMap <V > {
6363 private final Map <Path , Map <Path , V >> values = new ConcurrentHashMap <>();
6464 // ^^^^ ^^^^
65- // Parent File name (possibly a directory itself )
65+ // Parent File name (regular file or directory )
6666
6767 public @ Nullable V put (Path p , V value ) {
68- return apply (( parent , fileName ) -> put (parent , fileName , value ), p );
68+ return apply (put (value ), p );
6969 }
7070
7171 public @ Nullable V get (Path p ) {
@@ -77,15 +77,15 @@ public Set<Path> getParents() {
7777 }
7878
7979 public Set <Path > getFileNames (Path parent ) {
80- var nested = values .get (parent );
81- return nested == null ? Collections .emptySet () : (Set <Path >) nested .keySet ();
80+ var inner = values .get (parent );
81+ return inner == null ? Collections .emptySet () : (Set <Path >) inner .keySet ();
8282 }
8383
8484 public @ Nullable V remove (Path p ) {
8585 return apply (this ::remove , p );
8686 }
8787
88- private @ Nullable V apply (BiFunction <Path , Path , @ Nullable V > action , Path p ) {
88+ private static < V > @ Nullable V apply (BiFunction <Path , Path , @ Nullable V > action , Path p ) {
8989 var parent = p .getParent ();
9090 var fileName = p .getFileName ();
9191 if (parent != null && fileName != null ) {
@@ -95,46 +95,60 @@ public Set<Path> getFileNames(Path parent) {
9595 }
9696 }
9797
98+ private BiFunction <Path , Path , @ Nullable V > put (V value ) {
99+ return (parent , fileName ) -> put (parent , fileName , value );
100+ }
101+
98102 private @ Nullable V put (Path parent , Path fileName , V value ) {
99- // Let "here" and "there" refer to threads that perform this method
100- // (here) and a concurrent `remove` (there).
101- var nested = values .computeIfAbsent (parent , x -> new ConcurrentHashMap <>());
102- var previous = nested .put (fileName , value );
103- // <-- At this point, if a concurrent `values.remove(...)` has
104- // happened there, then `values.get(parent) != nested` is true
105- // here, so the put is retried here.
106- if (values .get (parent ) != nested ) {
107- return put (parent , fileName , value );
103+ var inner = values .computeIfAbsent (parent , x -> new ConcurrentHashMap <>());
104+
105+ // This thread (henceforth: "here") optimistically puts a new entry
106+ // in `inner`. However, another thread (henceforth: "there") may
107+ // concurrently remove `inner` from `values`. Thus, the new entry
108+ // may be lost. The comments below explain the countermeasures.
109+ var previous = inner .put (fileName , value );
110+
111+ // <-- At this point "here", if `values.remove(parent)` happens
112+ // "there", then `values.get(parent) != inner` becomes true
113+ // "here", so the new entry will be re-put "here".
114+ if (values .get (parent ) != inner ) {
115+ previous = put (parent , fileName , value );
108116 }
109- // <-- At this point, if a concurrent `values.remove(... )` has
110- // happened there, then `!nested.isEmpty( )` is true there, so
111- // the new entry is re-put there.
117+ // <-- At this point "here", `!inner.isEmpty( )` has become true
118+ // " there", so if `values.remove(parent )` happens " there", then
119+ // the new entry will be re-put " there" .
112120 return previous ;
113121 }
114122
115123 private @ Nullable V get (Path parent , Path fileName ) {
116- var nested = values .get (parent );
117- return nested == null ? null : nested .get (fileName );
124+ var inner = values .get (parent );
125+ return inner == null ? null : inner .get (fileName );
118126 }
119127
120128 private @ Nullable V remove (Path parent , Path fileName ) {
121- // Let "here" and "there" refer to threads that perform this method
122- // (here) and a concurrent `put` (there).
123- var nested = values .get (parent );
124- if (nested != null ) {
125- var removed = nested .remove (fileName );
126- if (nested .isEmpty () && values .remove (parent , nested )) {
127- // <-- At this point, if a concurrent `nested.put(...)` has
128- // happened there, then `!nested.isEmpty()` is true
129- // here, so the new entry is re-put here.
130- if (!nested .isEmpty ()) {
131- for (var e : nested .entrySet ()) {
129+ var inner = values .get (parent );
130+ if (inner != null ) {
131+ var removed = inner .remove (fileName );
132+
133+ // This thread (henceforth: "here") optimistically removes
134+ // `inner` from `values` when it has become empty. However,
135+ // another thread (henceforth: "there") may concurrently put a
136+ // new entry in `inner`. Thus, the new entry may be lost. The
137+ // comments below explain the countermeasures.
138+ if (inner .isEmpty () && values .remove (parent , inner )) {
139+
140+ // <-- At this point "here", if `inner.put(...)` happens
141+ // "there", then `!inner.isEmpty()` becomes true "here",
142+ // so the new entry is re-put "here".
143+ if (!inner .isEmpty ()) {
144+ for (var e : inner .entrySet ()) {
132145 put (parent , e .getKey (), e .getValue ());
133146 }
134147 }
135- // <-- At this point, if a concurrent `nested.put(...)` has
136- // happened there, then `values.get(parent) != nested`
137- // is true there, so the put is retried.
148+ // <-- At this point "here", `values.get(parent) != inner`
149+ // has become true "there", so if `inner.put(...)`
150+ // happens "there", then the new entry will be re-put
151+ // "there".
138152 }
139153 return removed ;
140154 } else {
0 commit comments