Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.reactivx.mantis.operators.OperatorGroupBy Maven / Gradle / Ivy
/*
* Copyright 2019 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.reactivx.mantis.operators;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import rx.Observable;
import rx.Observable.OnSubscribe;
import rx.Observable.Operator;
import rx.Observer;
import rx.Producer;
import rx.Subscriber;
import rx.exceptions.OnErrorThrowable;
import rx.functions.Action0;
import rx.functions.Func1;
import rx.internal.operators.NotificationLite;
import rx.observables.GroupedObservable;
import rx.subjects.Subject;
/**
* Groups the items emitted by an Observable according to a specified criterion, and emits these
* grouped items as Observables, one Observable per group.
*
*
*
* @param
* the key type
* @param
* the source and group value type
* @param
* the value type of the groups
*/
public class OperatorGroupBy implements Operator, T> {
private final static Func1 IDENTITY = new Func1() {
@Override
public Object call(Object t) {
return t;
}
};
final Func1 super T, ? extends K> keySelector;
final Func1 super T, ? extends R> valueSelector;
@SuppressWarnings("unchecked")
public OperatorGroupBy(final Func1 super T, ? extends K> keySelector) {
this(keySelector, (Func1) IDENTITY);
}
public OperatorGroupBy(
Func1 super T, ? extends K> keySelector,
Func1 super T, ? extends R> valueSelector) {
this.keySelector = keySelector;
this.valueSelector = valueSelector;
}
@Override
public Subscriber super T> call(final Subscriber super GroupedObservable> child) {
return new GroupBySubscriber(keySelector, valueSelector, child);
}
static final class GroupBySubscriber extends Subscriber {
@SuppressWarnings("rawtypes")
static final AtomicIntegerFieldUpdater COMPLETION_EMITTED_UPDATER = AtomicIntegerFieldUpdater.newUpdater(GroupBySubscriber.class, "completionEmitted");
@SuppressWarnings("rawtypes")
static final AtomicIntegerFieldUpdater TERMINATED_UPDATER = AtomicIntegerFieldUpdater.newUpdater(GroupBySubscriber.class, "terminated");
@SuppressWarnings("rawtypes")
static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(GroupBySubscriber.class, "requested");
@SuppressWarnings("rawtypes")
static final AtomicLongFieldUpdater BUFFERED_COUNT = AtomicLongFieldUpdater.newUpdater(GroupBySubscriber.class, "bufferedCount");
private static final int MAX_QUEUE_SIZE = 1024;
final GroupBySubscriber self = this;
final Func1 super T, ? extends K> keySelector;
final Func1 super T, ? extends R> elementSelector;
final Subscriber super GroupedObservable> child;
private final ConcurrentHashMap> groups = new ConcurrentHashMap>();
volatile int completionEmitted;
volatile int terminated;
volatile long requested;
volatile long bufferedCount;
public GroupBySubscriber(
Func1 super T, ? extends K> keySelector,
Func1 super T, ? extends R> elementSelector,
Subscriber super GroupedObservable> child) {
super();
this.keySelector = keySelector;
this.elementSelector = elementSelector;
this.child = child;
}
@Override
public void onStart() {
REQUESTED.set(this, MAX_QUEUE_SIZE);
request(MAX_QUEUE_SIZE);
}
@Override
public void onCompleted() {
if (TERMINATED_UPDATER.compareAndSet(this, 0, 1)) {
// if we receive onCompleted from our parent we onComplete children
// for each group check if it is ready to accept more events if so pass the oncomplete through else buffer it.
for (GroupState group : groups.values()) {
emitItem(group, NotificationLite.completed());
}
// special case (no groups emitted ... or all unsubscribed)
if (groups.size() == 0) {
// 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();
}
}
}
}
@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);
}
}
void requestFromGroupedObservable(long n, GroupState group) {
group.requested.getAndAdd(n);
if (group.count.getAndIncrement() == 0) {
pollQueue(group);
}
}
// The grouped observable propagates the 'producer.request' call from it's subscriber to this method
// Here we keep track of the requested count for each group
// If we already have items queued when a request comes in we vend those and decrement the outstanding request count
@Override
public void onNext(T t) {
try {
final K key = keySelector.call(t);
GroupState group = groups.get(key);
if (group == null) {
// this group doesn't exist
if (child.isUnsubscribed()) {
// we have been unsubscribed on the outer so won't send any more groups
return;
}
group = createNewGroup(key);
}
emitItem(group, NotificationLite.next(t));
} catch (Throwable e) {
onError(OnErrorThrowable.addValueAsLastCause(e, t));
}
}
private GroupState createNewGroup(final K key) {
final GroupState groupState = new GroupState();
GroupedObservable go = GroupedObservable.create(key, new OnSubscribe() {
@Override
public void call(final Subscriber super R> o) {
o.setProducer(new Producer() {
@Override
public void request(long n) {
requestFromGroupedObservable(n, groupState);
}
});
final AtomicBoolean once = new AtomicBoolean();
groupState.getObservable().doOnUnsubscribe(new Action0() {
@Override
public void call() {
if (once.compareAndSet(false, true)) {
// done once per instance, either onComplete or onUnSubscribe
cleanupGroup(key);
}
}
}).unsafeSubscribe(new Subscriber(o) {
@Override
public void onCompleted() {
o.onCompleted();
// eagerly cleanup instead of waiting for unsubscribe
if (once.compareAndSet(false, true)) {
// done once per instance, either onComplete or onUnSubscribe
cleanupGroup(key);
}
}
@Override
public void onError(Throwable e) {
o.onError(e);
}
@Override
public void onNext(T t) {
try {
o.onNext(elementSelector.call(t));
} catch (Throwable e) {
onError(OnErrorThrowable.addValueAsLastCause(e, t));
}
}
});
}
});
GroupState 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");
}
child.onNext(go);
return groupState;
}
private void cleanupGroup(K key) {
GroupState removed;
removed = groups.remove(key);
if (removed != null) {
if (removed.buffer.size() > 0) {
BUFFERED_COUNT.addAndGet(self, -removed.buffer.size());
}
completeInner();
// since we may have unsubscribed early with items in the buffer
// we remove those above and have freed up room to request more
// so give it a chance to request more now
requestMoreIfNecessary();
}
}
private void emitItem(GroupState groupState, Object item) {
Queue q = groupState.buffer;
AtomicLong keyRequested = groupState.requested;
REQUESTED.decrementAndGet(this);
// short circuit buffering
if (keyRequested != null && keyRequested.get() > 0 && (q == null || q.isEmpty())) {
@SuppressWarnings("unchecked")
Observer obs = (Observer) groupState.getObserver();
NotificationLite.accept(obs, item);
keyRequested.decrementAndGet();
} else {
q.add(item);
BUFFERED_COUNT.incrementAndGet(this);
if (groupState.count.getAndIncrement() == 0) {
pollQueue(groupState);
}
}
requestMoreIfNecessary();
}
private void pollQueue(GroupState groupState) {
do {
drainIfPossible(groupState);
long c = groupState.count.decrementAndGet();
if (c > 1) {
/*
* Set down to 1 and then iterate again.
* we lower it to 1 otherwise it could have grown very large while in the last poll loop
* and then we can end up looping all those times again here before existing even once we've drained
*/
groupState.count.set(1);
// we now loop again, and if anything tries scheduling again after this it will increment and cause us to loop again after
}
} while (groupState.count.get() > 0);
}
private void requestMoreIfNecessary() {
if (REQUESTED.get(this) < (MAX_QUEUE_SIZE) && terminated == 0) {
long requested = REQUESTED.get(this);
long toRequest = MAX_QUEUE_SIZE - REQUESTED.get(this) - BUFFERED_COUNT.get(this);
if (toRequest > 0 && REQUESTED.compareAndSet(this, requested, toRequest + requested)) {
request(toRequest);
}
}
}
private void drainIfPossible(GroupState groupState) {
while (groupState.requested.get() > 0) {
Object t = groupState.buffer.poll();
if (t != null) {
@SuppressWarnings("unchecked")
Observer obs = (Observer) groupState.getObserver();
NotificationLite.accept(obs, t);
groupState.requested.decrementAndGet();
BUFFERED_COUNT.decrementAndGet(this);
// if we have used up all the events we requested from upstream then figure out what to ask for this time based on the empty space in the buffer
requestMoreIfNecessary();
} else {
// queue is empty break
break;
}
}
}
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())) {
// completionEmitted ensures we only emit onCompleted once
if (COMPLETION_EMITTED_UPDATER.compareAndSet(this, 0, 1)) {
if (child.isUnsubscribed()) {
// if the entire groupBy has been unsubscribed and children are completed we will propagate the unsubscribe up.
unsubscribe();
}
child.onCompleted();
}
}
}
private static class GroupState {
private final Subject s = BufferUntilSubscriber.create();
private final AtomicLong requested = new AtomicLong();
private final AtomicLong count = new AtomicLong();
private final Queue buffer = new ConcurrentLinkedQueue(); // TODO should this be lazily created?
public Observable getObservable() {
return s;
}
public Observer getObserver() {
return s;
}
}
}
}