co.cask.cdap.gateway.router.NettyRouterTestBase.java Source code

Java tutorial

Introduction

Here is the source code for co.cask.cdap.gateway.router.NettyRouterTestBase.java

Source

/*
 * Copyright  2014-2015 Cask Data, 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 co.cask.cdap.gateway.router;

import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.discovery.ResolvingDiscoverable;
import co.cask.cdap.common.utils.Networks;
import co.cask.http.AbstractHttpHandler;
import co.cask.http.ChunkResponder;
import co.cask.http.HttpResponder;
import co.cask.http.NettyHttpService;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.AbstractIdleService;
import com.ning.http.client.AsyncCompletionHandler;
import com.ning.http.client.AsyncHttpClient;
import com.ning.http.client.AsyncHttpClientConfig;
import com.ning.http.client.HttpResponseBodyPart;
import com.ning.http.client.Request;
import com.ning.http.client.RequestBuilder;
import com.ning.http.client.Response;
import com.ning.http.client.providers.netty.NettyAsyncHttpProvider;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.util.EntityUtils;
import org.apache.twill.common.Cancellable;
import org.apache.twill.discovery.Discoverable;
import org.apache.twill.discovery.DiscoveryService;
import org.apache.twill.discovery.DiscoveryServiceClient;
import org.apache.twill.discovery.InMemoryDiscoveryService;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import javax.net.SocketFactory;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;

/**
 * Tests Netty Router.
 */
public abstract class NettyRouterTestBase {
    protected static final String HOSTNAME = "127.0.0.1";
    protected static final DiscoveryService DISCOVERY_SERVICE = new InMemoryDiscoveryService();
    protected static final String DEFAULT_SERVICE = Constants.Router.GATEWAY_DISCOVERY_NAME;
    protected static final String WEBAPP_SERVICE = Constants.Router.WEBAPP_DISCOVERY_NAME;
    protected static final String APP_FABRIC_SERVICE = Constants.Service.APP_FABRIC_HTTP;
    protected static final String WEB_APP_SERVICE_PREFIX = "webapp/";
    protected static final int CONNECTION_IDLE_TIMEOUT_SECS = 2;

    private static final Logger LOG = LoggerFactory.getLogger(NettyRouterTestBase.class);
    private static final int MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
    private static final int CHUNK_SIZE = 1024 * 1024; // NOTE: MAX_UPLOAD_BYTES % CHUNK_SIZE == 0

    private final Supplier<String> defaultServiceSupplier = new Supplier<String>() {
        @Override
        public String get() {
            return APP_FABRIC_SERVICE;
        }
    };

    private final Supplier<String> webappServiceSupplier = new Supplier<String>() {
        @Override
        public String get() {
            try {
                return WEB_APP_SERVICE_PREFIX
                        + Networks.normalizeWebappDiscoveryName(HOSTNAME + ":" + lookupService(WEBAPP_SERVICE));
            } catch (UnsupportedEncodingException e) {
                LOG.error("Got exception: ", e);
                throw Throwables.propagate(e);
            }
        }
    };

    private final Supplier<String> defaultWebappServiceSupplier1 = new Supplier<String>() {
        @Override
        public String get() {
            try {
                return WEB_APP_SERVICE_PREFIX + Networks.normalizeWebappDiscoveryName("default/abc");
            } catch (UnsupportedEncodingException e) {
                LOG.error("Got exception: ", e);
                throw Throwables.propagate(e);
            }
        }
    };

    private final Supplier<String> defaultWebappServiceSupplier2 = new Supplier<String>() {
        @Override
        public String get() {
            try {
                return WEB_APP_SERVICE_PREFIX + Networks.normalizeWebappDiscoveryName("default/def");
            } catch (UnsupportedEncodingException e) {
                LOG.error("Got exception: ", e);
                throw Throwables.propagate(e);
            }
        }
    };

