4646class 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