Skip to content

Commit d2a5d5d

Browse files
committed
Merge pull request #2997 from davidmoten/redo-fix-2
Fix retry() race conditions
2 parents f217edd + 23c06b1 commit d2a5d5d

File tree

3 files changed

+202
-118
lines changed

3 files changed

+202
-118
lines changed

src/main/java/rx/internal/operators/OnSubscribeRedo.java

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535

3636
import java.util.concurrent.atomic.AtomicBoolean;
3737
import java.util.concurrent.atomic.AtomicLong;
38-
import java.util.concurrent.atomic.AtomicReference;
3938

4039
import rx.Notification;
4140
import rx.Observable;
@@ -47,13 +46,15 @@
4746
import rx.functions.Action0;
4847
import rx.functions.Func1;
4948
import rx.functions.Func2;
49+
import rx.internal.producers.ProducerArbiter;
50+
import rx.observers.Subscribers;
5051
import rx.schedulers.Schedulers;
51-
import rx.subjects.PublishSubject;
52+
import rx.subjects.BehaviorSubject;
5253
import rx.subscriptions.SerialSubscription;
5354

5455
public final class OnSubscribeRedo<T> implements OnSubscribe<T> {
5556

56-
static final Func1<Observable<? extends Notification<?>>, Observable<?>> REDO_INIFINITE = new Func1<Observable<? extends Notification<?>>, Observable<?>>() {
57+
static final Func1<Observable<? extends Notification<?>>, Observable<?>> REDO_INFINITE = new Func1<Observable<? extends Notification<?>>, Observable<?>>() {
5758
@Override
5859
public Observable<?> call(Observable<? extends Notification<?>> ts) {
5960
return ts.map(new Func1<Notification<?>, Notification<?>>() {
@@ -120,7 +121,7 @@ public Notification<Integer> call(Notification<Integer> n, Notification<?> term)
120121
}
121122

122123
public static <T> Observable<T> retry(Observable<T> source) {
123-
return retry(source, REDO_INIFINITE);
124+
return retry(source, REDO_INFINITE);
124125
}
125126

126127
public static <T> Observable<T> retry(Observable<T> source, final long count) {
@@ -144,7 +145,7 @@ public static <T> Observable<T> repeat(Observable<T> source) {
144145
}
145146

146147
public static <T> Observable<T> repeat(Observable<T> source, Scheduler scheduler) {
147-
return repeat(source, REDO_INIFINITE, scheduler);
148+
return repeat(source, REDO_INFINITE, scheduler);
148149
}
149150

150151
public static <T> Observable<T> repeat(Observable<T> source, final long count) {
@@ -172,10 +173,10 @@ public static <T> Observable<T> redo(Observable<T> source, Func1<? super Observa
172173
return create(new OnSubscribeRedo<T>(source, notificationHandler, false, false, scheduler));
173174
}
174175

175-
private Observable<T> source;
176+
private final Observable<T> source;
176177
private final Func1<? super Observable<? extends Notification<?>>, ? extends Observable<?>> controlHandlerFunction;
177-
private boolean stopOnComplete;
178-
private boolean stopOnError;
178+
private final boolean stopOnComplete;
179+
private final boolean stopOnError;
179180
private final Scheduler scheduler;
180181

181182
private OnSubscribeRedo(Observable<T> source, Func1<? super Observable<? extends Notification<?>>, ? extends Observable<?>> f, boolean stopOnComplete, boolean stopOnError,
@@ -189,20 +190,31 @@ private OnSubscribeRedo(Observable<T> source, Func1<? super Observable<? extends
189190

190191
@Override
191192
public void call(final Subscriber<? super T> child) {
192-
final AtomicBoolean isLocked = new AtomicBoolean(true);
193+
194+
// when true is a marker to say we are ready to resubscribe to source
193195
final AtomicBoolean resumeBoundary = new AtomicBoolean(true);
196+
194197
// incremented when requests are made, decremented when requests are fulfilled
195198
final AtomicLong consumerCapacity = new AtomicLong(0l);
196-
final AtomicReference<Producer> currentProducer = new AtomicReference<Producer>();
197199

198200
final Scheduler.Worker worker = scheduler.createWorker();
199201
child.add(worker);
200202

201203
final SerialSubscription sourceSubscriptions = new SerialSubscription();
202204
child.add(sourceSubscriptions);
203205

204-
final PublishSubject<Notification<?>> terminals = PublishSubject.create();
205-
206+
// use a subject to receive terminals (onCompleted and onError signals) from
207+
// the source observable. We use a BehaviorSubject because subscribeToSource
208+
// may emit a terminal before the restarts observable (transformed terminals)
209+
// is subscribed
210+
final BehaviorSubject<Notification<?>> terminals = BehaviorSubject.create();
211+
final Subscriber<Notification<?>> dummySubscriber = Subscribers.empty();
212+
// subscribe immediately so the last emission will be replayed to the next
213+
// subscriber (which is the one we care about)
214+
terminals.subscribe(dummySubscriber);
215+
216+
final ProducerArbiter arbiter = new ProducerArbiter();
217+
206218
final Action0 subscribeToSource = new Action0() {
207219
@Override
208220
public void call() {
@@ -212,11 +224,11 @@ public void call() {
212224

213225
Subscriber<T> terminalDelegatingSubscriber = new Subscriber<T>() {
214226
boolean done;
227+
215228
@Override
216229
public void onCompleted() {
217230
if (!done) {
218231
done = true;
219-
currentProducer.set(null);
220232
unsubscribe();
221233
terminals.onNext(Notification.createOnCompleted());
222234
}
@@ -226,7 +238,6 @@ public void onCompleted() {
226238
public void onError(Throwable e) {
227239
if (!done) {
228240
done = true;
229-
currentProducer.set(null);
230241
unsubscribe();
231242
terminals.onNext(Notification.createOnError(e));
232243
}
@@ -235,20 +246,30 @@ public void onError(Throwable e) {
235246
@Override
236247
public void onNext(T v) {
237248
if (!done) {
238-
if (consumerCapacity.get() != Long.MAX_VALUE) {
239-
consumerCapacity.decrementAndGet();
240-
}
241249
child.onNext(v);
250+
decrementConsumerCapacity();
251+
arbiter.produced(1);
252+
}
253+
}
254+
255+
private void decrementConsumerCapacity() {
256+
// use a CAS loop because we don't want to decrement the
257+
// value if it is Long.MAX_VALUE
258+
while (true) {
259+
long cc = consumerCapacity.get();
260+
if (cc != Long.MAX_VALUE) {
261+
if (consumerCapacity.compareAndSet(cc, cc - 1)) {
262+
break;
263+
}
264+
} else {
265+
break;
266+
}
242267
}
243268
}
244269

245270
@Override
246271
public void setProducer(Producer producer) {
247-
currentProducer.set(producer);
248-
long c = consumerCapacity.get();
249-
if (c > 0) {
250-
producer.request(c);
251-
}
272+
arbiter.setProducer(producer);
252273
}
253274
};
254275
// new subscription each time so if it unsubscribes itself it does not prevent retries
@@ -278,12 +299,11 @@ public void onError(Throwable e) {
278299

279300
@Override
280301
public void onNext(Notification<?> t) {
281-
if (t.isOnCompleted() && stopOnComplete)
282-
child.onCompleted();
283-
else if (t.isOnError() && stopOnError)
284-
child.onError(t.getThrowable());
285-
else {
286-
isLocked.set(false);
302+
if (t.isOnCompleted() && stopOnComplete) {
303+
filteredTerminals.onCompleted();
304+
} else if (t.isOnError() && stopOnError) {
305+
filteredTerminals.onError(t.getThrowable());
306+
} else {
287307
filteredTerminals.onNext(t);
288308
}
289309
}
@@ -313,10 +333,15 @@ public void onError(Throwable e) {
313333

314334
@Override
315335
public void onNext(Object t) {
316-
if (!isLocked.get() && !child.isUnsubscribed()) {
336+
if (!child.isUnsubscribed()) {
337+
// perform a best endeavours check on consumerCapacity
338+
// with the intent of only resubscribing immediately
339+
// if there is outstanding capacity
317340
if (consumerCapacity.get() > 0) {
318341
worker.schedule(subscribeToSource);
319342
} else {
343+
// set this to true so that on next request
344+
// subscribeToSource will be scheduled
320345
resumeBoundary.compareAndSet(false, true);
321346
}
322347
}
@@ -334,13 +359,11 @@ public void setProducer(Producer producer) {
334359

335360
@Override
336361
public void request(final long n) {
337-
long c = BackpressureUtils.getAndAddRequest(consumerCapacity, n);
338-
Producer producer = currentProducer.get();
339-
if (producer != null) {
340-
producer.request(n);
341-
} else
342-
if (c == 0 && resumeBoundary.compareAndSet(true, false)) {
343-
worker.schedule(subscribeToSource);
362+
if (n > 0) {
363+
BackpressureUtils.getAndAddRequest(consumerCapacity, n);
364+
arbiter.request(n);
365+
if (resumeBoundary.compareAndSet(true, false))
366+
worker.schedule(subscribeToSource);
344367
}
345368
}
346369
});

0 commit comments

Comments
 (0)