4040import java .util .Set ;
4141import java .util .concurrent .ConcurrentHashMap ;
4242import java .util .concurrent .Executor ;
43+ import java .util .function .BiFunction ;
4344
4445import org .apache .logging .log4j .LogManager ;
4546import org .apache .logging .log4j .Logger ;
46- import org .checkerframework .checker .nullness .qual .KeyFor ;
4747import org .checkerframework .checker .nullness .qual .Nullable ;
4848
4949import engineering .swat .watch .WatchEvent ;
5252
5353public class IndexingRescanner extends MemorylessRescanner {
5454 private final Logger logger = LogManager .getLogger ();
55- private final Index index = new Index ();
55+ private final PathMap < FileTime > index = new PathMap <> ();
5656
5757 public IndexingRescanner (Executor exec , Path path , WatchScope scope ) {
5858 super (exec );
5959 new Indexer (path , scope ).walkFileTree (); // Make an initial scan to populate the index
6060 }
6161
62- private static class Index {
63- private final Map <Path , Map <Path , FileTime >> lastModifiedTimes = new ConcurrentHashMap <>();
62+ private static class PathMap < V > {
63+ private final Map <Path , Map <Path , V >> values = new ConcurrentHashMap <>();
6464 // ^^^^ ^^^^
6565 // Parent File name (possibly a directory itself)
6666
67- public @ Nullable FileTime putLastModifiedTime (Path p , FileTime time ) {
68- var parent = p .getParent ();
69- var fileName = p .getFileName ();
70- if (parent != null && fileName != null ) {
71- return putLastModifiedTime (parent , fileName , time );
72- } else {
73- throw new IllegalArgumentException ("A path key should have both a parent and a file name" );
74- }
67+ public @ Nullable V put (Path p , V value ) {
68+ return apply ((parent , fileName ) -> put (parent , fileName , value ), p );
7569 }
7670
77- public @ Nullable FileTime putLastModifiedTime (Path parent , Path fileName , FileTime time ) {
78- var nested = lastModifiedTimes .computeIfAbsent (parent , x -> new ConcurrentHashMap <>());
79- return nested .put (fileName , time );
71+ public @ Nullable V get (Path p ) {
72+ return apply (this ::get , p );
8073 }
8174
82- public @ Nullable FileTime getLastModifiedTime (Path p ) {
83- var parent = p .getParent ();
84- var fileName = p .getFileName ();
85- if (parent != null && fileName != null ) {
86- return getLastModifiedTime (parent , fileName );
87- } else {
88- throw new IllegalArgumentException ("A path key should have both a parent and a file name" );
89- }
90- }
91-
92- public @ Nullable FileTime getLastModifiedTime (Path parent , Path fileName ) {
93- var nested = lastModifiedTimes .get (parent );
94- return nested == null ? null : nested .get (fileName );
75+ public Set <Path > getParents () {
76+ return values .keySet ();
9577 }
9678
9779 public Set <Path > getFileNames (Path parent ) {
98- var nested = lastModifiedTimes .get (parent );
80+ var nested = values .get (parent );
9981 return nested == null ? Collections .emptySet () : (Set <Path >) nested .keySet ();
10082 }
10183
102- public @ Nullable FileTime remove (Path p ) {
84+ public @ Nullable V remove (Path p ) {
85+ return apply (this ::remove , p );
86+ }
87+
88+ private V apply (BiFunction <Path , Path , V > action , Path p ) {
10389 var parent = p .getParent ();
10490 var fileName = p .getFileName ();
10591 if (parent != null && fileName != null ) {
106- return remove (parent , fileName );
92+ return action . apply (parent , fileName );
10793 } else {
108- throw new IllegalArgumentException ("A path key should have both a parent and a file name" );
94+ throw new IllegalArgumentException ("The path should have both a parent and a file name" );
95+ }
96+ }
97+
98+ 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 );
109108 }
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.
112+ return previous ;
110113 }
111114
112- public @ Nullable FileTime remove (Path parent , Path fileName ) {
113- var nested = lastModifiedTimes .get (parent );
115+ private @ Nullable V get (Path parent , Path fileName ) {
116+ var nested = values .get (parent );
117+ return nested == null ? null : nested .get (fileName );
118+ }
119+
120+ 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 );
114124 if (nested != null ) {
115125 var removed = nested .remove (fileName );
116- if (nested .isEmpty ()) {
117- lastModifiedTimes .remove (parent , nested );
118- // Note: Between checking `nested` for non-emptiness and
119- // removing it from `lastModifiedTimes`, other threads may
120- // have put new entries in it. After the removal, these
121- // entries are lost, so the index doesn't completely reflect
122- // the file system anymore, and redundant events may be
123- // issued. This doesn't break the contract with the client,
124- // though (because this rescanner still provides an
125- // over-approximation). Avoiding this race would be costly
126- // in terms of synchronization.
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 ()) {
132+ put (parent , e .getKey (), e .getValue ());
133+ }
134+ }
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.
127138 }
128139 return removed ;
129140 } else {
@@ -140,14 +151,14 @@ public Indexer(Path path, WatchScope scope) {
140151 @ Override
141152 public FileVisitResult preVisitDirectory (Path dir , BasicFileAttributes attrs ) throws IOException {
142153 if (!path .equals (dir )) {
143- index .putLastModifiedTime (dir , attrs .lastModifiedTime ());
154+ index .put (dir , attrs .lastModifiedTime ());
144155 }
145156 return FileVisitResult .CONTINUE ;
146157 }
147158
148159 @ Override
149160 public FileVisitResult visitFile (Path file , BasicFileAttributes attrs ) throws IOException {
150- index .putLastModifiedTime (file , attrs .lastModifiedTime ());
161+ index .put (file , attrs .lastModifiedTime ());
151162 return FileVisitResult .CONTINUE ;
152163 }
153164 }
@@ -185,7 +196,7 @@ private void addToPeeked(Deque<Set<Path>> deque, Path p) {
185196
186197 @ Override
187198 protected void generateEvents (Path path , BasicFileAttributes attrs ) {
188- var lastModifiedTimeOld = index .getLastModifiedTime (path );
199+ var lastModifiedTimeOld = index .get (path );
189200 var lastModifiedTimeNew = attrs .lastModifiedTime ();
190201
191202 // The path isn't indexed yet
@@ -250,7 +261,7 @@ public void accept(EventHandlingWatch watch, WatchEvent event) {
250261 case MODIFIED :
251262 try {
252263 var lastModifiedTimeNew = Files .getLastModifiedTime (fullPath );
253- var lastModifiedTimeOld = index .putLastModifiedTime (fullPath , lastModifiedTimeNew );
264+ var lastModifiedTimeOld = index .put (fullPath , lastModifiedTimeNew );
254265
255266 // If a `MODIFIED` event happens for a path that wasn't in
256267 // the index yet, then a `CREATED` event has somehow been
0 commit comments