3535import io .objectbox .reactive .SubscriptionBuilder ;
3636
3737/**
38- * A {@link DataPublisher} that subscribes to an ObjectClassPublisher if there is at least one observer.
39- * Publishing is requested if the ObjectClassPublisher reports changes, a subscription is
40- * {@link SubscriptionBuilder#observer(DataObserver) observed} or {@link Query#publish()} is called.
41- * For publishing the query is re-run and the result delivered to the current observers.
42- * Results are published on a single thread, one at a time, in the order publishing was requested.
38+ * A {@link DataPublisher} that {@link BoxStore#subscribe(Class) subscribes to the Box} of its associated {@link Query}
39+ * while there is at least one observer (see {@link #subscribe(DataObserver, Object)} and
40+ * {@link #unsubscribe(DataObserver, Object)}).
41+ * <p>
42+ * Publishing is requested if the Box reports changes, a subscription is
43+ * {@link SubscriptionBuilder#observer(DataObserver) observed} (if {@link #publishSingle(DataObserver, Object)} is
44+ * called) or {@link Query#publish()} (calls {@link #publish()}) is called.
45+ * <p>
46+ * For publishing the query is re-run and the result data is delivered to the current observers.
47+ * <p>
48+ * Data is passed to observers on a single thread ({@link BoxStore#internalScheduleThread(Runnable)}), one at a time, in
49+ * the order observers were added.
4350 */
4451@ Internal
4552class QueryPublisher <T > implements DataPublisher <List <T >>, Runnable {
4653
54+ /**
55+ * If enabled, logs states of the publisher runnable. Useful to debug a query subscription.
56+ */
57+ public static boolean LOG_STATES = false ;
4758 private final Query <T > query ;
4859 private final Box <T > box ;
4960 private final Set <DataObserver <List <T >>> observers = new CopyOnWriteArraySet <>();
5061 private final Deque <DataObserver <List <T >>> publishQueue = new ArrayDeque <>();
5162 private volatile boolean publisherRunning = false ;
63+ private volatile boolean publisherStopped = false ;
5264
5365 private static class SubscribedObservers <T > implements DataObserver <List <T >> {
5466 @ Override
@@ -106,6 +118,10 @@ void publish() {
106118 */
107119 private void queueObserverAndScheduleRun (DataObserver <List <T >> observer ) {
108120 synchronized (publishQueue ) {
121+ // Check after obtaining the lock as the publisher may have been stopped while waiting on the lock
122+ if (publisherStopped ) {
123+ return ;
124+ }
109125 publishQueue .add (observer );
110126 if (!publisherRunning ) {
111127 publisherRunning = true ;
@@ -114,6 +130,31 @@ private void queueObserverAndScheduleRun(DataObserver<List<T>> observer) {
114130 }
115131 }
116132
133+ /**
134+ * Marks this publisher as stopped and if it is currently running waits on it to complete.
135+ * <p>
136+ * After calling this, this publisher will no longer run, even if observers subscribe or publishing is requested.
137+ */
138+ void stopAndAwait () {
139+ publisherStopped = true ;
140+ // Doing wait/notify waiting here; could also use the Future from BoxStore.internalScheduleThread() instead.
141+ // The latter would require another member though, which seems redundant.
142+ synchronized (this ) {
143+ while (publisherRunning ) {
144+ try {
145+ this .wait ();
146+ } catch (InterruptedException e ) {
147+ if (publisherRunning ) {
148+ // When called by Query.close() throwing here will leak the query. But not throwing would allow
149+ // close() to proceed in destroying the native query while it may still be active (run() of this
150+ // is at the query.find() call), which would trigger a VM crash.
151+ throw new RuntimeException ("Interrupted while waiting for publisher to finish" , e );
152+ }
153+ }
154+ }
155+ }
156+ }
157+
117158 /**
118159 * Processes publish requests for this query on a single thread to prevent
119160 * older query results getting delivered after newer query results.
@@ -122,9 +163,11 @@ private void queueObserverAndScheduleRun(DataObserver<List<T>> observer) {
122163 */
123164 @ Override
124165 public void run () {
166+ log ("started" );
125167 try {
126- while (true ) {
168+ while (! publisherStopped ) {
127169 // Get all queued observer(s), stop processing if none.
170+ log ("checking for observers" );
128171 List <DataObserver <List <T >>> singlePublishObservers = new ArrayList <>();
129172 boolean notifySubscribedObservers = false ;
130173 synchronized (publishQueue ) {
@@ -143,9 +186,12 @@ public void run() {
143186 }
144187
145188 // Query.
189+ log ("running query" );
190+ if (publisherStopped ) break ; // Check again to avoid running the query if possible
146191 List <T > result = query .find ();
147192
148193 // Notify observer(s).
194+ log ("notifying observers" );
149195 for (DataObserver <List <T >> observer : singlePublishObservers ) {
150196 observer .onData (result );
151197 }
@@ -158,8 +204,12 @@ public void run() {
158204 }
159205 }
160206 } finally {
207+ log ("stopped" );
161208 // Re-set if wrapped code throws, otherwise this publisher can no longer publish.
162209 publisherRunning = false ;
210+ synchronized (this ) {
211+ this .notifyAll ();
212+ }
163213 }
164214 }
165215
@@ -172,4 +222,8 @@ public synchronized void unsubscribe(DataObserver<List<T>> observer, @Nullable O
172222 }
173223 }
174224
225+ private static void log (String message ) {
226+ if (LOG_STATES ) System .out .println ("QueryPublisher: " + message );
227+ }
228+
175229}
0 commit comments