-
Notifications
You must be signed in to change notification settings - Fork 535
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
+tck #284 support "demand when all downstreams demand" Processor in TCK #287
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,13 +4,14 @@ | |
import org.reactivestreams.Publisher; | ||
import org.reactivestreams.Subscriber; | ||
import org.reactivestreams.Subscription; | ||
import org.reactivestreams.tck.support.NonFatal; | ||
import org.reactivestreams.tck.support.TCKVerificationSupport; | ||
import org.testng.annotations.AfterClass; | ||
import org.testng.annotations.BeforeClass; | ||
import org.testng.annotations.Test; | ||
|
||
import java.util.concurrent.ExecutorService; | ||
import java.util.concurrent.Executors; | ||
import java.util.concurrent.*; | ||
import java.util.concurrent.atomic.AtomicLong; | ||
|
||
/** | ||
* Validates that the TCK's {@link IdentityProcessorVerification} fails with nice human readable errors. | ||
|
@@ -27,25 +28,36 @@ public class IdentityProcessorVerificationTest extends TCKVerificationSupport { | |
@Test | ||
public void required_spec104_mustCallOnErrorOnAllItsSubscribersIfItEncountersANonRecoverableError_shouldBeIgnored() throws Throwable { | ||
requireTestSkip(new ThrowingRunnable() { | ||
@Override public void run() throws Throwable { | ||
new IdentityProcessorVerification<Integer>(newTestEnvironment(), DEFAULT_TIMEOUT_MILLIS){ | ||
@Override public Processor<Integer, Integer> createIdentityProcessor(int bufferSize) { | ||
@Override | ||
public void run() throws Throwable { | ||
new IdentityProcessorVerification<Integer>(newTestEnvironment(), DEFAULT_TIMEOUT_MILLIS) { | ||
@Override | ||
public Processor<Integer, Integer> createIdentityProcessor(int bufferSize) { | ||
return new NoopProcessor(); | ||
} | ||
|
||
@Override public ExecutorService publisherExecutorService() { return ex; } | ||
@Override | ||
public ExecutorService publisherExecutorService() { | ||
return ex; | ||
} | ||
|
||
@Override public Integer createElement(int element) { return element; } | ||
@Override | ||
public Integer createElement(int element) { | ||
return element; | ||
} | ||
|
||
@Override public Publisher<Integer> createHelperPublisher(long elements) { | ||
@Override | ||
public Publisher<Integer> createHelperPublisher(long elements) { | ||
return SKIP; | ||
} | ||
|
||
@Override public Publisher<Integer> createFailedPublisher() { | ||
@Override | ||
public Publisher<Integer> createFailedPublisher() { | ||
return SKIP; | ||
} | ||
|
||
@Override public long maxSupportedSubscribers() { | ||
@Override | ||
public long maxSupportedSubscribers() { | ||
return 1; // can only support 1 subscribe => unable to run this test | ||
} | ||
}.required_spec104_mustCallOnErrorOnAllItsSubscribersIfItEncountersANonRecoverableError(); | ||
|
@@ -115,6 +127,145 @@ public void required_spec104_mustCallOnErrorOnAllItsSubscribersIfItEncountersANo | |
}, "Did not receive expected error on downstream within " + DEFAULT_TIMEOUT_MILLIS); | ||
} | ||
|
||
@Test | ||
public void required_spec104_mustCallOnErrorOnAllItsSubscribersIfItEncountersANonRecoverableError_shouldAllowSignalingElementAfterBothDownstreamsDemand() throws Throwable { | ||
final TestEnvironment env = newTestEnvironment(); | ||
new IdentityProcessorVerification<Integer>(env, DEFAULT_TIMEOUT_MILLIS) { | ||
@Override | ||
public Processor<Integer, Integer> createIdentityProcessor(int bufferSize) { // knowingly ignoring buffer size, acting as-if 0 | ||
return new Processor<Integer, Integer>() { | ||
|
||
private volatile Subscription upstreamSubscription; | ||
|
||
private final CopyOnWriteArrayList<MySubscription> subs = new CopyOnWriteArrayList<MySubscription>(); | ||
private final CopyOnWriteArrayList<Subscriber<? super Integer>> subscribers = new CopyOnWriteArrayList<Subscriber<? super Integer>>(); | ||
private final AtomicLong demand1 = new AtomicLong(); | ||
private final AtomicLong demand2 = new AtomicLong(); | ||
private final CountDownLatch awaitLatch = new CountDownLatch(2); // to know when both subscribers have signalled demand | ||
|
||
@Override | ||
public void subscribe(final Subscriber<? super Integer> s) { | ||
int subscriberCount = subs.size(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This won't work if |
||
switch (subscriberCount) { | ||
case 0: | ||
s.onSubscribe(createSubscription(awaitLatch, s, demand1)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem with this is that the |
||
break; | ||
case 1: | ||
s.onSubscribe(createSubscription(awaitLatch, s, demand2)); | ||
break; | ||
default: | ||
throw new RuntimeException(String.format("This for-test-purposes-processor supports only 2 subscribers, yet got %s!", subscriberCount)); | ||
} | ||
} | ||
|
||
@Override | ||
public void onSubscribe(Subscription s) { | ||
this.upstreamSubscription = s; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the Subscribers come first and issue a request, how will this Subscription be notified about it? |
||
} | ||
|
||
@Override | ||
public void onNext(Integer elem) { | ||
for (Subscriber<? super Integer> subscriber : subscribers) { | ||
try { | ||
subscriber.onNext(elem); | ||
} catch (Throwable t) { | ||
env.flop(t, String.format("Calling onNext on [%s] should not throw! See https://github.com/reactive-streams/reactive-streams-jvm#2.13", subscriber)); | ||
} | ||
} | ||
} | ||
|
||
@Override | ||
public void onError(Throwable t) { | ||
for (Subscriber<? super Integer> subscriber : subscribers) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will retain all subscribers after the terminal event indefinitely. |
||
try { | ||
subscriber.onError(t); | ||
} catch (Exception ex) { | ||
env.flop(ex, String.format("Calling onError on [%s] should not throw! See https://github.com/reactive-streams/reactive-streams-jvm#2.13", subscriber)); | ||
} | ||
} | ||
} | ||
|
||
@Override | ||
public void onComplete() { | ||
for (Subscriber<? super Integer> subscriber : subscribers) { | ||
try { | ||
subscriber.onComplete(); | ||
} catch (Exception ex) { | ||
env.flop(ex, String.format("Calling onComplete on [%s] should not throw! See https://github.com/reactive-streams/reactive-streams-jvm#2.13", subscriber)); | ||
} | ||
} | ||
} | ||
|
||
private Subscription createSubscription(CountDownLatch awaitLatch, final Subscriber<? super Integer> s, final AtomicLong demand) { | ||
final MySubscription sub = new MySubscription(awaitLatch, s, demand); | ||
subs.add(sub); | ||
subscribers.add(s); | ||
return sub; | ||
} | ||
|
||
final class MySubscription implements Subscription { | ||
private final CountDownLatch awaitLatch; | ||
private final Subscriber<? super Integer> s; | ||
private final AtomicLong demand; | ||
|
||
public MySubscription(CountDownLatch awaitTwoLatch, Subscriber<? super Integer> s, AtomicLong demand) { | ||
this.awaitLatch = awaitTwoLatch; | ||
this.s = s; | ||
this.demand = demand; | ||
} | ||
|
||
@Override | ||
public void request(final long n) { | ||
ex.execute(new Runnable() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In order in respect to what? As I've said a couple of times, one can't expect request to be in sync with any other calls and every non-trivial producer has to be conservative and thus thread- and reentrant-safe. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Imagine a multithreaded ExecutorService. 2 request is issued, 2 Runnables are enqueued, Pool Thread A pulls out the first Runnable but gets preempted. Pool Thread B pulls out the second Runnable and starts executing it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Certainly. And they will call The problem is that I don't see any request coordination. For example, if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @akarnokd In any case, allowing concurrency within the Processor is of little utility and only serves to make the implementation harder to reason about. Had I more time then I'd implement an example Processor that could be reused for the TCK (as with the example publishers and subscribers). I have the utmost respect for your competency here so if you think you'd be able to contribute something like that, then I'd love to review it. Let me know if this is something you'd want to do. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've been trying to incorporate the TCK into RxJava 2.0 but I could only test a couple of our components. The problem with the TCK I see is that it expects a certain shape of One indication to this that we have a In addition, there is also the question if a I'm also guessing that having RxJava 2.0 as the dependency to support the TCK test is out of question. |
||
@Override | ||
public void run() { | ||
if (demand.get() >= 0) { | ||
demand.addAndGet(n); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a race-condition here between addAndGet and There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may work but can overflow demand into a negative value. |
||
awaitLatch.countDown(); | ||
try { | ||
awaitLatch.await(env.defaultTimeoutMillis(), TimeUnit.MILLISECONDS); | ||
final long d = demand.getAndSet(0); | ||
if (d > 0) upstreamSubscription.request(d); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At this point, |
||
} catch (InterruptedException e) { | ||
env.flop(e, "Interrupted while awaiting for all downstreams to signal some demand."); | ||
} catch (Throwable t) { | ||
env.flop(t, "Subscription#request has thrown an exception, which is illegal!"); | ||
} | ||
} // else cancel was called, do nothing | ||
} | ||
}); | ||
} | ||
|
||
@Override | ||
public void cancel() { | ||
demand.set(-1); // marks subscription as cancelled | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return String.format("IdentityProcessorVerificationTest:MySubscription(%s, demand = %s)", s, demand); | ||
} | ||
} | ||
}; | ||
} | ||
|
||
@Override | ||
public ExecutorService publisherExecutorService() { | ||
return ex; | ||
} | ||
|
||
@Override | ||
public Integer createElement(int element) { | ||
return element; | ||
} | ||
|
||
@Override | ||
public Publisher<Integer> createFailedPublisher() { | ||
return SKIP; | ||
} | ||
}.required_spec104_mustCallOnErrorOnAllItsSubscribersIfItEncountersANonRecoverableError(); | ||
} | ||
|
||
// FAILING IMPLEMENTATIONS // | ||
|
||
final Publisher<Integer> SKIP = null; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change is that: