Skip to content

Commit 198498c

Browse files
Merge pull request ReactiveX#371 from benjchristensen/retry
Operator: Retry
2 parents f8f378f + 30bcf5f commit 198498c

File tree

3 files changed

+251
-2
lines changed

3 files changed

+251
-2
lines changed

rxjava-core/src/main/java/rx/Observable.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import rx.operators.OperationOnErrorResumeNextViaObservable;
5353
import rx.operators.OperationOnErrorReturn;
5454
import rx.operators.OperationOnExceptionResumeNextViaObservable;
55+
import rx.operators.OperationRetry;
5556
import rx.operators.OperationSample;
5657
import rx.operators.OperationScan;
5758
import rx.operators.OperationSkip;
@@ -3128,6 +3129,42 @@ public static Observable<Double> averageDoubles(Observable<Double> source) {
31283129
public ConnectableObservable<T> replay() {
31293130
return OperationMulticast.multicast(this, ReplaySubject.<T> create());
31303131
}
3132+
3133+
/**
3134+
* Retry subscription to origin Observable upto given retry count.
3135+
* <p>
3136+
* If {@link Observer#onError} is invoked the source Observable will be re-subscribed to as many times as defined by retryCount.
3137+
* <p>
3138+
* Any {@link Observer#onNext} calls received on each attempt will be emitted and concatenated together.
3139+
* <p>
3140+
* For example, if an Observable fails on first time but emits [1, 2] then succeeds the second time and
3141+
* emits [1, 2, 3, 4, 5] then the complete output would be [1, 2, 1, 2, 3, 4, 5, onCompleted].
3142+
*
3143+
* @param retryCount
3144+
* Number of retry attempts before failing.
3145+
* @return Observable with retry logic.
3146+
*/
3147+
public Observable<T> retry(int retryCount) {
3148+
return create(OperationRetry.retry(this, retryCount));
3149+
}
3150+
3151+
/**
3152+
* Retry subscription to origin Observable whenever onError is called (infinite retry count).
3153+
* <p>
3154+
* If {@link Observer#onError} is invoked the source Observable will be re-subscribed to.
3155+
* <p>
3156+
* Any {@link Observer#onNext} calls received on each attempt will be emitted and concatenated together.
3157+
* <p>
3158+
* For example, if an Observable fails on first time but emits [1, 2] then succeeds the second time and
3159+
* emits [1, 2, 3, 4, 5] then the complete output would be [1, 2, 1, 2, 3, 4, 5, onCompleted].
3160+
*
3161+
* @param retryCount
3162+
* Number of retry attempts before failing.
3163+
* @return Observable with retry logic.
3164+
*/
3165+
public Observable<T> retry() {
3166+
return create(OperationRetry.retry(this));
3167+
}
31313168

31323169
/**
31333170
* This method has similar behavior to {@link #replay} except that this auto-subscribes to
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package rx.operators;
2+
3+
/**
4+
* Copyright 2013 Netflix, Inc.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
import static org.mockito.Matchers.*;
19+
import static org.mockito.Mockito.*;
20+
21+
import java.util.concurrent.atomic.AtomicInteger;
22+
23+
import org.junit.Test;
24+
import org.mockito.InOrder;
25+
26+
import rx.Observable;
27+
import rx.Observable.OnSubscribeFunc;
28+
import rx.Observer;
29+
import rx.Subscription;
30+
import rx.concurrency.Schedulers;
31+
import rx.subscriptions.CompositeSubscription;
32+
import rx.subscriptions.Subscriptions;
33+
import rx.util.functions.Action0;
34+
35+
public class OperationRetry {
36+
37+
private static final int INFINITE_RETRY = -1;
38+
39+
public static <T> OnSubscribeFunc<T> retry(final Observable<T> observable, final int retryCount) {
40+
return new Retry<T>(observable, retryCount);
41+
}
42+
43+
public static <T> OnSubscribeFunc<T> retry(final Observable<T> observable) {
44+
return new Retry<T>(observable, INFINITE_RETRY);
45+
}
46+
47+
private static class Retry<T> implements OnSubscribeFunc<T> {
48+
49+
private final Observable<T> source;
50+
private final int retryCount;
51+
private final AtomicInteger attempts = new AtomicInteger(0);
52+
private final CompositeSubscription subscription = new CompositeSubscription();
53+
54+
public Retry(Observable<T> source, int retryCount) {
55+
this.source = source;
56+
this.retryCount = retryCount;
57+
}
58+
59+
@Override
60+
public Subscription onSubscribe(Observer<? super T> observer) {
61+
subscription.add(Schedulers.currentThread().schedule(attemptSubscription(observer)));
62+
return subscription;
63+
}
64+
65+
private Action0 attemptSubscription(final Observer<? super T> observer) {
66+
return new Action0() {
67+
68+
@Override
69+
public void call() {
70+
attempts.incrementAndGet();
71+
source.subscribe(new Observer<T>() {
72+
73+
@Override
74+
public void onCompleted() {
75+
observer.onCompleted();
76+
}
77+
78+
@Override
79+
public void onError(Throwable e) {
80+
if ((retryCount == INFINITE_RETRY || attempts.get() <= retryCount) && !subscription.isUnsubscribed()) {
81+
// retry again
82+
// remove the last subscription since we have completed (so as we retry we don't build up a huge list)
83+
subscription.removeLast();
84+
// add the new subscription and schedule a retry
85+
subscription.add(Schedulers.currentThread().schedule(attemptSubscription(observer)));
86+
} else {
87+
// give up and pass the failure
88+
observer.onError(e);
89+
}
90+
}
91+
92+
@Override
93+
public void onNext(T v) {
94+
observer.onNext(v);
95+
}
96+
});
97+
98+
}
99+
};
100+
}
101+
102+
}
103+
104+
public static class UnitTest {
105+
106+
@Test
107+
public void testOriginFails() {
108+
@SuppressWarnings("unchecked")
109+
Observer<String> observer = mock(Observer.class);
110+
Observable<String> origin = Observable.create(new FuncWithErrors(2));
111+
origin.subscribe(observer);
112+
113+
InOrder inOrder = inOrder(observer);
114+
inOrder.verify(observer, times(1)).onNext("beginningEveryTime");
115+
inOrder.verify(observer, times(1)).onError(any(RuntimeException.class));
116+
inOrder.verify(observer, never()).onNext("onSuccessOnly");
117+
inOrder.verify(observer, never()).onCompleted();
118+
}
119+
120+
@Test
121+
public void testRetryFail() {
122+
int NUM_RETRIES = 1;
123+
int NUM_FAILURES = 2;
124+
@SuppressWarnings("unchecked")
125+
Observer<String> observer = mock(Observer.class);
126+
Observable<String> origin = Observable.create(new FuncWithErrors(NUM_FAILURES));
127+
Observable.create(retry(origin, NUM_RETRIES)).subscribe(observer);
128+
129+
InOrder inOrder = inOrder(observer);
130+
// should show 2 attempts (first time fail, second time (1st retry) fail)
131+
inOrder.verify(observer, times(1 + NUM_RETRIES)).onNext("beginningEveryTime");
132+
// should only retry once, fail again and emit onError
133+
inOrder.verify(observer, times(1)).onError(any(RuntimeException.class));
134+
// no success
135+
inOrder.verify(observer, never()).onNext("onSuccessOnly");
136+
inOrder.verify(observer, never()).onCompleted();
137+
inOrder.verifyNoMoreInteractions();
138+
}
139+
140+
@Test
141+
public void testRetrySuccess() {
142+
int NUM_RETRIES = 3;
143+
int NUM_FAILURES = 2;
144+
@SuppressWarnings("unchecked")
145+
Observer<String> observer = mock(Observer.class);
146+
Observable<String> origin = Observable.create(new FuncWithErrors(NUM_FAILURES));
147+
Observable.create(retry(origin, NUM_RETRIES)).subscribe(observer);
148+
149+
InOrder inOrder = inOrder(observer);
150+
// should show 3 attempts
151+
inOrder.verify(observer, times(1 + NUM_FAILURES)).onNext("beginningEveryTime");
152+
// should have no errors
153+
inOrder.verify(observer, never()).onError(any(Throwable.class));
154+
// should have a single success
155+
inOrder.verify(observer, times(1)).onNext("onSuccessOnly");
156+
// should have a single successful onCompleted
157+
inOrder.verify(observer, times(1)).onCompleted();
158+
inOrder.verifyNoMoreInteractions();
159+
}
160+
161+
@Test
162+
public void testInfiniteRetry() {
163+
int NUM_FAILURES = 20;
164+
@SuppressWarnings("unchecked")
165+
Observer<String> observer = mock(Observer.class);
166+
Observable<String> origin = Observable.create(new FuncWithErrors(NUM_FAILURES));
167+
Observable.create(retry(origin)).subscribe(observer);
168+
169+
InOrder inOrder = inOrder(observer);
170+
// should show 3 attempts
171+
inOrder.verify(observer, times(1 + NUM_FAILURES)).onNext("beginningEveryTime");
172+
// should have no errors
173+
inOrder.verify(observer, never()).onError(any(Throwable.class));
174+
// should have a single success
175+
inOrder.verify(observer, times(1)).onNext("onSuccessOnly");
176+
// should have a single successful onCompleted
177+
inOrder.verify(observer, times(1)).onCompleted();
178+
inOrder.verifyNoMoreInteractions();
179+
}
180+
181+
public static class FuncWithErrors implements OnSubscribeFunc<String> {
182+
183+
private final int numFailures;
184+
private final AtomicInteger count = new AtomicInteger(0);
185+
186+
FuncWithErrors(int count) {
187+
this.numFailures = count;
188+
}
189+
190+
@Override
191+
public Subscription onSubscribe(Observer<? super String> o) {
192+
o.onNext("beginningEveryTime");
193+
if (count.incrementAndGet() <= numFailures) {
194+
o.onError(new RuntimeException("forced failure: " + count.get()));
195+
} else {
196+
o.onNext("onSuccessOnly");
197+
o.onCompleted();
198+
}
199+
return Subscriptions.empty();
200+
}
201+
};
202+
}
203+
}

rxjava-core/src/main/java/rx/subscriptions/CompositeSubscription.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import java.util.ArrayList;
2121
import java.util.Collection;
2222
import java.util.List;
23-
import java.util.concurrent.ConcurrentLinkedQueue;
23+
import java.util.concurrent.LinkedBlockingDeque;
2424
import java.util.concurrent.atomic.AtomicBoolean;
2525
import java.util.concurrent.atomic.AtomicInteger;
2626

@@ -42,7 +42,7 @@ public class CompositeSubscription implements Subscription {
4242
* TODO evaluate whether use of synchronized is a performance issue here and if it's worth using an atomic state machine or other non-locking approach
4343
*/
4444
private AtomicBoolean unsubscribed = new AtomicBoolean(false);
45-
private final ConcurrentLinkedQueue<Subscription> subscriptions = new ConcurrentLinkedQueue<Subscription>();
45+
private final LinkedBlockingDeque<Subscription> subscriptions = new LinkedBlockingDeque<Subscription>();
4646

4747
public CompositeSubscription(List<Subscription> subscriptions) {
4848
this.subscriptions.addAll(subscriptions);
@@ -66,6 +66,15 @@ public synchronized void add(Subscription s) {
6666
}
6767
}
6868

69+
/**
70+
* Remove the last Subscription that was added.
71+
*
72+
* @return Subscription or null if none exists
73+
*/
74+
public synchronized Subscription removeLast() {
75+
return subscriptions.pollLast();
76+
}
77+
6978
@Override
7079
public synchronized void unsubscribe() {
7180
if (unsubscribed.compareAndSet(false, true)) {

0 commit comments

Comments
 (0)