Skip to content

Commit c693edb

Browse files
authored
3.x: Fix Flowable.groupBy eviction-completion-replenishment problems (ReactiveX#6988)
1 parent e2b1d2f commit c693edb

File tree

2 files changed

+96
-7
lines changed

2 files changed

+96
-7
lines changed

src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGroupBy.java

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,7 @@ public void onError(Throwable t) {
214214
g.onError(t);
215215
}
216216
groups.clear();
217-
if (evictedGroups != null) {
218-
evictedGroups.clear();
219-
}
217+
completeEvictions();
220218
downstream.onError(t);
221219
}
222220

@@ -226,10 +224,10 @@ public void onComplete() {
226224
for (GroupedUnicast<K, V> g : groups.values()) {
227225
g.onComplete();
228226
}
227+
229228
groups.clear();
230-
if (evictedGroups != null) {
231-
evictedGroups.clear();
232-
}
229+
completeEvictions();
230+
233231
done = true;
234232
downstream.onComplete();
235233
}
@@ -594,6 +592,11 @@ void cleanupQueue(long emitted, boolean polled) {
594592
while (queue.poll() != null) {
595593
emitted++;
596594
}
595+
596+
replenishParent(emitted, polled);
597+
}
598+
599+
void replenishParent(long emitted, boolean polled) {
597600
if (polled) {
598601
emitted++;
599602
}
@@ -618,6 +621,9 @@ boolean checkTerminated(boolean d, boolean empty, Subscriber<? super T> a,
618621
a.onError(e);
619622
} else {
620623
a.onComplete();
624+
// completion doesn't mean the parent has completed
625+
// because of evicted groups
626+
replenishParent(emitted, polled);
621627
}
622628
return true;
623629
}
@@ -632,6 +638,10 @@ boolean checkTerminated(boolean d, boolean empty, Subscriber<? super T> a,
632638
if (empty) {
633639
cancelled.lazySet(true);
634640
a.onComplete();
641+
642+
// completion doesn't mean the parent has completed
643+
// because of evicted groups
644+
replenishParent(emitted, polled);
635645
return true;
636646
}
637647
}

src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGroupByTest.java

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818
import static org.mockito.Mockito.*;
1919

2020
import java.io.IOException;
21+
import java.time.Duration;
2122
import java.util.*;
2223
import java.util.concurrent.*;
2324
import java.util.concurrent.atomic.*;
2425

25-
import org.junit.*;
26+
import org.junit.Test;
2627
import org.mockito.Mockito;
2728
import org.reactivestreams.*;
2829

@@ -2876,4 +2877,82 @@ public void issue6974Part2Case1ObserveOnHideLoop() {
28762877
}
28772878
}
28782879
*/
2880+
2881+
static <T> Function<Consumer<Object>, ConcurrentMap<T, Object>> ttlCapGuava(Duration ttl) {
2882+
return itemEvictConsumer ->
2883+
CacheBuilder
2884+
.newBuilder()
2885+
.expireAfterWrite(ttl)
2886+
.removalListener(n -> {
2887+
if (n.getCause() != com.google.common.cache.RemovalCause.EXPLICIT) {
2888+
try {
2889+
itemEvictConsumer.accept(n.getValue());
2890+
} catch (Throwable throwable) {
2891+
throw new RuntimeException(throwable);
2892+
}
2893+
}
2894+
}).<T, Object>build().asMap();
2895+
}
2896+
2897+
@Test
2898+
public void issue6982Case1() {
2899+
final int groups = 20;
2900+
2901+
int groupByBufferSize = 2;
2902+
int flatMapMaxConcurrency = 200 * groups;
2903+
2904+
// ~50% of executions - Not completed (latch = 1, values = 500000, errors = 0, completions = 0, timeout!,
2905+
// disposed!)
2906+
2907+
Flowable
2908+
.range(1, 500_000)
2909+
.map(i -> i % groups)
2910+
.groupBy(i -> i, i -> i, false, groupByBufferSize, ttlCapGuava(Duration.ofMillis(10)))
2911+
.flatMap(gf -> gf.observeOn(Schedulers.computation()), flatMapMaxConcurrency)
2912+
.test()
2913+
.awaitDone(5, TimeUnit.SECONDS)
2914+
.assertNoErrors()
2915+
.assertComplete();
2916+
}
2917+
2918+
/*
2919+
* Disabled: Takes very long. Run it locally only.
2920+
@Test
2921+
public void issue6982Case1Loop() {
2922+
for (int i = 0; i < 200; i++) {
2923+
System.out.println("issue6982Case1Loop " + i);
2924+
issue6982Case1();
2925+
}
2926+
}
2927+
*/
2928+
2929+
@Test
2930+
public void issue6982Case2() {
2931+
final int groups = 20;
2932+
2933+
int groupByBufferSize = groups * 30;
2934+
int flatMapMaxConcurrency = groups * 500;
2935+
// Always : Not completed (latch = 1, values = 14100, errors = 0, completions = 0, timeout!, disposed!)
2936+
2937+
Flowable
2938+
.range(1, 500_000)
2939+
.map(i -> i % groups)
2940+
.groupBy(i -> i, i -> i, false, groupByBufferSize, ttlCapGuava(Duration.ofMillis(10)))
2941+
.flatMap(gf -> gf.observeOn(Schedulers.computation()), flatMapMaxConcurrency)
2942+
.test()
2943+
.awaitDone(5, TimeUnit.SECONDS)
2944+
.assertNoErrors()
2945+
.assertComplete();
2946+
}
2947+
2948+
/*
2949+
* Disabled: Takes very long. Run it locally only.
2950+
@Test
2951+
public void issue6982Case2Loop() {
2952+
for (int i = 0; i < 200; i++) {
2953+
System.out.println("issue6982Case2Loop " + i);
2954+
issue6982Case2();
2955+
}
2956+
}
2957+
*/
28792958
}

0 commit comments

Comments
 (0)