    public final RouterService routerService = createRouterService();
    public final ServerService defaultServer1 = new ServerService(HOSTNAME, DISCOVERY_SERVICE,
            defaultServiceSupplier);
    public final ServerService defaultServer2 = new ServerService(HOSTNAME, DISCOVERY_SERVICE,
            defaultServiceSupplier);
    public final ServerService webappServer = new ServerService(HOSTNAME, DISCOVERY_SERVICE, webappServiceSupplier);
    public final ServerService defaultWebappServer1 = new ServerService(HOSTNAME, DISCOVERY_SERVICE,
            defaultWebappServiceSupplier1);
    public final ServerService defaultWebappServer2 = new ServerService(HOSTNAME, DISCOVERY_SERVICE,
            defaultWebappServiceSupplier2);
    public final List<ServerService> allServers = Lists.newArrayList(defaultServer1, defaultServer2, webappServer,
            defaultWebappServer1, defaultWebappServer2);

    protected abstract RouterService createRouterService();

    protected abstract String getProtocol();

    protected abstract DefaultHttpClient getHTTPClient() throws Exception;

    protected abstract SocketFactory getSocketFactory() throws Exception;

    protected int lookupService(String serviceName) {
        return routerService.lookupService(serviceName);
    }

    private String resolveURI(String serviceName, String path) throws URISyntaxException {
        return getBaseURI(serviceName).resolve(path).toASCIIString();
    }

    private URI getBaseURI(String serviceName) throws URISyntaxException {
        int servicePort = lookupService(serviceName);
        return new URI(String.format("%s://%s:%d", getProtocol(), HOSTNAME, servicePort));
    }

    @Before
    public void startUp() throws Exception {
        routerService.startAndWait();
        for (ServerService server : allServers) {
            server.clearState();
            server.startAndWait();
        }

        // Wait for both servers of defaultService to be registered
        Iterable<Discoverable> discoverables = ((DiscoveryServiceClient) DISCOVERY_SERVICE)
                .discover(defaultServiceSupplier.get());
        for (int i = 0; i < 50 && Iterables.size(discoverables) != 2; ++i) {
            TimeUnit.MILLISECONDS.sleep(50);
        }

        // Wait for server of webappService to be registered
        discoverables = ((DiscoveryServiceClient) DISCOVERY_SERVICE).discover(webappServiceSupplier.get());
        for (int i = 0; i < 50 && Iterables.size(discoverables) != 1; ++i) {
            TimeUnit.MILLISECONDS.sleep(50);
        }

        // Wait for server of defaultWebappServiceSupplier1 to be registered
        discoverables = ((DiscoveryServiceClient) DISCOVERY_SERVICE).discover(defaultWebappServiceSupplier1.get());
        for (int i = 0; i < 50 && Iterables.size(discoverables) != 1; ++i) {
            TimeUnit.MILLISECONDS.sleep(50);
        }

        // Wait for server of defaultWebappServiceSupplier2 to be registered
        discoverables = ((DiscoveryServiceClient) DISCOVERY_SERVICE).discover(defaultWebappServiceSupplier2.get());
        for (int i = 0; i < 50 && Iterables.size(discoverables) != 1; ++i) {
            TimeUnit.MILLISECONDS.sleep(50);
        }
    }

    @After
    public void tearDown() {
        for (ServerService server : allServers) {
            server.stopAndWait();
        }
        routerService.stopAndWait();
    }

    @Test
    public void testRouterSync() throws Exception {
        testSync(25);
        // sticky endpoint strategy used so the sum should be 25
        Assert.assertEquals(25, defaultServer1.getNumRequests() + defaultServer2.getNumRequests());
    }

