/** * Copyright (c) 2016-present, RxJava Contributors. * * 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.reactivex.internal.operators.flowable; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import org.junit.Test; import org.mockito.*; import org.reactivestreams.*; import io.reactivex.*; import io.reactivex.disposables.Disposable; import io.reactivex.exceptions.TestException; import io.reactivex.flowables.GroupedFlowable; import io.reactivex.functions.*; import io.reactivex.internal.subscriptions.BooleanSubscription; import io.reactivex.processors.PublishProcessor; import io.reactivex.schedulers.Schedulers; import io.reactivex.subscribers.*; public class FlowableRetryTest { @Test public void iterativeBackoff() { Subscriber<String> consumer = TestHelper.mockSubscriber(); Flowable<String> producer = Flowable.unsafeCreate(new Publisher<String>() { private AtomicInteger count = new AtomicInteger(4); long last = System.currentTimeMillis(); @Override public void subscribe(Subscriber<? super String> t1) { t1.onSubscribe(new BooleanSubscription()); System.out.println(count.get() + " @ " + String.valueOf(last - System.currentTimeMillis())); last = System.currentTimeMillis(); if (count.getAndDecrement() == 0) { t1.onNext("hello"); t1.onComplete(); } else { t1.onError(new RuntimeException()); } } }); TestSubscriber<String> ts = new TestSubscriber<String>(consumer); producer.retryWhen(new Function<Flowable<? extends Throwable>, Flowable<Object>>() { @Override public Flowable<Object> apply(Flowable<? extends Throwable> attempts) { // Worker w = Schedulers.computation().createWorker(); return attempts .map(new Function<Throwable, Tuple>() { @Override public Tuple apply(Throwable n) { return new Tuple(new Long(1), n); }}) .scan(new BiFunction<Tuple, Tuple, Tuple>() { @Override public Tuple apply(Tuple t, Tuple n) { return new Tuple(t.count + n.count, n.n); }}) .flatMap(new Function<Tuple, Flowable<Object>>() { @Override public Flowable<Object> apply(Tuple t) { System.out.println("Retry # " + t.count); return t.count > 20 ? Flowable.<Object>error(t.n) : Flowable.timer(t.count * 1L, TimeUnit.MILLISECONDS) .cast(Object.class); }}); } }).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); InOrder inOrder = inOrder(consumer); inOrder.verify(consumer, never()).onError(any(Throwable.class)); inOrder.verify(consumer, times(1)).onNext("hello"); inOrder.verify(consumer, times(1)).onComplete(); inOrder.verifyNoMoreInteractions(); } public static class Tuple { Long count; Throwable n; Tuple(Long c, Throwable n) { count = c; this.n = n; } } @Test public void testRetryIndefinitely() { Subscriber<String> observer = TestHelper.mockSubscriber(); int NUM_RETRIES = 20; Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(NUM_RETRIES)); origin.retry().subscribe(new TestSubscriber<String>(observer)); InOrder inOrder = inOrder(observer); // should show 3 attempts inOrder.verify(observer, times(NUM_RETRIES + 1)).onNext("beginningEveryTime"); // should have no errors inOrder.verify(observer, never()).onError(any(Throwable.class)); // should have a single success inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); // should have a single successful onComplete inOrder.verify(observer, times(1)).onComplete(); inOrder.verifyNoMoreInteractions(); } @Test public void testSchedulingNotificationHandler() { Subscriber<String> observer = TestHelper.mockSubscriber(); int NUM_RETRIES = 2; Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(NUM_RETRIES)); TestSubscriber<String> subscriber = new TestSubscriber<String>(observer); origin.retryWhen(new Function<Flowable<? extends Throwable>, Flowable<Object>>() { @Override public Flowable<Object> apply(Flowable<? extends Throwable> t1) { return t1.observeOn(Schedulers.computation()).map(new Function<Throwable, Integer>() { @Override public Integer apply(Throwable t1) { return 1; } }).startWith(1).cast(Object.class); } }) .doOnError(new Consumer<Throwable>() { @Override public void accept(Throwable e) { e.printStackTrace(); } }) .subscribe(subscriber); subscriber.awaitTerminalEvent(); InOrder inOrder = inOrder(observer); // should show 3 attempts inOrder.verify(observer, times(1 + NUM_RETRIES)).onNext("beginningEveryTime"); // should have no errors inOrder.verify(observer, never()).onError(any(Throwable.class)); // should have a single success inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); // should have a single successful onComplete inOrder.verify(observer, times(1)).onComplete(); inOrder.verifyNoMoreInteractions(); } @Test public void testOnNextFromNotificationHandler() { Subscriber<String> observer = TestHelper.mockSubscriber(); int NUM_RETRIES = 2; Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(NUM_RETRIES)); origin.retryWhen(new Function<Flowable<? extends Throwable>, Flowable<Object>>() { @Override public Flowable<Object> apply(Flowable<? extends Throwable> t1) { return t1.map(new Function<Throwable, Integer>() { @Override public Integer apply(Throwable t1) { return 0; } }).startWith(0).cast(Object.class); } }).subscribe(observer); InOrder inOrder = inOrder(observer); // should show 3 attempts inOrder.verify(observer, times(NUM_RETRIES + 1)).onNext("beginningEveryTime"); // should have no errors inOrder.verify(observer, never()).onError(any(Throwable.class)); // should have a single success inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); // should have a single successful onComplete inOrder.verify(observer, times(1)).onComplete(); inOrder.verifyNoMoreInteractions(); } @Test public void testOnCompletedFromNotificationHandler() { Subscriber<String> observer = TestHelper.mockSubscriber(); Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(1)); TestSubscriber<String> subscriber = new TestSubscriber<String>(observer); origin.retryWhen(new Function<Flowable<? extends Throwable>, Flowable<Object>>() { @Override public Flowable<Object> apply(Flowable<? extends Throwable> t1) { return Flowable.empty(); } }).subscribe(subscriber); InOrder inOrder = inOrder(observer); inOrder.verify(observer).onSubscribe((Subscription)notNull()); inOrder.verify(observer, never()).onNext("beginningEveryTime"); inOrder.verify(observer, never()).onNext("onSuccessOnly"); inOrder.verify(observer, times(1)).onComplete(); inOrder.verify(observer, never()).onError(any(Exception.class)); inOrder.verifyNoMoreInteractions(); } @Test public void testOnErrorFromNotificationHandler() { Subscriber<String> observer = TestHelper.mockSubscriber(); Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(2)); origin.retryWhen(new Function<Flowable<? extends Throwable>, Flowable<Object>>() { @Override public Flowable<Object> apply(Flowable<? extends Throwable> t1) { return Flowable.error(new RuntimeException()); } }).subscribe(observer); InOrder inOrder = inOrder(observer); inOrder.verify(observer).onSubscribe((Subscription)notNull()); inOrder.verify(observer, never()).onNext("beginningEveryTime"); inOrder.verify(observer, never()).onNext("onSuccessOnly"); inOrder.verify(observer, never()).onComplete(); inOrder.verify(observer, times(1)).onError(any(RuntimeException.class)); inOrder.verifyNoMoreInteractions(); } @Test public void testSingleSubscriptionOnFirst() throws Exception { final AtomicInteger inc = new AtomicInteger(0); Publisher<Integer> onSubscribe = new Publisher<Integer>() { @Override public void subscribe(Subscriber<? super Integer> subscriber) { subscriber.onSubscribe(new BooleanSubscription()); final int emit = inc.incrementAndGet(); subscriber.onNext(emit); subscriber.onComplete(); } }; int first = Flowable.unsafeCreate(onSubscribe) .retryWhen(new Function<Flowable<? extends Throwable>, Flowable<Object>>() { @Override public Flowable<Object> apply(Flowable<? extends Throwable> attempt) { return attempt.zipWith(Flowable.just(1), new BiFunction<Throwable, Integer, Object>() { @Override public Object apply(Throwable o, Integer integer) { return 0; } }); } }) .blockingFirst(); assertEquals("Observer did not receive the expected output", 1, first); assertEquals("Subscribe was not called once", 1, inc.get()); } @Test public void testOriginFails() { Subscriber<String> observer = TestHelper.mockSubscriber(); Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(1)); origin.subscribe(observer); InOrder inOrder = inOrder(observer); inOrder.verify(observer, times(1)).onNext("beginningEveryTime"); inOrder.verify(observer, times(1)).onError(any(RuntimeException.class)); inOrder.verify(observer, never()).onNext("onSuccessOnly"); inOrder.verify(observer, never()).onComplete(); } @Test public void testRetryFail() { int NUM_RETRIES = 1; int NUM_FAILURES = 2; Subscriber<String> observer = TestHelper.mockSubscriber(); Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(NUM_FAILURES)); origin.retry(NUM_RETRIES).subscribe(observer); InOrder inOrder = inOrder(observer); // should show 2 attempts (first time fail, second time (1st retry) fail) inOrder.verify(observer, times(1 + NUM_RETRIES)).onNext("beginningEveryTime"); // should only retry once, fail again and emit onError inOrder.verify(observer, times(1)).onError(any(RuntimeException.class)); // no success inOrder.verify(observer, never()).onNext("onSuccessOnly"); inOrder.verify(observer, never()).onComplete(); inOrder.verifyNoMoreInteractions(); } @Test public void testRetrySuccess() { int NUM_FAILURES = 1; Subscriber<String> observer = TestHelper.mockSubscriber(); Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(NUM_FAILURES)); origin.retry(3).subscribe(observer); InOrder inOrder = inOrder(observer); // should show 3 attempts inOrder.verify(observer, times(1 + NUM_FAILURES)).onNext("beginningEveryTime"); // should have no errors inOrder.verify(observer, never()).onError(any(Throwable.class)); // should have a single success inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); // should have a single successful onComplete inOrder.verify(observer, times(1)).onComplete(); inOrder.verifyNoMoreInteractions(); } @Test public void testInfiniteRetry() { int NUM_FAILURES = 20; Subscriber<String> observer = TestHelper.mockSubscriber(); Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(NUM_FAILURES)); origin.retry().subscribe(observer); InOrder inOrder = inOrder(observer); // should show 3 attempts inOrder.verify(observer, times(1 + NUM_FAILURES)).onNext("beginningEveryTime"); // should have no errors inOrder.verify(observer, never()).onError(any(Throwable.class)); // should have a single success inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); // should have a single successful onComplete inOrder.verify(observer, times(1)).onComplete(); inOrder.verifyNoMoreInteractions(); } /* * Checks in a simple and synchronous way that retry resubscribes * after error. This test fails against 0.16.1-0.17.4, hangs on 0.17.5 and * passes in 0.17.6 thanks to fix for issue #1027. */ @SuppressWarnings("unchecked") @Test public void testRetrySubscribesAgainAfterError() throws Exception { // record emitted values with this action Consumer<Integer> record = mock(Consumer.class); InOrder inOrder = inOrder(record); // always throw an exception with this action Consumer<Integer> throwException = mock(Consumer.class); doThrow(new RuntimeException()).when(throwException).accept(Mockito.anyInt()); // create a retrying observable based on a PublishProcessor PublishProcessor<Integer> subject = PublishProcessor.create(); subject // record item .doOnNext(record) // throw a RuntimeException .doOnNext(throwException) // retry on error .retry() // subscribe and ignore .subscribe(); inOrder.verifyNoMoreInteractions(); subject.onNext(1); inOrder.verify(record).accept(1); subject.onNext(2); inOrder.verify(record).accept(2); subject.onNext(3); inOrder.verify(record).accept(3); inOrder.verifyNoMoreInteractions(); } public static class FuncWithErrors implements Publisher<String> { private final int numFailures; private final AtomicInteger count = new AtomicInteger(0); FuncWithErrors(int count) { this.numFailures = count; } @Override public void subscribe(final Subscriber<? super String> o) { o.onSubscribe(new Subscription() { final AtomicLong req = new AtomicLong(); // 0 = not set, 1 = fast path, 2 = backpressure final AtomicInteger path = new AtomicInteger(0); volatile boolean done; @Override public void request(long n) { if (n == Long.MAX_VALUE && path.compareAndSet(0, 1)) { o.onNext("beginningEveryTime"); int i = count.getAndIncrement(); if (i < numFailures) { o.onError(new RuntimeException("forced failure: " + (i + 1))); } else { o.onNext("onSuccessOnly"); o.onComplete(); } return; } if (n > 0 && req.getAndAdd(n) == 0 && (path.get() == 2 || path.compareAndSet(0, 2)) && !done) { int i = count.getAndIncrement(); if (i < numFailures) { o.onNext("beginningEveryTime"); o.onError(new RuntimeException("forced failure: " + (i + 1))); done = true; } else { do { if (i == numFailures) { o.onNext("beginningEveryTime"); } else if (i > numFailures) { o.onNext("onSuccessOnly"); o.onComplete(); done = true; break; } i = count.getAndIncrement(); } while (req.decrementAndGet() > 0); } } } @Override public void cancel() { // TODO Auto-generated method stub } }); } } @Test public void testUnsubscribeFromRetry() { PublishProcessor<Integer> subject = PublishProcessor.create(); final AtomicInteger count = new AtomicInteger(0); Disposable sub = subject.retry().subscribe(new Consumer<Integer>() { @Override public void accept(Integer n) { count.incrementAndGet(); } }); subject.onNext(1); sub.dispose(); subject.onNext(2); assertEquals(1, count.get()); } @Test public void testRetryAllowsSubscriptionAfterAllSubscriptionsUnsubscribed() throws InterruptedException { final AtomicInteger subsCount = new AtomicInteger(0); Publisher<String> onSubscribe = new Publisher<String>() { @Override public void subscribe(Subscriber<? super String> s) { subsCount.incrementAndGet(); s.onSubscribe(new Subscription() { @Override public void request(long n) { } @Override public void cancel() { subsCount.decrementAndGet(); } }); } }; Flowable<String> stream = Flowable.unsafeCreate(onSubscribe); Flowable<String> streamWithRetry = stream.retry(); Disposable sub = streamWithRetry.subscribe(); assertEquals(1, subsCount.get()); sub.dispose(); assertEquals(0, subsCount.get()); streamWithRetry.subscribe(); assertEquals(1, subsCount.get()); } @Test public void testSourceObservableCallsUnsubscribe() throws InterruptedException { final AtomicInteger subsCount = new AtomicInteger(0); final TestSubscriber<String> ts = new TestSubscriber<String>(); Publisher<String> onSubscribe = new Publisher<String>() { @Override public void subscribe(Subscriber<? super String> s) { BooleanSubscription bs = new BooleanSubscription(); // if isUnsubscribed is true that means we have a bug such as // https://github.com/ReactiveX/RxJava/issues/1024 if (!bs.isCancelled()) { subsCount.incrementAndGet(); s.onError(new RuntimeException("failed")); // it unsubscribes the child directly // this simulates various error/completion scenarios that could occur // or just a source that proactively triggers cleanup // FIXME can't unsubscribe child // s.unsubscribe(); bs.cancel(); } else { s.onError(new RuntimeException()); } } }; Flowable.unsafeCreate(onSubscribe).retry(3).subscribe(ts); assertEquals(4, subsCount.get()); // 1 + 3 retries } @Test public void testSourceObservableRetry1() throws InterruptedException { final AtomicInteger subsCount = new AtomicInteger(0); final TestSubscriber<String> ts = new TestSubscriber<String>(); Publisher<String> onSubscribe = new Publisher<String>() { @Override public void subscribe(Subscriber<? super String> s) { s.onSubscribe(new BooleanSubscription()); subsCount.incrementAndGet(); s.onError(new RuntimeException("failed")); } }; Flowable.unsafeCreate(onSubscribe).retry(1).subscribe(ts); assertEquals(2, subsCount.get()); } @Test public void testSourceObservableRetry0() throws InterruptedException { final AtomicInteger subsCount = new AtomicInteger(0); final TestSubscriber<String> ts = new TestSubscriber<String>(); Publisher<String> onSubscribe = new Publisher<String>() { @Override public void subscribe(Subscriber<? super String> s) { s.onSubscribe(new BooleanSubscription()); subsCount.incrementAndGet(); s.onError(new RuntimeException("failed")); } }; Flowable.unsafeCreate(onSubscribe).retry(0).subscribe(ts); assertEquals(1, subsCount.get()); } static final class SlowFlowable implements Publisher<Long> { final AtomicInteger efforts = new AtomicInteger(0); final AtomicInteger active = new AtomicInteger(0); final AtomicInteger maxActive = new AtomicInteger(0); final AtomicInteger nextBeforeFailure; private final int emitDelay; SlowFlowable(int emitDelay, int countNext) { this.emitDelay = emitDelay; this.nextBeforeFailure = new AtomicInteger(countNext); } @Override public void subscribe(final Subscriber<? super Long> subscriber) { final AtomicBoolean terminate = new AtomicBoolean(false); subscriber.onSubscribe(new Subscription() { @Override public void request(long n) { // TODO Auto-generated method stub } @Override public void cancel() { terminate.set(true); active.decrementAndGet(); } }); efforts.getAndIncrement(); active.getAndIncrement(); maxActive.set(Math.max(active.get(), maxActive.get())); final Thread thread = new Thread() { @Override public void run() { long nr = 0; try { while (!terminate.get()) { Thread.sleep(emitDelay); if (nextBeforeFailure.getAndDecrement() > 0) { subscriber.onNext(nr++); } else { subscriber.onError(new RuntimeException("expected-failed")); } } } catch (InterruptedException t) { } } }; thread.start(); } } /** Observer for listener on seperate thread. */ static final class AsyncObserver<T> extends DefaultSubscriber<T> { protected CountDownLatch latch = new CountDownLatch(1); protected Subscriber<T> target; /** * Wrap existing Observer. * @param target the target subscriber */ AsyncObserver(Subscriber<T> target) { this.target = target; } /** Wait. */ public void await() { try { latch.await(); } catch (InterruptedException e) { fail("Test interrupted"); } } // Observer implementation @Override public void onComplete() { target.onComplete(); latch.countDown(); } @Override public void onError(Throwable t) { target.onError(t); latch.countDown(); } @Override public void onNext(T v) { target.onNext(v); } } @Test(timeout = 10000) public void testUnsubscribeAfterError() { @SuppressWarnings("unchecked") DefaultSubscriber<Long> observer = mock(DefaultSubscriber.class); // Flowable that always fails after 100ms SlowFlowable so = new SlowFlowable(100, 0); Flowable<Long> o = Flowable.unsafeCreate(so).retry(5); AsyncObserver<Long> async = new AsyncObserver<Long>(observer); o.subscribe(async); async.await(); InOrder inOrder = inOrder(observer); // Should fail once inOrder.verify(observer, times(1)).onError(any(Throwable.class)); inOrder.verify(observer, never()).onComplete(); assertEquals("Start 6 threads, retry 5 then fail on 6", 6, so.efforts.get()); assertEquals("Only 1 active subscription", 1, so.maxActive.get()); } @Test//(timeout = 10000) public void testTimeoutWithRetry() { @SuppressWarnings("unchecked") DefaultSubscriber<Long> observer = mock(DefaultSubscriber.class); // Flowable that sends every 100ms (timeout fails instead) SlowFlowable so = new SlowFlowable(100, 10); Flowable<Long> o = Flowable.unsafeCreate(so).timeout(80, TimeUnit.MILLISECONDS).retry(5); AsyncObserver<Long> async = new AsyncObserver<Long>(observer); o.subscribe(async); async.await(); InOrder inOrder = inOrder(observer); // Should fail once inOrder.verify(observer, times(1)).onError(any(Throwable.class)); inOrder.verify(observer, never()).onComplete(); assertEquals("Start 6 threads, retry 5 then fail on 6", 6, so.efforts.get()); } @Test//(timeout = 15000) public void testRetryWithBackpressure() throws InterruptedException { final int NUM_LOOPS = 1; for (int j = 0;j < NUM_LOOPS; j++) { final int NUM_RETRIES = Flowable.bufferSize() * 2; for (int i = 0; i < 400; i++) { Subscriber<String> observer = TestHelper.mockSubscriber(); Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(NUM_RETRIES)); TestSubscriber<String> ts = new TestSubscriber<String>(observer); origin.retry().observeOn(Schedulers.computation()).subscribe(ts); ts.awaitTerminalEvent(5, TimeUnit.SECONDS); InOrder inOrder = inOrder(observer); // should have no errors verify(observer, never()).onError(any(Throwable.class)); // should show NUM_RETRIES attempts inOrder.verify(observer, times(NUM_RETRIES + 1)).onNext("beginningEveryTime"); // should have a single success inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); // should have a single successful onComplete inOrder.verify(observer, times(1)).onComplete(); inOrder.verifyNoMoreInteractions(); } } } @Test//(timeout = 15000) public void testRetryWithBackpressureParallel() throws InterruptedException { final int NUM_LOOPS = 1; final int NUM_RETRIES = Flowable.bufferSize() * 2; int ncpu = Runtime.getRuntime().availableProcessors(); ExecutorService exec = Executors.newFixedThreadPool(Math.max(ncpu / 2, 2)); try { for (int r = 0; r < NUM_LOOPS; r++) { if (r % 10 == 0) { System.out.println("testRetryWithBackpressureParallelLoop -> " + r); } final AtomicInteger timeouts = new AtomicInteger(); final Map<Integer, List<String>> data = new ConcurrentHashMap<Integer, List<String>>(); int m = 5000; final CountDownLatch cdl = new CountDownLatch(m); for (int i = 0; i < m; i++) { final int j = i; exec.execute(new Runnable() { @Override public void run() { final AtomicInteger nexts = new AtomicInteger(); try { Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(NUM_RETRIES)); TestSubscriber<String> ts = new TestSubscriber<String>(); origin.retry() .observeOn(Schedulers.computation()).subscribe(ts); ts.awaitTerminalEvent(2500, TimeUnit.MILLISECONDS); List<String> onNextEvents = new ArrayList<String>(ts.values()); if (onNextEvents.size() != NUM_RETRIES + 2) { for (Throwable t : ts.errors()) { onNextEvents.add(t.toString()); } for (long err = ts.completions(); err != 0; err--) { onNextEvents.add("onComplete"); } data.put(j, onNextEvents); } } catch (Throwable t) { timeouts.incrementAndGet(); System.out.println(j + " | " + cdl.getCount() + " !!! " + nexts.get()); } cdl.countDown(); } }); } cdl.await(); assertEquals(0, timeouts.get()); if (data.size() > 0) { fail("Data content mismatch: " + allSequenceFrequency(data)); } } } finally { exec.shutdown(); } } static <T> StringBuilder allSequenceFrequency(Map<Integer, List<T>> its) { StringBuilder b = new StringBuilder(); for (Map.Entry<Integer, List<T>> e : its.entrySet()) { if (b.length() > 0) { b.append(", "); } b.append(e.getKey()).append("={"); b.append(sequenceFrequency(e.getValue())); b.append("}"); } return b; } static <T> StringBuilder sequenceFrequency(Iterable<T> it) { StringBuilder sb = new StringBuilder(); Object prev = null; int cnt = 0; for (Object curr : it) { if (sb.length() > 0) { if (!curr.equals(prev)) { if (cnt > 1) { sb.append(" x ").append(cnt); cnt = 1; } sb.append(", "); sb.append(curr); } else { cnt++; } } else { sb.append(curr); cnt++; } prev = curr; } if (cnt > 1) { sb.append(" x ").append(cnt); } return sb; } @Test//(timeout = 3000) public void testIssue1900() throws InterruptedException { Subscriber<String> observer = TestHelper.mockSubscriber(); final int NUM_MSG = 1034; final AtomicInteger count = new AtomicInteger(); Flowable<String> origin = Flowable.range(0, NUM_MSG) .map(new Function<Integer, String>() { @Override public String apply(Integer t1) { return "msg: " + count.incrementAndGet(); } }); origin.retry() .groupBy(new Function<String, String>() { @Override public String apply(String t1) { return t1; } }) .flatMap(new Function<GroupedFlowable<String,String>, Flowable<String>>() { @Override public Flowable<String> apply(GroupedFlowable<String, String> t1) { return t1.take(1); } }) .subscribe(new TestSubscriber<String>(observer)); InOrder inOrder = inOrder(observer); // should show 3 attempts inOrder.verify(observer, times(NUM_MSG)).onNext(any(java.lang.String.class)); // // should have no errors inOrder.verify(observer, never()).onError(any(Throwable.class)); // should have a single success //inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); // should have a single successful onComplete inOrder.verify(observer, times(1)).onComplete(); inOrder.verifyNoMoreInteractions(); } @Test//(timeout = 3000) public void testIssue1900SourceNotSupportingBackpressure() { Subscriber<String> observer = TestHelper.mockSubscriber(); final int NUM_MSG = 1034; final AtomicInteger count = new AtomicInteger(); Flowable<String> origin = Flowable.unsafeCreate(new Publisher<String>() { @Override public void subscribe(Subscriber<? super String> o) { o.onSubscribe(new BooleanSubscription()); for (int i = 0; i < NUM_MSG; i++) { o.onNext("msg:" + count.incrementAndGet()); } o.onComplete(); } }); origin.retry() .groupBy(new Function<String, String>() { @Override public String apply(String t1) { return t1; } }) .flatMap(new Function<GroupedFlowable<String,String>, Flowable<String>>() { @Override public Flowable<String> apply(GroupedFlowable<String, String> t1) { return t1.take(1); } }) .subscribe(new TestSubscriber<String>(observer)); InOrder inOrder = inOrder(observer); // should show 3 attempts inOrder.verify(observer, times(NUM_MSG)).onNext(any(java.lang.String.class)); // // should have no errors inOrder.verify(observer, never()).onError(any(Throwable.class)); // should have a single success //inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); // should have a single successful onComplete inOrder.verify(observer, times(1)).onComplete(); inOrder.verifyNoMoreInteractions(); } @SuppressWarnings({ "rawtypes", "unchecked" }) @Test public void retryWhenDefaultScheduler() { TestSubscriber<Integer> ts = TestSubscriber.create(); Flowable.just(1) .concatWith(Flowable.<Integer>error(new TestException())) .retryWhen((Function)new Function<Flowable, Flowable>() { @Override public Flowable apply(Flowable o) { return o.take(2); } }).subscribe(ts); ts.assertValues(1, 1); ts.assertNoErrors(); ts.assertComplete(); } @SuppressWarnings({ "rawtypes", "unchecked" }) @Test public void retryWhenTrampolineScheduler() { TestSubscriber<Integer> ts = TestSubscriber.create(); Flowable.just(1) .concatWith(Flowable.<Integer>error(new TestException())) .subscribeOn(Schedulers.trampoline()) .retryWhen((Function)new Function<Flowable, Flowable>() { @Override public Flowable apply(Flowable o) { return o.take(2); } }).subscribe(ts); ts.assertValues(1, 1); ts.assertNoErrors(); ts.assertComplete(); } @Test public void retryPredicate() { Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) .retry(new Predicate<Throwable>() { @Override public boolean test(Throwable v) throws Exception { return true; } }) .take(5) .test() .assertResult(1, 1, 1, 1, 1); } @Test public void retryLongPredicateInvalid() { try { Flowable.just(1).retry(-99, new Predicate<Throwable>() { @Override public boolean test(Throwable e) throws Exception { return true; } }); fail("Should have thrown"); } catch (IllegalArgumentException ex) { assertEquals("times >= 0 required but it was -99", ex.getMessage()); } } @Test public void retryUntil() { Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) .retryUntil(new BooleanSupplier() { @Override public boolean getAsBoolean() throws Exception { return false; } }) .take(5) .test() .assertResult(1, 1, 1, 1, 1); } @Test public void shouldDisposeInnerObservable() { final PublishProcessor<Object> subject = PublishProcessor.create(); final Disposable disposable = Flowable.error(new RuntimeException("Leak")) .retryWhen(new Function<Flowable<Throwable>, Flowable<Object>>() { @Override public Flowable<Object> apply(Flowable<Throwable> errors) throws Exception { return errors.switchMap(new Function<Throwable, Flowable<Object>>() { @Override public Flowable<Object> apply(Throwable ignore) throws Exception { return subject; } }); } }) .subscribe(); assertTrue(subject.hasSubscribers()); disposable.dispose(); assertFalse(subject.hasSubscribers()); } }