Skip to content

Commit cd75166

Browse files
committed
2.x: fix GroupBy MissingBackpressureException due to main/group overflow
The problem with GroupBy is that a request(1) from the main source may result in a new group, a value for another group or value for the intended group. The latter two is handled by unicasting but the former was not properly handled.
1 parent aa400d1 commit cd75166

File tree

2 files changed

+223
-63
lines changed

2 files changed

+223
-63
lines changed

src/main/java/io/reactivex/internal/operators/OperatorGroupBy.java

Lines changed: 180 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121
import org.reactivestreams.*;
2222

2323
import io.reactivex.Observable.Operator;
24-
import io.reactivex.internal.queue.SpscLinkedArrayQueue;
24+
import io.reactivex.exceptions.MissingBackpressureException;
25+
import io.reactivex.internal.queue.*;
2526
import io.reactivex.internal.subscriptions.*;
26-
import io.reactivex.internal.util.BackpressureHelper;
27+
import io.reactivex.internal.util.*;
2728
import io.reactivex.observables.GroupedObservable;
29+
import io.reactivex.plugins.RxJavaPlugins;
2830

2931
public final class OperatorGroupBy<T, K, V> implements Operator<GroupedObservable<K, V>, T>{
3032
final Function<? super T, ? extends K> keySelector;
@@ -44,7 +46,9 @@ public Subscriber<? super T> apply(Subscriber<? super GroupedObservable<K, V>> t
4446
return new GroupBySubscriber<>(t, keySelector, valueSelector, bufferSize, delayError);
4547
}
4648

47-
public static final class GroupBySubscriber<T, K, V> extends AtomicInteger implements Subscriber<T>, Subscription {
49+
public static final class GroupBySubscriber<T, K, V>
50+
extends AtomicInteger
51+
implements Subscriber<T>, Subscription {
4852
/** */
4953
private static final long serialVersionUID = -3688291656102519502L;
5054

@@ -54,6 +58,7 @@ public static final class GroupBySubscriber<T, K, V> extends AtomicInteger imple
5458
final int bufferSize;
5559
final boolean delayError;
5660
final Map<Object, GroupedUnicast<K, V>> groups;
61+
final Queue<T> queue;
5762

5863
static final Object NULL_KEY = new Object();
5964

@@ -64,14 +69,28 @@ public static final class GroupBySubscriber<T, K, V> extends AtomicInteger imple
6469
static final AtomicIntegerFieldUpdater<GroupBySubscriber> CANCELLED =
6570
AtomicIntegerFieldUpdater.newUpdater(GroupBySubscriber.class, "cancelled");
6671

72+
volatile long requested;
73+
@SuppressWarnings("rawtypes")
74+
static final AtomicLongFieldUpdater<GroupBySubscriber> REQUESTED =
75+
AtomicLongFieldUpdater.newUpdater(GroupBySubscriber.class, "requested");
76+
77+
volatile int groupCount;
78+
@SuppressWarnings("rawtypes")
79+
static final AtomicIntegerFieldUpdater<GroupBySubscriber> GROUP_COUNT =
80+
AtomicIntegerFieldUpdater.newUpdater(GroupBySubscriber.class, "groupCount");
81+
82+
Throwable error;
83+
volatile boolean done;
84+
6785
public GroupBySubscriber(Subscriber<? super GroupedObservable<K, V>> actual, Function<? super T, ? extends K> keySelector, Function<? super T, ? extends V> valueSelector, int bufferSize, boolean delayError) {
6886
this.actual = actual;
6987
this.keySelector = keySelector;
7088
this.valueSelector = valueSelector;
7189
this.bufferSize = bufferSize;
7290
this.delayError = delayError;
7391
this.groups = new ConcurrentHashMap<>();
74-
this.lazySet(1);
92+
this.queue = new SpscLinkedArrayQueue<>(bufferSize);
93+
GROUP_COUNT.lazySet(this, 1);
7594
}
7695

7796
@Override
@@ -82,90 +101,60 @@ public void onSubscribe(Subscription s) {
82101

83102
this.s = s;
84103
actual.onSubscribe(this);
104+
s.request(bufferSize);
85105
}
86106

87107
@Override
88108
public void onNext(T t) {
89-
K key;
90-
try {
91-
key = keySelector.apply(t);
92-
} catch (Throwable e) {
93-
s.cancel();
94-
onError(e);
109+
if (done) {
95110
return;
96111
}
97-
98-
boolean notNew = true;
99-
Object mapKey = key != null ? key : NULL_KEY;
100-
GroupedUnicast<K, V> group = groups.get(mapKey);
101-
if (group == null) {
102-
// if the main has been cancelled, stop creating groups
103-
// and skip this value
104-
if (cancelled != 0) {
105-
s.request(1);
106-
return;
107-
}
108-
notNew = true;
109-
110-
group = GroupedUnicast.createWith(key, bufferSize, this, delayError);
111-
groups.put(mapKey, group);
112-
113-
getAndIncrement();
114-
115-
actual.onNext(group);
116-
}
117-
118-
V v;
119-
try {
120-
v = valueSelector.apply(t);
121-
} catch (Throwable e) {
112+
if (!queue.offer(t)) {
122113
s.cancel();
123-
onError(e);
114+
onError(new MissingBackpressureException("Queue full?!"));
124115
return;
125116
}
126-
127-
group.onNext(v);
128-
129-
if (notNew) {
130-
s.request(1); // we spent this t on an existing group, request one more
131-
}
117+
drain();
132118
}
133119

134120
@Override
135121
public void onError(Throwable t) {
136-
List<GroupedUnicast<K, V>> list = new ArrayList<>(groups.values());
137-
groups.clear();
138-
139-
for (GroupedUnicast<K, V> e : list) {
140-
e.onError(t);
122+
if (done) {
123+
RxJavaPlugins.onError(t);
124+
return;
141125
}
142-
143-
actual.onError(t);
126+
error = t;
127+
done = true;
128+
GROUP_COUNT.decrementAndGet(this);
129+
drain();
144130
}
145131

146132
@Override
147133
public void onComplete() {
148-
List<GroupedUnicast<K, V>> list = new ArrayList<>(groups.values());
149-
groups.clear();
150-
151-
for (GroupedUnicast<K, V> e : list) {
152-
e.onComplete();
134+
if (done) {
135+
return;
153136
}
154-
155-
actual.onComplete();
137+
done = true;
138+
GROUP_COUNT.decrementAndGet(this);
139+
drain();
156140
}
157141

158142
@Override
159143
public void request(long n) {
160-
s.request(n);
144+
if (SubscriptionHelper.validateRequest(n)) {
145+
return;
146+
}
147+
148+
BackpressureHelper.add(REQUESTED, this, n);
149+
drain();
161150
}
162151

163152
@Override
164153
public void cancel() {
165154
// cancelling the main source means we don't want any more groups
166155
// but running groups still require new values
167156
if (CANCELLED.compareAndSet(this, 0, 1)) {
168-
if (decrementAndGet() == 0) {
157+
if (GROUP_COUNT.decrementAndGet(this) == 0) {
169158
s.cancel();
170159
}
171160
}
@@ -174,10 +163,142 @@ public void cancel() {
174163
public void cancel(K key) {
175164
Object mapKey = key != null ? key : NULL_KEY;
176165
groups.remove(mapKey);
177-
if (decrementAndGet() == 0) {
166+
if (GROUP_COUNT.decrementAndGet(this) == 0) {
178167
s.cancel();
179168
}
180169
}
170+
171+
void drain() {
172+
if (getAndIncrement() != 0) {
173+
return;
174+
}
175+
176+
int missed = 1;
177+
178+
final Queue<T> q = this.queue;
179+
final Subscriber<? super GroupedObservable<K, V>> a = this.actual;
180+
181+
for (;;) {
182+
183+
if (checkTerminated(done, q.isEmpty(), a, q)) {
184+
return;
185+
}
186+
187+
long r = requested;
188+
boolean unbounded = r == Long.MAX_VALUE;
189+
long e = 0L;
190+
191+
while (r != 0) {
192+
boolean d = done;
193+
194+
T t = q.poll();
195+
196+
boolean empty = t == null;
197+
198+
if (checkTerminated(d, empty, a, q)) {
199+
return;
200+
}
201+
202+
if (empty) {
203+
break;
204+
}
205+
206+
K key;
207+
try {
208+
key = keySelector.apply(t);
209+
} catch (Throwable ex) {
210+
s.cancel();
211+
errorAll(a, q, ex);
212+
return;
213+
}
214+
215+
boolean notNew = true;
216+
Object mapKey = key != null ? key : NULL_KEY;
217+
GroupedUnicast<K, V> group = groups.get(mapKey);
218+
if (group == null) {
219+
// if the main has been cancelled, stop creating groups
220+
// and skip this value
221+
if (cancelled == 0) {
222+
group = GroupedUnicast.createWith(key, bufferSize, this, delayError);
223+
groups.put(mapKey, group);
224+
225+
GROUP_COUNT.getAndIncrement(this);
226+
227+
actual.onNext(group);
228+
229+
notNew = false;
230+
} else {
231+
continue;
232+
}
233+
}
234+
235+
V v;
236+
try {
237+
v = valueSelector.apply(t);
238+
} catch (Throwable ex) {
239+
s.cancel();
240+
errorAll(a, q, ex);
241+
return;
242+
}
243+
244+
group.onNext(v);
245+
246+
if (notNew) {
247+
s.request(1); // we spent this t on an existing group, request one more
248+
} else {
249+
r--;
250+
e--;
251+
}
252+
}
253+
254+
if (e != 0L) {
255+
if (!unbounded) {
256+
REQUESTED.addAndGet(this, e);
257+
}
258+
s.request(-e);
259+
}
260+
261+
missed = addAndGet(-missed);
262+
if (missed == 0) {
263+
break;
264+
}
265+
}
266+
}
267+
268+
void errorAll(Subscriber<? super GroupedObservable<K, V>> a, Queue<T> q, Throwable ex) {
269+
q.clear();
270+
List<GroupedUnicast<K, V>> list = new ArrayList<>(groups.values());
271+
groups.clear();
272+
273+
for (GroupedUnicast<K, V> e : list) {
274+
e.onError(ex);
275+
}
276+
277+
a.onError(ex);
278+
}
279+
280+
boolean checkTerminated(boolean d, boolean empty,
281+
Subscriber<? super GroupedObservable<K, V>> a, Queue<T> q) {
282+
if (d) {
283+
Throwable err = error;
284+
if (err != null) {
285+
errorAll(a, q, err);
286+
return true;
287+
} else
288+
if (empty) {
289+
List<GroupedUnicast<K, V>> list = new ArrayList<>(groups.values());
290+
groups.clear();
291+
292+
for (GroupedUnicast<K, V> e : list) {
293+
e.onComplete();
294+
}
295+
296+
actual.onComplete();
297+
return true;
298+
}
299+
}
300+
return false;
301+
}
181302
}
182303

183304
static final class GroupedUnicast<K, T> extends GroupedObservable<K, T> {
@@ -247,7 +368,6 @@ public void request(long n) {
247368
return;
248369
}
249370
BackpressureHelper.add(REQUESTED, this, n);
250-
parent.request(n);
251371
drain();
252372
}
253373

src/test/java/io/reactivex/internal/operators/OperatorGroupByTest.java

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
import java.util.concurrent.atomic.*;
2323
import java.util.function.*;
2424

25-
import org.junit.*;
26-
import org.mockito.*;
25+
import org.junit.Test;
26+
import org.mockito.Matchers;
2727
import org.reactivestreams.*;
2828

2929
import io.reactivex.Observable;
@@ -1288,7 +1288,9 @@ public void testgroupByBackpressure2() throws InterruptedException {
12881288

12891289
TestSubscriber<String> ts = new TestSubscriber<>();
12901290

1291-
Observable.range(1, 4000).groupBy(IS_EVEN2).flatMap(new Function<GroupedObservable<Boolean, Integer>, Observable<String>>() {
1291+
Observable.range(1, 4000)
1292+
.doOnNext(v -> System.out.println("testgroupByBackpressure2 >> " + v))
1293+
.groupBy(IS_EVEN2).flatMap(new Function<GroupedObservable<Boolean, Integer>, Observable<String>>() {
12921294

12931295
@Override
12941296
public Observable<String> apply(final GroupedObservable<Boolean, Integer> g) {
@@ -1471,4 +1473,42 @@ public void onNext(Integer t) {
14711473
}});
14721474
assertTrue(completed.get());
14731475
}
1476+
1477+
/**
1478+
* Issue #3425.
1479+
*
1480+
* The problem is that a request of 1 may create a new group, emit to the desired group
1481+
* or emit to a completely different group. In this test, the merge requests N which
1482+
* must be produced by the range, however it will create a bunch of groups before the actual
1483+
* group receives a value.
1484+
*/
1485+
@Test
1486+
public void testBackpressureObserveOnOuter() {
1487+
for (int j = 0; j < 1000; j++) {
1488+
Observable.merge(
1489+
Observable.range(0, 500)
1490+
.groupBy(i -> i % (Observable.bufferSize() + 2))
1491+
.observeOn(Schedulers.computation())
1492+
).toBlocking().last();
1493+
}
1494+
}
1495+
1496+
/**
1497+
* Synchronous verification of issue #3425.
1498+
*/
1499+
@Test
1500+
public void testBackpressureInnerDoesntOverflowOuter() {
1501+
TestSubscriber<GroupedObservable<Integer, Integer>> ts = new TestSubscriber<>((Long)null);
1502+
1503+
Observable.fromArray(1, 2)
1504+
.groupBy(v -> v)
1505+
.doOnNext(g -> g.subscribe()) // this will request Long.MAX_VALUE
1506+
.subscribe(ts)
1507+
;
1508+
ts.request(1);
1509+
1510+
ts.assertNotComplete();
1511+
ts.assertNoErrors();
1512+
ts.assertValueCount(1);
1513+
}
14741514
}

0 commit comments

Comments
 (0)