    @Test
    public void testRouterAsync() throws Exception {
        int numElements = 123;
        AsyncHttpClientConfig.Builder configBuilder = new AsyncHttpClientConfig.Builder();

        final AsyncHttpClient asyncHttpClient = new AsyncHttpClient(
                new NettyAsyncHttpProvider(configBuilder.build()), configBuilder.build());

        final CountDownLatch latch = new CountDownLatch(numElements);
        final AtomicInteger numSuccessfulRequests = new AtomicInteger(0);
        for (int i = 0; i < numElements; ++i) {
            final int elem = i;
            final Request request = new RequestBuilder("GET")
                    .setUrl(resolveURI(DEFAULT_SERVICE, String.format("%s/%s-%d", "/v1/ping", "async", i))).build();
            asyncHttpClient.executeRequest(request, new AsyncCompletionHandler<Void>() {
                @Override
                public Void onCompleted(Response response) throws Exception {
                    latch.countDown();
                    Assert.assertEquals(HttpResponseStatus.OK.getCode(), response.getStatusCode());
                    numSuccessfulRequests.incrementAndGet();
                    return null;
                }

                @Override
                public void onThrowable(Throwable t) {
                    LOG.error("Got exception while posting {}", elem, t);
                    latch.countDown();
                }
            });

            // Sleep so as not to overrun the server.
            TimeUnit.MILLISECONDS.sleep(1);
        }
        latch.await();
        asyncHttpClient.close();

        Assert.assertEquals(numElements, numSuccessfulRequests.get());
        // we use sticky endpoint strategy so the sum of requests from the two gateways should be NUM_ELEMENTS
        Assert.assertTrue(numElements == (defaultServer1.getNumRequests() + defaultServer2.getNumRequests()));
    }

    @Test
    public void testRouterOneServerDown() throws Exception {
        try {
            // Bring down defaultServer1
            defaultServer1.cancelRegistration();

            testSync(25);
        } finally {
            Assert.assertEquals(0, defaultServer1.getNumRequests());
            Assert.assertTrue(defaultServer2.getNumRequests() > 0);

            defaultServer1.registerServer();
        }
    }

    @Test
    public void testRouterAllServersDown() throws Exception {
        try {
            // Bring down all servers
            defaultServer1.cancelRegistration();
            defaultServer2.cancelRegistration();

            testSyncServiceUnavailable();
        } finally {
            Assert.assertEquals(0, defaultServer1.getNumRequests());
            Assert.assertEquals(0, defaultServer2.getNumRequests());

            defaultServer1.registerServer();
            defaultServer2.registerServer();
        }
    }

    @Test
    public void testHostForward() throws Exception {
        // Test defaultService
        HttpResponse response = get(resolveURI(DEFAULT_SERVICE, String.format("%s/%s", "/v1/ping", "sync")));
        Assert.assertEquals(HttpResponseStatus.OK.getCode(), response.getStatusLine().getStatusCode());
        Assert.assertEquals(defaultServiceSupplier.get(), EntityUtils.toString(response.getEntity()));

        // Test webappService
        response = get(resolveURI(WEBAPP_SERVICE, String.format("%s/%s", "/v1/ping", "sync")));
        Assert.assertEquals(HttpResponseStatus.OK.getCode(), response.getStatusLine().getStatusCode());
        Assert.assertEquals(webappServiceSupplier.get(), EntityUtils.toString(response.getEntity()));

        // Test default
        response = get(resolveURI(WEBAPP_SERVICE, String.format("%s/%s", "/abc/v1/ping", "sync")),
                new Header[] { new BasicHeader(HttpHeaders.Names.HOST, "www.abc.com") });
        Assert.assertEquals(HttpResponseStatus.OK.getCode(), response.getStatusLine().getStatusCode());
        Assert.assertEquals(defaultWebappServiceSupplier1.get(), EntityUtils.toString(response.getEntity()));

        // Test default, port 80
        response = get(resolveURI(WEBAPP_SERVICE, String.format("%s/%s", "/abc/v1/ping", "sync")),
                new Header[] { new BasicHeader(HttpHeaders.Names.HOST, "www.def.com" + ":80") });
        Assert.assertEquals(HttpResponseStatus.OK.getCode(), response.getStatusLine().getStatusCode());
        Assert.assertEquals(defaultWebappServiceSupplier1.get(), EntityUtils.toString(response.getEntity()));

        // Test default, port random port
        response = get(resolveURI(WEBAPP_SERVICE, String.format("%s/%s", "/def/v1/ping", "sync")),
                new Header[] { new BasicHeader(HttpHeaders.Names.HOST, "www.ghi.net" + ":" + "5678") });
        Assert.assertEquals(HttpResponseStatus.OK.getCode(), response.getStatusLine().getStatusCode());
        Assert.assertEquals(defaultWebappServiceSupplier2.get(), EntityUtils.toString(response.getEntity()));
    }

