46
46
class MacWatchKey implements WatchKey {
47
47
private final MacWatchable watchable ;
48
48
private final MacWatchService service ;
49
- private final BlockingQueue < WatchEvent <?>> pendingEvents ;
49
+ private final PendingEvents pendingEvents ;
50
50
private final NativeEventStream stream ;
51
51
52
52
private volatile Configuration config = new Configuration ();
53
- private volatile boolean signalled = false ; // `!signalled` means "ready"
54
53
private volatile boolean cancelled = false ;
55
54
56
55
MacWatchKey (MacWatchable watchable , MacWatchService service ) throws IOException {
57
56
this .watchable = watchable ;
58
57
this .service = service ;
59
- this .pendingEvents = new LinkedBlockingQueue <> ();
58
+ this .pendingEvents = new PendingEvents ();
60
59
this .stream = new NativeEventStream (watchable .getPath (), new OfferWatchEvent ());
61
60
}
62
61
@@ -77,29 +76,75 @@ MacWatchKey initialize(Kind<?>[] kinds, Modifier[] modifiers) throws IOException
77
76
return this ;
78
77
}
79
78
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
+ }
102
117
}
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.
103
148
}
104
149
105
150
/**
@@ -129,8 +174,7 @@ public T context() {
129
174
}
130
175
};
131
176
132
- pendingEvents .offer (event );
133
- signalWhen (!signalled );
177
+ pendingEvents .offerAndSignal (event );
134
178
}
135
179
}
136
180
}
@@ -190,9 +234,7 @@ public boolean isValid() {
190
234
191
235
@ Override
192
236
public List <WatchEvent <?>> pollEvents () {
193
- var list = new ArrayList <WatchEvent <?>>(pendingEvents .size ());
194
- pendingEvents .drainTo (list );
195
- return list ;
237
+ return pendingEvents .drain ();
196
238
}
197
239
198
240
@ Override
@@ -201,10 +243,7 @@ public boolean reset() {
201
243
return false ;
202
244
}
203
245
204
- if (signalled ) {
205
- signalled = false ;
206
- signalWhen (!pendingEvents .isEmpty ());
207
- }
246
+ pendingEvents .resignalIfNonEmpty ();
208
247
209
248
// Invalidation of this key *during* the invocation of this method is
210
249
// observationally equivalent to invalidation immediately *after*. Thus,
0 commit comments