/*
* Copyright 2016, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package io.grpc.testing.integration;
import static org.junit.Assert.assertTrue;
import com.google.common.collect.ImmutableList;
import io.grpc.ManagedChannel;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.ServerInterceptor;
import io.grpc.ServerInterceptors;
import io.grpc.netty.HandlerSettings;
import io.grpc.netty.NegotiationType;
import io.grpc.netty.NettyChannelBuilder;
import io.grpc.netty.NettyServerBuilder;
import io.grpc.stub.StreamObserver;
import io.grpc.testing.integration.Messages.ResponseParameters;
import io.grpc.testing.integration.Messages.StreamingOutputCallRequest;
import io.grpc.testing.integration.Messages.StreamingOutputCallResponse;
import io.netty.util.concurrent.DefaultThreadFactory;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class NettyFlowControlTest {
// in bytes
private static final int LOW_BAND = 2 * 1024 * 1024;
private static final int HIGH_BAND = 30 * 1024 * 1024;
// in milliseconds
private static final int MED_LAT = 10;
// in bytes
private static final int TINY_WINDOW = 1;
private static final int REGULAR_WINDOW = 64 * 1024;
private static final int MAX_WINDOW = 8 * 1024 * 1024;
private static ManagedChannel channel;
private static Server server;
private static TrafficControlProxy proxy;
private int proxyPort;
private int serverPort;
private static final ThreadPoolExecutor executor =
new ThreadPoolExecutor(1, 10, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
new DefaultThreadFactory("flowcontrol-test-pool", true));
@BeforeClass
public static void setUp() {
HandlerSettings.enable(true);
HandlerSettings.autoWindowOn(true);
}
@AfterClass
public static void shutDownTests() {
executor.shutdown();
}
@Before
public void initTest() {
startServer(REGULAR_WINDOW);
serverPort = server.getPort();
}
@After
public void endTest() throws IOException {
proxy.shutDown();
server.shutdown();
}
@Test
public void largeBdp() throws InterruptedException, IOException {
proxy = new TrafficControlProxy(serverPort, HIGH_BAND, MED_LAT, TimeUnit.MILLISECONDS);
proxy.start();
proxyPort = proxy.getPort();
resetConnection(REGULAR_WINDOW);
doTest(HIGH_BAND, MED_LAT);
}
@Test
public void smallBdp() throws InterruptedException, IOException {
proxy = new TrafficControlProxy(serverPort, LOW_BAND, MED_LAT, TimeUnit.MILLISECONDS);
proxy.start();
proxyPort = proxy.getPort();
resetConnection(REGULAR_WINDOW);
doTest(LOW_BAND, MED_LAT);
}
@Test
public void verySmallWindowMakesProgress() throws InterruptedException, IOException {
proxy = new TrafficControlProxy(serverPort, HIGH_BAND, MED_LAT, TimeUnit.MILLISECONDS);
proxy.start();
proxyPort = proxy.getPort();
resetConnection(TINY_WINDOW);
doTest(HIGH_BAND, MED_LAT);
}
/**
* Main testing method. Streams 2 MB of data from a server and records the final window and
* average bandwidth usage.
*/
private void doTest(int bandwidth, int latency) throws InterruptedException {
int streamSize = 1 * 1024 * 1024;
long expectedWindow = (latency * (bandwidth / TimeUnit.SECONDS.toMillis(1)));
TestServiceGrpc.TestServiceStub stub = TestServiceGrpc.newStub(channel);
StreamingOutputCallRequest.Builder builder = StreamingOutputCallRequest.newBuilder()
.addResponseParameters(ResponseParameters.newBuilder().setSize(streamSize / 16))
.addResponseParameters(ResponseParameters.newBuilder().setSize(streamSize / 16))
.addResponseParameters(ResponseParameters.newBuilder().setSize(streamSize / 8))
.addResponseParameters(ResponseParameters.newBuilder().setSize(streamSize / 4))
.addResponseParameters(ResponseParameters.newBuilder().setSize(streamSize / 2));
StreamingOutputCallRequest request = builder.build();
TestStreamObserver observer = new TestStreamObserver(expectedWindow);
stub.streamingOutputCall(request, observer);
int lastWindow = observer.waitFor();
// deal with cases that either don't cause a window update or hit max window
expectedWindow = Math.min(MAX_WINDOW, (Math.max(expectedWindow, REGULAR_WINDOW)));
// Range looks large, but this allows for only one extra/missed window update
// (one extra update causes a 2x difference and one missed update causes a .5x difference)
assertTrue("Window was " + lastWindow + " expecting " + expectedWindow,
lastWindow < 2 * expectedWindow);
assertTrue("Window was " + lastWindow + " expecting " + expectedWindow,
expectedWindow < 2 * lastWindow);
}
/**
* Resets client/server and their flow control windows.
*/
private void resetConnection(int clientFlowControlWindow)
throws InterruptedException {
if (channel != null) {
if (!channel.isShutdown()) {
channel.shutdown();
channel.awaitTermination(100, TimeUnit.MILLISECONDS);
}
}
channel = NettyChannelBuilder.forAddress(new InetSocketAddress("localhost", proxyPort))
.flowControlWindow(clientFlowControlWindow)
.negotiationType(NegotiationType.PLAINTEXT)
.build();
}
private void startServer(int serverFlowControlWindow) {
ServerBuilder<?> builder =
NettyServerBuilder.forAddress(new InetSocketAddress("localhost", 0))
.flowControlWindow(serverFlowControlWindow);
builder.addService(ServerInterceptors.intercept(
new TestServiceImpl(Executors.newScheduledThreadPool(2)),
ImmutableList.<ServerInterceptor>of()));
try {
server = builder.build().start();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Simple stream observer to measure elapsed time of the call.
*/
private static class TestStreamObserver implements StreamObserver<StreamingOutputCallResponse> {
long startRequestNanos;
long endRequestNanos;
private final CountDownLatch latch = new CountDownLatch(1);
long expectedWindow;
int lastWindow;
public TestStreamObserver(long window) {
startRequestNanos = System.nanoTime();
expectedWindow = window;
}
@Override
public void onNext(StreamingOutputCallResponse value) {
lastWindow = HandlerSettings.getLatestClientWindow();
if (lastWindow >= expectedWindow) {
onCompleted();
}
}
@Override
public void onError(Throwable t) {
latch.countDown();
throw new RuntimeException(t);
}
@Override
public void onCompleted() {
latch.countDown();
}
public long getElapsedTime() {
return endRequestNanos - startRequestNanos;
}
public int waitFor() throws InterruptedException {
latch.await();
return lastWindow;
}
}
}