    @Test
    public void testUpload() throws Exception {
        AsyncHttpClientConfig.Builder configBuilder = new AsyncHttpClientConfig.Builder();

        final AsyncHttpClient asyncHttpClient = new AsyncHttpClient(
                new NettyAsyncHttpProvider(configBuilder.build()), configBuilder.build());

        byte[] requestBody = generatePostData();
        final Request request = new RequestBuilder("POST").setUrl(resolveURI(DEFAULT_SERVICE, "/v1/upload"))
                .setContentLength(requestBody.length).setBody(new ByteEntityWriter(requestBody)).build();

        final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Future<Void> future = asyncHttpClient.executeRequest(request, new AsyncCompletionHandler<Void>() {
            @Override
            public Void onCompleted(Response response) throws Exception {
                return null;
            }

            @Override
            public STATE onBodyPartReceived(HttpResponseBodyPart content) throws Exception {
                //TimeUnit.MILLISECONDS.sleep(RANDOM.nextInt(10));
                content.writeTo(byteArrayOutputStream);
                return super.onBodyPartReceived(content);
            }
        });

        future.get();
        Assert.assertArrayEquals(requestBody, byteArrayOutputStream.toByteArray());
    }

    @Test
    public void testConnectionClose() throws Exception {
        URL[] urls = new URL[] { new URL(resolveURI(Constants.Router.GATEWAY_DISCOVERY_NAME, "/abc/v1/status")),
                new URL(resolveURI(Constants.Router.GATEWAY_DISCOVERY_NAME, "/def/v1/status")) };

        // Make bunch of requests to one service to 2 difference urls, with the first one keep-alive, second one not.
        // This make router creates two backend service connections on the same inbound connection
        // This is to verify on the close of the second one, it won't close the the inbound if there is an
        // in-flight request happening already (if reached another round of the following for-loop).
        int times = 1000;
        boolean keepAlive = true;
        for (int i = 0; i < times; i++) {
            HttpURLConnection urlConn = openURL(urls[i % urls.length]);
            try {
                urlConn.setRequestProperty(HttpHeaders.Names.CONNECTION,
                        keepAlive ? HttpHeaders.Values.KEEP_ALIVE : HttpHeaders.Values.CLOSE);
                Assert.assertEquals(HttpURLConnection.HTTP_OK, urlConn.getResponseCode());
            } finally {
                keepAlive = !keepAlive;
                urlConn.disconnect();
            }
        }

        Assert.assertEquals(times, defaultServer1.getNumRequests() + defaultServer2.getNumRequests());
    }

    // have a timeout of 10 seconds, in case the final call to reader.read hangs (in the case that connection isn't
    // disconnected)
    @Test(timeout = 10000)
    public void testConnectionIdleTimeout() throws Exception {
        defaultServer2.cancelRegistration();

        String path = "/v2/ping";
        URI uri = new URI(resolveURI(Constants.Router.GATEWAY_DISCOVERY_NAME, path));
        Socket socket = getSocketFactory().createSocket(uri.getHost(), uri.getPort());
        PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
        InputStream inputStream = socket.getInputStream();

        // make a request
        String firstLine = makeRequest(uri, out, inputStream);
        Assert.assertEquals("HTTP/1.1 200 OK\r", firstLine);

        // sleep for 500 ms below the configured idle timeout; the connection on server side should not get closed by then
        TimeUnit.MILLISECONDS.sleep(TimeUnit.SECONDS.toMillis(CONNECTION_IDLE_TIMEOUT_SECS) - 500);
        firstLine = makeRequest(uri, out, inputStream);
        Assert.assertEquals("HTTP/1.1 200 OK\r", firstLine);

        // sleep for 500 ms over the configured idle timeout; the connection on server side should get closed by then
        TimeUnit.MILLISECONDS.sleep(TimeUnit.SECONDS.toMillis(CONNECTION_IDLE_TIMEOUT_SECS) + 500);
        // Due to timeout the client connection will be closed, and hence this request should not go to the server
        makeRequest(uri, out, inputStream);

        // assert that the connection is closed on the server side
        Assert.assertEquals(2, defaultServer1.getNumRequests() + defaultServer2.getNumRequests());
        Assert.assertEquals(1, defaultServer1.getNumConnectionsOpened() + defaultServer2.getNumConnectionsOpened());
        Assert.assertEquals(1, defaultServer1.getNumConnectionsClosed() + defaultServer2.getNumConnectionsClosed());
    }

