From b594a04dc12ad31c2b96be0bd45cae83abc4e1fc Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 7 Apr 2016 13:52:08 +0200 Subject: [PATCH] 1.x: fix takeLast() backpressure --- .../internal/operators/BackpressureUtils.java | 99 ++++++++++-- .../internal/operators/OperatorTakeLast.java | 95 +++++++----- .../operators/OperatorTakeLastTimed.java | 142 +++++++++++------- .../operators/TakeLastQueueProducer.java | 124 --------------- .../operators/OperatorTakeLastTest.java | 98 ++++++++++-- .../operators/OperatorTakeLastTimedTest.java | 80 +++++++++- 6 files changed, 396 insertions(+), 242 deletions(-) delete mode 100644 src/main/java/rx/internal/operators/TakeLastQueueProducer.java diff --git a/src/main/java/rx/internal/operators/BackpressureUtils.java b/src/main/java/rx/internal/operators/BackpressureUtils.java index 3d199567c6..4a2fa90f48 100644 --- a/src/main/java/rx/internal/operators/BackpressureUtils.java +++ b/src/main/java/rx/internal/operators/BackpressureUtils.java @@ -19,6 +19,8 @@ import java.util.concurrent.atomic.*; import rx.Subscriber; +import rx.functions.Func1; +import rx.internal.util.UtilityFunctions; /** * Utility functions for use with backpressure. @@ -140,6 +142,59 @@ public static long addCap(long a, long b) { * @param actual the subscriber to receive the values */ public static void postCompleteDone(AtomicLong requested, Queue queue, Subscriber actual) { + postCompleteDone(requested, queue, actual, UtilityFunctions.identity()); + } + + /** + * Accumulates requests (validated) and handles the completed mode draining of the queue based on the requests. + * + *

+ * Post-completion backpressure handles the case when a source produces values based on + * requests when it is active but more values are available even after its completion. + * In this case, the onCompleted() can't just emit the contents of the queue but has to + * coordinate with the requested amounts. This requires two distinct modes: active and + * completed. In active mode, requests flow through and the queue is not accessed but + * in completed mode, requests no-longer reach the upstream but help in draining the queue. + * + * @param the value type to emit + * @param requested the holder of current requested amount + * @param n the value requested; + * @param queue the queue holding values to be emitted after completion + * @param actual the subscriber to receive the values + * @return true if in the active mode and the request amount of n can be relayed to upstream, false if + * in the post-completed mode and the queue is draining. + */ + public static boolean postCompleteRequest(AtomicLong requested, long n, Queue queue, Subscriber actual) { + return postCompleteRequest(requested, n, queue, actual, UtilityFunctions.identity()); + } + + /** + * Signals the completion of the main sequence and switches to post-completion replay mode + * and allows exit transformation on the queued values. + * + *

+ * Don't modify the queue after calling this method! + * + *

+ * Post-completion backpressure handles the case when a source produces values based on + * requests when it is active but more values are available even after its completion. + * In this case, the onCompleted() can't just emit the contents of the queue but has to + * coordinate with the requested amounts. This requires two distinct modes: active and + * completed. In active mode, requests flow through and the queue is not accessed but + * in completed mode, requests no-longer reach the upstream but help in draining the queue. + *

+ * The algorithm utilizes the most significant bit (bit 63) of a long value (AtomicLong) since + * request amount only goes up to Long.MAX_VALUE (bits 0-62) and negative values aren't + * allowed. + * + * @param the value type in the queue + * @param the value type to emit + * @param requested the holder of current requested amount + * @param queue the queue holding values to be emitted after completion + * @param actual the subscriber to receive the values + * @param exitTransform the transformation to apply on the dequeued value to get the value to be emitted + */ + public static void postCompleteDone(AtomicLong requested, Queue queue, Subscriber actual, Func1 exitTransform) { for (;;) { long r = requested.get(); @@ -156,7 +211,7 @@ public static void postCompleteDone(AtomicLong requested, Queue queue, Su // are requests available start draining the queue if (r != 0L) { // if the switch happened when there was outstanding requests, start draining - postCompleteDrain(requested, queue, actual); + postCompleteDrain(requested, queue, actual, exitTransform); } return; } @@ -164,7 +219,8 @@ public static void postCompleteDone(AtomicLong requested, Queue queue, Su } /** - * Accumulates requests (validated) and handles the completed mode draining of the queue based on the requests. + * Accumulates requests (validated) and handles the completed mode draining of the queue based on the requests + * and allows exit transformation on the queued values. * *

* Post-completion backpressure handles the case when a source produces values based on @@ -174,15 +230,17 @@ public static void postCompleteDone(AtomicLong requested, Queue queue, Su * completed. In active mode, requests flow through and the queue is not accessed but * in completed mode, requests no-longer reach the upstream but help in draining the queue. * - * @param the value type to emit + * @param the value type in the queue + * @param the value type to emit * @param requested the holder of current requested amount * @param n the value requested; * @param queue the queue holding values to be emitted after completion * @param actual the subscriber to receive the values + * @param exitTransform the transformation to apply on the dequeued value to get the value to be emitted * @return true if in the active mode and the request amount of n can be relayed to upstream, false if * in the post-completed mode and the queue is draining. */ - public static boolean postCompleteRequest(AtomicLong requested, long n, Queue queue, Subscriber actual) { + public static boolean postCompleteRequest(AtomicLong requested, long n, Queue queue, Subscriber actual, Func1 exitTransform) { if (n < 0L) { throw new IllegalArgumentException("n >= 0 required but it was " + n); } @@ -209,7 +267,7 @@ public static boolean postCompleteRequest(AtomicLong requested, long n, Queu // if there was no outstanding request before and in // the post-completed state, start draining if (r == COMPLETED_MASK) { - postCompleteDrain(requested, queue, actual); + postCompleteDrain(requested, queue, actual, exitTransform); return false; } // returns true for active mode and false if the completed flag was set @@ -219,16 +277,37 @@ public static boolean postCompleteRequest(AtomicLong requested, long n, Queu } /** - * Drains the queue based on the outstanding requests in post-completed mode (only!). + * Drains the queue based on the outstanding requests in post-completed mode (only!) + * and allows exit transformation on the queued values. * - * @param the value type to emit + * @param the value type in the queue + * @param the value type to emit * @param requested the holder of current requested amount * @param queue the queue holding values to be emitted after completion - * @param actual the subscriber to receive the values + * @param subscriber the subscriber to receive the values + * @param exitTransform the transformation to apply on the dequeued value to get the value to be emitted */ - static void postCompleteDrain(AtomicLong requested, Queue queue, Subscriber subscriber) { + static void postCompleteDrain(AtomicLong requested, Queue queue, Subscriber subscriber, Func1 exitTransform) { long r = requested.get(); + + // Run on a fast-path if the downstream is unbounded + if (r == Long.MAX_VALUE) { + for (;;) { + if (subscriber.isUnsubscribed()) { + return; + } + + T v = queue.poll(); + + if (v == null) { + subscriber.onCompleted(); + return; + } + + subscriber.onNext(exitTransform.call(v)); + } + } /* * Since we are supposed to be in the post-complete state, * requested will have its top bit set. @@ -264,7 +343,7 @@ static void postCompleteDrain(AtomicLong requested, Queue queue, Subscrib return; } - subscriber.onNext(v); + subscriber.onNext(exitTransform.call(v)); e++; } diff --git a/src/main/java/rx/internal/operators/OperatorTakeLast.java b/src/main/java/rx/internal/operators/OperatorTakeLast.java index 2812c4e87c..77f8c93993 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeLast.java +++ b/src/main/java/rx/internal/operators/OperatorTakeLast.java @@ -16,15 +16,18 @@ package rx.internal.operators; import java.util.ArrayDeque; -import java.util.Deque; +import java.util.concurrent.atomic.AtomicLong; +import rx.*; import rx.Observable.Operator; -import rx.Subscriber; +import rx.functions.Func1; /** * Returns an Observable that emits the at most the last count items emitted by the source Observable. *

* + * + * @param the value type */ public final class OperatorTakeLast implements Operator { @@ -39,44 +42,62 @@ public OperatorTakeLast(int count) { @Override public Subscriber call(final Subscriber subscriber) { - final Deque deque = new ArrayDeque(); - final NotificationLite notification = NotificationLite.instance(); - final TakeLastQueueProducer producer = new TakeLastQueueProducer(notification, deque, subscriber); - subscriber.setProducer(producer); - - return new Subscriber(subscriber) { - - // no backpressure up as it wants to receive and discard all but the last + final TakeLastSubscriber parent = new TakeLastSubscriber(subscriber, count); + + subscriber.add(parent); + subscriber.setProducer(new Producer() { @Override - public void onStart() { - // we do this to break the chain of the child subscriber being passed through - request(Long.MAX_VALUE); + public void request(long n) { + parent.requestMore(n); } - - @Override - public void onCompleted() { - deque.offer(notification.completed()); - producer.startEmitting(); - } - - @Override - public void onError(Throwable e) { - deque.clear(); - subscriber.onError(e); + }); + + return parent; + } + + static final class TakeLastSubscriber extends Subscriber implements Func1 { + final Subscriber actual; + final AtomicLong requested; + final ArrayDeque queue; + final int count; + final NotificationLite nl; + + public TakeLastSubscriber(Subscriber actual, int count) { + this.actual = actual; + this.count = count; + this.requested = new AtomicLong(); + this.queue = new ArrayDeque(); + this.nl = NotificationLite.instance(); + } + + @Override + public void onNext(T t) { + if (queue.size() == count) { + queue.poll(); } - - @Override - public void onNext(T value) { - if (count == 0) { - // If count == 0, we do not need to put value into deque and - // remove it at once. We can ignore the value directly. - return; - } - if (deque.size() == count) { - deque.removeFirst(); - } - deque.offerLast(notification.next(value)); + queue.offer(nl.next(t)); + } + + @Override + public void onError(Throwable e) { + queue.clear(); + actual.onError(e); + } + + @Override + public void onCompleted() { + BackpressureUtils.postCompleteDone(requested, queue, actual, this); + } + + @Override + public T call(Object t) { + return nl.getValue(t); + } + + void requestMore(long n) { + if (n > 0L) { + BackpressureUtils.postCompleteRequest(requested, n, queue, actual, this); } - }; + } } } diff --git a/src/main/java/rx/internal/operators/OperatorTakeLastTimed.java b/src/main/java/rx/internal/operators/OperatorTakeLastTimed.java index ec7cc12493..383f41715c 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeLastTimed.java +++ b/src/main/java/rx/internal/operators/OperatorTakeLastTimed.java @@ -15,18 +15,20 @@ */ package rx.internal.operators; -import rx.Observable.Operator; -import rx.Scheduler; -import rx.Subscriber; - import java.util.ArrayDeque; -import java.util.Deque; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import rx.*; +import rx.Observable.Operator; +import rx.functions.Func1; /** * Returns an Observable that emits the last count items emitted by the source Observable. *

* + * + * @param the value type */ public final class OperatorTakeLastTimed implements Operator { @@ -51,60 +53,92 @@ public OperatorTakeLastTimed(int count, long time, TimeUnit unit, Scheduler sche @Override public Subscriber call(final Subscriber subscriber) { - final Deque buffer = new ArrayDeque(); - final Deque timestampBuffer = new ArrayDeque(); - final NotificationLite notification = NotificationLite.instance(); - final TakeLastQueueProducer producer = new TakeLastQueueProducer(notification, buffer, subscriber); - subscriber.setProducer(producer); - return new Subscriber(subscriber) { - - protected void runEvictionPolicy(long now) { - // trim size - while (count >= 0 && buffer.size() > count) { - timestampBuffer.pollFirst(); - buffer.pollFirst(); - } - // remove old entries - while (!buffer.isEmpty()) { - long v = timestampBuffer.peekFirst(); - if (v < now - ageMillis) { - timestampBuffer.pollFirst(); - buffer.pollFirst(); - } else { - break; - } - } - } - - // no backpressure up as it wants to receive and discard all but the last + final TakeLastTimedSubscriber parent = new TakeLastTimedSubscriber(subscriber, count, ageMillis, scheduler); + + subscriber.add(parent); + subscriber.setProducer(new Producer() { @Override - public void onStart() { - // we do this to break the chain of the child subscriber being passed through - request(Long.MAX_VALUE); - } - - @Override - public void onNext(T args) { - long t = scheduler.now(); - timestampBuffer.add(t); - buffer.add(notification.next(args)); - runEvictionPolicy(t); + public void request(long n) { + parent.requestMore(n); } + }); + + return parent; + } + + static final class TakeLastTimedSubscriber extends Subscriber implements Func1 { + final Subscriber actual; + final long ageMillis; + final Scheduler scheduler; + final int count; + final AtomicLong requested; + final ArrayDeque queue; + final ArrayDeque queueTimes; + final NotificationLite nl; - @Override - public void onError(Throwable e) { - timestampBuffer.clear(); - buffer.clear(); - subscriber.onError(e); + public TakeLastTimedSubscriber(Subscriber actual, int count, long ageMillis, Scheduler scheduler) { + this.actual = actual; + this.count = count; + this.ageMillis = ageMillis; + this.scheduler = scheduler; + this.requested = new AtomicLong(); + this.queue = new ArrayDeque(); + this.queueTimes = new ArrayDeque(); + this.nl = NotificationLite.instance(); + } + + @Override + public void onNext(T t) { + if (count != 0) { + long now = scheduler.now(); + + if (queue.size() == count) { + queue.poll(); + queueTimes.poll(); + } + + evictOld(now); + + queue.offer(nl.next(t)); + queueTimes.offer(now); } + } - @Override - public void onCompleted() { - runEvictionPolicy(scheduler.now()); - timestampBuffer.clear(); - buffer.offer(notification.completed()); - producer.startEmitting(); + protected void evictOld(long now) { + long minTime = now - ageMillis; + for (;;) { + Long time = queueTimes.peek(); + if (time == null || time >= minTime) { + break; + } + queue.poll(); + queueTimes.poll(); } - }; + } + + @Override + public void onError(Throwable e) { + queue.clear(); + queueTimes.clear(); + actual.onError(e); + } + + @Override + public void onCompleted() { + evictOld(scheduler.now()); + + queueTimes.clear(); + + BackpressureUtils.postCompleteDone(requested, queue, actual, this); + } + + @Override + public T call(Object t) { + return nl.getValue(t); + } + + void requestMore(long n) { + BackpressureUtils.postCompleteRequest(requested, n, queue, actual, this); + } } } diff --git a/src/main/java/rx/internal/operators/TakeLastQueueProducer.java b/src/main/java/rx/internal/operators/TakeLastQueueProducer.java deleted file mode 100644 index 664dfd0e3a..0000000000 --- a/src/main/java/rx/internal/operators/TakeLastQueueProducer.java +++ /dev/null @@ -1,124 +0,0 @@ -/** - * 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.internal.operators; - - -import java.util.Deque; -import java.util.concurrent.atomic.AtomicLong; - -import rx.Producer; -import rx.Subscriber; -import rx.exceptions.Exceptions; - -final class TakeLastQueueProducer extends AtomicLong implements Producer { - - private final NotificationLite notification; - private final Deque deque; - private final Subscriber subscriber; - private volatile boolean emittingStarted = false; - - public TakeLastQueueProducer(NotificationLite n, Deque q, Subscriber subscriber) { - this.notification = n; - this.deque = q; - this.subscriber = subscriber; - } - - void startEmitting() { - if (!emittingStarted) { - emittingStarted = true; - emit(0); // start emitting - } - } - - @Override - public void request(long n) { - if (get() == Long.MAX_VALUE) { - return; - } - long _c; - if (n == Long.MAX_VALUE) { - _c = getAndSet(Long.MAX_VALUE); - } else { - _c = BackpressureUtils.getAndAddRequest(this, n); - } - if (!emittingStarted) { - // we haven't started yet, so record what was requested and return - return; - } - emit(_c); - } - - void emit(long previousRequested) { - if (get() == Long.MAX_VALUE) { - // fast-path without backpressure - if (previousRequested == 0) { - try { - for (Object value : deque) { - if (subscriber.isUnsubscribed()) - return; - notification.accept(subscriber, value); - } - } catch (Throwable e) { - Exceptions.throwOrReport(e, subscriber); - } finally { - deque.clear(); - } - } else { - // backpressure path will handle Long.MAX_VALUE and emit the rest events. - } - } else { - // backpressure is requested - if (previousRequested == 0) { - while (true) { - /* - * This complicated logic is done to avoid touching the volatile `requested` value - * during the loop itself. If it is touched during the loop the performance is impacted significantly. - */ - long numToEmit = get(); - int emitted = 0; - Object o; - while (--numToEmit >= 0 && (o = deque.poll()) != null) { - if (subscriber.isUnsubscribed()) { - return; - } - if (notification.accept(subscriber, o)) { - // terminal event - return; - } else { - emitted++; - } - } - for (; ; ) { - long oldRequested = get(); - long newRequested = oldRequested - emitted; - if (oldRequested == Long.MAX_VALUE) { - // became unbounded during the loop - // continue the outer loop to emit the rest events. - break; - } - if (compareAndSet(oldRequested, newRequested)) { - if (newRequested == 0) { - // we're done emitting the number requested so return - return; - } - break; - } - } - } - } - } - } -} diff --git a/src/test/java/rx/internal/operators/OperatorTakeLastTest.java b/src/test/java/rx/internal/operators/OperatorTakeLastTest.java index c3297db0a0..154b3067b0 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeLastTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeLastTest.java @@ -17,28 +17,24 @@ import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.junit.*; import org.mockito.InOrder; import rx.Observable; import rx.Observer; +import rx.Scheduler.Worker; import rx.Subscriber; -import rx.functions.Func1; -import rx.internal.util.RxRingBuffer; -import rx.internal.util.UtilityFunctions; +import rx.functions.*; +import rx.internal.util.*; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; +import rx.subjects.PublishSubject; public class OperatorTakeLastTest { @@ -323,4 +319,76 @@ public void onNext(Integer t) { }}); assertEquals(50, list.size()); } + + @Test(timeout = 30000) // original could get into an infinite loop + public void completionRequestRace() { + Worker w = Schedulers.computation().createWorker(); + try { + final int n = 1000; + for (int i = 0; i < 25000; i++) { + if (i % 1000 == 0) { + System.out.println("completionRequestRace >> " + i); + } + PublishSubject ps = PublishSubject.create(); + final TestSubscriber ts = new TestSubscriber(0); + + ps.takeLast(n).subscribe(ts); + + for (int j = 0; j < n; j++) { + ps.onNext(j); + } + + final AtomicBoolean go = new AtomicBoolean(); + + w.schedule(new Action0() { + @Override + public void call() { + while (!go.get()); + ts.requestMore(n + 1); + } + }); + + go.set(true); + ps.onCompleted(); + + ts.awaitTerminalEvent(1, TimeUnit.SECONDS); + + ts.assertValueCount(n); + ts.assertNoErrors(); + ts.assertCompleted(); + + List list = ts.getOnNextEvents(); + for (int j = 0; j < n; j++) { + Assert.assertEquals(j, list.get(j).intValue()); + } + } + } finally { + w.unsubscribe(); + } + } + + @Test + public void nullElements() { + TestSubscriber ts = new TestSubscriber(0); + + Observable.from(new Integer[] { 1, null, 2}).takeLast(4) + .subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(2); + + ts.assertValues(1, null, 2); + ts.assertCompleted(); + ts.assertNoErrors(); + } + } diff --git a/src/test/java/rx/internal/operators/OperatorTakeLastTimedTest.java b/src/test/java/rx/internal/operators/OperatorTakeLastTimedTest.java index 800a2cd673..c227339702 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeLastTimedTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeLastTimedTest.java @@ -22,15 +22,20 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.Test; +import org.junit.*; import org.mockito.InOrder; import rx.Observable; import rx.Observer; +import rx.Scheduler.Worker; import rx.exceptions.TestException; -import rx.schedulers.TestScheduler; +import rx.functions.Action0; +import rx.observers.TestSubscriber; +import rx.schedulers.*; import rx.subjects.PublishSubject; public class OperatorTakeLastTimedTest { @@ -208,4 +213,75 @@ public void takeLastTimedWithZeroCapacity() { verify(o, never()).onNext(any()); verify(o, never()).onError(any(Throwable.class)); } + + @Test(timeout = 30000) // original could get into an infinite loop + public void completionRequestRace() { + Worker w = Schedulers.computation().createWorker(); + try { + final int n = 1000; + for (int i = 0; i < 25000; i++) { + if (i % 1000 == 0) { + System.out.println("completionRequestRace >> " + i); + } + PublishSubject ps = PublishSubject.create(); + final TestSubscriber ts = new TestSubscriber(0); + + ps.takeLast(n, 1, TimeUnit.DAYS).subscribe(ts); + + for (int j = 0; j < n; j++) { + ps.onNext(j); + } + + final AtomicBoolean go = new AtomicBoolean(); + + w.schedule(new Action0() { + @Override + public void call() { + while (!go.get()); + ts.requestMore(n + 1); + } + }); + + go.set(true); + ps.onCompleted(); + + ts.awaitTerminalEvent(1, TimeUnit.SECONDS); + + ts.assertValueCount(n); + ts.assertNoErrors(); + ts.assertCompleted(); + + List list = ts.getOnNextEvents(); + for (int j = 0; j < n; j++) { + Assert.assertEquals(j, list.get(j).intValue()); + } + } + } finally { + w.unsubscribe(); + } + } + + @Test + public void nullElements() { + TestSubscriber ts = new TestSubscriber(0); + + Observable.from(new Integer[] { 1, null, 2}).takeLast(4, 1, TimeUnit.DAYS) + .subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(2); + + ts.assertValues(1, null, 2); + ts.assertCompleted(); + ts.assertNoErrors(); + } }