diff --git a/core/aws-core/pom.xml b/core/aws-core/pom.xml
index 8ccd66975307..89db5e1b7122 100644
--- a/core/aws-core/pom.xml
+++ b/core/aws-core/pom.xml
@@ -73,6 +73,10 @@
utils${awsjavasdk.version}
+
+ org.slf4j
+ slf4j-api
+ software.amazon.eventstreameventstream
diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/eventstream/EventStreamAsyncResponseTransformer.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/eventstream/EventStreamAsyncResponseTransformer.java
index 95b664774423..d8437707427e 100644
--- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/eventstream/EventStreamAsyncResponseTransformer.java
+++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/eventstream/EventStreamAsyncResponseTransformer.java
@@ -15,35 +15,41 @@
package software.amazon.awssdk.awscore.eventstream;
-import static java.util.Collections.emptyList;
-import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static software.amazon.awssdk.core.http.HttpResponseHandler.X_AMZN_REQUEST_ID_HEADER;
import static software.amazon.awssdk.core.http.HttpResponseHandler.X_AMZN_REQUEST_ID_HEADERS;
import static software.amazon.awssdk.core.http.HttpResponseHandler.X_AMZ_ID_2_HEADER;
+import static software.amazon.awssdk.utils.FunctionalUtils.runAndLogError;
import java.io.ByteArrayInputStream;
import java.nio.ByteBuffer;
import java.util.HashMap;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Supplier;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import org.reactivestreams.Publisher;
+import org.reactivestreams.Subscriber;
+import org.reactivestreams.Subscription;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.core.SdkResponse;
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
import software.amazon.awssdk.core.async.SdkPublisher;
import software.amazon.awssdk.core.exception.SdkClientException;
-import software.amazon.awssdk.core.exception.SdkException;
import software.amazon.awssdk.core.http.HttpResponseHandler;
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute;
import software.amazon.awssdk.http.AbortableInputStream;
+import software.amazon.awssdk.http.SdkCancellationException;
import software.amazon.awssdk.http.SdkHttpFullResponse;
-import software.amazon.awssdk.utils.Logger;
-import software.amazon.awssdk.utils.Validate;
+import software.amazon.awssdk.utils.BinaryUtils;
import software.amazon.awssdk.utils.http.SdkHttpUtils;
import software.amazon.eventstream.Message;
import software.amazon.eventstream.MessageDecoder;
@@ -58,7 +64,12 @@
@SdkProtectedApi
public final class EventStreamAsyncResponseTransformer
implements AsyncResponseTransformer {
- private static final Logger log = Logger.loggerFor(EventStreamAsyncResponseTransformer.class);
+
+ private static final Logger log = LoggerFactory.getLogger(EventStreamAsyncResponseTransformer.class);
+
+ private static final Object ON_COMPLETE_EVENT = new Object();
+
+ private static final ExecutionAttributes EMPTY_EXECUTION_ATTRIBUTES = new ExecutionAttributes();
/**
* {@link EventStreamResponseHandler} provided by customer.
@@ -80,7 +91,51 @@ public final class EventStreamAsyncResponseTransformer
*/
private final HttpResponseHandler extends Throwable> exceptionResponseHandler;
- private final Supplier attributesFactory;
+ /**
+ * Remaining demand (i.e number of unmarshalled events) we need to provide to the customers subscriber.
+ */
+ private final AtomicLong remainingDemand = new AtomicLong(0);
+
+ /**
+ * Reference to customers subscriber to events.
+ */
+ private final AtomicReference> subscriberRef = new AtomicReference<>();
+
+ private final AtomicReference dataSubscription = new AtomicReference<>();
+
+ /**
+ * Event stream message decoder that decodes the binary data into "frames". These frames are then passed to the
+ * unmarshaller to produce the event POJO.
+ */
+ private final MessageDecoder decoder = new MessageDecoder(this::handleMessage);
+
+ /**
+ * Tracks whether we have delivered a terminal notification to the subscriber and response handler
+ * (i.e. exception or completion).
+ */
+ private volatile boolean isDone = false;
+
+ /**
+ * Executor to deliver events to the subscriber
+ */
+ private final Executor executor;
+
+ /**
+ * Queue of events to deliver to downstream subscriber. Will contain mostly objects
+ * of type EventT, the special {@link #ON_COMPLETE_EVENT} will be added when all events
+ * have been added to the queue.
+ */
+ private final Queue
-
- org.reactivestreams
- reactive-streams-tck
- test
-
diff --git a/utils/src/main/java/software/amazon/awssdk/utils/async/DelegatingSubscriber.java b/utils/src/main/java/software/amazon/awssdk/utils/async/DelegatingSubscriber.java
index 04e4725fd670..72b2fbe9269c 100644
--- a/utils/src/main/java/software/amazon/awssdk/utils/async/DelegatingSubscriber.java
+++ b/utils/src/main/java/software/amazon/awssdk/utils/async/DelegatingSubscriber.java
@@ -15,15 +15,14 @@
package software.amazon.awssdk.utils.async;
-import java.util.concurrent.atomic.AtomicBoolean;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import software.amazon.awssdk.annotations.SdkProtectedApi;
@SdkProtectedApi
public abstract class DelegatingSubscriber implements Subscriber {
+
protected final Subscriber super U> subscriber;
- private final AtomicBoolean complete = new AtomicBoolean(false);
protected DelegatingSubscriber(Subscriber super U> subscriber) {
this.subscriber = subscriber;
@@ -36,15 +35,12 @@ public void onSubscribe(Subscription subscription) {
@Override
public void onError(Throwable throwable) {
- if (complete.compareAndSet(false, true)) {
- subscriber.onError(throwable);
- }
+ subscriber.onError(throwable);
}
@Override
public void onComplete() {
- if (complete.compareAndSet(false, true)) {
- subscriber.onComplete();
- }
+ subscriber.onComplete();
}
+
}
diff --git a/utils/src/main/java/software/amazon/awssdk/utils/async/EventListeningSubscriber.java b/utils/src/main/java/software/amazon/awssdk/utils/async/EventListeningSubscriber.java
deleted file mode 100644
index 7639c57086be..000000000000
--- a/utils/src/main/java/software/amazon/awssdk/utils/async/EventListeningSubscriber.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.utils.async;
-
-import java.util.function.Consumer;
-import org.reactivestreams.Subscriber;
-import org.reactivestreams.Subscription;
-import software.amazon.awssdk.annotations.SdkProtectedApi;
-import software.amazon.awssdk.utils.Logger;
-
-/**
- * A {@link Subscriber} that can invoke callbacks during various parts of the subscriber and subscription lifecycle.
- */
-@SdkProtectedApi
-public final class EventListeningSubscriber extends DelegatingSubscriber {
- private static final Logger log = Logger.loggerFor(EventListeningSubscriber.class);
-
- private final Runnable afterCompleteListener;
- private final Consumer afterErrorListener;
- private final Runnable afterCancelListener;
-
- public EventListeningSubscriber(Subscriber subscriber,
- Runnable afterCompleteListener,
- Consumer afterErrorListener,
- Runnable afterCancelListener) {
- super(subscriber);
- this.afterCompleteListener = afterCompleteListener;
- this.afterErrorListener = afterErrorListener;
- this.afterCancelListener = afterCancelListener;
- }
-
- @Override
- public void onNext(T t) {
- super.subscriber.onNext(t);
- }
-
- @Override
- public void onSubscribe(Subscription subscription) {
- super.onSubscribe(new CancelListeningSubscriber(subscription));
- }
-
- @Override
- public void onError(Throwable throwable) {
- super.onError(throwable);
- if (afterErrorListener != null) {
- callListener(() -> afterErrorListener.accept(throwable),
- "Post-onError callback failed. This exception will be dropped.");
- }
- }
-
- @Override
- public void onComplete() {
- super.onComplete();
- callListener(afterCompleteListener, "Post-onComplete callback failed. This exception will be dropped.");
- }
-
- private class CancelListeningSubscriber extends DelegatingSubscription {
- protected CancelListeningSubscriber(Subscription s) {
- super(s);
- }
-
- @Override
- public void cancel() {
- super.cancel();
- callListener(afterCompleteListener, "Post-cancel callback failed. This exception will be dropped.");
- }
- }
-
- private void callListener(Runnable listener, String listenerFailureMessage) {
- if (listener != null) {
- try {
- listener.run();
- } catch (RuntimeException e) {
- log.error(() -> listenerFailureMessage, e);
- }
- }
- }
-}
diff --git a/utils/src/main/java/software/amazon/awssdk/utils/async/FlatteningSubscriber.java b/utils/src/main/java/software/amazon/awssdk/utils/async/FlatteningSubscriber.java
index 3303485e1250..08c11556a836 100644
--- a/utils/src/main/java/software/amazon/awssdk/utils/async/FlatteningSubscriber.java
+++ b/utils/src/main/java/software/amazon/awssdk/utils/async/FlatteningSubscriber.java
@@ -15,82 +15,48 @@
package software.amazon.awssdk.utils.async;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.LinkedList;
+import java.util.Queue;
import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import software.amazon.awssdk.annotations.SdkProtectedApi;
-import software.amazon.awssdk.utils.Logger;
-import software.amazon.awssdk.utils.Validate;
@SdkProtectedApi
public class FlatteningSubscriber extends DelegatingSubscriber, U> {
- private static final Logger log = Logger.loggerFor(FlatteningSubscriber.class);
- /**
- * The amount of unfulfilled demand open against the upstream subscriber.
- */
- private final AtomicLong upstreamDemand = new AtomicLong(0);
+ private final AtomicLong demand = new AtomicLong(0);
+ private final Object lock = new Object();
- /**
- * The amount of unfulfilled demand the downstream subscriber has opened against us.
- */
- private final AtomicLong downstreamDemand = new AtomicLong(0);
-
- /**
- * A flag that is used to ensure that only one thread is handling updates to the state of this subscriber at a time. This
- * allows us to ensure that the downstream onNext, onComplete and onError are only ever invoked serially.
- */
- private final AtomicBoolean handlingStateUpdate = new AtomicBoolean(false);
-
- /**
- * Items given to us by the upstream subscriber that we will use to fulfill demand of the downstream subscriber.
- */
- private final LinkedBlockingQueue allItems = new LinkedBlockingQueue<>();
-
- /**
- * Whether the upstream subscriber has called onError on us. If this is null, we haven't gotten an onError. If it's non-null
- * this will be the exception that the upstream passed to our onError. After we get an onError, we'll call onError on the
- * downstream subscriber as soon as possible.
- */
- private final AtomicReference onErrorFromUpstream = new AtomicReference<>(null);
-
- /**
- * Whether we have called onComplete or onNext on the downstream subscriber.
- */
- private volatile boolean terminalCallMadeDownstream = false;
-
- /**
- * Whether the upstream subscriber has called onComplete on us. After this happens, we'll drain any outstanding items in the
- * allItems queue and then call onComplete on the downstream subscriber.
- */
- private volatile boolean onCompleteCalledByUpstream = false;
-
- /**
- * The subscription to the upstream subscriber.
- */
- private Subscription upstreamSubscription;
+ private boolean requestedNextBatch;
+ private Queue currentBatch;
+ private boolean onCompleteCalled = false;
+ private Subscription sourceSubscription;
public FlatteningSubscriber(Subscriber super U> subscriber) {
super(subscriber);
+ currentBatch = new LinkedList<>();
}
@Override
public void onSubscribe(Subscription subscription) {
- if (upstreamSubscription != null) {
- log.warn(() -> "Received duplicate subscription, cancelling the duplicate.", new IllegalStateException());
- subscription.cancel();
- return;
- }
-
- upstreamSubscription = subscription;
+ sourceSubscription = subscription;
subscriber.onSubscribe(new Subscription() {
@Override
public void request(long l) {
- addDownstreamDemand(l);
- handleStateUpdate();
+ synchronized (lock) {
+ demand.addAndGet(l);
+ // Execution goes into `if` block only once for the initial request
+ // After that requestedNextBatch is always true and more requests are made in fulfillDemand()
+ if (!requestedNextBatch) {
+ requestedNextBatch = true;
+ sourceSubscription.request(1);
+ } else {
+ fulfillDemand();
+ }
+ }
}
@Override
@@ -102,165 +68,34 @@ public void cancel() {
@Override
public void onNext(Iterable nextItems) {
- try {
- nextItems.forEach(item -> {
- Validate.notNull(nextItems, "Collections flattened by the flattening subscriber must not contain null.");
- allItems.add(item);
- });
- } catch (NullPointerException e) {
- upstreamSubscription.cancel();
- onError(e);
- throw e;
- }
-
- upstreamDemand.decrementAndGet();
- handleStateUpdate();
- }
-
- @Override
- public void onError(Throwable throwable) {
- onErrorFromUpstream.compareAndSet(null, throwable);
- handleStateUpdate();
- }
-
- @Override
- public void onComplete() {
- onCompleteCalledByUpstream = true;
- handleStateUpdate();
- }
-
- /**
- * Increment the downstream demand by the provided value, accounting for overflow.
- */
- private void addDownstreamDemand(long l) {
- Validate.isTrue(l > 0, "Demand must not be negative.");
- downstreamDemand.getAndUpdate(current -> {
- long newValue = current + l;
- return newValue >= 0 ? newValue : Long.MAX_VALUE;
- });
- }
-
- /**
- * This is invoked after each downstream request or upstream onNext, onError or onComplete.
- */
- private void handleStateUpdate() {
- do {
- // Anything that happens after this if statement and before we set handlingStateUpdate to false is guaranteed to only
- // happen on one thread. For that reason, we should only invoke onNext, onComplete or onError within that block.
- if (!handlingStateUpdate.compareAndSet(false, true)) {
- return;
- }
-
- try {
- // If we've already called onComplete or onError, don't do anything.
- if (terminalCallMadeDownstream) {
- return;
- }
-
- // Call onNext, onComplete and onError as needed based on the current subscriber state.
- handleOnNextState();
- handleUpstreamDemandState();
- handleOnCompleteState();
- handleOnErrorState();
- } catch (Error e) {
- throw e;
- } catch (Throwable e) {
- log.error(() -> "Unexpected exception encountered that violates the reactive streams specification. Attempting "
- + "to terminate gracefully.", e);
- upstreamSubscription.cancel();
- onError(e);
- } finally {
- handlingStateUpdate.set(false);
- }
-
- // It's possible we had an important state change between when we decided to release the state update flag, and we
- // actually released it. If that seems to have happened, try to handle that state change on this thread, because
- // another thread is not guaranteed to come around and do so.
- } while (onNextNeeded() || upstreamDemandNeeded() || onCompleteNeeded() || onErrorNeeded());
- }
-
- /**
- * Fulfill downstream demand by pulling items out of the item queue and sending them downstream.
- */
- private void handleOnNextState() {
- while (onNextNeeded() && !onErrorNeeded()) {
- downstreamDemand.decrementAndGet();
- subscriber.onNext(allItems.poll());
+ synchronized (lock) {
+ currentBatch = StreamSupport.stream(nextItems.spliterator(), false)
+ .collect(Collectors.toCollection(LinkedList::new));
+ fulfillDemand();
}
}
- /**
- * Returns true if we need to call onNext downstream. If this is executed outside the handling-state-update condition, the
- * result is subject to change.
- */
- private boolean onNextNeeded() {
- return !allItems.isEmpty() && downstreamDemand.get() > 0;
- }
-
- /**
- * Request more upstream demand if it's needed.
- */
- private void handleUpstreamDemandState() {
- if (upstreamDemandNeeded()) {
- ensureUpstreamDemandExists();
+ private void fulfillDemand() {
+ while (demand.get() > 0 && !currentBatch.isEmpty()) {
+ demand.decrementAndGet();
+ subscriber.onNext(currentBatch.poll());
}
- }
- /**
- * Returns true if we need to increase our upstream demand.
- */
- private boolean upstreamDemandNeeded() {
- return upstreamDemand.get() <= 0 && downstreamDemand.get() > 0 && allItems.isEmpty();
- }
-
- /**
- * If there are zero pending items in the queue and the upstream has called onComplete, then tell the downstream
- * we're done.
- */
- private void handleOnCompleteState() {
- if (onCompleteNeeded()) {
- terminalCallMadeDownstream = true;
+ if (onCompleteCalled && currentBatch.isEmpty()) {
subscriber.onComplete();
+ } else if (currentBatch.isEmpty() && demand.get() > 0) {
+ requestedNextBatch = true;
+ sourceSubscription.request(1);
}
}
- /**
- * Returns true if we need to call onNext downstream. If this is executed outside the handling-state-update condition, the
- * result is subject to change.
- */
- private boolean onCompleteNeeded() {
- return allItems.isEmpty() && onCompleteCalledByUpstream && !terminalCallMadeDownstream;
- }
-
- /**
- * If the upstream has called onError, then tell the downstream we're done, no matter what state the queue is in.
- */
- private void handleOnErrorState() {
- if (onErrorNeeded()) {
- terminalCallMadeDownstream = true;
- subscriber.onError(onErrorFromUpstream.get());
- }
- }
-
- /**
- * Returns true if we need to call onError downstream. If this is executed outside the handling-state-update condition, the
- * result is subject to change.
- */
- private boolean onErrorNeeded() {
- return onErrorFromUpstream.get() != null && !terminalCallMadeDownstream;
- }
-
- /**
- * Ensure that we have at least 1 demand upstream, so that we can get more items.
- */
- private void ensureUpstreamDemandExists() {
- if (this.upstreamDemand.get() < 0) {
- log.error(() -> "Upstream delivered more data than requested. Resetting state to prevent a frozen stream.",
- new IllegalStateException());
- upstreamDemand.set(1);
- upstreamSubscription.request(1);
- } else if (this.upstreamDemand.compareAndSet(0, 1)) {
- upstreamSubscription.request(1);
+ @Override
+ public void onComplete() {
+ synchronized (lock) {
+ onCompleteCalled = true;
+ if (currentBatch.isEmpty()) {
+ subscriber.onComplete();
+ }
}
}
}
diff --git a/utils/src/main/java/software/amazon/awssdk/utils/async/SequentialSubscriber.java b/utils/src/main/java/software/amazon/awssdk/utils/async/SequentialSubscriber.java
index 77db8e2d15b5..e66afb50d2bd 100644
--- a/utils/src/main/java/software/amazon/awssdk/utils/async/SequentialSubscriber.java
+++ b/utils/src/main/java/software/amazon/awssdk/utils/async/SequentialSubscriber.java
@@ -28,6 +28,7 @@
*/
@SdkProtectedApi
public class SequentialSubscriber implements Subscriber {
+
private final Consumer consumer;
private final CompletableFuture> future;
private Subscription subscription;
diff --git a/utils/src/test/java/software/amazon/awssdk/utils/async/FlatteningSubscriberTckTest.java b/utils/src/test/java/software/amazon/awssdk/utils/async/FlatteningSubscriberTckTest.java
deleted file mode 100644
index b4c27f991849..000000000000
--- a/utils/src/test/java/software/amazon/awssdk/utils/async/FlatteningSubscriberTckTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.utils.async;
-
-import java.util.Arrays;
-import java.util.concurrent.CompletableFuture;
-import org.reactivestreams.Subscriber;
-import org.reactivestreams.Subscription;
-import org.reactivestreams.tck.SubscriberWhiteboxVerification;
-import org.reactivestreams.tck.TestEnvironment;
-
-public class FlatteningSubscriberTckTest extends SubscriberWhiteboxVerification> {
- protected FlatteningSubscriberTckTest() {
- super(new TestEnvironment());
- }
-
- @Override
- public Subscriber> createSubscriber(WhiteboxSubscriberProbe> probe) {
- Subscriber foo = new SequentialSubscriber<>(s -> {}, new CompletableFuture<>());
- return new FlatteningSubscriber(foo) {
- @Override
- public void onError(Throwable throwable) {
- super.onError(throwable);
- probe.registerOnError(throwable);
- }
-
- @Override
- public void onSubscribe(Subscription subscription) {
- super.onSubscribe(subscription);
- probe.registerOnSubscribe(new SubscriberPuppet() {
- @Override
- public void triggerRequest(long elements) {
- subscription.request(elements);
- }
-
- @Override
- public void signalCancel() {
- subscription.cancel();
- }
- });
- }
-
- @Override
- public void onNext(Iterable nextItems) {
- super.onNext(nextItems);
- probe.registerOnNext(nextItems);
- }
-
- @Override
- public void onComplete() {
- super.onComplete();
- probe.registerOnComplete();
- }
- };
- }
-
- @Override
- public Iterable createElement(int element) {
- return Arrays.asList(element, element);
- }
-}
\ No newline at end of file
diff --git a/utils/src/test/java/software/amazon/awssdk/utils/async/FlatteningSubscriberTest.java b/utils/src/test/java/software/amazon/awssdk/utils/async/FlatteningSubscriberTest.java
deleted file mode 100644
index fc03fdead024..000000000000
--- a/utils/src/test/java/software/amazon/awssdk/utils/async/FlatteningSubscriberTest.java
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.utils.async;
-
-import static org.mockito.Mockito.times;
-
-import java.util.Arrays;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mockito;
-import org.reactivestreams.Subscriber;
-import org.reactivestreams.Subscription;
-
-public class FlatteningSubscriberTest {
- private Subscriber mockDelegate;
- private Subscription mockUpstream;
- private FlatteningSubscriber flatteningSubscriber;
-
- @Before
- @SuppressWarnings("unchecked")
- public void setup() {
- mockDelegate = Mockito.mock(Subscriber.class);
- mockUpstream = Mockito.mock(Subscription.class);
- flatteningSubscriber = new FlatteningSubscriber<>(mockDelegate);
- }
-
- @Test
- public void requestOne() {
- flatteningSubscriber.onSubscribe(mockUpstream);
-
- Subscription downstream = getDownstreamFromDelegate();
- downstream.request(1);
- Mockito.verify(mockUpstream).request(1);
-
- flatteningSubscriber.onNext(Arrays.asList("foo", "bar"));
-
- Mockito.verify(mockDelegate).onNext("foo");
-
- Mockito.verifyNoMoreInteractions(mockUpstream, mockDelegate);
- }
-
- @Test
- public void requestTwo() {
- flatteningSubscriber.onSubscribe(mockUpstream);
-
- Subscription downstream = getDownstreamFromDelegate();
- downstream.request(2);
-
- Mockito.verify(mockUpstream).request(1);
-
- flatteningSubscriber.onNext(Arrays.asList("foo", "bar"));
-
- Mockito.verify(mockDelegate).onNext("foo");
- Mockito.verify(mockDelegate).onNext("bar");
- Mockito.verifyNoMoreInteractions(mockUpstream, mockDelegate);
- }
-
- @Test
- public void requestThree() {
- flatteningSubscriber.onSubscribe(mockUpstream);
-
- Subscription downstream = getDownstreamFromDelegate();
- downstream.request(3);
-
- Mockito.verify(mockUpstream, times(1)).request(1);
- Mockito.verifyNoMoreInteractions(mockUpstream, mockDelegate);
- Mockito.reset(mockUpstream, mockDelegate);
-
- flatteningSubscriber.onNext(Arrays.asList("foo", "bar"));
-
- Mockito.verify(mockDelegate).onNext("foo");
- Mockito.verify(mockDelegate).onNext("bar");
- Mockito.verify(mockUpstream).request(1);
- Mockito.verifyNoMoreInteractions(mockUpstream, mockDelegate);
- Mockito.reset(mockUpstream, mockDelegate);
-
- flatteningSubscriber.onNext(Arrays.asList("baz"));
-
- Mockito.verify(mockDelegate).onNext("baz");
- Mockito.verifyNoMoreInteractions(mockUpstream, mockDelegate);
- }
-
- @Test
- public void requestInfinite() {
- flatteningSubscriber.onSubscribe(mockUpstream);
-
- Subscription downstream = getDownstreamFromDelegate();
- downstream.request(1);
- downstream.request(Long.MAX_VALUE);
- downstream.request(Long.MAX_VALUE);
- downstream.request(Long.MAX_VALUE);
- downstream.request(Long.MAX_VALUE);
-
- Mockito.verify(mockUpstream, times(1)).request(1);
-
- flatteningSubscriber.onNext(Arrays.asList("foo", "bar"));
- flatteningSubscriber.onComplete();
-
- Mockito.verify(mockDelegate).onNext("foo");
- Mockito.verify(mockDelegate).onNext("bar");
- Mockito.verify(mockDelegate).onComplete();
- Mockito.verifyNoMoreInteractions(mockDelegate);
- }
-
- @Test
- public void onCompleteDelayedUntilAllDataDelivered() {
- flatteningSubscriber.onSubscribe(mockUpstream);
-
- Subscription downstream = getDownstreamFromDelegate();
- downstream.request(1);
-
- Mockito.verify(mockUpstream).request(1);
-
- flatteningSubscriber.onNext(Arrays.asList("foo", "bar"));
- flatteningSubscriber.onComplete();
-
- Mockito.verify(mockDelegate).onNext("foo");
- Mockito.verifyNoMoreInteractions(mockUpstream, mockDelegate);
- Mockito.reset(mockUpstream, mockDelegate);
-
- downstream.request(1);
- Mockito.verify(mockDelegate).onNext("bar");
- Mockito.verify(mockDelegate).onComplete();
- Mockito.verifyNoMoreInteractions(mockUpstream, mockDelegate);
- }
-
- @Test
- public void onErrorDropsBufferedData() {
- Throwable t = new Throwable();
-
- flatteningSubscriber.onSubscribe(mockUpstream);
-
- Subscription downstream = getDownstreamFromDelegate();
- downstream.request(1);
-
- Mockito.verify(mockUpstream).request(1);
-
- flatteningSubscriber.onNext(Arrays.asList("foo", "bar"));
- flatteningSubscriber.onError(t);
-
- Mockito.verify(mockDelegate).onNext("foo");
- Mockito.verify(mockDelegate).onError(t);
- Mockito.verifyNoMoreInteractions(mockUpstream, mockDelegate);
- }
-
- @Test
- public void requestsFromDownstreamDoNothingAfterOnComplete() {
- flatteningSubscriber.onSubscribe(mockUpstream);
-
- Subscription downstream = getDownstreamFromDelegate();
- downstream.request(1);
-
- Mockito.verify(mockUpstream).request(1);
-
- flatteningSubscriber.onComplete();
-
- Mockito.verify(mockDelegate).onComplete();
- Mockito.verifyNoMoreInteractions(mockUpstream, mockDelegate);
-
- downstream.request(1);
- Mockito.verifyNoMoreInteractions(mockUpstream, mockDelegate);
- }
-
- @Test
- public void requestsFromDownstreamDoNothingAfterOnError() {
- Throwable t = new Throwable();
-
- flatteningSubscriber.onSubscribe(mockUpstream);
-
- Subscription downstream = getDownstreamFromDelegate();
- downstream.request(1);
-
- Mockito.verify(mockUpstream).request(1);
-
- flatteningSubscriber.onError(t);
-
- Mockito.verify(mockDelegate).onError(t);
- Mockito.verifyNoMoreInteractions(mockUpstream, mockDelegate);
-
- downstream.request(1);
- Mockito.verifyNoMoreInteractions(mockUpstream, mockDelegate);
- }
-
- private Subscription getDownstreamFromDelegate() {
- ArgumentCaptor subscriptionCaptor = ArgumentCaptor.forClass(Subscription.class);
- Mockito.verify(mockDelegate).onSubscribe(subscriptionCaptor.capture());
- return subscriptionCaptor.getValue();
- }
-
-}
\ No newline at end of file