    private String makeRequest(URI uri, PrintWriter out, InputStream inputStream) throws IOException {

        //Send request
        out.print("GET " + uri.getPath() + " HTTP/1.1\r\n" + "Host: " + uri.getHost() + "\r\n"
                + "Connection: keep-alive\r\n\r\n");
        out.flush();

        byte[] buffer = new byte[1024];
        int length = 0;
        for (int i = 0; i < 20; ++i) {
            int read = inputStream.read(buffer, length, 1024 - length);
            length += read < 0 ? 0 : read;
            // Output returned is 108 bytes in length for /v2/ping
            if (length >= 108) {
                break;
            }
        }
        // Return only first line of the response
        return Iterables.getFirst(Splitter.on("\n").split(new String(buffer, Charsets.UTF_8.name())), "");
    }

    @Test
    public void testConnectionIdleTimeoutWithMultipleServers() throws Exception {
        defaultServer2.cancelRegistration();

        URL url = new URL(resolveURI(Constants.Router.GATEWAY_DISCOVERY_NAME, "/v2/ping"));
        HttpURLConnection urlConnection = openURL(url);
        Assert.assertEquals(200, urlConnection.getResponseCode());
        urlConnection.getInputStream().close();
        urlConnection.disconnect();

        // requests past this point will go to defaultServer2
        defaultServer1.cancelRegistration();
        defaultServer2.registerServer();

        for (int i = 0; i < 4; i++) {
            // this is an assumption that CONNECTION_IDLE_TIMEOUT_SECS is more than 1 second
            TimeUnit.SECONDS.sleep(1);
            url = new URL(resolveURI(Constants.Router.GATEWAY_DISCOVERY_NAME, "/v1/ping/" + i));
            urlConnection = openURL(url);
            Assert.assertEquals(200, urlConnection.getResponseCode());
            urlConnection.getInputStream().close();
            urlConnection.disconnect();
        }

        // for the past 4 seconds, we've been making requests to defaultServer2; therefore, defaultServer1 will have closed
        // its single connection
        Assert.assertEquals(1, defaultServer1.getNumConnectionsOpened());
        Assert.assertEquals(1, defaultServer1.getNumConnectionsClosed());

        // however, the connection to defaultServer2 is not timed out, because we've been making requests to it
        Assert.assertEquals(1, defaultServer2.getNumConnectionsOpened());
        Assert.assertEquals(0, defaultServer2.getNumConnectionsClosed());

        defaultServer2.registerServer();
        defaultServer1.cancelRegistration();
        url = new URL(resolveURI(Constants.Router.GATEWAY_DISCOVERY_NAME, "/v2/ping"));
        urlConnection = openURL(url);
        Assert.assertEquals(200, urlConnection.getResponseCode());
        urlConnection.getInputStream().close();
        urlConnection.disconnect();
    }

    @Test
    public void testConnectionNoIdleTimeout() throws Exception {
        // even though the handler will sleep for 500ms over the configured idle timeout before responding, the connection
        // is not closed because the http request is in progress
        long timeoutMillis = TimeUnit.SECONDS.toMillis(CONNECTION_IDLE_TIMEOUT_SECS) + 500;
        URL url = new URL(resolveURI(Constants.Router.GATEWAY_DISCOVERY_NAME, "/v1/timeout/" + timeoutMillis));
        HttpURLConnection urlConnection = openURL(url);
        Assert.assertEquals(200, urlConnection.getResponseCode());
        urlConnection.disconnect();
    }

    protected HttpURLConnection openURL(URL url) throws Exception {
        return (HttpURLConnection) url.openConnection();
    }

