-
Notifications
You must be signed in to change notification settings - Fork 7.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix issues that GroupBy and Sample doesn't call 'unsubscribe' and also NPE when the key is null in GroupBy #1959
Changes from 3 commits
f53b1e4
af3aff1
4163352
b13d662
d85b87d
ec89896
e309e12
b88457f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<K, T, R> extends Subscriber<T> { | |
final Func1<? super T, ? extends K> keySelector; | ||
final Func1<? super T, ? extends R> elementSelector; | ||
final Subscriber<? super GroupedObservable<K, R>> child; | ||
final Subscription parentSubscription = this; | ||
|
||
public GroupBySubscriber( | ||
Func1<? super T, ? extends K> keySelector, | ||
|
@@ -84,6 +87,17 @@ public GroupBySubscriber( | |
this.keySelector = keySelector; | ||
this.elementSelector = elementSelector; | ||
this.child = child; | ||
child.add(Subscriptions.create(new Action0() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like a good change. It was already correctly not emitting groups if it was unsubscribed, so this is only applicable to a scenario where there no groups have been emitted, such as a stream with no data, correct? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or I suppose a stream where all groups have been unsubscribed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Found a race condition. I used a lock to fix it... Any suggestion for a lock-free approach? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With a atomic wip counter I guess. But then you need a CAS loop to check if wip == 0 and do nothing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @akarnokd could you take a look at my latest commit? Is it exactly what you mean? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not really. I'd do
In words, start out wip as 1 because there is the main subscription. Each group created ++wip conditionally: if wip = 0 then an unsubscription happened and there was no active group and thus don't create a new group. Once each group terminates or gets unsubscribed, --wip and if it reaches zero, upstream is unsubscribed. This of course assuming completeInner is idempotent per group. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Brilliant. Done. |
||
|
||
@Override | ||
public void call() { | ||
// if no group we unsubscribe up otherwise wait until group ends | ||
if (groups.isEmpty()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not certain what the correct thing to do here. If there are multiple groups in flight and the outer observable is unsubscribed, do we want to unsubscribe the open groups, enqueue onCompleted elements on them but let them run, or something else? Window has this anomaly as well where the unsubscribed outer may never deliver an onCompleted on the inner windows and thus they stall. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe we should have the same behavior for groupby and window, so I followed the discussion in #1546. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is crazy complicated logic. This operator was re-written 3 times I think to get all of that right so I'm trusting that the unit tests are making sure we're still correct. We can not unsubscribe the inner groups when the outer receives an unsubscribe, they must all run but now we just don't emit any new groups. The reason is that something like a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, the code makes more sense now with that requirement. Might worth checking Window for this as well. |
||
parentSubscription.unsubscribe(); | ||
} | ||
} | ||
|
||
})); | ||
} | ||
|
||
private static class GroupState<K, T> { | ||
|
@@ -102,7 +116,7 @@ public Observer<T> getObserver() { | |
|
||
} | ||
|
||
private final ConcurrentHashMap<K, GroupState<K, T>> groups = new ConcurrentHashMap<K, GroupState<K, T>>(); | ||
private final ConcurrentHashMap<Object, GroupState<K, T>> groups = new ConcurrentHashMap<Object, GroupState<K, T>>(); | ||
|
||
private static final NotificationLite<Object> nl = NotificationLite.instance(); | ||
|
||
|
@@ -166,10 +180,18 @@ void requestFromGroupedObservable(long n, GroupState<K, T> 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<K, T> group = groups.get(key); | ||
if (group == null) { | ||
// this group doesn't exist | ||
|
@@ -185,10 +207,10 @@ public void onNext(T t) { | |
} | ||
} | ||
|
||
private GroupState<K, T> createNewGroup(final K key) { | ||
private GroupState<K, T> createNewGroup(final Object key) { | ||
final GroupState<K, T> groupState = new GroupState<K, T>(); | ||
|
||
GroupedObservable<K, R> go = GroupedObservable.create(key, new OnSubscribe<R>() { | ||
GroupedObservable<K, R> go = GroupedObservable.create(getKey(key), new OnSubscribe<R>() { | ||
|
||
@Override | ||
public void call(final Subscriber<? super R> o) { | ||
|
@@ -252,7 +274,7 @@ public void onNext(T t) { | |
return groupState; | ||
} | ||
|
||
private void cleanupGroup(K key) { | ||
private void cleanupGroup(Object key) { | ||
GroupState<K, T> removed; | ||
removed = groups.remove(key); | ||
if (removed != null) { | ||
|
@@ -342,8 +364,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(); | ||
} | ||
} | ||
} | ||
|
@@ -357,4 +380,5 @@ public Object call(Object t) { | |
} | ||
}; | ||
|
||
private static final Object NULL_KEY = new Object(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -68,6 +68,7 @@ static final class SamplerSubscriber<T> extends Subscriber<T> implements Action0 | |
static final AtomicReferenceFieldUpdater<SamplerSubscriber, Object> VALUE_UPDATER | ||
= AtomicReferenceFieldUpdater.newUpdater(SamplerSubscriber.class, Object.class, "value"); | ||
public SamplerSubscriber(Subscriber<? super T> subscriber) { | ||
super(subscriber); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will unsubscribe the downstream. I suggest instead having There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't had a chance to try this code yet, but I want to make sure this doesn't break the backpressure functionality where this operator requests There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Could you elaborate? I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
sample with time does not support backpressure as it uses time to control data flow, right? |
||
this.subscriber = subscriber; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a
self
member variable which also points to this.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point.