/**
* Copyright 2014 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 rx;
import static org.junit.Assert.*;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import org.junit.*;
import org.junit.rules.TestName;
import rx.Observable.OnSubscribe;
import rx.exceptions.MissingBackpressureException;
import rx.functions.*;
import rx.internal.util.RxRingBuffer;
import rx.observers.TestSubscriber;
import rx.schedulers.Schedulers;
import rx.test.TestObstructionDetection;
public class BackpressureTests {
@Rule
public TestName testName = new TestName();
@After
public void doAfterTest() {
TestObstructionDetection.checkObstruction();
}
@Test
public void testObserveOn() {
int NUM = (int) (RxRingBuffer.SIZE * 2.1);
AtomicInteger c = new AtomicInteger();
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
incrementingIntegers(c).observeOn(Schedulers.computation()).take(NUM).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
System.out.println("testObserveOn => Received: " + ts.getOnNextEvents().size() + " Emitted: " + c.get());
assertEquals(NUM, ts.getOnNextEvents().size());
assertTrue(c.get() < RxRingBuffer.SIZE * 4);
}
@Test
public void testObserveOnWithSlowConsumer() {
int NUM = (int) (RxRingBuffer.SIZE * 0.2);
AtomicInteger c = new AtomicInteger();
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
incrementingIntegers(c).observeOn(Schedulers.computation()).map(new Func1<Integer, Integer>() {
@Override
public Integer call(Integer i) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return i;
}
}).take(NUM).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
System.out.println("testObserveOnWithSlowConsumer => Received: " + ts.getOnNextEvents().size() + " Emitted: " + c.get());
assertEquals(NUM, ts.getOnNextEvents().size());
assertTrue(c.get() < RxRingBuffer.SIZE * 2);
}
@Test
public void testMergeSync() {
int NUM = (int) (RxRingBuffer.SIZE * 4.1);
AtomicInteger c1 = new AtomicInteger();
AtomicInteger c2 = new AtomicInteger();
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
Observable<Integer> merged = Observable.merge(incrementingIntegers(c1), incrementingIntegers(c2));
merged.take(NUM).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
System.out.println("Expected: " + NUM + " got: " + ts.getOnNextEvents().size());
System.out.println("testMergeSync => Received: " + ts.getOnNextEvents().size() + " Emitted: " + c1.get() + " / " + c2.get());
assertEquals(NUM, ts.getOnNextEvents().size());
// either one can starve the other, but neither should be capable of doing more than 5 batches (taking 4.1)
// TODO is it possible to make this deterministic rather than one possibly starving the other?
// benjchristensen => In general I'd say it's not worth trying to make it so, as "fair" algoritms generally take a performance hit
assertTrue(c1.get() < RxRingBuffer.SIZE * 5);
assertTrue(c2.get() < RxRingBuffer.SIZE * 5);
}
@Test
public void testMergeAsync() {
int NUM = (int) (RxRingBuffer.SIZE * 4.1);
AtomicInteger c1 = new AtomicInteger();
AtomicInteger c2 = new AtomicInteger();
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
Observable<Integer> merged = Observable.merge(
incrementingIntegers(c1).subscribeOn(Schedulers.computation()),
incrementingIntegers(c2).subscribeOn(Schedulers.computation()));
merged.take(NUM).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
System.out.println("testMergeAsync => Received: " + ts.getOnNextEvents().size() + " Emitted: " + c1.get() + " / " + c2.get());
assertEquals(NUM, ts.getOnNextEvents().size());
// either one can starve the other, but neither should be capable of doing more than 5 batches (taking 4.1)
// TODO is it possible to make this deterministic rather than one possibly starving the other?
// benjchristensen => In general I'd say it's not worth trying to make it so, as "fair" algoritms generally take a performance hit
assertTrue(c1.get() < RxRingBuffer.SIZE * 5);
assertTrue(c2.get() < RxRingBuffer.SIZE * 5);
}
@Test
public void testMergeAsyncThenObserveOn() {
int NUM = (int) (RxRingBuffer.SIZE * 4.1);
AtomicInteger c1 = new AtomicInteger();
AtomicInteger c2 = new AtomicInteger();
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
Observable<Integer> merged = Observable.merge(
incrementingIntegers(c1).subscribeOn(Schedulers.computation()),
incrementingIntegers(c2).subscribeOn(Schedulers.computation()));
merged.observeOn(Schedulers.newThread()).take(NUM).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
System.out.println("testMergeAsyncThenObserveOn => Received: " + ts.getOnNextEvents().size() + " Emitted: " + c1.get() + " / " + c2.get());
assertEquals(NUM, ts.getOnNextEvents().size());
// either one can starve the other, but neither should be capable of doing more than 5 batches (taking 4.1)
// TODO is it possible to make this deterministic rather than one possibly starving the other?
// benjchristensen => In general I'd say it's not worth trying to make it so, as "fair" algoritms generally take a performance hit
// akarnokd => run this in a loop over 10k times and never saw values get as high as 7*SIZE, but since observeOn delays the unsubscription non-deterministically, the test will remain unreliable
assertTrue(c1.get() < RxRingBuffer.SIZE * 7);
assertTrue(c2.get() < RxRingBuffer.SIZE * 7);
}
@Test
public void testFlatMapSync() {
int NUM = (int) (RxRingBuffer.SIZE * 2.1);
AtomicInteger c = new AtomicInteger();
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
incrementingIntegers(c).flatMap(new Func1<Integer, Observable<Integer>>() {
@Override
public Observable<Integer> call(Integer i) {
return incrementingIntegers(new AtomicInteger()).take(10);
}
}).take(NUM).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
System.out.println("testFlatMapSync => Received: " + ts.getOnNextEvents().size() + " Emitted: " + c.get());
assertEquals(NUM, ts.getOnNextEvents().size());
// expect less than 1 buffer since the flatMap is emitting 10 each time, so it is NUM/10 that will be taken.
assertTrue(c.get() < RxRingBuffer.SIZE);
}
@Test
@Ignore // the test is non-deterministic and can't be made deterministic
public void testFlatMapAsync() {
int NUM = (int) (RxRingBuffer.SIZE * 2.1);
AtomicInteger c = new AtomicInteger();
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
incrementingIntegers(c).subscribeOn(Schedulers.computation()).flatMap(new Func1<Integer, Observable<Integer>>() {
@Override
public Observable<Integer> call(Integer i) {
return incrementingIntegers(new AtomicInteger()).take(10).subscribeOn(Schedulers.computation());
}
}).take(NUM).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
System.out.println("testFlatMapAsync => Received: " + ts.getOnNextEvents().size() + " Emitted: " + c.get() + " Size: " + RxRingBuffer.SIZE);
assertEquals(NUM, ts.getOnNextEvents().size());
// even though we only need 10, it will request at least RxRingBuffer.SIZE, and then as it drains keep requesting more
// and then it will be non-deterministic when the take() causes the unsubscribe as it is scheduled on 10 different schedulers (threads)
// normally this number is ~250 but can get up to ~1200 when RxRingBuffer.SIZE == 1024
assertTrue(c.get() <= RxRingBuffer.SIZE * 2);
}
@Test
public void testZipSync() {
int NUM = (int) (RxRingBuffer.SIZE * 4.1);
AtomicInteger c1 = new AtomicInteger();
AtomicInteger c2 = new AtomicInteger();
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
Observable<Integer> zipped = Observable.zip(
incrementingIntegers(c1),
incrementingIntegers(c2),
new Func2<Integer, Integer, Integer>() {
@Override
public Integer call(Integer t1, Integer t2) {
return t1 + t2;
}
});
zipped.take(NUM).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
System.out.println("testZipSync => Received: " + ts.getOnNextEvents().size() + " Emitted: " + c1.get() + " / " + c2.get());
assertEquals(NUM, ts.getOnNextEvents().size());
assertTrue(c1.get() < RxRingBuffer.SIZE * 5);
assertTrue(c2.get() < RxRingBuffer.SIZE * 5);
}
@Test
public void testZipAsync() {
int NUM = (int) (RxRingBuffer.SIZE * 2.1);
AtomicInteger c1 = new AtomicInteger();
AtomicInteger c2 = new AtomicInteger();
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
Observable<Integer> zipped = Observable.zip(
incrementingIntegers(c1).subscribeOn(Schedulers.computation()),
incrementingIntegers(c2).subscribeOn(Schedulers.computation()),
new Func2<Integer, Integer, Integer>() {
@Override
public Integer call(Integer t1, Integer t2) {
return t1 + t2;
}
});
zipped.take(NUM).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
System.out.println("testZipAsync => Received: " + ts.getOnNextEvents().size() + " Emitted: " + c1.get() + " / " + c2.get());
assertEquals(NUM, ts.getOnNextEvents().size());
assertTrue(c1.get() < RxRingBuffer.SIZE * 3);
assertTrue(c2.get() < RxRingBuffer.SIZE * 3);
}
@Test
public void testSubscribeOnScheduling() {
// in a loop for repeating the concurrency in this to increase chance of failure
for (int i = 0; i < 100; i++) {
int NUM = (int) (RxRingBuffer.SIZE * 2.1);
AtomicInteger c = new AtomicInteger();
ConcurrentLinkedQueue<Thread> threads = new ConcurrentLinkedQueue<Thread>();
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
// observeOn is there to make it async and need backpressure
incrementingIntegers(c, threads).subscribeOn(Schedulers.computation()).observeOn(Schedulers.computation()).take(NUM).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
System.out.println("testSubscribeOnScheduling => Received: " + ts.getOnNextEvents().size() + " Emitted: " + c.get());
assertEquals(NUM, ts.getOnNextEvents().size());
assertTrue(c.get() < RxRingBuffer.SIZE * 4);
Thread first = null;
for (Thread t : threads) {
System.out.println("testSubscribeOnScheduling => thread: " + t);
if (first == null) {
first = t;
} else {
if (!first.equals(t)) {
fail("Expected to see the same thread");
}
}
}
System.out.println("testSubscribeOnScheduling => Number of batch requests seen: " + threads.size());
assertTrue(threads.size() > 1);
System.out.println("-------------------------------------------------------------------------------------------");
}
}
@Test
public void testTakeFilterSkipChainAsync() {
int NUM = (int) (RxRingBuffer.SIZE * 2.1);
AtomicInteger c = new AtomicInteger();
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
incrementingIntegers(c).observeOn(Schedulers.computation())
.skip(10000)
.filter(new Func1<Integer, Boolean>() {
@Override
public Boolean call(Integer i) {
return i > 11000;
}
}).take(NUM).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
// emit 10000 that are skipped
// emit next 1000 that are filtered out
// take NUM
// so emitted is at least 10000+1000+NUM + extra for buffer size/threshold
int expected = 10000 + 1000 + RxRingBuffer.SIZE * 3 + RxRingBuffer.SIZE / 2;
System.out.println("testTakeFilterSkipChain => Received: " + ts.getOnNextEvents().size() + " Emitted: " + c.get() + " Expected: " + expected);
assertEquals(NUM, ts.getOnNextEvents().size());
assertTrue(c.get() < expected);
}
@Test
public void testUserSubscriberUsingRequestSync() {
AtomicInteger c = new AtomicInteger();
final AtomicInteger totalReceived = new AtomicInteger();
final AtomicInteger batches = new AtomicInteger();
final AtomicInteger received = new AtomicInteger();
incrementingIntegers(c).subscribe(new Subscriber<Integer>() {
@Override
public void onStart() {
request(100);
}
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(Integer t) {
int total = totalReceived.incrementAndGet();
received.incrementAndGet();
if (total >= 2000) {
unsubscribe();
}
if (received.get() == 100) {
batches.incrementAndGet();
request(100);
received.set(0);
}
}
});
System.out.println("testUserSubscriberUsingRequestSync => Received: " + totalReceived.get() + " Emitted: " + c.get() + " Request Batches: " + batches.get());
assertEquals(2000, c.get());
assertEquals(2000, totalReceived.get());
assertEquals(20, batches.get());
}
@Test
public void testUserSubscriberUsingRequestAsync() throws InterruptedException {
AtomicInteger c = new AtomicInteger();
final AtomicInteger totalReceived = new AtomicInteger();
final AtomicInteger received = new AtomicInteger();
final AtomicInteger batches = new AtomicInteger();
final CountDownLatch latch = new CountDownLatch(1);
incrementingIntegers(c).subscribeOn(Schedulers.newThread()).subscribe(new Subscriber<Integer>() {
@Override
public void onStart() {
request(100);
}
@Override
public void onCompleted() {
latch.countDown();
}
@Override
public void onError(Throwable e) {
latch.countDown();
}
@Override
public void onNext(Integer t) {
int total = totalReceived.incrementAndGet();
received.incrementAndGet();
boolean done = false;
if (total >= 2000) {
done = true;
unsubscribe();
}
if (received.get() == 100) {
batches.incrementAndGet();
received.set(0);
if (!done) {
request(100);
}
}
if (done) {
latch.countDown();
}
}
});
latch.await();
System.out.println("testUserSubscriberUsingRequestAsync => Received: " + totalReceived.get() + " Emitted: " + c.get() + " Request Batches: " + batches.get());
assertEquals(2000, c.get());
assertEquals(2000, totalReceived.get());
assertEquals(20, batches.get());
}
@Test(timeout = 2000)
public void testFirehoseFailsAsExpected() {
AtomicInteger c = new AtomicInteger();
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
firehose(c).observeOn(Schedulers.computation()).map(SLOW_PASS_THRU).subscribe(ts);
ts.awaitTerminalEvent();
System.out.println("testFirehoseFailsAsExpected => Received: " + ts.getOnNextEvents().size() + " Emitted: " + c.get());
assertEquals(1, ts.getOnErrorEvents().size());
assertTrue(ts.getOnErrorEvents().get(0) instanceof MissingBackpressureException);
}
@Test(timeout = 10000)
public void testOnBackpressureDrop() {
for (int i = 0; i < 100; i++) {
int NUM = (int) (RxRingBuffer.SIZE * 1.1); // > 1 so that take doesn't prevent buffer overflow
AtomicInteger c = new AtomicInteger();
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
firehose(c).onBackpressureDrop()
.observeOn(Schedulers.computation())
.map(SLOW_PASS_THRU).take(NUM).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
List<Integer> onNextEvents = ts.getOnNextEvents();
assertEquals(NUM, onNextEvents.size());
Integer lastEvent = onNextEvents.get(NUM - 1);
System.out.println("testOnBackpressureDrop => Received: " + onNextEvents.size() + " Emitted: " + c.get() + " Last value: " + lastEvent);
// it drop, so we should get some number far higher than what would have sequentially incremented
assertTrue(NUM - 1 <= lastEvent.intValue());
}
}
@Test(timeout = 10000)
public void testOnBackpressureDropWithAction() {
for (int i = 0; i < 100; i++) {
final AtomicInteger emitCount = new AtomicInteger();
final AtomicInteger dropCount = new AtomicInteger();
final AtomicInteger passCount = new AtomicInteger();
final int NUM = RxRingBuffer.SIZE * 3; // > 1 so that take doesn't prevent buffer overflow
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
firehose(emitCount).onBackpressureDrop(new Action1<Integer>() {
@Override
public void call(Integer i) {
dropCount.incrementAndGet();
}
})
.doOnNext(new Action1<Integer>() {
@Override
public void call(Integer integer) {
passCount.incrementAndGet();
}
})
.observeOn(Schedulers.computation())
.map(SLOW_PASS_THRU).take(NUM).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
List<Integer> onNextEvents = ts.getOnNextEvents();
Integer lastEvent = onNextEvents.get(NUM - 1);
System.out.println(testName.getMethodName() + " => Received: " + onNextEvents.size() + " Passed: " + passCount.get() + " Dropped: " + dropCount.get() + " Emitted: " + emitCount.get() + " Last value: " + lastEvent);
assertEquals(NUM, onNextEvents.size());
// in reality, NUM < passCount
assertTrue(NUM <= passCount.get());
// it drop, so we should get some number far higher than what would have sequentially incremented
assertTrue(NUM - 1 <= lastEvent.intValue());
assertTrue(0 < dropCount.get());
assertEquals(emitCount.get(), passCount.get() + dropCount.get());
}
}
@Test(timeout = 10000)
public void testOnBackpressureDropSynchronous() {
for (int i = 0; i < 100; i++) {
int NUM = (int) (RxRingBuffer.SIZE * 1.1); // > 1 so that take doesn't prevent buffer overflow
AtomicInteger c = new AtomicInteger();
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
firehose(c).onBackpressureDrop()
.map(SLOW_PASS_THRU).take(NUM).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
List<Integer> onNextEvents = ts.getOnNextEvents();
assertEquals(NUM, onNextEvents.size());
Integer lastEvent = onNextEvents.get(NUM - 1);
System.out.println("testOnBackpressureDrop => Received: " + onNextEvents.size() + " Emitted: " + c.get() + " Last value: " + lastEvent);
// it drop, so we should get some number far higher than what would have sequentially incremented
assertTrue(NUM - 1 <= lastEvent.intValue());
}
}
@Test(timeout = 10000)
public void testOnBackpressureDropSynchronousWithAction() {
for (int i = 0; i < 100; i++) {
final AtomicInteger dropCount = new AtomicInteger();
int NUM = (int) (RxRingBuffer.SIZE * 1.1); // > 1 so that take doesn't prevent buffer overflow
AtomicInteger c = new AtomicInteger();
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
firehose(c).onBackpressureDrop(new Action1<Integer>() {
@Override
public void call(Integer i) {
dropCount.incrementAndGet();
}
})
.map(SLOW_PASS_THRU).take(NUM).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
List<Integer> onNextEvents = ts.getOnNextEvents();
assertEquals(NUM, onNextEvents.size());
Integer lastEvent = onNextEvents.get(NUM - 1);
System.out.println("testOnBackpressureDrop => Received: " + onNextEvents.size() + " Dropped: " + dropCount.get() + " Emitted: " + c.get() + " Last value: " + lastEvent);
// it drop, so we should get some number far higher than what would have sequentially incremented
assertTrue(NUM - 1 <= lastEvent.intValue());
// no drop in synchronous mode
assertEquals(0, dropCount.get());
assertEquals(c.get(), onNextEvents.size());
}
}
@Test(timeout = 2000)
public void testOnBackpressureBuffer() {
int NUM = (int) (RxRingBuffer.SIZE * 1.1); // > 1 so that take doesn't prevent buffer overflow
AtomicInteger c = new AtomicInteger();
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
firehose(c).takeWhile(new Func1<Integer, Boolean>() {
@Override
public Boolean call(Integer t1) {
return t1 < 100000;
}
}).onBackpressureBuffer().observeOn(Schedulers.computation()).map(SLOW_PASS_THRU).take(NUM).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
System.out.println("testOnBackpressureBuffer => Received: " + ts.getOnNextEvents().size() + " Emitted: " + c.get());
assertEquals(NUM, ts.getOnNextEvents().size());
// it buffers, so we should get the right value sequentially
assertEquals(NUM - 1, ts.getOnNextEvents().get(NUM - 1).intValue());
}
/**
* A synchronous Observable that will emit incrementing integers as requested.
*
* @param counter
* @return
*/
private static Observable<Integer> incrementingIntegers(final AtomicInteger counter) {
return incrementingIntegers(counter, null);
}
private static Observable<Integer> incrementingIntegers(final AtomicInteger counter, final ConcurrentLinkedQueue<Thread> threadsSeen) {
return Observable.create(new OnSubscribe<Integer>() {
final AtomicLong requested = new AtomicLong();
@Override
public void call(final Subscriber<? super Integer> s) {
s.setProducer(new Producer() {
int i = 0;
@Override
public void request(long n) {
if (n == 0) {
// nothing to do
return;
}
if (threadsSeen != null) {
threadsSeen.offer(Thread.currentThread());
}
long _c = requested.getAndAdd(n);
if (_c == 0) {
while (!s.isUnsubscribed()) {
counter.incrementAndGet();
s.onNext(i++);
if (requested.decrementAndGet() == 0) {
// we're done emitting the number requested so return
return;
}
}
}
}
});
}
});
}
/**
* Incrementing int without backpressure.
*
* @param counter
* @return
*/
private static Observable<Integer> firehose(final AtomicInteger counter) {
return Observable.create(new OnSubscribe<Integer>() {
int i = 0;
@Override
public void call(final Subscriber<? super Integer> s) {
while (!s.isUnsubscribed()) {
s.onNext(i++);
counter.incrementAndGet();
}
System.out.println("unsubscribed after: " + i);
}
});
}
final static Func1<Integer, Integer> SLOW_PASS_THRU = new Func1<Integer, Integer>() {
volatile int sink;
@Override
public Integer call(Integer t1) {
// be slow ... but faster than Thread.sleep(1)
String t = "";
int s = sink;
for (int i = 1000; i >= 0; i--) {
t = String.valueOf(i + t.hashCode() + s);
}
sink = t.hashCode();
return t1;
}
};
}