    private void testSync(int numRequests) throws Exception {
        for (int i = 0; i < numRequests; ++i) {
            LOG.trace("Sending request " + i);
            HttpResponse response = get(resolveURI(Constants.Router.GATEWAY_DISCOVERY_NAME,
                    String.format("%s/%s-%d", "/v1/ping", "sync", i)));
            Assert.assertEquals(HttpResponseStatus.OK.getCode(), response.getStatusLine().getStatusCode());
        }
    }

    private void testSyncServiceUnavailable() throws Exception {
        for (int i = 0; i < 25; ++i) {
            LOG.trace("Sending request " + i);
            HttpResponse response = get(
                    resolveURI(DEFAULT_SERVICE, String.format("%s/%s-%d", "/v1/ping", "sync", i)));
            Assert.assertEquals(HttpResponseStatus.SERVICE_UNAVAILABLE.getCode(),
                    response.getStatusLine().getStatusCode());
        }
    }

    private byte[] generatePostData() {
        byte[] bytes = new byte[MAX_UPLOAD_BYTES];

        for (int i = 0; i < MAX_UPLOAD_BYTES; ++i) {
            bytes[i] = (byte) i;
        }

        return bytes;
    }

    private static class ByteEntityWriter implements Request.EntityWriter {
        private final byte[] bytes;

        private ByteEntityWriter(byte[] bytes) {
            this.bytes = bytes;
        }

        @Override
        public void writeEntity(OutputStream out) throws IOException {
            for (int i = 0; i < MAX_UPLOAD_BYTES; i += CHUNK_SIZE) {
                out.write(bytes, i, CHUNK_SIZE);
            }
        }
    }

    private HttpResponse get(String url) throws Exception {
        return get(url, null);
    }

    private HttpResponse get(String url, Header[] headers) throws Exception {
        DefaultHttpClient client = getHTTPClient();
        HttpGet get = new HttpGet(url);
        if (headers != null) {
            get.setHeaders(headers);
        }
        return client.execute(get);
    }

    /**
     * A server for the router.
     */
    public abstract static class RouterService extends AbstractIdleService {
        public abstract int lookupService(String serviceName);
    }

    /**
     * A generic server for testing router.
     */
    public static class ServerService extends AbstractIdleService {
        private static final Logger log = LoggerFactory.getLogger(ServerService.class);

        private final String hostname;
        private final DiscoveryService discoveryService;
        private final Supplier<String> serviceNameSupplier;
        private final AtomicInteger numRequests = new AtomicInteger(0);
        private final AtomicInteger numConnectionsOpened = new AtomicInteger(0);
        private final AtomicInteger numConnectionsClosed = new AtomicInteger(0);

        private NettyHttpService httpService;
        private Cancellable cancelDiscovery;

        private ServerService(String hostname, DiscoveryService discoveryService,
                Supplier<String> serviceNameSupplier) {
            this.hostname = hostname;
            this.discoveryService = discoveryService;
            this.serviceNameSupplier = serviceNameSupplier;
        }

        @Override
        protected void startUp() {
            NettyHttpService.Builder builder = NettyHttpService.builder();
            builder.addHttpHandlers(ImmutableSet.of(new ServerHandler()));
            builder.setHost(hostname);
            builder.setPort(0);
            builder.modifyChannelPipeline(new Function<ChannelPipeline, ChannelPipeline>() {
                @Nullable
                @Override
                public ChannelPipeline apply(ChannelPipeline input) {
                    input.addLast("connection-counter", new SimpleChannelHandler() {
                        @Override
                        public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
                            numConnectionsOpened.incrementAndGet();
                            super.channelOpen(ctx, e);
                        }

                        @Override
                        public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
                            numConnectionsClosed.incrementAndGet();
                            super.channelClosed(ctx, e);
                        }
                    });
                    return input;
                }
            });
            httpService = builder.build();
            httpService.startAndWait();

            registerServer();

