From f53b1e424d7ad591c5431d18f8b903d7be7df8d3 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Fri, 12 Dec 2014 11:18:01 +0800 Subject: [PATCH 1/8] Fix the issue that Sample doesn't call 'unsubscribe' --- .../operators/OperatorSampleWithTime.java | 1 + .../operators/OperatorSampleTest.java | 21 +++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorSampleWithTime.java b/src/main/java/rx/internal/operators/OperatorSampleWithTime.java index ea94a7db21..a476dfd96a 100644 --- a/src/main/java/rx/internal/operators/OperatorSampleWithTime.java +++ b/src/main/java/rx/internal/operators/OperatorSampleWithTime.java @@ -68,6 +68,7 @@ static final class SamplerSubscriber extends Subscriber implements Action0 static final AtomicReferenceFieldUpdater VALUE_UPDATER = AtomicReferenceFieldUpdater.newUpdater(SamplerSubscriber.class, Object.class, "value"); public SamplerSubscriber(Subscriber subscriber) { + super(subscriber); this.subscriber = subscriber; } diff --git a/src/test/java/rx/internal/operators/OperatorSampleTest.java b/src/test/java/rx/internal/operators/OperatorSampleTest.java index 0a8c9da58d..815d002061 100644 --- a/src/test/java/rx/internal/operators/OperatorSampleTest.java +++ b/src/test/java/rx/internal/operators/OperatorSampleTest.java @@ -28,14 +28,12 @@ import org.junit.Test; import org.mockito.InOrder; -import rx.Observable; +import rx.*; import rx.Observable.OnSubscribe; -import rx.Observer; -import rx.Scheduler; -import rx.Subscriber; import rx.functions.Action0; import rx.schedulers.TestScheduler; import rx.subjects.PublishSubject; +import rx.subscriptions.Subscriptions; public class OperatorSampleTest { private TestScheduler scheduler; @@ -271,4 +269,19 @@ public void sampleWithSamplerThrows() { inOrder.verify(observer2, times(1)).onError(any(RuntimeException.class)); verify(observer, never()).onCompleted(); } + + @Test + public void testSampleUnsubscribe() { + final Subscription s = mock(Subscription.class); + Observable o = Observable.create( + new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + subscriber.add(s); + } + } + ); + o.throttleLast(1, TimeUnit.MILLISECONDS).subscribe().unsubscribe(); + verify(s).unsubscribe(); + } } From af3aff169ce0b846882e9cec2a81b44f8936e663 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Fri, 12 Dec 2014 14:23:48 +0800 Subject: [PATCH 2/8] Fix the issue that GroupBy may not call 'unsubscribe' --- .../internal/operators/OperatorGroupBy.java | 17 +++++++++++++- .../operators/OperatorGroupByTest.java | 23 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index f934856164..bab6f1839b 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -29,11 +29,13 @@ import rx.Observer; import rx.Producer; import rx.Subscriber; +import rx.Subscription; import rx.exceptions.OnErrorThrowable; import rx.functions.Action0; import rx.functions.Func1; import rx.observables.GroupedObservable; import rx.subjects.Subject; +import rx.subscriptions.Subscriptions; /** * Groups the items emitted by an Observable according to a specified criterion, and emits these @@ -75,6 +77,7 @@ static final class GroupBySubscriber extends Subscriber { final Func1 keySelector; final Func1 elementSelector; final Subscriber> child; + final Subscription parentSubscription = this; public GroupBySubscriber( Func1 keySelector, @@ -84,6 +87,17 @@ public GroupBySubscriber( this.keySelector = keySelector; this.elementSelector = elementSelector; this.child = child; + child.add(Subscriptions.create(new Action0() { + + @Override + public void call() { + // if no group we unsubscribe up otherwise wait until group ends + if (groups.isEmpty()) { + parentSubscription.unsubscribe(); + } + } + + })); } private static class GroupState { @@ -342,8 +356,9 @@ private void completeInner() { if (child.isUnsubscribed()) { // if the entire groupBy has been unsubscribed and children are completed we will propagate the unsubscribe up. unsubscribe(); + } else { + child.onCompleted(); } - child.onCompleted(); } } } diff --git a/src/test/java/rx/internal/operators/OperatorGroupByTest.java b/src/test/java/rx/internal/operators/OperatorGroupByTest.java index 432266ea32..272379dac1 100644 --- a/src/test/java/rx/internal/operators/OperatorGroupByTest.java +++ b/src/test/java/rx/internal/operators/OperatorGroupByTest.java @@ -46,6 +46,7 @@ import rx.Observable.OnSubscribe; import rx.Observer; import rx.Subscriber; +import rx.Subscription; import rx.exceptions.TestException; import rx.functions.Action0; import rx.functions.Action1; @@ -1357,4 +1358,24 @@ public Observable call(GroupedObservable t) { }; -} \ No newline at end of file + @Test + public void testGroupByUnsubscribe() { + final Subscription s = mock(Subscription.class); + Observable o = Observable.create( + new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + subscriber.add(s); + } + } + ); + o.groupBy(new Func1() { + + @Override + public Integer call(Integer integer) { + return null; + } + }).subscribe().unsubscribe(); + verify(s).unsubscribe(); + } +} From 4163352ce51601273a96c394b3765d7f86d5a991 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Fri, 12 Dec 2014 14:41:49 +0800 Subject: [PATCH 3/8] Fix NPE when the key is null in GroupBy --- .../internal/operators/OperatorGroupBy.java | 19 ++++++++---- .../operators/OperatorGroupByTest.java | 29 +++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index bab6f1839b..1317cce60b 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -116,7 +116,7 @@ public Observer getObserver() { } - private final ConcurrentHashMap> groups = new ConcurrentHashMap>(); + private final ConcurrentHashMap> groups = new ConcurrentHashMap>(); private static final NotificationLite nl = NotificationLite.instance(); @@ -180,10 +180,18 @@ void requestFromGroupedObservable(long n, GroupState group) { } } + private Object groupedKey(K key) { + return key == null ? NULL_KEY : key; + } + + private K getKey(Object groupedKey) { + return groupedKey == NULL_KEY ? null : (K) groupedKey; + } + @Override public void onNext(T t) { try { - final K key = keySelector.call(t); + final Object key = groupedKey(keySelector.call(t)); GroupState group = groups.get(key); if (group == null) { // this group doesn't exist @@ -199,10 +207,10 @@ public void onNext(T t) { } } - private GroupState createNewGroup(final K key) { + private GroupState createNewGroup(final Object key) { final GroupState groupState = new GroupState(); - GroupedObservable go = GroupedObservable.create(key, new OnSubscribe() { + GroupedObservable go = GroupedObservable.create(getKey(key), new OnSubscribe() { @Override public void call(final Subscriber o) { @@ -266,7 +274,7 @@ public void onNext(T t) { return groupState; } - private void cleanupGroup(K key) { + private void cleanupGroup(Object key) { GroupState removed; removed = groups.remove(key); if (removed != null) { @@ -372,4 +380,5 @@ public Object call(Object t) { } }; + private static final Object NULL_KEY = new Object(); } diff --git a/src/test/java/rx/internal/operators/OperatorGroupByTest.java b/src/test/java/rx/internal/operators/OperatorGroupByTest.java index 272379dac1..427073d62e 100644 --- a/src/test/java/rx/internal/operators/OperatorGroupByTest.java +++ b/src/test/java/rx/internal/operators/OperatorGroupByTest.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; @@ -1378,4 +1379,32 @@ public Integer call(Integer integer) { }).subscribe().unsubscribe(); verify(s).unsubscribe(); } + + @Test + public void testGroupWithNullKey() { + final String[] key = new String[]{"uninitialized"}; + final List values = new ArrayList(); + Observable.just("a", "b", "c").groupBy(new Func1() { + + @Override + public String call(String value) { + return null; + } + }).subscribe(new Action1>() { + + @Override + public void call(GroupedObservable groupedObservable) { + key[0] = groupedObservable.getKey(); + groupedObservable.subscribe(new Action1() { + + @Override + public void call(String s) { + values.add(s); + } + }); + } + }); + assertEquals(null, key[0]); + assertEquals(Arrays.asList("a", "b", "c"), values); + } } From b13d66275148d8de2b69fcc0a5f60ef63bd5d15b Mon Sep 17 00:00:00 2001 From: zsxwing Date: Fri, 12 Dec 2014 16:00:20 +0800 Subject: [PATCH 4/8] Use 'self' in GroupBySubscriber to unsubscribe --- src/main/java/rx/internal/operators/OperatorGroupBy.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index 1317cce60b..7231bb52b3 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -77,7 +77,6 @@ static final class GroupBySubscriber extends Subscriber { final Func1 keySelector; final Func1 elementSelector; final Subscriber> child; - final Subscription parentSubscription = this; public GroupBySubscriber( Func1 keySelector, @@ -93,7 +92,7 @@ public GroupBySubscriber( public void call() { // if no group we unsubscribe up otherwise wait until group ends if (groups.isEmpty()) { - parentSubscription.unsubscribe(); + self.unsubscribe(); } } From d85b87df2638f647ff214d72d242e0c582b48eb8 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Fri, 12 Dec 2014 17:51:53 +0800 Subject: [PATCH 5/8] Fix the race condition and use isEmpty instead of size --- .../internal/operators/OperatorGroupBy.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index 7231bb52b3..4364fc7696 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -78,6 +78,10 @@ static final class GroupBySubscriber extends Subscriber { final Func1 elementSelector; final Subscriber> child; + final Object lock = new Object(); + // Guarded by "lock" + boolean isUnsubscribed = false; + public GroupBySubscriber( Func1 keySelector, Func1 elementSelector, @@ -90,8 +94,12 @@ public GroupBySubscriber( @Override public void call() { - // if no group we unsubscribe up otherwise wait until group ends - if (groups.isEmpty()) { + synchronized (lock) { + if (groups.isEmpty()) { + isUnsubscribed = true; + } + } + if (isUnsubscribed) { self.unsubscribe(); } } @@ -151,7 +159,7 @@ public void onCompleted() { } // special case (no groups emitted ... or all unsubscribed) - if (groups.size() == 0) { + if (groups.isEmpty()) { // we must track 'completionEmitted' seperately from 'completed' since `completeInner` can result in childObserver.onCompleted() being emitted if (COMPLETION_EMITTED_UPDATER.compareAndSet(this, 0, 1)) { child.onCompleted(); @@ -263,7 +271,13 @@ public void onNext(T t) { } }); - GroupState putIfAbsent = groups.putIfAbsent(key, groupState); + GroupState putIfAbsent; + synchronized (lock) { + if (isUnsubscribed) { + return null; + } + putIfAbsent = groups.putIfAbsent(key, groupState); + } if (putIfAbsent != null) { // this shouldn't happen (because we receive onNext sequentially) and would mean we have a bug throw new IllegalStateException("Group already existed while creating a new one"); @@ -277,7 +291,7 @@ private void cleanupGroup(Object key) { GroupState removed; removed = groups.remove(key); if (removed != null) { - if (removed.buffer.size() > 0) { + if (!removed.buffer.isEmpty()) { BUFFERED_COUNT.addAndGet(self, -removed.buffer.size()); } completeInner(); @@ -356,7 +370,7 @@ private void drainIfPossible(GroupState groupState) { private void completeInner() { // if we have no outstanding groups (all completed or unsubscribe) and terminated/unsubscribed on outer - if (groups.size() == 0 && (terminated == 1 || child.isUnsubscribed())) { + if (groups.isEmpty() && (terminated == 1 || child.isUnsubscribed())) { // completionEmitted ensures we only emit onCompleted once if (COMPLETION_EMITTED_UPDATER.compareAndSet(this, 0, 1)) { From ec89896e029afc051b51de31e1bbb0c29c6aee39 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Fri, 12 Dec 2014 19:16:58 +0800 Subject: [PATCH 6/8] Eliminate the lock --- .../internal/operators/OperatorGroupBy.java | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index 4364fc7696..c08fc276a3 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -19,6 +19,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLongFieldUpdater; @@ -29,7 +30,6 @@ import rx.Observer; import rx.Producer; import rx.Subscriber; -import rx.Subscription; import rx.exceptions.OnErrorThrowable; import rx.functions.Action0; import rx.functions.Func1; @@ -78,8 +78,9 @@ static final class GroupBySubscriber extends Subscriber { final Func1 elementSelector; final Subscriber> child; - final Object lock = new Object(); - // Guarded by "lock" + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater WIP_FOR_UNSUBSCRIBE_UPDATER = AtomicIntegerFieldUpdater.newUpdater(GroupBySubscriber.class, "wipForUnsubscribe"); + volatile int wipForUnsubscribe = 0; boolean isUnsubscribed = false; public GroupBySubscriber( @@ -94,11 +95,14 @@ public GroupBySubscriber( @Override public void call() { - synchronized (lock) { + if (WIP_FOR_UNSUBSCRIBE_UPDATER.getAndIncrement(self) == 0) { if (groups.isEmpty()) { isUnsubscribed = true; } + } else { + // someone is putting, so groups is not empty } + WIP_FOR_UNSUBSCRIBE_UPDATER.decrementAndGet(self); if (isUnsubscribed) { self.unsubscribe(); } @@ -208,7 +212,9 @@ public void onNext(T t) { } group = createNewGroup(key); } - emitItem(group, nl.next(t)); + if (group != null) { + emitItem(group, nl.next(t)); + } } catch (Throwable e) { onError(OnErrorThrowable.addValueAsLastCause(e, t)); } @@ -272,11 +278,17 @@ public void onNext(T t) { }); GroupState putIfAbsent; - synchronized (lock) { - if (isUnsubscribed) { - return null; + while (true) { + if (WIP_FOR_UNSUBSCRIBE_UPDATER.getAndIncrement(this) == 0) { + if (isUnsubscribed) { + WIP_FOR_UNSUBSCRIBE_UPDATER.decrementAndGet(this); + return null; + } + putIfAbsent = groups.putIfAbsent(key, groupState); + WIP_FOR_UNSUBSCRIBE_UPDATER.decrementAndGet(this); + break; } - putIfAbsent = groups.putIfAbsent(key, groupState); + WIP_FOR_UNSUBSCRIBE_UPDATER.decrementAndGet(this); } if (putIfAbsent != null) { // this shouldn't happen (because we receive onNext sequentially) and would mean we have a bug From e309e12b65d46c80000d48337750c3a4784bf39f Mon Sep 17 00:00:00 2001 From: zsxwing Date: Fri, 12 Dec 2014 22:18:52 +0800 Subject: [PATCH 7/8] Update as per @akarnokd's suggestion --- .../internal/operators/OperatorGroupBy.java | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index c08fc276a3..0743718a39 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -80,8 +80,7 @@ static final class GroupBySubscriber extends Subscriber { @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater WIP_FOR_UNSUBSCRIBE_UPDATER = AtomicIntegerFieldUpdater.newUpdater(GroupBySubscriber.class, "wipForUnsubscribe"); - volatile int wipForUnsubscribe = 0; - boolean isUnsubscribed = false; + volatile int wipForUnsubscribe = 1; public GroupBySubscriber( Func1 keySelector, @@ -95,15 +94,7 @@ public GroupBySubscriber( @Override public void call() { - if (WIP_FOR_UNSUBSCRIBE_UPDATER.getAndIncrement(self) == 0) { - if (groups.isEmpty()) { - isUnsubscribed = true; - } - } else { - // someone is putting, so groups is not empty - } - WIP_FOR_UNSUBSCRIBE_UPDATER.decrementAndGet(self); - if (isUnsubscribed) { + if (WIP_FOR_UNSUBSCRIBE_UPDATER.decrementAndGet(self) == 0) { self.unsubscribe(); } } @@ -278,17 +269,15 @@ public void onNext(T t) { }); GroupState putIfAbsent; - while (true) { - if (WIP_FOR_UNSUBSCRIBE_UPDATER.getAndIncrement(this) == 0) { - if (isUnsubscribed) { - WIP_FOR_UNSUBSCRIBE_UPDATER.decrementAndGet(this); - return null; - } + for (;;) { + int wip = wipForUnsubscribe; + if (wip <= 0) { + return null; + } + if (WIP_FOR_UNSUBSCRIBE_UPDATER.compareAndSet(this, wip, wip + 1)) { putIfAbsent = groups.putIfAbsent(key, groupState); - WIP_FOR_UNSUBSCRIBE_UPDATER.decrementAndGet(this); break; } - WIP_FOR_UNSUBSCRIBE_UPDATER.decrementAndGet(this); } if (putIfAbsent != null) { // this shouldn't happen (because we receive onNext sequentially) and would mean we have a bug @@ -381,6 +370,9 @@ private void drainIfPossible(GroupState groupState) { } private void completeInner() { + if (WIP_FOR_UNSUBSCRIBE_UPDATER.decrementAndGet(this) == 0) { + unsubscribe(); + } // if we have no outstanding groups (all completed or unsubscribe) and terminated/unsubscribed on outer if (groups.isEmpty() && (terminated == 1 || child.isUnsubscribed())) { // completionEmitted ensures we only emit onCompleted once From b88457f938cd4407ab0055660699f67ff8a12dcb Mon Sep 17 00:00:00 2001 From: zsxwing Date: Fri, 12 Dec 2014 22:54:57 +0800 Subject: [PATCH 8/8] Add 'unsubscribe' in onError --- src/main/java/rx/internal/operators/OperatorGroupBy.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index 0743718a39..7be49e450d 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -166,8 +166,13 @@ public void onCompleted() { @Override public void onError(Throwable e) { if (TERMINATED_UPDATER.compareAndSet(this, 0, 1)) { - // we immediately tear everything down if we receive an error - child.onError(e); + try { + // we immediately tear everything down if we receive an error + child.onError(e); + } finally { + // We have not chained the subscribers, so need to call it explicitly. + unsubscribe(); + } } }