Skip to content

Commit e467798

Browse files
davidmotenakarnokd
authored andcommitted
1.x: fix and deprecate evicting groupBy and add new overload (#5917)
* 1.x: fix bug and deprecate groupBy overload with evictingMapFactory, add new groupBy evicting overload (#5868) * groupBy, do a null check on g because cancel(K) could have cleared the map
1 parent 6e6c514 commit e467798

File tree

5 files changed

+928
-6
lines changed

5 files changed

+928
-6
lines changed

build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ dependencies {
2626

2727
testCompile 'junit:junit:4.12'
2828
testCompile 'org.mockito:mockito-core:1.10.19'
29-
testCompile 'com.google.guava:guava:19.0'
29+
testCompile 'com.google.guava:guava:24.0-jre'
3030
testCompile 'com.pushtorefresh.java-private-constructor-checker:checker:1.2.0'
3131

3232
perfCompile 'org.openjdk.jmh:jmh-core:1.11.3'

src/main/java/rx/Observable.java

+73-2
Original file line numberDiff line numberDiff line change
@@ -7275,7 +7275,7 @@ public final void forEach(final Action1<? super T> onNext, final Action1<Throwab
72757275
* @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX operators documentation: GroupBy</a>
72767276
*/
72777277
public final <K, R> Observable<GroupedObservable<K, R>> groupBy(final Func1<? super T, ? extends K> keySelector, final Func1<? super T, ? extends R> elementSelector) {
7278-
return lift(new OperatorGroupBy<T, K, R>(keySelector, elementSelector));
7278+
return lift(new OperatorGroupByEvicting<T, K, R>(keySelector, elementSelector));
72797279
}
72807280

72817281
/**
@@ -7334,7 +7334,12 @@ public final <K, R> Observable<GroupedObservable<K, R>> groupBy(final Func1<? su
73347334
* if {@code evictingMapFactory} is null
73357335
* @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX operators documentation: GroupBy</a>
73367336
* @since 1.3
7337+
* @deprecated since 1.3.7, use {@link #groupBy(Func1, Func1, int, boolean, Func1)}
7338+
* instead which uses much less memory. Please take note of the
7339+
* usage difference involving the evicting action which now expects
7340+
* the value from the map instead of the key.
73377341
*/
7342+
@Deprecated
73387343
public final <K, R> Observable<GroupedObservable<K, R>> groupBy(final Func1<? super T, ? extends K> keySelector,
73397344
final Func1<? super T, ? extends R> elementSelector, final Func1<Action1<K>, Map<K, Object>> evictingMapFactory) {
73407345
if (evictingMapFactory == null) {
@@ -7369,6 +7374,72 @@ public final <K, R> Observable<GroupedObservable<K, R>> groupBy(final Func1<? su
73697374
*
73707375
* @param keySelector
73717376
* a function that extracts the key for each item
7377+
* @param elementSelector
7378+
* a function that extracts the return element for each item
7379+
* @param bufferSize
7380+
* the size of the buffer ({@link RxRingBuffer.SIZE} may be suitable).
7381+
* @param delayError
7382+
* if and only if false then onError emissions can shortcut onNext emissions (emissions may be buffered)
7383+
* @param evictingMapFactory
7384+
* a function that given an eviction action returns a {@link Map} instance that will be used to assign
7385+
* items to the appropriate {@code GroupedObservable}s. The {@code Map} instance must be thread-safe
7386+
* and any eviction must trigger a call to the supplied action (synchronously or asynchronously).
7387+
* This can be used to limit the size of the map by evicting entries by map maximum size or access time for
7388+
* instance. Here's an example using Guava's {@code CacheBuilder} from v24.0:
7389+
* <pre>
7390+
* {@code
7391+
* Func1<Action1<Object>, Map<K, Object>> mapFactory
7392+
* = action -> CacheBuilder.newBuilder()
7393+
* .maximumSize(1000)
7394+
* .expireAfterAccess(12, TimeUnit.HOURS)
7395+
* .removalListener(entry -> action.call(entry.getValue()))
7396+
* .<K, Object> build().asMap();
7397+
* }
7398+
* </pre>
7399+
*
7400+
* @param <K>
7401+
* the key type
7402+
* @param <R>
7403+
* the element type
7404+
* @return an {@code Observable} that emits {@link GroupedObservable}s, each of which corresponds to a
7405+
* unique key value and each of which emits those items from the source Observable that share that
7406+
* key value
7407+
* @throws NullPointerException
7408+
* if {@code evictingMapFactory} is null
7409+
* @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX operators documentation: GroupBy</a>
7410+
* @since 1.3.7
7411+
*/
7412+
@Experimental
7413+
public final <K, R> Observable<GroupedObservable<K, R>> groupBy(final Func1<? super T, ? extends K> keySelector,
7414+
final Func1<? super T, ? extends R> elementSelector, int bufferSize, boolean delayError,
7415+
final Func1<Action1<Object>, Map<K, Object>> evictingMapFactory) {
7416+
if (evictingMapFactory == null) {
7417+
throw new NullPointerException("evictingMapFactory cannot be null");
7418+
}
7419+
return lift(new OperatorGroupByEvicting<T, K, R>(
7420+
keySelector, elementSelector, bufferSize, delayError, evictingMapFactory));
7421+
}
7422+
7423+
/**
7424+
* Groups the items emitted by an {@code Observable} according to a specified criterion, and emits these
7425+
* grouped items as {@link GroupedObservable}s. The emitted {@code GroupedObservable} allows only a single
7426+
* {@link Subscriber} during its lifetime and if this {@code Subscriber} unsubscribes before the
7427+
* source terminates, the next emission by the source having the same key will trigger a new
7428+
* {@code GroupedObservable} emission.
7429+
* <p>
7430+
* <img width="640" height="360" src="https://raw.github.com./wiki/ReactiveX/RxJava/images/rx-operators/groupBy.png" alt="">
7431+
* <p>
7432+
* <em>Note:</em> A {@link GroupedObservable} will cache the items it is to emit until such time as it
7433+
* is subscribed to. For this reason, in order to avoid memory leaks, you should not simply ignore those
7434+
* {@code GroupedObservable}s that do not concern you. Instead, you can signal to them that they may
7435+
* discard their buffers by applying an operator like {@link #ignoreElements} to them.
7436+
* <dl>
7437+
* <dt><b>Scheduler:</b></dt>
7438+
* <dd>{@code groupBy} does not operate by default on a particular {@link Scheduler}.</dd>
7439+
* </dl>
7440+
*
7441+
* @param keySelector
7442+
* a function that extracts the key for each item
73727443
* @param <K>
73737444
* the key type
73747445
* @return an {@code Observable} that emits {@link GroupedObservable}s, each of which corresponds to a
@@ -7377,7 +7448,7 @@ public final <K, R> Observable<GroupedObservable<K, R>> groupBy(final Func1<? su
73777448
* @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX operators documentation: GroupBy</a>
73787449
*/
73797450
public final <K> Observable<GroupedObservable<K, T>> groupBy(final Func1<? super T, ? extends K> keySelector) {
7380-
return lift(new OperatorGroupBy<T, K, T>(keySelector));
7451+
return lift(new OperatorGroupByEvicting<T, K, T>(keySelector));
73817452
}
73827453

73837454
/**

src/main/java/rx/internal/operators/OperatorGroupBy.java

+21-3
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,16 @@
4242
* the source and group value type
4343
* @param <V>
4444
* the value type of the groups
45+
* @deprecated
46+
* since 1.3.7, use {@link OperatorGroupByEvicting} instead
4547
*/
48+
@Deprecated
4649
public final class OperatorGroupBy<T, K, V> implements Operator<GroupedObservable<K, V>, T> {
4750
final Func1<? super T, ? extends K> keySelector;
4851
final Func1<? super T, ? extends V> valueSelector;
4952
final int bufferSize;
5053
final boolean delayError;
51-
final Func1<Action1<K>, Map<K, Object>> mapFactory;
54+
final Func1<Action1<K>, Map<K, Object>> mapFactory; //nullable
5255

5356
@SuppressWarnings({ "unchecked", "rawtypes" })
5457
public OperatorGroupBy(Func1<? super T, ? extends K> keySelector) {
@@ -116,6 +119,10 @@ public static final class GroupBySubscriber<T, K, V>
116119
final int bufferSize;
117120
final boolean delayError;
118121
final Map<Object, GroupedUnicast<K, V>> groups;
122+
123+
// double store the groups to workaround the bug in the
124+
// signature of groupBy with evicting map factory
125+
final Map<Object, GroupedUnicast<K, V>> groupsCopy;
119126
final Queue<GroupedObservable<K, V>> queue;
120127
final GroupByProducer producer;
121128
final Queue<K> evictedKeys;
@@ -134,7 +141,7 @@ public static final class GroupBySubscriber<T, K, V>
134141
volatile boolean done;
135142

136143
final AtomicInteger wip;
137-
144+
138145
public GroupBySubscriber(Subscriber<? super GroupedObservable<K, V>> actual, Func1<? super T, ? extends K> keySelector,
139146
Func1<? super T, ? extends V> valueSelector, int bufferSize, boolean delayError,
140147
Func1<Action1<K>, Map<K, Object>> mapFactory) {
@@ -158,6 +165,7 @@ public GroupBySubscriber(Subscriber<? super GroupedObservable<K, V>> actual, Fun
158165
this.evictedKeys = new ConcurrentLinkedQueue<K>();
159166
this.groups = createMap(mapFactory, new EvictionAction<K>(evictedKeys));
160167
}
168+
this.groupsCopy = new ConcurrentHashMap<Object, GroupedUnicast<K, V>>();
161169
}
162170

163171
static class EvictionAction<K> implements Action1<K> {
@@ -211,6 +219,9 @@ public void onNext(T t) {
211219
if (!cancelled.get()) {
212220
group = GroupedUnicast.createWith(key, bufferSize, this, delayError);
213221
groups.put(mapKey, group);
222+
if (evictedKeys != null) {
223+
groupsCopy.put(mapKey, group);
224+
}
214225

215226
groupCount.getAndIncrement();
216227

@@ -234,7 +245,9 @@ public void onNext(T t) {
234245
if (evictedKeys != null) {
235246
K evictedKey;
236247
while ((evictedKey = evictedKeys.poll()) != null) {
237-
GroupedUnicast<K, V> g = groups.get(evictedKey);
248+
GroupedUnicast<K, V> g = groupsCopy.remove(evictedKey);
249+
// do a null check on g because cancel(K) could have cleared
250+
// the map
238251
if (g != null) {
239252
g.onComplete();
240253
}
@@ -270,6 +283,7 @@ public void onCompleted() {
270283
}
271284
groups.clear();
272285
if (evictedKeys != null) {
286+
groupsCopy.clear();
273287
evictedKeys.clear();
274288
}
275289

@@ -304,6 +318,9 @@ public void cancel(K key) {
304318
unsubscribe();
305319
}
306320
}
321+
if (evictedKeys != null) {
322+
groupsCopy.remove(mapKey);
323+
}
307324
}
308325

309326
void drain() {
@@ -364,6 +381,7 @@ void errorAll(Subscriber<? super GroupedObservable<K, V>> a, Queue<?> q, Throwab
364381
List<GroupedUnicast<K, V>> list = new ArrayList<GroupedUnicast<K, V>>(groups.values());
365382
groups.clear();
366383
if (evictedKeys != null) {
384+
groupsCopy.clear();
367385
evictedKeys.clear();
368386
}
369387

0 commit comments

Comments
 (0)