            log.info("Started test server on {}", httpService.getBindAddress());
        }

        @Override
        protected void shutDown() {
            cancelDiscovery.cancel();
            httpService.stopAndWait();
        }

        public int getNumRequests() {
            return numRequests.get();
        }

        public int getNumConnectionsOpened() {
            return numConnectionsOpened.get();
        }

        public int getNumConnectionsClosed() {
            return numConnectionsClosed.get();
        }

        public void clearState() {
            numRequests.set(0);
            numConnectionsOpened.set(0);
            numConnectionsClosed.set(0);
        }

        public void registerServer() {
            // Register services of test server
            log.info("Registering service {}", serviceNameSupplier.get());
            cancelDiscovery = discoveryService.register(ResolvingDiscoverable.of(new Discoverable() {
                @Override
                public String getName() {
                    return serviceNameSupplier.get();
                }

                @Override
                public InetSocketAddress getSocketAddress() {
                    return httpService.getBindAddress();
                }
            }));
        }

        public void cancelRegistration() {
            log.info("Cancelling discovery registration of service {}", serviceNameSupplier.get());
            cancelDiscovery.cancel();
        }

        /**
         * Simple handler for server.
         */
        public class ServerHandler extends AbstractHttpHandler {
            private final Logger log = LoggerFactory.getLogger(ServerHandler.class);

            @GET
            @Path("/v1/ping/{text}")
            public void ping(@SuppressWarnings("UnusedParameters") HttpRequest request,
                    final HttpResponder responder, @PathParam("text") String text) {
                numRequests.incrementAndGet();
                log.trace("Got text {}", text);

                responder.sendString(HttpResponseStatus.OK, serviceNameSupplier.get());
            }

            @GET
            @Path("/abc/v1/ping/{text}")
            public void abcPing(@SuppressWarnings("UnusedParameters") HttpRequest request,
                    final HttpResponder responder, @PathParam("text") String text) {
                numRequests.incrementAndGet();
                log.trace("Got text {}", text);

                responder.sendString(HttpResponseStatus.OK, serviceNameSupplier.get());
            }

            @GET
            @Path("/def/v1/ping/{text}")
            public void defPing(@SuppressWarnings("UnusedParameters") HttpRequest request,
                    final HttpResponder responder, @PathParam("text") String text) {
                numRequests.incrementAndGet();
                log.trace("Got text {}", text);

                responder.sendString(HttpResponseStatus.OK, serviceNameSupplier.get());
            }

            @GET
            @Path("/v2/ping")
            public void gateway(@SuppressWarnings("UnusedParameters") HttpRequest request,
                    final HttpResponder responder) {
                numRequests.incrementAndGet();

                responder.sendString(HttpResponseStatus.OK, serviceNameSupplier.get());
            }

            @GET
            @Path("/abc/v1/status")
            public void abcStatus(HttpRequest request, HttpResponder responder) {
                numRequests.incrementAndGet();
                responder.sendStatus(HttpResponseStatus.OK);
            }

            @GET
            @Path("/def/v1/status")
            public void defStatus(HttpRequest request, HttpResponder responder) {
                numRequests.incrementAndGet();
                responder.sendStatus(HttpResponseStatus.OK);
            }

            @GET
            @Path("/v1/timeout/{timeout-millis}")
            public void timeout(HttpRequest request, HttpResponder responder,
                    @PathParam("timeout-millis") int timeoutMillis) throws InterruptedException {
                numRequests.incrementAndGet();
                TimeUnit.MILLISECONDS.sleep(timeoutMillis);
                responder.sendStatus(HttpResponseStatus.OK);
            }

            @POST
            @Path("/v1/upload")
            public void upload(HttpRequest request, HttpResponder responder) throws IOException {
                ChannelBuffer content = request.getContent();

                int readableBytes;
                ChunkResponder chunkResponder = responder.sendChunkStart(HttpResponseStatus.OK,
                        ImmutableMultimap.<String, String>of());
                while ((readableBytes = content.readableBytes()) > 0) {
                    int read = Math.min(readableBytes, CHUNK_SIZE);
                    chunkResponder.sendChunk(content.readSlice(read));
                    //TimeUnit.MILLISECONDS.sleep(RANDOM.nextInt(1));
                }
                chunkResponder.close();
            }
        }
    }
}