Skip to content

Commit

Permalink
Fix ObserveOn, NewThreadScheduler and ScheduledObserver bugs
Browse files Browse the repository at this point in the history
@headinthebox and I were working on some code and found differences in behavior between Rx.Net and RxJava with observeOn. This commit should fix that.
  • Loading branch information
benjchristensen committed Sep 19, 2013
1 parent 2878c2a commit 1ea15ba
Show file tree
Hide file tree
Showing 8 changed files with 380 additions and 84 deletions.
78 changes: 64 additions & 14 deletions rxjava-core/src/main/java/rx/concurrency/NewThreadScheduler.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
*/
package rx.concurrency;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import rx.Scheduler;
import rx.Subscription;
import rx.operators.SafeObservableSubscription;
import rx.subscriptions.CompositeSubscription;
import rx.subscriptions.Subscriptions;
import rx.util.functions.Func2;
Expand All @@ -29,27 +32,74 @@
* Schedules work on a new thread.
*/
public class NewThreadScheduler extends Scheduler {
private static final NewThreadScheduler INSTANCE = new NewThreadScheduler();

private final static NewThreadScheduler INSTANCE = new NewThreadScheduler();
private final static AtomicLong count = new AtomicLong();

public static NewThreadScheduler getInstance() {
return INSTANCE;
}

@Override
public <T> Subscription schedule(final T state, final Func2<? super Scheduler, ? super T, ? extends Subscription> action) {
final SafeObservableSubscription subscription = new SafeObservableSubscription();
final Scheduler _scheduler = this;
private NewThreadScheduler() {

Thread t = new Thread(new Runnable() {
@Override
public void run() {
subscription.wrap(action.call(_scheduler, state));
}
}, "RxNewThreadScheduler");
}

t.start();
private static class EventLoopScheduler extends Scheduler {
private final ExecutorService executor;

return subscription;
private EventLoopScheduler() {
executor = Executors.newFixedThreadPool(1, new ThreadFactory() {

@Override
public Thread newThread(Runnable r) {
return new Thread(r, "RxNewThreadScheduler-" + count.incrementAndGet());
}
});
}

@Override
public <T> Subscription schedule(final T state, final Func2<? super Scheduler, ? super T, ? extends Subscription> action) {
final Scheduler _scheduler = this;
return Subscriptions.from(executor.submit(new Runnable() {

@Override
public void run() {
action.call(_scheduler, state);
}
}));
}

@Override
public <T> Subscription schedule(final T state, final Func2<? super Scheduler, ? super T, ? extends Subscription> action, final long delayTime, final TimeUnit unit) {
// we will use the system scheduler since it doesn't make sense to launch a new Thread and then sleep
// we will instead schedule the event then launch the thread after the delay has passed
final Scheduler _scheduler = this;
final CompositeSubscription subscription = new CompositeSubscription();
ScheduledFuture<?> f = GenericScheduledExecutorService.getInstance().schedule(new Runnable() {

@Override
public void run() {
if (!subscription.isUnsubscribed()) {
// when the delay has passed we now do the work on the actual scheduler
Subscription s = _scheduler.schedule(state, action);
// add the subscription to the CompositeSubscription so it is unsubscribed
subscription.add(s);
}
}
}, delayTime, unit);

// add the ScheduledFuture as a subscription so we can cancel the scheduled action if an unsubscribe happens
subscription.add(Subscriptions.create(f));

return subscription;
}

}

@Override
public <T> Subscription schedule(final T state, final Func2<? super Scheduler, ? super T, ? extends Subscription> action) {
EventLoopScheduler s = new EventLoopScheduler();
return s.schedule(state, action);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.Test;
import org.mockito.InOrder;
Expand All @@ -33,6 +34,8 @@
import rx.Subscription;
import rx.concurrency.ImmediateScheduler;
import rx.concurrency.Schedulers;
import rx.subscriptions.CompositeSubscription;
import rx.util.functions.Func2;

/**
* Asynchronously notify Observers on the specified Scheduler.
Expand Down Expand Up @@ -60,7 +63,9 @@ public Subscription onSubscribe(final Observer<? super T> observer) {
// do nothing if we request ImmediateScheduler so we don't invoke overhead
return source.subscribe(observer);
} else {
return source.subscribe(new ScheduledObserver<T>(observer, scheduler));
CompositeSubscription s = new CompositeSubscription();
s.add(source.subscribe(new ScheduledObserver<T>(s, observer, scheduler)));
return s;

This comment has been minimized.

Copy link
@mttkay

mttkay Oct 11, 2013

Contributor

@benjchristensen Do you remember why you made this change? What's the value of wrapping a single subscription in a CompositeSubscription?

This comment has been minimized.

Copy link
@benjchristensen

benjchristensen Oct 11, 2013

Author Member

If I remember right it is because of recursion that happens within the ScheduledObserver. This subscription becomes the "parent" that the child subscriptions keep being added to and removed from while it recurses for each onNext.

That relates back to the functionality bug this was fixing which is that observeOn needs to act like a recursive event-loop, whereas before it was rescheduling each onNext which could mean each one was on a different thread instead of looping on a single thread. To achieve that it does "trampolining" in effect by recursively scheduling itself. That no-so-trivial logic is here: 1ea15ba#diff-549642cd76f5998a553c8ac89c3cb185R67

This comment has been minimized.

Copy link
@mttkay

mttkay Oct 11, 2013

Contributor

Have to admit that sometimes Rx is nothing short of mind boggling. Thanks for pointing it out--I missed the bit where it's used as the parent subscription in ScheduledObserver. I actually stumbled upon it while looking at reversed byte code that ProGuard produced after optimizing our build, which didn't exactly help in shedding light on it :-) Thanks Ben.

}
}
}
Expand Down
25 changes: 14 additions & 11 deletions rxjava-core/src/main/java/rx/operators/OperationRetry.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
import rx.Observable;
import rx.Observable.OnSubscribeFunc;
import rx.Observer;
import rx.Scheduler;
import rx.Subscription;
import rx.concurrency.Schedulers;
import rx.subscriptions.CompositeSubscription;
import rx.subscriptions.MultipleAssignmentSubscription;
import rx.subscriptions.Subscriptions;
import rx.util.functions.Action0;
import rx.util.functions.Func2;

public class OperationRetry {

Expand Down Expand Up @@ -58,17 +60,19 @@ public Retry(Observable<T> source, int retryCount) {

@Override
public Subscription onSubscribe(Observer<? super T> observer) {
subscription.add(Schedulers.currentThread().schedule(attemptSubscription(observer)));
MultipleAssignmentSubscription rescursiveSubscription = new MultipleAssignmentSubscription();
subscription.add(Schedulers.currentThread().schedule(rescursiveSubscription, attemptSubscription(observer)));
subscription.add(rescursiveSubscription);
return subscription;
}

private Action0 attemptSubscription(final Observer<? super T> observer) {
return new Action0() {
private Func2<Scheduler, MultipleAssignmentSubscription, Subscription> attemptSubscription(final Observer<? super T> observer) {
return new Func2<Scheduler, MultipleAssignmentSubscription, Subscription>() {

@Override
public void call() {
public Subscription call(final Scheduler scheduler, final MultipleAssignmentSubscription rescursiveSubscription) {
attempts.incrementAndGet();
source.subscribe(new Observer<T>() {
return source.subscribe(new Observer<T>() {

@Override
public void onCompleted() {
Expand All @@ -79,10 +83,8 @@ public void onCompleted() {
public void onError(Throwable e) {
if ((retryCount == INFINITE_RETRY || attempts.get() <= retryCount) && !subscription.isUnsubscribed()) {
// retry again
// remove the last subscription since we have completed (so as we retry we don't build up a huge list)
subscription.removeLast();
// add the new subscription and schedule a retry
subscription.add(Schedulers.currentThread().schedule(attemptSubscription(observer)));
// add the new subscription and schedule a retry recursively
rescursiveSubscription.setSubscription(scheduler.schedule(rescursiveSubscription, attemptSubscription(observer)));
} else {
// give up and pass the failure
observer.onError(e);
Expand All @@ -96,6 +98,7 @@ public void onNext(T v) {
});

}

};
}

Expand Down Expand Up @@ -157,7 +160,7 @@ public void testRetrySuccess() {
inOrder.verify(observer, times(1)).onCompleted();
inOrder.verifyNoMoreInteractions();
}

@Test
public void testInfiniteRetry() {
int NUM_FAILURES = 20;
Expand Down
112 changes: 78 additions & 34 deletions rxjava-core/src/main/java/rx/operators/ScheduledObserver.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,28 @@
package rx.operators;

import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import rx.Notification;
import rx.Observer;
import rx.Scheduler;
import rx.util.functions.Action0;
import rx.Subscription;
import rx.subscriptions.CompositeSubscription;
import rx.subscriptions.MultipleAssignmentSubscription;
import rx.util.functions.Func2;

/* package */class ScheduledObserver<T> implements Observer<T> {
private final Observer<? super T> underlying;
private final Scheduler scheduler;
private final CompositeSubscription parentSubscription;
private final EventLoop eventLoop = new EventLoop();

private final ConcurrentLinkedQueue<Notification<? extends T>> queue = new ConcurrentLinkedQueue<Notification<? extends T>>();
private final AtomicInteger counter = new AtomicInteger(0);
private final AtomicBoolean started = new AtomicBoolean();

public ScheduledObserver(Observer<? super T> underlying, Scheduler scheduler) {
public ScheduledObserver(CompositeSubscription s, Observer<? super T> underlying, Scheduler scheduler) {
this.parentSubscription = s;
this.underlying = underlying;
this.scheduler = scheduler;
}
Expand All @@ -50,46 +57,83 @@ public void onNext(final T args) {
enqueue(new Notification<T>(args));
}

final AtomicInteger counter = new AtomicInteger();

private void enqueue(Notification<? extends T> notification) {
// this must happen before 'counter' is used to provide synchronization between threads
// this must happen before synchronization between threads
queue.offer(notification);

// we now use counter to atomically determine if we need to start processing or not
// it will be 0 if it's the first notification or the scheduler has finished processing work
// and we need to start doing it again
if (counter.getAndIncrement() == 0) {
processQueue();
/**
* If the counter is currently at 0 (before incrementing with this addition)
* we will schedule the work.
*/
if (counter.getAndIncrement() <= 0) {
if (!started.get() && started.compareAndSet(false, true)) {
// first time we use the parent scheduler to start the event loop
MultipleAssignmentSubscription recursiveSubscription = new MultipleAssignmentSubscription();
parentSubscription.add(scheduler.schedule(recursiveSubscription, eventLoop));
parentSubscription.add(recursiveSubscription);
} else {
// subsequent times we reschedule existing one
eventLoop.reschedule();
}
}
}

private void processQueue() {
scheduler.schedule(new Action0() {
@Override
public void call() {
Notification<? extends T> not = queue.poll();

switch (not.getKind()) {
case OnNext:
underlying.onNext(not.getValue());
break;
case OnError:
underlying.onError(not.getThrowable());
break;
case OnCompleted:
underlying.onCompleted();
break;
default:
throw new IllegalStateException("Unknown kind of notification " + not);
private class EventLoop implements Func2<Scheduler, MultipleAssignmentSubscription, Subscription> {

}
volatile Scheduler _recursiveScheduler;
volatile MultipleAssignmentSubscription _recursiveSubscription;

// decrement count and if we still have work to do
// recursively schedule ourselves to process again
if (counter.decrementAndGet() > 0) {
scheduler.schedule(this);
}
public void reschedule() {
_recursiveSubscription.setSubscription(_recursiveScheduler.schedule(_recursiveSubscription, this));
}

@Override
public Subscription call(Scheduler s, MultipleAssignmentSubscription recursiveSubscription) {
/*
* --------------------------------------------------------------------------------------
* Set these the first time through so we can externally trigger recursive execution again
*/
if (_recursiveScheduler == null) {
_recursiveScheduler = s;
}
if (_recursiveSubscription == null) {
_recursiveSubscription = recursiveSubscription;
}
});
/*
* Back to regular flow
* --------------------------------------------------------------------------------------
*/

do {
Notification<? extends T> notification = queue.poll();
// if we got a notification, send it
if (notification != null) {

// if unsubscribed stop working
if (parentSubscription.isUnsubscribed()) {
return parentSubscription;
}
// process notification

switch (notification.getKind()) {
case OnNext:
underlying.onNext(notification.getValue());
break;
case OnError:
underlying.onError(notification.getThrowable());
break;
case OnCompleted:
underlying.onCompleted();
break;
default:
throw new IllegalStateException("Unknown kind of notification " + notification);
}
}
} while (counter.decrementAndGet() > 0);

return parentSubscription;
}
}
}
Loading

0 comments on commit 1ea15ba

Please sign in to comment.