Skip to content

Commit 3b9efdd

Browse files
committed
Improve comments about thread-safety of concurrent puts/removes in the index
1 parent 5209921 commit 3b9efdd

File tree

1 file changed

+47
-33
lines changed

1 file changed

+47
-33
lines changed

src/main/java/engineering/swat/watch/impl/overflows/IndexingRescanner.java

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)