/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.http.netty.pipelining; import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.test.ESTestCase; import org.jboss.netty.bootstrap.ClientBootstrap; import org.jboss.netty.bootstrap.ServerBootstrap; import org.jboss.netty.channel.*; import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory; import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; import org.jboss.netty.handler.codec.http.*; import org.jboss.netty.util.HashedWheelTimer; import org.jboss.netty.util.Timeout; import org.jboss.netty.util.TimerTask; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.jboss.netty.buffer.ChannelBuffers.EMPTY_BUFFER; import static org.jboss.netty.buffer.ChannelBuffers.copiedBuffer; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*; import static org.jboss.netty.handler.codec.http.HttpHeaders.Values.CHUNKED; import static org.jboss.netty.handler.codec.http.HttpHeaders.Values.KEEP_ALIVE; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.OK; import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; import static org.jboss.netty.util.CharsetUtil.UTF_8; /** * */ public class HttpPipeliningHandlerTests extends ESTestCase { private static final long RESPONSE_TIMEOUT = 10000L; private static final long CONNECTION_TIMEOUT = 10000L; private static final String CONTENT_TYPE_TEXT = "text/plain; charset=UTF-8"; // TODO make me random private static final InetSocketAddress HOST_ADDR = new InetSocketAddress(InetAddress.getLoopbackAddress(), 9080); private static final String PATH1 = "/1"; private static final String PATH2 = "/2"; private static final String SOME_RESPONSE_TEXT = "some response for "; private ClientBootstrap clientBootstrap; private ServerBootstrap serverBootstrap; private CountDownLatch responsesIn; private final List<String> responses = new ArrayList<>(2); private HashedWheelTimer timer; @Before public void startBootstraps() { clientBootstrap = new ClientBootstrap(new NioClientSocketChannelFactory()); clientBootstrap.setPipelineFactory(new ChannelPipelineFactory() { @Override public ChannelPipeline getPipeline() throws Exception { return Channels.pipeline( new HttpClientCodec(), new ClientHandler() ); } }); serverBootstrap = new ServerBootstrap(new NioServerSocketChannelFactory()); serverBootstrap.setPipelineFactory(new ChannelPipelineFactory() { @Override public ChannelPipeline getPipeline() throws Exception { return Channels.pipeline( new HttpRequestDecoder(), new HttpResponseEncoder(), new HttpPipeliningHandler(10000), new ServerHandler() ); } }); serverBootstrap.bind(HOST_ADDR); timer = new HashedWheelTimer(); } @After public void releaseResources() { timer.stop(); serverBootstrap.shutdown(); serverBootstrap.releaseExternalResources(); clientBootstrap.shutdown(); clientBootstrap.releaseExternalResources(); } @Test public void shouldReturnMessagesInOrder() throws InterruptedException { responsesIn = new CountDownLatch(1); responses.clear(); final ChannelFuture connectionFuture = clientBootstrap.connect(HOST_ADDR); assertTrue(connectionFuture.await(CONNECTION_TIMEOUT)); final Channel clientChannel = connectionFuture.getChannel(); // NetworkAddress.format makes a proper HOST header. final HttpRequest request1 = new DefaultHttpRequest( HTTP_1_1, HttpMethod.GET, PATH1); request1.headers().add(HOST, NetworkAddress.format(HOST_ADDR)); final HttpRequest request2 = new DefaultHttpRequest( HTTP_1_1, HttpMethod.GET, PATH2); request2.headers().add(HOST, NetworkAddress.format(HOST_ADDR)); clientChannel.write(request1); clientChannel.write(request2); responsesIn.await(RESPONSE_TIMEOUT, MILLISECONDS); assertTrue(responses.contains(SOME_RESPONSE_TEXT + PATH1)); assertTrue(responses.contains(SOME_RESPONSE_TEXT + PATH2)); } public class ClientHandler extends SimpleChannelUpstreamHandler { @Override public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent e) { final Object message = e.getMessage(); if (message instanceof HttpChunk) { final HttpChunk response = (HttpChunk) e.getMessage(); if (!response.isLast()) { final String content = response.getContent().toString(UTF_8); responses.add(content); if (content.equals(SOME_RESPONSE_TEXT + PATH2)) { responsesIn.countDown(); } } } } } public class ServerHandler extends SimpleChannelUpstreamHandler { private final AtomicBoolean sendFinalChunk = new AtomicBoolean(false); @Override public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent e) throws InterruptedException { final HttpRequest request = (HttpRequest) e.getMessage(); final OrderedUpstreamMessageEvent oue = (OrderedUpstreamMessageEvent) e; final String uri = request.getUri(); final HttpResponse initialChunk = new DefaultHttpResponse(HTTP_1_1, OK); initialChunk.headers().add(CONTENT_TYPE, CONTENT_TYPE_TEXT); initialChunk.headers().add(CONNECTION, KEEP_ALIVE); initialChunk.headers().add(TRANSFER_ENCODING, CHUNKED); ctx.sendDownstream(new OrderedDownstreamChannelEvent(oue, 0, false, initialChunk)); timer.newTimeout(new ChunkWriter(ctx, e, uri, oue, 1), 0, MILLISECONDS); } private class ChunkWriter implements TimerTask { private final ChannelHandlerContext ctx; private final MessageEvent e; private final String uri; private final OrderedUpstreamMessageEvent oue; private final int subSequence; public ChunkWriter(final ChannelHandlerContext ctx, final MessageEvent e, final String uri, final OrderedUpstreamMessageEvent oue, final int subSequence) { this.ctx = ctx; this.e = e; this.uri = uri; this.oue = oue; this.subSequence = subSequence; } @Override public void run(final Timeout timeout) { if (sendFinalChunk.get() && subSequence > 1) { final HttpChunk finalChunk = new DefaultHttpChunk(EMPTY_BUFFER); ctx.sendDownstream(new OrderedDownstreamChannelEvent(oue, subSequence, true, finalChunk)); } else { final HttpChunk chunk = new DefaultHttpChunk(copiedBuffer(SOME_RESPONSE_TEXT + uri, UTF_8)); ctx.sendDownstream(new OrderedDownstreamChannelEvent(oue, subSequence, false, chunk)); timer.newTimeout(new ChunkWriter(ctx, e, uri, oue, subSequence + 1), 0, MILLISECONDS); if (uri.equals(PATH2)) { sendFinalChunk.set(true); } } } } } }