diff --git a/src/main/java/rx/BackpressureOverflow.java b/src/main/java/rx/BackpressureOverflow.java new file mode 100644 index 0000000000..325cc7d0c9 --- /dev/null +++ b/src/main/java/rx/BackpressureOverflow.java @@ -0,0 +1,90 @@ +/** + * Copyright 2016 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; + +import rx.annotations.Experimental; +import rx.exceptions.MissingBackpressureException; + +/** + * Generic strategy and default implementations to deal with backpressure buffer overflows. + */ +@Experimental +public final class BackpressureOverflow { + + public interface Strategy { + + /** + * Whether the Backpressure manager should attempt to drop the oldest item, or simply + * drop the item currently causing backpressure. + * + * @return true to request drop of the oldest item, false to drop the newest. + * @throws MissingBackpressureException + */ + boolean mayAttemptDrop() throws MissingBackpressureException; + } + + public static final BackpressureOverflow.Strategy ON_OVERFLOW_DEFAULT = Error.INSTANCE; + @SuppressWarnings("unused") + public static final BackpressureOverflow.Strategy ON_OVERFLOW_ERROR = Error.INSTANCE; + @SuppressWarnings("unused") + public static final BackpressureOverflow.Strategy ON_OVERFLOW_DROP_OLDEST = DropOldest.INSTANCE; + @SuppressWarnings("unused") + public static final BackpressureOverflow.Strategy ON_OVERFLOW_DROP_LATEST = DropLatest.INSTANCE; + + /** + * Drop oldest items from the buffer making room for newer ones. + */ + static class DropOldest implements BackpressureOverflow.Strategy { + static final DropOldest INSTANCE = new DropOldest(); + + private DropOldest() {} + + @Override + public boolean mayAttemptDrop() { + return true; + } + } + + /** + * Drop most recent items, but not {@code onError} nor unsubscribe from source + * (as {code OperatorOnBackpressureDrop}). + */ + static class DropLatest implements BackpressureOverflow.Strategy { + static final DropLatest INSTANCE = new DropLatest(); + + private DropLatest() {} + + @Override + public boolean mayAttemptDrop() { + return false; + } + } + + /** + * {@code onError} a MissingBackpressureException and unsubscribe from source. + */ + static class Error implements BackpressureOverflow.Strategy { + + static final Error INSTANCE = new Error(); + + private Error() {} + + @Override + public boolean mayAttemptDrop() throws MissingBackpressureException { + throw new MissingBackpressureException("Overflowed buffer"); + } + } +} diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 93b91a8332..08335eb0ef 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -6399,7 +6399,8 @@ public final Observable onBackpressureBuffer() { *
{@code onBackpressureBuffer} does not operate by default on a particular {@link Scheduler}.
* * - * @return the source Observable modified to buffer items up to the given capacity + * @param capacity number of slots available in the buffer. + * @return the source {@code Observable} modified to buffer items up to the given capacity. * @see ReactiveX operators documentation: backpressure operators * @since 1.1.0 */ @@ -6419,7 +6420,9 @@ public final Observable onBackpressureBuffer(long capacity) { *
{@code onBackpressureBuffer} does not operate by default on a particular {@link Scheduler}.
* * - * @return the source Observable modified to buffer items up to the given capacity + * @param capacity number of slots available in the buffer. + * @param onOverflow action to execute if an item needs to be buffered, but there are no available slots. Null is allowed. + * @return the source {@code Observable} modified to buffer items up to the given capacity * @see ReactiveX operators documentation: backpressure operators * @since 1.1.0 */ @@ -6427,6 +6430,41 @@ public final Observable onBackpressureBuffer(long capacity, Action0 onOverflo return lift(new OperatorOnBackpressureBuffer(capacity, onOverflow)); } + /** + * Instructs an Observable that is emitting items faster than its observer can consume them to buffer up to + * a given amount of items until they can be emitted. The resulting Observable will behave as determined + * by {@code overflowStrategy} if the buffer capacity is exceeded. + * + *
    + *
  • {@code BackpressureOverflow.Strategy.ON_OVERFLOW_ERROR} (default) will {@code onError} dropping all undelivered items, + * unsubscribing from the source, and notifying the producer with {@code onOverflow}.
  • + *
  • {@code BackpressureOverflow.Strategy.ON_OVERFLOW_DROP_LATEST} will drop any new items emitted by the producer while + * the buffer is full, without generating any {@code onError}. Each drop will however invoke {@code onOverflow} + * to signal the overflow to the producer.
  • j + *
  • {@code BackpressureOverflow.Strategy.ON_OVERFLOW_DROP_OLDEST} will drop the oldest items in the buffer in order to make + * room for newly emitted ones. Overflow will not generate an{@code onError}, but each drop will invoke + * {@code onOverflow} to signal the overflow to the producer.
  • + *
+ * + *

+ * + *

+ *
Scheduler:
+ *
{@code onBackpressureBuffer} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param capacity number of slots available in the buffer. + * @param onOverflow action to execute if an item needs to be buffered, but there are no available slots. Null is allowed. + * @param overflowStrategy how should the {@code Observable} react to buffer overflows. Null is not allowed. + * @return the source {@code Observable} modified to buffer items up to the given capacity + * @see ReactiveX operators documentation: backpressure operators + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + */ + @Experimental + public final Observable onBackpressureBuffer(long capacity, Action0 onOverflow, BackpressureOverflow.Strategy overflowStrategy) { + return lift(new OperatorOnBackpressureBuffer(capacity, onOverflow, overflowStrategy)); + } + /** * Instructs an Observable that is emitting items faster than its observer can consume them to discard, * rather than emit, those items that its observer is not prepared to observe. diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java index 9ab8f82869..4f66bbb4d7 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java @@ -19,6 +19,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import rx.BackpressureOverflow; import rx.Observable.Operator; import rx.Producer; import rx.Subscriber; @@ -27,15 +28,18 @@ import rx.functions.Action0; import rx.internal.util.BackpressureDrainManager; +import static rx.BackpressureOverflow.*; + public class OperatorOnBackpressureBuffer implements Operator { private final Long capacity; private final Action0 onOverflow; + private final BackpressureOverflow.Strategy overflowStrategy; private static class Holder { static final OperatorOnBackpressureBuffer INSTANCE = new OperatorOnBackpressureBuffer(); } - + @SuppressWarnings("unchecked") public static OperatorOnBackpressureBuffer instance() { return (OperatorOnBackpressureBuffer) Holder.INSTANCE; @@ -44,18 +48,48 @@ public static OperatorOnBackpressureBuffer instance() { OperatorOnBackpressureBuffer() { this.capacity = null; this.onOverflow = null; + this.overflowStrategy = ON_OVERFLOW_DEFAULT; } + /** + * Construct a new instance that will handle overflows with {@code ON_OVERFLOW_DEFAULT}, providing the + * following behavior config: + * + * @param capacity the max number of items to be admitted in the buffer, must be greater than 0. + */ public OperatorOnBackpressureBuffer(long capacity) { - this(capacity, null); + this(capacity, null, ON_OVERFLOW_DEFAULT); } + /** + * Construct a new instance that will handle overflows with {@code ON_OVERFLOW_DEFAULT}, providing the + * following behavior config: + * + * @param capacity the max number of items to be admitted in the buffer, must be greater than 0. + * @param onOverflow the {@code Action0} to execute when the buffer overflows, may be null. + */ public OperatorOnBackpressureBuffer(long capacity, Action0 onOverflow) { + this(capacity, onOverflow, ON_OVERFLOW_DEFAULT); + } + + /** + * Construct a new instance feeding the following behavior config: + * + * @param capacity the max number of items to be admitted in the buffer, must be greater than 0. + * @param onOverflow the {@code Action0} to execute when the buffer overflows, may be null. + * @param overflowStrategy the {@code BackpressureOverflow.Strategy} to handle overflows, it must not be null. + */ + public OperatorOnBackpressureBuffer(long capacity, Action0 onOverflow, + BackpressureOverflow.Strategy overflowStrategy) { if (capacity <= 0) { throw new IllegalArgumentException("Buffer capacity must be > 0"); } + if (overflowStrategy == null) { + throw new NullPointerException("The BackpressureOverflow strategy must not be null"); + } this.capacity = capacity; this.onOverflow = onOverflow; + this.overflowStrategy = overflowStrategy; } @Override @@ -63,7 +97,8 @@ public Subscriber call(final Subscriber child) { // don't pass through subscriber as we are async and doing queue draining // a parent being unsubscribed should not affect the children - BufferSubscriber parent = new BufferSubscriber(child, capacity, onOverflow); + BufferSubscriber parent = new BufferSubscriber(child, capacity, onOverflow, + overflowStrategy); // if child unsubscribes it should unsubscribe the parent, but not the other way around child.add(parent); @@ -71,6 +106,7 @@ public Subscriber call(final Subscriber child) { return parent; } + private static final class BufferSubscriber extends Subscriber implements BackpressureDrainManager.BackpressureQueueCallback { // TODO get a different queue implementation private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); @@ -81,14 +117,18 @@ private static final class BufferSubscriber extends Subscriber implements private final BackpressureDrainManager manager; private final NotificationLite on = NotificationLite.instance(); private final Action0 onOverflow; + private final BackpressureOverflow.Strategy overflowStrategy; - public BufferSubscriber(final Subscriber child, Long capacity, Action0 onOverflow) { + public BufferSubscriber(final Subscriber child, Long capacity, Action0 onOverflow, + BackpressureOverflow.Strategy overflowStrategy) { this.child = child; this.baseCapacity = capacity; this.capacity = capacity != null ? new AtomicLong(capacity) : null; this.onOverflow = onOverflow; this.manager = new BackpressureDrainManager(this); + this.overflowStrategy = overflowStrategy; } + @Override public void onStart() { request(Long.MAX_VALUE); @@ -141,7 +181,7 @@ public Object poll() { } return value; } - + private boolean assertCapacity() { if (capacity == null) { return true; @@ -151,24 +191,30 @@ private boolean assertCapacity() { do { currCapacity = capacity.get(); if (currCapacity <= 0) { - if (saturated.compareAndSet(false, true)) { - unsubscribe(); - child.onError(new MissingBackpressureException( - "Overflowed buffer of " - + baseCapacity)); - if (onOverflow != null) { - try { - onOverflow.call(); - } catch (Throwable e) { - Exceptions.throwIfFatal(e); - manager.terminateAndDrain(e); - // this line not strictly necessary but nice for clarity - // and in case of future changes to code after this catch block - return false; - } + boolean hasCapacity = false; + try { + // ok if we're allowed to drop, and there is indeed an item to discard + hasCapacity = overflowStrategy.mayAttemptDrop() && poll() != null; + } catch (MissingBackpressureException e) { + if (saturated.compareAndSet(false, true)) { + unsubscribe(); + child.onError(e); } } - return false; + if (onOverflow != null) { + try { + onOverflow.call(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + manager.terminateAndDrain(e); + // this line not strictly necessary but nice for clarity + // and in case of future changes to code after this catch block + return false; + } + } + if (!hasCapacity) { + return false; + } } // ensure no other thread stole our slot, or retry } while (!capacity.compareAndSet(currCapacity, currCapacity - 1)); diff --git a/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java b/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java index 48fa099735..59a971e1c1 100644 --- a/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java +++ b/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java @@ -1,5 +1,5 @@ /** - * Copyright 2014 Netflix, Inc. + * Copyright 2016 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,20 +18,17 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static rx.BackpressureOverflow.*; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Test; -import org.mockito.Mock; -import org.mockito.Mockito; -import rx.Observable; +import rx.*; import rx.Observable.OnSubscribe; -import rx.Observer; -import rx.Subscriber; -import rx.Subscription; import rx.exceptions.MissingBackpressureException; import rx.functions.Action0; import rx.functions.Action1; @@ -101,24 +98,19 @@ public void testFixBackpressureBufferZeroCapacity() throws InterruptedException Observable.empty().onBackpressureBuffer(0); } + @Test(expected = NullPointerException.class) + public void testFixBackpressureBufferNullStrategy() throws InterruptedException { + Observable.empty().onBackpressureBuffer(10, new Action0() { + @Override + public void call() { } + }, null); + } + @Test public void testFixBackpressureBoundedBuffer() throws InterruptedException { final CountDownLatch l1 = new CountDownLatch(100); final CountDownLatch backpressureCallback = new CountDownLatch(1); - TestSubscriber ts = new TestSubscriber(new Observer() { - - @Override - public void onCompleted() { } - - @Override - public void onError(Throwable e) { } - - @Override - public void onNext(Long t) { - l1.countDown(); - } - - }); + final TestSubscriber ts = testSubscriber(l1); ts.requestMore(100); Subscription s = infinite.subscribeOn(Schedulers.computation()) @@ -128,11 +120,11 @@ public void call() { backpressureCallback.countDown(); } }).take(1000).subscribe(ts); - l1.await(); + assertTrue(l1.await(2, TimeUnit.SECONDS)); ts.requestMore(50); - assertTrue(backpressureCallback.await(500, TimeUnit.MILLISECONDS)); + assertTrue(backpressureCallback.await(2, TimeUnit.SECONDS)); assertTrue(ts.getOnErrorEvents().get(0) instanceof MissingBackpressureException); int size = ts.getOnNextEvents().size(); @@ -141,6 +133,100 @@ public void call() { assertTrue(s.isUnsubscribed()); } + @Test + public void testFixBackpressureBoundedBufferDroppingOldest() + throws InterruptedException { + List events = overflowBufferWithBehaviour(100, 10, ON_OVERFLOW_DROP_OLDEST); + + // The consumer takes 100 initial elements, then 10 are temporarily + // buffered and the oldest (100, 101, etc.) are dropped to make room for + // higher items. + int i = 0; + for (Long n : events) { + if (i < 100) { // backpressure is expected to kick in after the + // initial batch is consumed + assertEquals(Long.valueOf(i), n); + } else { + assertTrue(i < n); + } + i++; + } + } + + @Test + public void testFixBackpressueBoundedBufferDroppingLatest() + throws InterruptedException { + + List events = overflowBufferWithBehaviour(100, 10, ON_OVERFLOW_DROP_LATEST); + + // The consumer takes 100 initial elements, then 10 are temporarily + // buffered and the newest are dropped to make room for higher items. + int i = 0; + for (Long n : events) { + if (i < 110) { + assertEquals(Long.valueOf(i), n); + } else { + assertTrue(i < n); + } + i++; + } + } + + private List overflowBufferWithBehaviour(int initialRequest, int bufSize, + BackpressureOverflow.Strategy backpressureStrategy) + throws InterruptedException { + + final CountDownLatch l1 = new CountDownLatch(initialRequest * 2); + final CountDownLatch backpressureCallback = new CountDownLatch(1); + + final TestSubscriber ts = testSubscriber(l1); + + ts.requestMore(initialRequest); + Subscription s = infinite.subscribeOn(Schedulers.computation()) + .onBackpressureBuffer(bufSize, new Action0() { + @Override + public void call() { + backpressureCallback.countDown(); + } + }, backpressureStrategy + ).subscribe(ts); + + assertTrue(backpressureCallback.await(2, TimeUnit.SECONDS)); + + ts.requestMore(initialRequest); + + assertTrue(l1.await(2, TimeUnit.SECONDS)); + + // Stop receiving elements + s.unsubscribe(); + + // No failure despite overflows + assertTrue(ts.getOnErrorEvents().isEmpty()); + assertEquals(initialRequest * 2, ts.getOnNextEvents().size()); + + assertTrue(ts.isUnsubscribed()); + + return ts.getOnNextEvents(); + } + + static TestSubscriber testSubscriber(final CountDownLatch latch) { + return new TestSubscriber(new Observer() { + + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(T t) { + latch.countDown(); + } + }); + } + static final Observable infinite = Observable.create(new OnSubscribe() { @Override diff --git a/src/test/java/rx/internal/operators/OperatorOnExceptionResumeNextViaObservableTest.java b/src/test/java/rx/internal/operators/OperatorOnExceptionResumeNextViaObservableTest.java index 2ac3e6eadb..6b2d792e9c 100644 --- a/src/test/java/rx/internal/operators/OperatorOnExceptionResumeNextViaObservableTest.java +++ b/src/test/java/rx/internal/operators/OperatorOnExceptionResumeNextViaObservableTest.java @@ -118,7 +118,7 @@ public void testThrowablePassesThru() { @Test public void testErrorPassesThru() { // Trigger failure on second element - TestObservable f = new TestObservable("one", "ERROR", "two", "three"); + TestObservable f = new TestObservable("one", "ON_OVERFLOW_ERROR", "two", "three"); Observable w = Observable.create(f); Observable resume = Observable.just("twoResume", "threeResume"); Observable observable = w.onExceptionResumeNext(resume); @@ -240,7 +240,7 @@ public void run() { throw new Exception("Forced Exception"); else if ("RUNTIMEEXCEPTION".equals(s)) throw new RuntimeException("Forced RuntimeException"); - else if ("ERROR".equals(s)) + else if ("ON_OVERFLOW_ERROR".equals(s)) throw new Error("Forced Error"); else if ("THROWABLE".equals(s)) throw new Throwable("Forced Throwable");