Skip to content

Commit 5b04d16

Browse files
committed
Add helper inner class to group thread-safety-critical code together and improve comments
1 parent ee77302 commit 5b04d16

File tree

1 file changed

+73
-34
lines changed

1 file changed

+73
-34
lines changed

src/main/java/engineering/swat/watch/impl/mac/MacWatchKey.java

+73-34
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,16 @@
4646
class MacWatchKey implements WatchKey {
4747
private final MacWatchable watchable;
4848
private final MacWatchService service;
49-
private final BlockingQueue<WatchEvent<?>> pendingEvents;
49+
private final PendingEvents pendingEvents;
5050
private final NativeEventStream stream;
5151

5252
private volatile Configuration config = new Configuration();
53-
private volatile boolean signalled = false; // `!signalled` means "ready"
5453
private volatile boolean cancelled = false;
5554

5655
MacWatchKey(MacWatchable watchable, MacWatchService service) throws IOException {
5756
this.watchable = watchable;
5857
this.service = service;
59-
this.pendingEvents = new LinkedBlockingQueue<>();
58+
this.pendingEvents = new PendingEvents();
6059
this.stream = new NativeEventStream(watchable.getPath(), new OfferWatchEvent());
6160
}
6261

@@ -77,29 +76,75 @@ MacWatchKey initialize(Kind<?>[] kinds, Modifier[] modifiers) throws IOException
7776
return this;
7877
}
7978

80-
private void signalWhen(boolean condition) {
81-
if (condition) {
82-
signalled = true;
83-
service.offer(this);
84-
// The order of these statements is important. If it's the other way
85-
// around, then the following harmful interleaving of an "offering
86-
// thread" (Thread 1) and a "polling thread" (Thread 2) can happen:
87-
// - Thread 1:
88-
// - `handle`: Add event to `pendingEvents`
89-
// - `handle`, `signalWhen`: Test `!signalled` is true
90-
// - `signalWhen`: Offer `this` to `service`.
91-
// - Thread 2:
92-
// - `MacWatchService.poll`: Poll `this` from `service`
93-
// - `pollEvents`: Drain events from `pendingEvents`
94-
// - `reset`: Set `signalled` to false []
95-
// - `reset`, `signalWhen`: Test `!pendingEvents.empty()` is false
96-
// - Thread 1:
97-
// - `signalWhen`: Set `signalled` to true. At this point
98-
// `this` isn't offered to `service`, but subsequent
99-
// invocations of `handle` will not cause `this` to be
100-
// offered. As a result, no subsequent events are
101-
// propagated.
79+
/**
80+
* Auxiliary container to manage the internal state of this watch key in a
81+
* single place (to make it easier to reason about concurrent accesses).
82+
*/
83+
private class PendingEvents {
84+
private final BlockingQueue<WatchEvent<?>> pendingEvents = new LinkedBlockingQueue<>();
85+
private volatile boolean signalled = false;
86+
87+
// Following the documentation `WatchKey`, initially, this watch key is
88+
// *ready* (i.e., `signalled` is false). When an event is offered, this
89+
// watch key becomes *signalled* and is enqueued at `service`.
90+
// Subsequently, this watch key remains signalled until it is reset; not
91+
// until the pending events are polled. Thus, at the same time,
92+
// `pendingEvents` can be empty and `signalled` can be true. The
93+
// interplay between `pendingEvents` and `signalled` is quite tricky,
94+
// and potentially subject to harmful races. The comments below the
95+
// following methods argue why such harmful races won't happen.
96+
97+
void offerAndSignal(WatchEvent<?> event) {
98+
pendingEvents.offer(event);
99+
if (!signalled) {
100+
signalled = true;
101+
service.offer(MacWatchKey.this);
102+
}
103+
}
104+
105+
List<WatchEvent<?>> drain() {
106+
var list = new ArrayList<WatchEvent<?>>(pendingEvents.size());
107+
pendingEvents.drainTo(list);
108+
return list;
109+
}
110+
111+
void resignalIfNonEmpty() {
112+
if (signalled && !pendingEvents.isEmpty()) {
113+
service.offer(MacWatchKey.this);
114+
} else {
115+
signalled = false;
116+
}
102117
}
118+
119+
// The crucial property that needs to be maintained is that when
120+
// `resignalIfNonEmpty` returns, either this watch key has been, or will
121+
// be, enqueued at `service`, or `signalled` is false. Otherwise, until
122+
// a next invocation of `reset` (including `resignalIfNonEmpty`),
123+
// consumers of `service` won't be able to dequeue this watch key (it
124+
// won't be queued by `offerAndSignal` while `signalled` is true), even
125+
// when `pendingEvents` becomes non-empty---this causes consumers to
126+
// miss events. Note: The documentation of `WatchService` doesn't
127+
// specify the need for a next invocation of `reset` after a succesful
128+
// one.
129+
//
130+
// To argue that the property holds, there are two cases to analyze:
131+
//
132+
// - If the then-branch of `resignalIfNonEmpty` is executed, then
133+
// this watch key has been enqueued at `service`, so the property
134+
// holds. Note: It doesn't matter if, by the time
135+
// `resignalIfNonEmpty` returns, this watch key has already been
136+
// dequeued by another thread. This is because that other thread is
137+
// then responsible to make a next invocation of `reset` (including
138+
// `resignalIfNonEmpty`) after its usage of this watch key.
139+
//
140+
// - If the else-branch of `resignalIfNonEmpty` is executed, then
141+
// `signalled` may become `true` right after it's set to `false`.
142+
// This happens when another thread concurrently invokes
143+
// `offerAndSignal`. (There are no other places where `signalled`
144+
// is modified.) But then, as part of `offerAndSignal`, this watch
145+
// key will be enqueued at `service` by the other thread, too, so
146+
// the property holds. Note: If we were to change the order of the
147+
// statements in `offerAndSignal`, the property no longer holds.
103148
}
104149

105150
/**
@@ -129,8 +174,7 @@ public T context() {
129174
}
130175
};
131176

132-
pendingEvents.offer(event);
133-
signalWhen(!signalled);
177+
pendingEvents.offerAndSignal(event);
134178
}
135179
}
136180
}
@@ -190,9 +234,7 @@ public boolean isValid() {
190234

191235
@Override
192236
public List<WatchEvent<?>> pollEvents() {
193-
var list = new ArrayList<WatchEvent<?>>(pendingEvents.size());
194-
pendingEvents.drainTo(list);
195-
return list;
237+
return pendingEvents.drain();
196238
}
197239

198240
@Override
@@ -201,10 +243,7 @@ public boolean reset() {
201243
return false;
202244
}
203245

204-
if (signalled) {
205-
signalled = false;
206-
signalWhen(!pendingEvents.isEmpty());
207-
}
246+
pendingEvents.resignalIfNonEmpty();
208247

209248
// Invalidation of this key *during* the invocation of this method is
210249
// observationally equivalent to invalidation immediately *after*. Thus,

0 commit comments

Comments
 (0)