Permalink
Browse files

`RequestN` reduces with completed subscribers.

#### Problem

Re-calculation of max `requestN` per subscriber did not take into account removed subscribers correctly. This causes the `requestN` value to decrease over time when subscribers are completed.

#### Modification

- Modified `recalculateMaxPerSubscriber` to take old and new subscriber count instead of trying to determine the correct value from the current subscriber queue size.
- `subscribers.remove()` does not happen on unsubscribe but only from the task that recalculates the max `requestN` values. This also coalesces multiple removes into a single task run.

#### Result

Consistent `requestN` values with new subscribes and completions.
  • Loading branch information...
NiteshKant committed Nov 17, 2016
1 parent 208b804 commit be121448b6a20d81ded675ab81d1cb175a7fe9a9
@@ -36,6 +36,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -358,6 +359,7 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
*/
private int perSubscriberMaxRequest = MAX_PER_SUBSCRIBER_REQUEST;
private Channel channel;
private boolean removeTaskScheduled; // Guarded by this
BytesWriteInterceptor(String parentHandlerName) {
this.parentHandlerName = parentHandlerName;
@@ -389,16 +391,22 @@ public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exceptio
}
public WriteStreamSubscriber newSubscriber(final ChannelHandlerContext ctx, ChannelPromise promise) {
recalculateMaxPerSubscriber();
int currentSubCount = subscribers.size();
recalculateMaxPerSubscriber(currentSubCount, currentSubCount + 1);
final WriteStreamSubscriber sub = new WriteStreamSubscriber(ctx, promise, perSubscriberMaxRequest);
sub.add(Subscriptions.create(new Action0() {
@Override
public void call() {
/*Remove the subscriber on unsubscribe.*/
subscribers.remove(sub);
ctx.channel().eventLoop().execute(BytesWriteInterceptor.this);
boolean _schedule;
/*Schedule the task once as the task runs through and removes all unsubscribed subscribers*/
synchronized (BytesWriteInterceptor.this) {
_schedule = !removeTaskScheduled;
removeTaskScheduled = true;
}
if (_schedule) {
ctx.channel().eventLoop().execute(BytesWriteInterceptor.this);
}
}
}));
@@ -422,21 +430,37 @@ private void requestMoreIfWritable(Channel channel) {
@Override
public void run() {
recalculateMaxPerSubscriber();
synchronized (this) {
removeTaskScheduled = false;
}
int oldSubCount = subscribers.size();
for (Iterator<WriteStreamSubscriber> iterator = subscribers.iterator(); iterator.hasNext(); ) {
WriteStreamSubscriber subscriber = iterator.next();
if (subscriber.isUnsubscribed()) {
iterator.remove();
}
}
int newSubCount = subscribers.size();
recalculateMaxPerSubscriber(oldSubCount, newSubCount);
}
/**
* Called from within the eventloop, whenever the subscriber queue is modified. This modifies the per subscriber
* request limit by equally distributing the demand. Minimum demand to any subscriber is 1.
*/
private void recalculateMaxPerSubscriber() {
private void recalculateMaxPerSubscriber(int oldSubCount, int newSubCount) {
assert channel.eventLoop().inEventLoop();
int subCount = subscribers.size();
perSubscriberMaxRequest = 0 == subCount ? perSubscriberMaxRequest
: perSubscriberMaxRequest * subCount / (subCount + 1);
perSubscriberMaxRequest = newSubCount == 0 || oldSubCount == 0
? MAX_PER_SUBSCRIBER_REQUEST
: perSubscriberMaxRequest * oldSubCount / newSubCount;
perSubscriberMaxRequest = Math.max(1, perSubscriberMaxRequest);
if (logger.isDebugEnabled()) {
logger.debug("Channel {}. Modifying per subscriber max request. Old subscribers count {}, " +
"new subscribers count {}. New Value {} ", channel, oldSubCount, newSubCount,
perSubscriberMaxRequest);
}
}
}
@@ -540,7 +564,7 @@ public void operationComplete(ChannelFuture future) throws Exception {
boolean _isPromiseCompletedOnWriteComplete;
/**
/*
* The intent here is to NOT give listener callbacks via promise completion within the sync block.
* So, a co-ordination b/w the thread sending Observable terminal event and thread sending write
* completion event is required.
@@ -553,7 +577,7 @@ public void operationComplete(ChannelFuture future) throws Exception {
synchronized (guard) {
listeningTo--;
if (0 == listeningTo && isDone) {
/**
/*
* If the listening count is 0 and no more items will arrive, this thread wins the race of
* completing the overarchingWritePromise
*/
@@ -562,7 +586,7 @@ public void operationComplete(ChannelFuture future) throws Exception {
_isPromiseCompletedOnWriteComplete = isPromiseCompletedOnWriteComplete;
}
/**
/*
* Exceptions are not buffered but completion is only sent when there are no more items to be
* received for write.
*/
@@ -597,7 +621,7 @@ private void onTermination(Throwable throwableIfAny) {
boolean _shouldCompletePromise;
final boolean enqueueFlush;
/**
/*
* The intent here is to NOT give listener callbacks via promise completion within the sync block.
* So, a co-ordination b/w the thread sending Observable terminal event and thread sending write
* completion event is required.
@@ -611,7 +635,7 @@ private void onTermination(Throwable throwableIfAny) {
enqueueFlush = atleastOneWriteEnqueued;
isDone = true;
_listeningTo = listeningTo;
/**
/*
* Flag to indicate whether the write complete thread won the race and will complete the
* overarchingWritePromise
*/
@@ -21,7 +21,6 @@
import io.reactivex.netty.channel.BackpressureManagingHandler.BytesWriteInterceptor;
import io.reactivex.netty.channel.BackpressureManagingHandler.WriteStreamSubscriber;
import io.reactivex.netty.test.util.MockProducer;
import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExternalResource;
@@ -46,23 +45,18 @@ public void testAddSubscriber() throws Exception {
assertThat("Subscriber not added.", inspectorRule.interceptor.getSubscribers(), contains(sub1));
sub1.unsubscribe();
inspectorRule.channel.runPendingTasks();
assertThat("Subscriber not removed post unsubscribe", inspectorRule.interceptor.getSubscribers(), is(empty()));
}
@Test(timeout = 60000)
public void testRequestMore() throws Exception {
WriteStreamSubscriber sub1 = inspectorRule.newSubscriber();
MockProducer mockProducer = InspectorRule.setupSubscriber(sub1);
MockProducer mockProducer = inspectorRule.setupSubscriberAndValidate(sub1, 1);
assertThat("Unexpected items requested from producer.", mockProducer.getRequested(), is(defaultRequestN()));
assertThat("Subscriber not added.", inspectorRule.interceptor.getSubscribers(), hasSize(1));
assertThat("Subscriber not added.", inspectorRule.interceptor.getSubscribers(), contains(sub1));
String msg = "Hello";
inspectorRule.channel.writeAndFlush(msg);
inspectorRule.sendMessages(1);
assertThat("Channel not writable post write.", inspectorRule.channel.isWritable(), is(true));
assertThat("Unexpected items requested.", mockProducer.getRequested(), is(defaultRequestN()));
@@ -72,13 +66,9 @@ public void testRequestMore() throws Exception {
public void testRequestMorePostFlush() throws Exception {
WriteStreamSubscriber sub1 = inspectorRule.newSubscriber();
MockProducer mockProducer = InspectorRule.setupSubscriber(sub1);
MockProducer mockProducer = inspectorRule.setupSubscriberAndValidate(sub1, 1);
assertThat("Unexpected items requested from producer.", mockProducer.getRequested(), is(defaultRequestN()));
assertThat("Subscriber not added.", inspectorRule.interceptor.getSubscribers(), hasSize(1));
assertThat("Subscriber not added.", inspectorRule.interceptor.getSubscribers(), contains(sub1));
inspectorRule.channel.config().setWriteBufferWaterMark(new WriteBufferWaterMark(1, 2)); /*Make sure that the channel is not writable on writing.*/
String msg = "Hello";
@@ -97,19 +87,12 @@ public void testRequestMorePostFlush() throws Exception {
@Test(timeout = 60000)
public void testMultiSubscribers() throws Exception {
WriteStreamSubscriber sub1 = inspectorRule.newSubscriber();
MockProducer producer1 = InspectorRule.setupSubscriber(sub1);
assertThat("Subscriber not added.", inspectorRule.interceptor.getSubscribers(), hasSize(1));
assertThat("Subscriber not added.", inspectorRule.interceptor.getSubscribers(), contains(sub1));
MockProducer producer1 = inspectorRule.setupSubscriberAndValidate(sub1, 1);
WriteStreamSubscriber sub2 = inspectorRule.newSubscriber();
MockProducer producer2 = InspectorRule.setupSubscriber(sub2);
MockProducer producer2 = inspectorRule.setupSubscriberAndValidate(sub2, 2);
assertThat("Subscriber not added.", inspectorRule.interceptor.getSubscribers(), hasSize(2));
assertThat("Subscriber not added.", inspectorRule.interceptor.getSubscribers(), contains(sub1, sub2));
String msg = "Hello";
inspectorRule.channel.writeAndFlush(msg);
inspectorRule.sendMessages(1);
assertThat("Channel not writable post write.", inspectorRule.channel.isWritable(), is(true));
assertThat("Unexpected items requested from first subscriber.", producer1.getRequested(),
@@ -118,6 +101,47 @@ public void testMultiSubscribers() throws Exception {
is(defaultRequestN() / 2));
}
@Test(timeout = 10000)
public void testOneLongWriteAndManySmallWrites() throws Exception {
WriteStreamSubscriber sub1 = inspectorRule.newSubscriber();
MockProducer producer1 = inspectorRule.setupSubscriberAndValidate(sub1, 1);
assertThat("Unexpected items requested from producer.", producer1.getRequested(), is(defaultRequestN()));
inspectorRule.setupNewSubscriberAndComplete(2, true);
inspectorRule.setupNewSubscriberAndComplete(2, true);
inspectorRule.sendMessages(sub1, 33);
assertThat("Unexpected items requested.", producer1.getRequested(), is(97L));
}
@Test(timeout = 10000)
public void testBatchedSubscriberRemoves() throws Exception {
WriteStreamSubscriber sub1 = inspectorRule.newSubscriber();
MockProducer producer1 = inspectorRule.setupSubscriberAndValidate(sub1, 1);
assertThat("Unexpected items requested from producer.", producer1.getRequested(), is(defaultRequestN()));
for (int i=1; i < 5; i++) {
inspectorRule.setupNewSubscriberAndComplete(i+1, false);
}
inspectorRule.channel.runPendingTasks();
inspectorRule.sendMessages(sub1, 35);
assertThat("Unexpected items requested.", producer1.getRequested(), is(95L));
}
@Test(timeout = 10000)
public void testMinRequestN() throws Exception {
for (int i=1; i < 66; i++) {
inspectorRule.setupNewSubscriberAndComplete(i, false);
}
WriteStreamSubscriber sub1 = inspectorRule.newSubscriber();
MockProducer producer1 = inspectorRule.setupSubscriberAndValidate(sub1, 66);
assertThat("Unexpected items requested from producer.", producer1.getRequested(), is(1L));
inspectorRule.channel.runPendingTasks();
inspectorRule.sendMessages(sub1, 35);
assertThat("Unexpected items requested.", producer1.getRequested(), greaterThan(1L));
}
public static class InspectorRule extends ExternalResource {
private BytesWriteInterceptor interceptor;
@@ -136,18 +160,52 @@ public void evaluate() throws Throwable {
}
WriteStreamSubscriber newSubscriber() {
return interceptor.newSubscriber(channel.pipeline().firstContext(), channel.newPromise());
return interceptor.newSubscriber(channel.pipeline().lastContext(), channel.newPromise());
}
private static MockProducer setupSubscriber(WriteStreamSubscriber sub1) {
sub1.onStart();
private MockProducer setupSubscriberAndValidate(WriteStreamSubscriber sub, int expectedSubCount) {
MockProducer mockProducer = setupSubscriber(sub);
assertThat("Subscriber not added.", interceptor.getSubscribers(), hasSize(expectedSubCount));
assertThat("Subscriber not added.", interceptor.getSubscribers().get(expectedSubCount - 1), equalTo(sub));
return mockProducer;
}
private static MockProducer setupSubscriber(WriteStreamSubscriber sub) {
sub.onStart();
MockProducer mockProducer = new MockProducer();
sub1.setProducer(mockProducer);
sub.setProducer(mockProducer);
return mockProducer;
}
public static Long defaultRequestN() {
return Long.valueOf(MAX_PER_SUBSCRIBER_REQUEST);
}
public void sendMessages(WriteStreamSubscriber subscriber, int msgCount) {
for(int i=0; i < msgCount; i++) {
subscriber.onNext("Hello");
channel.write("Hello");
}
channel.flush();
}
public void sendMessages(int msgCount) {
for(int i=0; i < msgCount; i++) {
channel.write("Hello");
}
channel.flush();
}
public void setupNewSubscriberAndComplete(int expectedSubCount, boolean runPendingTasks) {
WriteStreamSubscriber sub2 = newSubscriber();
MockProducer producer2 = setupSubscriberAndValidate(sub2, expectedSubCount);
assertThat("Unexpected items requested from producer.", producer2.getRequested(),
lessThanOrEqualTo(Math.max(1, defaultRequestN()/expectedSubCount)));
sub2.onCompleted();
sub2.unsubscribe();
if (runPendingTasks) {
channel.runPendingTasks();
}
}
}
}

0 comments on commit be12144

Please sign in to comment.