Skip to content

Commit 8bb52a0

Browse files
Merge pull request #1033 from benjchristensen/retry-1027
Manual Merge #1027
2 parents a9e7975 + 1e503b4 commit 8bb52a0

File tree

2 files changed

+191
-100
lines changed

2 files changed

+191
-100
lines changed

rxjava-core/src/main/java/rx/operators/OperatorRetry.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@
3737
import rx.Observable.Operator;
3838
import rx.Scheduler.Inner;
3939
import rx.Subscriber;
40+
import rx.functions.Action0;
4041
import rx.functions.Action1;
4142
import rx.schedulers.Schedulers;
4243
import rx.subscriptions.SerialSubscription;
44+
import rx.subscriptions.Subscriptions;
4345

4446
public class OperatorRetry<T> implements Operator<T, Observable<T>> {
4547

@@ -82,7 +84,9 @@ public void call(final Inner inner) {
8284
final Action1<Inner> _self = this;
8385
attempts.incrementAndGet();
8486

85-
Subscriber<T> subscriber = new Subscriber<T>(child) {
87+
// new subscription each time so if it unsubscribes itself it does not prevent retries
88+
// by unsubscribing the child subscription
89+
Subscriber<T> subscriber = new Subscriber<T>() {
8690

8791
@Override
8892
public void onCompleted() {

rxjava-core/src/test/java/rx/operators/OperatorRetryTest.java

Lines changed: 186 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,30 @@
1515
*/
1616
package rx.operators;
1717

18-
import static org.junit.Assert.*;
19-
import static org.mockito.Matchers.*;
20-
import static org.mockito.Mockito.*;
18+
import static org.junit.Assert.assertEquals;
19+
import static org.junit.Assert.fail;
20+
import static org.mockito.Matchers.any;
21+
import static org.mockito.Mockito.inOrder;
22+
import static org.mockito.Mockito.mock;
23+
import static org.mockito.Mockito.never;
24+
import static org.mockito.Mockito.times;
2125

2226
import java.util.concurrent.CountDownLatch;
2327
import java.util.concurrent.TimeUnit;
28+
import java.util.concurrent.atomic.AtomicBoolean;
2429
import java.util.concurrent.atomic.AtomicInteger;
2530

2631
import org.junit.Test;
2732
import org.mockito.InOrder;
2833

2934
import rx.Observable;
30-
import rx.Observable.OnSubscribeFunc;
35+
import rx.Observable.OnSubscribe;
3136
import rx.Observer;
3237
import rx.Subscriber;
3338
import rx.Subscription;
39+
import rx.functions.Action0;
3440
import rx.functions.Action1;
41+
import rx.observers.TestSubscriber;
3542
import rx.subjects.PublishSubject;
3643
import rx.subscriptions.Subscriptions;
3744

@@ -112,7 +119,7 @@ public void testInfiniteRetry() {
112119
inOrder.verifyNoMoreInteractions();
113120
}
114121

115-
public static class FuncWithErrors implements Observable.OnSubscribeFunc<String> {
122+
public static class FuncWithErrors implements Observable.OnSubscribe<String> {
116123

117124
private final int numFailures;
118125
private final AtomicInteger count = new AtomicInteger(0);
@@ -122,15 +129,14 @@ public static class FuncWithErrors implements Observable.OnSubscribeFunc<String>
122129
}
123130

124131
@Override
125-
public Subscription onSubscribe(Observer<? super String> o) {
132+
public void call(Subscriber<? super String> o) {
126133
o.onNext("beginningEveryTime");
127134
if (count.incrementAndGet() <= numFailures) {
128135
o.onError(new RuntimeException("forced failure: " + count.get()));
129136
} else {
130137
o.onNext("onSuccessOnly");
131138
o.onCompleted();
132139
}
133-
return Subscriptions.empty();
134140
}
135141
}
136142

@@ -150,101 +156,14 @@ public void call(Integer n) {
150156
assertEquals(1, count.get());
151157
}
152158

153-
public static class SlowFuncAlwaysFails implements Observable.OnSubscribe<String> {
154-
155-
final AtomicInteger nextSeq=new AtomicInteger();
156-
final AtomicInteger activeSubs=new AtomicInteger();
157-
final AtomicInteger concurrentSubs=new AtomicInteger();
158-
159-
public void call(final Subscriber<? super String> s)
160-
{
161-
final int seq=nextSeq.incrementAndGet();
162-
163-
int cur=activeSubs.incrementAndGet();
164-
// Track concurrent subscriptions
165-
concurrentSubs.set(Math.max(cur,concurrentSubs.get()));
166-
167-
// Use async error
168-
new Thread(new Runnable() {
169-
@Override
170-
public void run() {
171-
try {
172-
Thread.sleep(100);
173-
} catch (InterruptedException e) {
174-
// ignore
175-
}
176-
s.onError(new RuntimeException("Subscriber #"+seq+" fails"));
177-
}
178-
}).start();
179-
180-
// Track unsubscribes
181-
s.add(new Subscription()
182-
{
183-
private boolean active=true;
184-
185-
public void unsubscribe()
186-
{
187-
if (active) {
188-
activeSubs.decrementAndGet();
189-
active=false;
190-
}
191-
}
192-
193-
public boolean isUnsubscribed()
194-
{
195-
return !active;
196-
}
197-
});
198-
}
199-
}
200-
201-
@Test
202-
public void testUnsubscribeAfterError() {
203-
204-
final CountDownLatch check=new CountDownLatch(1);
205-
final SlowFuncAlwaysFails sf=new SlowFuncAlwaysFails();
206-
207-
Observable
208-
.create(sf)
209-
.retry(4)
210-
.subscribe(
211-
new Action1<String>()
212-
{
213-
@Override
214-
public void call(String v)
215-
{
216-
fail("Should never happen");
217-
}
218-
},
219-
new Action1<Throwable>()
220-
{
221-
public void call(Throwable throwable)
222-
{
223-
check.countDown();
224-
}
225-
}
226-
);
227-
228-
try
229-
{
230-
check.await(1, TimeUnit.SECONDS);
231-
} catch (InterruptedException e)
232-
{
233-
fail("interrupted");
234-
}
235-
236-
assertEquals("5 Subscribers created", 5, sf.nextSeq.get());
237-
assertEquals("1 Active Subscriber", 1, sf.concurrentSubs.get());
238-
}
239-
240159
@Test
241-
public void testRetryAllowsSubscriptionAfterAllSubscriptionsUnsubsribed() throws InterruptedException {
160+
public void testRetryAllowsSubscriptionAfterAllSubscriptionsUnsubscribed() throws InterruptedException {
242161
final AtomicInteger subsCount = new AtomicInteger(0);
243-
OnSubscribeFunc<String> onSubscribe = new OnSubscribeFunc<String>() {
162+
OnSubscribe<String> onSubscribe = new OnSubscribe<String>() {
244163
@Override
245-
public Subscription onSubscribe(Observer<? super String> observer) {
164+
public void call(Subscriber<? super String> s) {
246165
subsCount.incrementAndGet();
247-
return new Subscription() {
166+
s.add(new Subscription() {
248167
boolean unsubscribed = false;
249168

250169
@Override
@@ -257,7 +176,7 @@ public void unsubscribe() {
257176
public boolean isUnsubscribed() {
258177
return unsubscribed;
259178
}
260-
};
179+
});
261180
}
262181
};
263182
Observable<String> stream = Observable.create(onSubscribe);
@@ -269,4 +188,172 @@ public boolean isUnsubscribed() {
269188
streamWithRetry.subscribe();
270189
assertEquals(1, subsCount.get());
271190
}
191+
192+
@Test
193+
public void testSourceObservableCallsUnsubscribe() throws InterruptedException {
194+
final AtomicInteger subsCount = new AtomicInteger(0);
195+
196+
final TestSubscriber<String> ts = new TestSubscriber<String>();
197+
198+
OnSubscribe<String> onSubscribe = new OnSubscribe<String>() {
199+
@Override
200+
public void call(Subscriber<? super String> s) {
201+
// if isUnsubscribed is true that means we have a bug such as https://github.com/Netflix/RxJava/issues/1024
202+
if (!s.isUnsubscribed()) {
203+
subsCount.incrementAndGet();
204+
s.onError(new RuntimeException("failed"));
205+
// it unsubscribes the child directly
206+
// this simulates various error/completion scenarios that could occur
207+
// or just a source that proactively triggers cleanup
208+
s.unsubscribe();
209+
}
210+
}
211+
};
212+
213+
Observable.create(onSubscribe).retry(3).subscribe(ts);
214+
assertEquals(4, subsCount.get()); // 1 + 3 retries
215+
}
216+
217+
class SlowObservable implements Observable.OnSubscribe<Long> {
218+
219+
private AtomicInteger efforts = new AtomicInteger(0);
220+
private AtomicInteger active = new AtomicInteger(0), maxActive = new AtomicInteger(0);
221+
private AtomicInteger nextBeforeFailure;
222+
223+
private final int emitDelay;
224+
225+
public SlowObservable(int emitDelay, int countNext) {
226+
this.emitDelay = emitDelay;
227+
this.nextBeforeFailure = new AtomicInteger(countNext);
228+
}
229+
230+
public void call(final Subscriber<? super Long> subscriber) {
231+
final AtomicBoolean terminate = new AtomicBoolean(false);
232+
efforts.getAndIncrement();
233+
active.getAndIncrement();
234+
maxActive.set(Math.max(active.get(), maxActive.get()));
235+
final Thread thread = new Thread() {
236+
@Override
237+
public void run() {
238+
long nr = 0;
239+
try {
240+
while (!terminate.get()) {
241+
Thread.sleep(emitDelay);
242+
if (nextBeforeFailure.getAndDecrement() > 0) {
243+
subscriber.onNext(nr++);
244+
}
245+
else {
246+
subscriber.onError(new RuntimeException("expected-failed"));
247+
}
248+
}
249+
}
250+
catch (InterruptedException t) {
251+
}
252+
}
253+
};
254+
thread.start();
255+
subscriber.add(Subscriptions.create(new Action0() {
256+
@Override
257+
public void call() {
258+
terminate.set(true);
259+
active.decrementAndGet();
260+
}
261+
}));
262+
}
263+
}
264+
265+
/** Observer for listener on seperate thread */
266+
class AsyncObserver<T> implements Observer<T> {
267+
268+
protected CountDownLatch latch = new CountDownLatch(1);
269+
270+
protected Observer<T> target;
271+
272+
/** Wrap existing Observer */
273+
public AsyncObserver(Observer<T> target) {
274+
this.target = target;
275+
}
276+
277+
/** Wait */
278+
public void await() {
279+
try {
280+
latch.await();
281+
} catch (InterruptedException e) {
282+
fail("Test interrupted");
283+
}
284+
}
285+
286+
// Observer implementation
287+
288+
@Override
289+
public void onCompleted() {
290+
target.onCompleted();
291+
latch.countDown();
292+
}
293+
294+
@Override
295+
public void onError(Throwable t) {
296+
target.onError(t);
297+
latch.countDown();
298+
}
299+
300+
@Override
301+
public void onNext(T v) {
302+
target.onNext(v);
303+
}
304+
}
305+
306+
@Test(timeout = 1000)
307+
public void testUnsubscribeAfterError() {
308+
309+
@SuppressWarnings("unchecked")
310+
Observer<Long> observer = mock(Observer.class);
311+
312+
// Observable that always fails after 100ms
313+
SlowObservable so = new SlowObservable(100, 0);
314+
Observable<Long> o = Observable
315+
.create(so)
316+
.retry(5);
317+
318+
AsyncObserver<Long> async = new AsyncObserver<Long>(observer);
319+
320+
o.subscribe(async);
321+
322+
async.await();
323+
324+
InOrder inOrder = inOrder(observer);
325+
// Should fail once
326+
inOrder.verify(observer, times(1)).onError(any(Throwable.class));
327+
inOrder.verify(observer, never()).onCompleted();
328+
329+
assertEquals("Start 6 threads, retry 5 then fail on 6", 6, so.efforts.get());
330+
assertEquals("Only 1 active subscription", 1, so.maxActive.get());
331+
}
332+
333+
@Test(timeout = 1000)
334+
public void testTimeoutWithRetry() {
335+
336+
@SuppressWarnings("unchecked")
337+
Observer<Long> observer = mock(Observer.class);
338+
339+
// Observable that sends every 100ms (timeout fails instead)
340+
SlowObservable so = new SlowObservable(100, 10);
341+
Observable<Long> o = Observable
342+
.create(so)
343+
.timeout(80, TimeUnit.MILLISECONDS)
344+
.retry(5);
345+
346+
AsyncObserver<Long> async = new AsyncObserver<Long>(observer);
347+
348+
o.subscribe(async);
349+
350+
async.await();
351+
352+
InOrder inOrder = inOrder(observer);
353+
// Should fail once
354+
inOrder.verify(observer, times(1)).onError(any(Throwable.class));
355+
inOrder.verify(observer, never()).onCompleted();
356+
357+
assertEquals("Start 6 threads, retry 5 then fail on 6", 6, so.efforts.get());
358+
}
272359
}

0 commit comments

Comments
 (0)