15
15
*/
16
16
package rx .internal .operators ;
17
17
18
+ import java .util .Map ;
18
19
import java .util .Queue ;
19
20
import java .util .concurrent .ConcurrentHashMap ;
20
21
import java .util .concurrent .ConcurrentLinkedQueue ;
21
22
import java .util .concurrent .atomic .AtomicBoolean ;
23
+ import java .util .concurrent .atomic .AtomicInteger ;
22
24
import java .util .concurrent .atomic .AtomicIntegerFieldUpdater ;
23
25
import java .util .concurrent .atomic .AtomicLong ;
24
26
import java .util .concurrent .atomic .AtomicLongFieldUpdater ;
34
36
import rx .functions .Func1 ;
35
37
import rx .observables .GroupedObservable ;
36
38
import rx .subjects .Subject ;
39
+ import rx .subscriptions .Subscriptions ;
37
40
38
41
/**
39
42
* Groups the items emitted by an Observable according to a specified criterion, and emits these
@@ -76,6 +79,13 @@ static final class GroupBySubscriber<K, T, R> extends Subscriber<T> {
76
79
final Func1 <? super T , ? extends R > elementSelector ;
77
80
final Subscriber <? super GroupedObservable <K , R >> child ;
78
81
82
+ // We should not call `unsubscribe()` until `groups.isEmpty() && child.isUnsubscribed()` is true.
83
+ // Use `WIP_FOR_UNSUBSCRIBE_UPDATER` to monitor these statuses and call `unsubscribe()` properly.
84
+ // Should check both when `child.unsubscribe` is called and any group is removed.
85
+ @ SuppressWarnings ("rawtypes" )
86
+ static final AtomicIntegerFieldUpdater <GroupBySubscriber > WIP_FOR_UNSUBSCRIBE_UPDATER = AtomicIntegerFieldUpdater .newUpdater (GroupBySubscriber .class , "wipForUnsubscribe" );
87
+ volatile int wipForUnsubscribe = 1 ;
88
+
79
89
public GroupBySubscriber (
80
90
Func1 <? super T , ? extends K > keySelector ,
81
91
Func1 <? super T , ? extends R > elementSelector ,
@@ -84,6 +94,16 @@ public GroupBySubscriber(
84
94
this .keySelector = keySelector ;
85
95
this .elementSelector = elementSelector ;
86
96
this .child = child ;
97
+ child .add (Subscriptions .create (new Action0 () {
98
+
99
+ @ Override
100
+ public void call () {
101
+ if (WIP_FOR_UNSUBSCRIBE_UPDATER .decrementAndGet (self ) == 0 ) {
102
+ self .unsubscribe ();
103
+ }
104
+ }
105
+
106
+ }));
87
107
}
88
108
89
109
private static class GroupState <K , T > {
@@ -107,7 +127,13 @@ public Observer<T> getObserver() {
107
127
private static final NotificationLite <Object > nl = NotificationLite .instance ();
108
128
109
129
volatile int completionEmitted ;
110
- volatile int terminated ;
130
+
131
+ private static final int UNTERMINATED = 0 ;
132
+ private static final int TERMINATED_WITH_COMPLETED = 1 ;
133
+ private static final int TERMINATED_WITH_ERROR = 2 ;
134
+
135
+ // Must be one of `UNTERMINATED`, `TERMINATED_WITH_COMPLETED`, `TERMINATED_WITH_ERROR`
136
+ volatile int terminated = UNTERMINATED ;
111
137
112
138
@ SuppressWarnings ("rawtypes" )
113
139
static final AtomicIntegerFieldUpdater <GroupBySubscriber > COMPLETION_EMITTED_UPDATER = AtomicIntegerFieldUpdater .newUpdater (GroupBySubscriber .class , "completionEmitted" );
@@ -130,15 +156,15 @@ public void onStart() {
130
156
131
157
@ Override
132
158
public void onCompleted () {
133
- if (TERMINATED_UPDATER .compareAndSet (this , 0 , 1 )) {
159
+ if (TERMINATED_UPDATER .compareAndSet (this , UNTERMINATED , TERMINATED_WITH_COMPLETED )) {
134
160
// if we receive onCompleted from our parent we onComplete children
135
161
// for each group check if it is ready to accept more events if so pass the oncomplete through else buffer it.
136
162
for (GroupState <K , T > group : groups .values ()) {
137
163
emitItem (group , nl .completed ());
138
164
}
139
165
140
166
// special case (no groups emitted ... or all unsubscribed)
141
- if (groups .size () == 0 ) {
167
+ if (groups .isEmpty () ) {
142
168
// we must track 'completionEmitted' seperately from 'completed' since `completeInner` can result in childObserver.onCompleted() being emitted
143
169
if (COMPLETION_EMITTED_UPDATER .compareAndSet (this , 0 , 1 )) {
144
170
child .onCompleted ();
@@ -149,9 +175,19 @@ public void onCompleted() {
149
175
150
176
@ Override
151
177
public void onError (Throwable e ) {
152
- if (TERMINATED_UPDATER .compareAndSet (this , 0 , 1 )) {
153
- // we immediately tear everything down if we receive an error
154
- child .onError (e );
178
+ if (TERMINATED_UPDATER .compareAndSet (this , UNTERMINATED , TERMINATED_WITH_ERROR )) {
179
+ // It's safe to access all groups and emit the error.
180
+ // onNext and onError are in sequence so no group will be created in the loop.
181
+ for (GroupState <K , T > group : groups .values ()) {
182
+ emitItem (group , nl .error (e ));
183
+ }
184
+ try {
185
+ // we immediately tear everything down if we receive an error
186
+ child .onError (e );
187
+ } finally {
188
+ // We have not chained the subscribers, so need to call it explicitly.
189
+ unsubscribe ();
190
+ }
155
191
}
156
192
}
157
193
@@ -187,7 +223,9 @@ public void onNext(T t) {
187
223
}
188
224
group = createNewGroup (key );
189
225
}
190
- emitItem (group , nl .next (t ));
226
+ if (group != null ) {
227
+ emitItem (group , nl .next (t ));
228
+ }
191
229
} catch (Throwable e ) {
192
230
onError (OnErrorThrowable .addValueAsLastCause (e , t ));
193
231
}
@@ -236,6 +274,11 @@ public void onCompleted() {
236
274
@ Override
237
275
public void onError (Throwable e ) {
238
276
o .onError (e );
277
+ // eagerly cleanup instead of waiting for unsubscribe
278
+ if (once .compareAndSet (false , true )) {
279
+ // done once per instance, either onComplete or onUnSubscribe
280
+ cleanupGroup (key );
281
+ }
239
282
}
240
283
241
284
@ Override
@@ -250,7 +293,17 @@ public void onNext(T t) {
250
293
}
251
294
});
252
295
253
- GroupState <K , T > putIfAbsent = groups .putIfAbsent (key , groupState );
296
+ GroupState <K , T > putIfAbsent ;
297
+ for (;;) {
298
+ int wip = wipForUnsubscribe ;
299
+ if (wip <= 0 ) {
300
+ return null ;
301
+ }
302
+ if (WIP_FOR_UNSUBSCRIBE_UPDATER .compareAndSet (this , wip , wip + 1 )) {
303
+ putIfAbsent = groups .putIfAbsent (key , groupState );
304
+ break ;
305
+ }
306
+ }
254
307
if (putIfAbsent != null ) {
255
308
// this shouldn't happen (because we receive onNext sequentially) and would mean we have a bug
256
309
throw new IllegalStateException ("Group already existed while creating a new one" );
@@ -264,7 +317,7 @@ private void cleanupGroup(Object key) {
264
317
GroupState <K , T > removed ;
265
318
removed = groups .remove (key );
266
319
if (removed != null ) {
267
- if (removed .buffer .size () > 0 ) {
320
+ if (! removed .buffer .isEmpty () ) {
268
321
BUFFERED_COUNT .addAndGet (self , -removed .buffer .size ());
269
322
}
270
323
completeInner ();
@@ -342,15 +395,14 @@ private void drainIfPossible(GroupState<K, T> groupState) {
342
395
}
343
396
344
397
private void completeInner () {
345
- // if we have no outstanding groups (all completed or unsubscribe) and terminated/unsubscribed on outer
346
- if (groups .size () == 0 && (terminated == 1 || child .isUnsubscribed ())) {
398
+ // A group is removed, so check if we need to call `unsubscribe`
399
+ if (WIP_FOR_UNSUBSCRIBE_UPDATER .decrementAndGet (this ) == 0 ) {
400
+ // It means `groups.isEmpty() && child.isUnsubscribed()` is true
401
+ unsubscribe ();
402
+ } else if (groups .isEmpty () && terminated == TERMINATED_WITH_COMPLETED ) {
403
+ // if we have no outstanding groups (all completed or unsubscribe) and terminated on outer
347
404
// completionEmitted ensures we only emit onCompleted once
348
405
if (COMPLETION_EMITTED_UPDATER .compareAndSet (this , 0 , 1 )) {
349
-
350
- if (child .isUnsubscribed ()) {
351
- // if the entire groupBy has been unsubscribed and children are completed we will propagate the unsubscribe up.
352
- unsubscribe ();
353
- }
354
406
child .onCompleted ();
355
407
}
356
408
}
0 commit comments