diff --git a/jetty-rhttp/README.txt b/jetty-rhttp/README.txt new file mode 100644 index 00000000000..46ca4e743e4 --- /dev/null +++ b/jetty-rhttp/README.txt @@ -0,0 +1,33 @@ +Reverse HTTP + +The HTTP server paradigm is a valuable abstraction for browsing and accessing data and applications in a RESTful fashion from thin clients or +other applications. However, when it comes to mobile devices, the server paradigm is often not available because those devices exist on +restricted networks that do not allow inbound connections. These devices (eg. phones, tablets, industrial controllers, etc.) often have +signficant content (eg. photos, video, music, contacts, etc.) and services (eg. GPS, phone, modem, camera, sound) that are worthwile to access +remotely and often the HTTP server model is very applicable. + +The Jetty reverse HTTP module provides a gateway that efficiently allows HTTP connectivety to servers running in outbound-only networks. There are two key components: + +The reverse HTTP connector is a jetty connector (like the HTTP, SSL, AJP connectors) that accepts HTTP requests for the Jetty server instance. However, the reverse HTTP connector does not accept inbound TCP/IP connections. Instead it makes an outbound HTTP connection to the reverse HTTP gateway and uses a long polling mechanism to efficiently and asynchronously fetch requests and send responses. + +The reverse HTTP gateway is a jetty server that accepts inbound connections from one or more Reverse HTTP connectors and makes them available as normal HTTP targets. + +To demonstrate this from a source release, first run a gateway instance: + + cd jetty-reverse-http/reverse-http-gateway + mvn exec:java + +In another window, you can run 3 test servers with reverse connectors with: + + cd jetty-reverse-http/reverse-http-connector + mvn exec:java + + +The three servers are using context path ID's at the gateway (virtual host and cookie based mappings can also be done), so you can access the +three servers via the gateway at: + + http://localhost:8080/gw/A + http://localhost:8080/gw/B + http://localhost:8080/gw/C + + diff --git a/jetty-rhttp/jetty-rhttp-client/pom.xml b/jetty-rhttp/jetty-rhttp-client/pom.xml new file mode 100644 index 00000000000..8fcf7253b2d --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-client/pom.xml @@ -0,0 +1,56 @@ + + + + org.eclipse.jetty.rhttp + jetty-rhttp-project + 9.0.0-SNAPSHOT + + + 4.0.0 + reverse-http-client + jar + Jetty :: Reverse HTTP :: Client + + + + org.eclipse.jetty + jetty-util + + + org.eclipse.jetty + jetty-io + + + org.eclipse.jetty + jetty-client + ${project.version} + + + org.eclipse.jetty + jetty-http + ${project.version} + + + org.apache.httpcomponents + httpclient + 4.0 + + + net.jcip + jcip-annotations + 1.0 + provided + + + junit + junit + + + org.eclipse.jetty + jetty-server + ${project.version} + test + + + + diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/AbstractClient.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/AbstractClient.java new file mode 100644 index 00000000000..afdf0a5e860 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/AbstractClient.java @@ -0,0 +1,270 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.client; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * @version $Revision$ $Date$ + */ +public abstract class AbstractClient extends AbstractLifeCycle implements RHTTPClient +{ + private final Logger logger = Log.getLogger("org.mortbay.jetty.rhttp.client"); + private final List listeners = new CopyOnWriteArrayList(); + private final List clientListeners = new CopyOnWriteArrayList(); + private final String targetId; + private volatile Status status = Status.DISCONNECTED; + + public AbstractClient(String targetId) + { + this.targetId = targetId; + } + + public String getGatewayURI() + { + return "http://"+getHost()+":"+getPort()+getPath(); + } + + public String getTargetId() + { + return targetId; + } + + public Logger getLogger() + { + return logger; + } + + public void addListener(RHTTPListener listener) + { + listeners.add(listener); + } + + public void removeListener(RHTTPListener listener) + { + listeners.remove(listener); + } + + public void addClientListener(ClientListener listener) + { + clientListeners.add(listener); + } + + public void removeClientListener(ClientListener listener) + { + clientListeners.remove(listener); + } + + protected void notifyRequests(List requests) + { + for (RHTTPRequest request : requests) + { + for (RHTTPListener listener : listeners) + { + try + { + listener.onRequest(request); + } + catch (Throwable x) + { + logger.warn("Listener " + listener + " threw", x); + try + { + deliver(newExceptionResponse(request.getId(), x)); + } + catch (IOException xx) + { + logger.debug("Could not deliver exception response", xx); + } + } + } + } + } + + protected RHTTPResponse newExceptionResponse(int requestId, Throwable x) + { + try + { + int statusCode = 500; + String statusMessage = "Internal Server Error"; + Map headers = new HashMap(); + byte[] body = x.toString().getBytes("UTF-8"); + return new RHTTPResponse(requestId, statusCode, statusMessage, headers, body); + } + catch (UnsupportedEncodingException xx) + { + throw new AssertionError(xx); + } + } + + protected void notifyConnectRequired() + { + for (ClientListener listener : clientListeners) + { + try + { + listener.connectRequired(); + } + catch (Throwable x) + { + logger.warn("ClientListener " + listener + " threw", x); + } + } + } + + protected void notifyConnectException() + { + for (ClientListener listener : clientListeners) + { + try + { + listener.connectException(); + } + catch (Throwable x) + { + logger.warn("ClientListener " + listener + " threw", x); + } + } + } + + protected void notifyConnectClosed() + { + for (ClientListener listener : clientListeners) + { + try + { + listener.connectClosed(); + } + catch (Throwable xx) + { + logger.warn("ClientListener " + listener + " threw", xx); + } + } + } + + protected void notifyDeliverException(RHTTPResponse response) + { + for (ClientListener listener : clientListeners) + { + try + { + listener.deliverException(response); + } + catch (Throwable x) + { + logger.warn("ClientListener " + listener + " threw", x); + } + } + } + + protected String urlEncode(String value) + { + try + { + return URLEncoder.encode(value, "UTF-8"); + } + catch (UnsupportedEncodingException x) + { + getLogger().debug("", x); + return null; + } + } + + protected boolean isConnected() + { + return status == Status.CONNECTED; + } + + protected boolean isDisconnecting() + { + return status == Status.DISCONNECTING; + } + + protected boolean isDisconnected() + { + return status == Status.DISCONNECTED; + } + + public void connect() throws IOException + { + if (isDisconnected()) + status = Status.CONNECTING; + + syncHandshake(); + this.status = Status.CONNECTED; + + asyncConnect(); + } + + public void disconnect() throws IOException + { + if (isConnected()) + { + status = Status.DISCONNECTING; + try + { + syncDisconnect(); + } + finally + { + status = Status.DISCONNECTED; + } + } + } + + public void deliver(RHTTPResponse response) throws IOException + { + asyncDeliver(response); + } + + protected abstract void syncHandshake() throws IOException; + + protected abstract void asyncConnect(); + + protected abstract void syncDisconnect() throws IOException; + + protected abstract void asyncDeliver(RHTTPResponse response); + + protected void connectComplete(byte[] responseContent) throws IOException + { + List requests = RHTTPRequest.fromFrameBytes(responseContent); + getLogger().debug("Client {} connect returned from gateway, requests {}", getTargetId(), requests); + + // Requests are arrived, reconnect while we process them + if (!isDisconnecting() && !isDisconnected()) + asyncConnect(); + + notifyRequests(requests); + } + + protected enum Status + { + CONNECTING, CONNECTED, DISCONNECTING, DISCONNECTED + } +} diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/ApacheClient.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/ApacheClient.java new file mode 100644 index 00000000000..38723666a8a --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/ApacheClient.java @@ -0,0 +1,156 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.client; + +import java.io.IOException; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.NoHttpResponseException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.util.EntityUtils; + +/** + * Implementation of {@link RHTTPClient} that uses Apache's HttpClient. + * + * @version $Revision$ $Date$ + */ +public class ApacheClient extends AbstractClient +{ + private final HttpClient httpClient; + private final String gatewayPath; + + public ApacheClient(HttpClient httpClient, String gatewayPath, String targetId) + { + super(targetId); + this.httpClient = httpClient; + this.gatewayPath = gatewayPath; + } + + public String getHost() + { + return ((HttpHost)httpClient.getParams().getParameter("http.default-host")).getHostName(); + } + + public int getPort() + { + return ((HttpHost)httpClient.getParams().getParameter("http.default-host")).getPort(); + } + + public String getPath() + { + return gatewayPath; + } + + protected void syncHandshake() throws IOException + { + HttpPost handshake = new HttpPost(gatewayPath + "/" + urlEncode(getTargetId()) + "/handshake"); + HttpResponse response = httpClient.execute(handshake); + int statusCode = response.getStatusLine().getStatusCode(); + HttpEntity entity = response.getEntity(); + if (entity != null) + entity.consumeContent(); + if (statusCode != HttpStatus.SC_OK) + throw new IOException("Handshake failed"); + getLogger().debug("Client {} handshake returned from gateway", getTargetId(), null); + } + + protected void asyncConnect() + { + new Thread() + { + @Override + public void run() + { + try + { + HttpPost connect = new HttpPost(gatewayPath + "/" + urlEncode(getTargetId()) + "/connect"); + getLogger().debug("Client {} connect sent to gateway", getTargetId(), null); + HttpResponse response = httpClient.execute(connect); + int statusCode = response.getStatusLine().getStatusCode(); + HttpEntity entity = response.getEntity(); + byte[] responseContent = EntityUtils.toByteArray(entity); + if (statusCode == HttpStatus.SC_OK) + connectComplete(responseContent); + else if (statusCode == HttpStatus.SC_UNAUTHORIZED) + notifyConnectRequired(); + else + notifyConnectException(); + } + catch (NoHttpResponseException x) + { + notifyConnectClosed(); + } + catch (IOException x) + { + getLogger().debug("", x); + notifyConnectException(); + } + } + }.start(); + } + + protected void syncDisconnect() throws IOException + { + HttpPost disconnect = new HttpPost(gatewayPath + "/" + urlEncode(getTargetId()) + "/disconnect"); + HttpResponse response = httpClient.execute(disconnect); + int statusCode = response.getStatusLine().getStatusCode(); + HttpEntity entity = response.getEntity(); + if (entity != null) + entity.consumeContent(); + if (statusCode != HttpStatus.SC_OK) + throw new IOException("Disconnect failed"); + getLogger().debug("Client {} disconnect returned from gateway", getTargetId(), null); + } + + protected void asyncDeliver(final RHTTPResponse response) + { + new Thread() + { + @Override + public void run() + { + try + { + HttpPost deliver = new HttpPost(gatewayPath + "/" + urlEncode(getTargetId()) + "/deliver"); + deliver.setEntity(new ByteArrayEntity(response.getFrameBytes())); + getLogger().debug("Client {} deliver sent to gateway, response {}", getTargetId(), response); + HttpResponse httpResponse = httpClient.execute(deliver); + int statusCode = httpResponse.getStatusLine().getStatusCode(); + HttpEntity entity = httpResponse.getEntity(); + if (entity != null) + entity.consumeContent(); + if (statusCode == HttpStatus.SC_UNAUTHORIZED) + notifyConnectRequired(); + else if (statusCode != HttpStatus.SC_OK) + notifyDeliverException(response); + } + catch (IOException x) + { + getLogger().debug("", x); + notifyDeliverException(response); + } + } + }.start(); + } +} diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/ClientListener.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/ClientListener.java new file mode 100644 index 00000000000..7af843fffb1 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/ClientListener.java @@ -0,0 +1,67 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.client; + +/** + * A listener for network-related events happening on the gateway client. + * + * @version $Revision$ $Date$ + */ +public interface ClientListener +{ + /** + * Called when the client detects that the server requested a new connect. + */ + public void connectRequired(); + + /** + * Called when the client detects that the connection has been closed by the server. + */ + public void connectClosed(); + + /** + * Called when the client detects a generic exception while trying to connect to the server. + */ + public void connectException(); + + /** + * Called when the client detects a generic exception while tryint to deliver to the server. + * @param response the Response object that should have been sent to the server + */ + public void deliverException(RHTTPResponse response); + + public static class Adapter implements ClientListener + { + public void connectRequired() + { + } + + public void connectClosed() + { + } + + public void connectException() + { + } + + public void deliverException(RHTTPResponse response) + { + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/JettyClient.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/JettyClient.java new file mode 100644 index 00000000000..26d7e519141 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/JettyClient.java @@ -0,0 +1,306 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.client; + +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; + +import org.eclipse.jetty.client.Address; +import org.eclipse.jetty.client.ContentExchange; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpExchange; +import org.eclipse.jetty.http.HttpMethods; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.io.EofException; + +/** + * Implementation of {@link RHTTPClient} that uses Jetty's HttpClient. + * + * @version $Revision$ $Date$ + */ +public class JettyClient extends AbstractClient +{ + private final HttpClient httpClient; + private final Address gatewayAddress; + private final String gatewayPath; + + public JettyClient(HttpClient httpClient, Address gatewayAddress, String gatewayPath, String targetId) + { + super(targetId); + this.httpClient = httpClient; + this.gatewayAddress = gatewayAddress; + this.gatewayPath = gatewayPath; + } + + public JettyClient(HttpClient httpClient, String gatewayURI, String targetId) + { + super(targetId); + + HttpURI uri = new HttpURI(gatewayURI); + + this.httpClient = httpClient; + this.gatewayAddress = new Address(uri.getHost(),uri.getPort()); + this.gatewayPath = uri.getPath(); + } + + public String getHost() + { + return gatewayAddress.getHost(); + } + + public int getPort() + { + return gatewayAddress.getPort(); + } + + public String getPath() + { + return gatewayPath; + } + + @Override + protected void doStart() throws Exception + { + httpClient.start(); + super.doStart(); + } + + @Override + protected void doStop() throws Exception + { + super.doStop(); + httpClient.stop(); + } + + protected void syncHandshake() throws IOException + { + HandshakeExchange exchange = new HandshakeExchange(); + exchange.setMethod(HttpMethods.POST); + exchange.setAddress(gatewayAddress); + exchange.setURI(gatewayPath + "/" + urlEncode(getTargetId()) + "/handshake"); + httpClient.send(exchange); + getLogger().debug("Client {} handshake sent to gateway", getTargetId(), null); + + try + { + int exchangeStatus = exchange.waitForDone(); + if (exchangeStatus != HttpExchange.STATUS_COMPLETED) + throw new IOException("Handshake failed"); + if (exchange.getResponseStatus() != 200) + throw new IOException("Handshake failed"); + getLogger().debug("Client {} handshake returned from gateway", getTargetId(), null); + } + catch (InterruptedException x) + { + Thread.currentThread().interrupt(); + throw newIOException(x); + } + } + + private IOException newIOException(Throwable x) + { + return (IOException)new IOException().initCause(x); + } + + protected void asyncConnect() + { + try + { + ConnectExchange exchange = new ConnectExchange(); + exchange.setMethod(HttpMethods.POST); + exchange.setAddress(gatewayAddress); + exchange.setURI(gatewayPath + "/" + urlEncode(getTargetId()) + "/connect"); + httpClient.send(exchange); + getLogger().debug("Client {} connect sent to gateway", getTargetId(), null); + } + catch (IOException x) + { + getLogger().debug("Could not send exchange", x); + throw new RuntimeException(x); + } + } + + protected void syncDisconnect() throws IOException + { + DisconnectExchange exchange = new DisconnectExchange(); + exchange.setMethod(HttpMethods.POST); + exchange.setAddress(gatewayAddress); + exchange.setURI(gatewayPath + "/" + urlEncode(getTargetId()) + "/disconnect"); + httpClient.send(exchange); + getLogger().debug("Client {} disconnect sent to gateway", getTargetId(), null); + try + { + int status = exchange.waitForDone(); + if (status != HttpExchange.STATUS_COMPLETED) + throw new IOException("Disconnect failed"); + if (exchange.getResponseStatus() != 200) + throw new IOException("Disconnect failed"); + getLogger().debug("Client {} disconnect returned from gateway", getTargetId(), null); + } + catch (InterruptedException x) + { + Thread.currentThread().interrupt(); + throw newIOException(x); + } + } + + protected void asyncDeliver(RHTTPResponse response) + { + try + { + DeliverExchange exchange = new DeliverExchange(response); + exchange.setMethod(HttpMethods.POST); + exchange.setAddress(gatewayAddress); + exchange.setURI(gatewayPath + "/" + urlEncode(getTargetId()) + "/deliver"); + exchange.setRequestContent(new ByteArrayBuffer(response.getFrameBytes())); + httpClient.send(exchange); + getLogger().debug("Client {} deliver sent to gateway, response {}", getTargetId(), response); + } + catch (IOException x) + { + getLogger().debug("Could not send exchange", x); + throw new RuntimeException(x); + } + } + + protected class HandshakeExchange extends ContentExchange + { + protected HandshakeExchange() + { + super(true); + } + + @Override + protected void onConnectionFailed(Throwable x) + { + getLogger().warn(x.toString()); + getLogger().debug(x); + } + } + + protected class ConnectExchange extends ContentExchange + { + private final ByteArrayOutputStream content = new ByteArrayOutputStream(); + + protected ConnectExchange() + { + super(true); + } + + @Override + protected void onResponseContent(Buffer buffer) throws IOException + { + buffer.writeTo(content); + } + + @Override + protected void onResponseComplete() + { + int responseStatus = getResponseStatus(); + if (responseStatus == 200) + { + try + { + connectComplete(content.toByteArray()); + } + catch (IOException x) + { + onException(x); + } + } + else if (responseStatus == 401) + { + notifyConnectRequired(); + } + else + { + notifyConnectException(); + } + } + + @Override + protected void onException(Throwable x) + { + getLogger().debug(x); + if (x instanceof EofException || x instanceof EOFException) + { + notifyConnectClosed(); + } + else + { + notifyConnectException(); + } + } + + @Override + protected void onConnectionFailed(Throwable x) + { + getLogger().debug(x); + } + } + + protected class DisconnectExchange extends ContentExchange + { + protected DisconnectExchange() + { + super(true); + } + } + + protected class DeliverExchange extends ContentExchange + { + private final RHTTPResponse response; + + protected DeliverExchange(RHTTPResponse response) + { + super(true); + this.response = response; + } + + @Override + protected void onResponseComplete() throws IOException + { + int responseStatus = getResponseStatus(); + if (responseStatus == 401) + { + notifyConnectRequired(); + } + else if (responseStatus != 200) + { + notifyDeliverException(response); + } + } + + @Override + protected void onException(Throwable x) + { + getLogger().debug(x); + notifyDeliverException(response); + } + + @Override + protected void onConnectionFailed(Throwable x) + { + getLogger().debug(x); + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPClient.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPClient.java new file mode 100644 index 00000000000..bdaa1303558 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPClient.java @@ -0,0 +1,133 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.client; + +import java.io.IOException; + +/** + *

RHTTPClient represent a client of the gateway server.

+ *

A Client has a server side counterpart with which communicates + * using a comet protocol.
The Client, its server-side + * counterpart and the comet protocol form the Half-Object plus Protocol + * pattern.

+ *

A Client must first connect to the gateway server, to let the gateway + * server know its targetId, an identifier that uniquely distinguish this + * Client from other Clients.

+ *

Once connected, the gateway server will use a comet procotol to notify the + * Client of server-side events, and the Client can send + * information to the gateway server to notify it of client-side events.

+ *

Server-side event are notified to {@link RHTTPListener}s, while relevant + * network events are communicated to {@link ClientListener}s.

+ * + * @version $Revision$ $Date$ + */ +public interface RHTTPClient +{ + /** + * @return The gateway uri, typically "http://gatewayhost:gatewayport/gatewaypath". + */ + public String getGatewayURI(); + + /** + * @return The gateway host + */ + public String getHost(); + + /** + * @return The gateway port + */ + public int getPort(); + + /** + * @return The gateway path + */ + public String getPath(); + + /** + * @return the targetId that uniquely identifies this client. + */ + public String getTargetId(); + + /** + *

Connects to the gateway server, establishing the long poll communication + * with the gateway server to be notified of server-side events.

+ *

The connect is performed in two steps: + *

    + *
  • first, a connect message is sent to the gateway server; the gateway server + * will notice this is a first connect message and reply immediately with + * an empty response
  • + *
  • second, another connect message is sent to the gateway server which interprets + * it as a long poll request
  • + *
+ * The long poll request may return either because one or more server-side events + * happened, or because it expired.

+ *

Any connect message after the first is treated as a long poll request.

+ * + * @throws IOException if it is not possible to connect to the gateway server + * @see #disconnect() + */ + public void connect() throws IOException; + + /** + *

Disconnects from the gateway server.

+ *

Just after the disconnect request is processed by to the gateway server, it will + * return the currently outstanding long poll request.

+ *

If this client is not connected, it does nothing

+ * + * @throws IOException if it is not possible to contact the gateway server to disconnect + * @see #connect() + */ + public void disconnect() throws IOException; + + /** + *

Sends a response to the gateway server.

+ * + * @param response the response to send + * @throws IOException if it is not possible to contact the gateway server + */ + public void deliver(RHTTPResponse response) throws IOException; + + /** + *

Adds the given listener to this client.

+ * @param listener the listener to add + * @see #removeListener(RHTTPListener) + */ + public void addListener(RHTTPListener listener); + + /** + *

Removes the given listener from this client.

+ * @param listener the listener to remove + * @see #addListener(RHTTPListener) + */ + public void removeListener(RHTTPListener listener); + + /** + *

Adds the given client listener to this client.

+ * @param listener the client listener to add + * @see #removeClientListener(ClientListener) + */ + public void addClientListener(ClientListener listener); + + /** + *

Removes the given client listener from this client.

+ * @param listener the client listener to remove + * @see #addClientListener(ClientListener) + */ + public void removeClientListener(ClientListener listener); +} diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPListener.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPListener.java new file mode 100644 index 00000000000..3e679f8adca --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPListener.java @@ -0,0 +1,36 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.client; + +/** + *

Implementations of this class listen for requests arriving from the gateway server + * and notified by {@link RHTTPClient}.

+ * + * @version $Revision$ $Date$ + */ +public interface RHTTPListener +{ + /** + * Callback method called by {@link RHTTPClient} to inform that the gateway server + * sent a request to the gateway client. + * @param request the request sent by the gateway server. + * @throws Exception allowed to be thrown by implementations + */ + public void onRequest(RHTTPRequest request) throws Exception; +} diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPRequest.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPRequest.java new file mode 100644 index 00000000000..0daf4bccca3 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPRequest.java @@ -0,0 +1,266 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.client; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jetty.http.HttpParser; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.ByteArrayBuffer; + +/** + *

Represents the external request information that is carried over the comet protocol.

+ *

Instances of this class are converted into an opaque byte array of the form:

+ *
+ * <request-id> SPACE <request-length> CRLF
+ * <external-request>
+ * 
+ *

The byte array form is carried as body of a normal HTTP response returned by the gateway server + * to the gateway client.

+ * @see RHTTPResponse + * @version $Revision$ $Date$ + */ +public class RHTTPRequest +{ + private static final String CRLF = "\r\n"; + private static final byte[] CRLF_BYTES = CRLF.getBytes(); + + private final int id; + private final byte[] requestBytes; + private final byte[] frameBytes; + private volatile String method; + private volatile String uri; + private volatile Map headers; + private volatile byte[] body; + + public static List fromFrameBytes(byte[] bytes) + { + List result = new ArrayList(); + int start = 0; + while (start < bytes.length) + { + // Scan until we find the space + int end = start; + while (bytes[end] != ' ') ++end; + int requestId = Integer.parseInt(new String(bytes, start, end - start)); + start = end + 1; + + // Scan until end of line + while (bytes[end] != '\n') ++end; + int length = Integer.parseInt(new String(bytes, start, end - start - 1)); + start = end + 1; + + byte[] requestBytes = new byte[length]; + System.arraycopy(bytes, start, requestBytes, 0, length); + RHTTPRequest request = fromRequestBytes(requestId, requestBytes); + result.add(request); + start += length; + } + return result; + } + + public static RHTTPRequest fromRequestBytes(int requestId, byte[] requestBytes) + { + return new RHTTPRequest(requestId, requestBytes); + } + + public RHTTPRequest(int id, String method, String uri, Map headers, byte[] body) + { + this.id = id; + this.method = method; + this.uri = uri; + this.headers = headers; + this.body = body; + this.requestBytes = toRequestBytes(); + this.frameBytes = toFrameBytes(requestBytes); + } + + private RHTTPRequest(int id, byte[] requestBytes) + { + this.id = id; + this.requestBytes = requestBytes; + this.frameBytes = toFrameBytes(requestBytes); + // Other fields are lazily initialized + } + + private void initialize() + { + try + { + final ByteArrayOutputStream body = new ByteArrayOutputStream(); + HttpParser parser = new HttpParser(new ByteArrayBuffer(requestBytes), new HttpParser.EventHandler() + { + @Override + public void startRequest(Buffer method, Buffer uri, Buffer httpVersion) throws IOException + { + RHTTPRequest.this.method = method.toString("UTF-8"); + RHTTPRequest.this.uri = uri.toString("UTF-8"); + RHTTPRequest.this.headers = new LinkedHashMap(); + } + + @Override + public void startResponse(Buffer httpVersion, int statusCode, Buffer statusMessage) throws IOException + { + } + + @Override + public void parsedHeader(Buffer name, Buffer value) throws IOException + { + RHTTPRequest.this.headers.put(name.toString("UTF-8"), value.toString("UTF-8")); + } + + @Override + public void content(Buffer content) throws IOException + { + content.writeTo(body); + } + }); + parser.parse(); + this.body = body.toByteArray(); + } + catch (IOException x) + { + // Cannot happen: we're parsing from a byte[], not from an I/O stream + throw new AssertionError(x); + } + } + + public int getId() + { + return id; + } + + public byte[] getRequestBytes() + { + return requestBytes; + } + + public byte[] getFrameBytes() + { + return frameBytes; + } + + public String getMethod() + { + if (method == null) + initialize(); + return method; + } + + public String getURI() + { + if (uri == null) + initialize(); + return uri; + } + + public Map getHeaders() + { + if (headers == null) + initialize(); + return headers; + } + + public byte[] getBody() + { + if (body == null) + initialize(); + return body; + } + + private byte[] toRequestBytes() + { + try + { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write(method.getBytes("UTF-8")); + bytes.write(' '); + bytes.write(uri.getBytes("UTF-8")); + bytes.write(' '); + bytes.write("HTTP/1.1".getBytes("UTF-8")); + bytes.write(CRLF_BYTES); + for (Map.Entry entry : headers.entrySet()) + { + bytes.write(entry.getKey().getBytes("UTF-8")); + bytes.write(':'); + bytes.write(' '); + bytes.write(entry.getValue().getBytes("UTF-8")); + bytes.write(CRLF_BYTES); + } + bytes.write(CRLF_BYTES); + bytes.write(body); + bytes.close(); + return bytes.toByteArray(); + } + catch (IOException x) + { + throw new AssertionError(x); + } + } + + private byte[] toFrameBytes(byte[] requestBytes) + { + try + { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write(String.valueOf(id).getBytes("UTF-8")); + bytes.write(' '); + bytes.write(String.valueOf(requestBytes.length).getBytes("UTF-8")); + bytes.write(CRLF_BYTES); + bytes.write(requestBytes); + bytes.close(); + return bytes.toByteArray(); + } + catch (IOException x) + { + throw new AssertionError(x); + } + } + + @Override + public String toString() + { + // Use fields to avoid initialization + StringBuilder builder = new StringBuilder(); + builder.append(id).append(" "); + builder.append(method).append(" "); + builder.append(uri).append(" "); + builder.append(requestBytes.length).append("/"); + builder.append(frameBytes.length); + return builder.toString(); + } + + public String toLongString() + { + // Use getters to trigger initialization + StringBuilder builder = new StringBuilder(); + builder.append(id).append(" "); + builder.append(getMethod()).append(" "); + builder.append(getURI()).append(CRLF); + for (Map.Entry header : getHeaders().entrySet()) + builder.append(header.getKey()).append(": ").append(header.getValue()).append(CRLF); + builder.append(getBody().length).append(" body bytes").append(CRLF); + return builder.toString(); + } +} diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPResponse.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPResponse.java new file mode 100644 index 00000000000..3357fabf17a --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RHTTPResponse.java @@ -0,0 +1,256 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.client; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.eclipse.jetty.http.HttpParser; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.ByteArrayBuffer; + +/** + *

Represents the resource provider response information that is carried over the comet protocol.

+ *

Instances of this class are converted into an opaque byte array of the form:

+ *
+ * <request-id> SPACE <response-length> CRLF
+ * <resource-response>
+ * 
+ *

The byte array form is carried as body of a normal HTTP request made by the gateway client to + * the gateway server.

+ * @see RHTTPRequest + * @version $Revision$ $Date$ + */ +public class RHTTPResponse +{ + private static final String CRLF = "\r\n"; + private static final byte[] CRLF_BYTES = CRLF.getBytes(); + + private final int id; + private final byte[] responseBytes; + private final byte[] frameBytes; + private volatile int code; + private volatile String message; + private volatile Map headers; + private volatile byte[] body; + + public static RHTTPResponse fromFrameBytes(byte[] bytes) + { + int start = 0; + // Scan until we find the space + int end = start; + while (bytes[end] != ' ') ++end; + int responseId = Integer.parseInt(new String(bytes, start, end - start)); + start = end + 1; + + // Scan until end of line + while (bytes[end] != '\n') ++end; + int length = Integer.parseInt(new String(bytes, start, end - start - 1)); + start = end + 1; + + byte[] responseBytes = new byte[length]; + System.arraycopy(bytes, start, responseBytes, 0, length); + return fromResponseBytes(responseId, responseBytes); + } + + public static RHTTPResponse fromResponseBytes(int id, byte[] responseBytes) + { + return new RHTTPResponse(id, responseBytes); + } + + public RHTTPResponse(int id, int code, String message, Map headers, byte[] body) + { + this.id = id; + this.code = code; + this.message = message; + this.headers = headers; + this.body = body; + this.responseBytes = toResponseBytes(); + this.frameBytes = toFrameBytes(responseBytes); + } + + private RHTTPResponse(int id, byte[] responseBytes) + { + this.id = id; + this.responseBytes = responseBytes; + this.frameBytes = toFrameBytes(responseBytes); + // Other fields are lazily initialized + } + + private void initialize() + { + try + { + final ByteArrayOutputStream body = new ByteArrayOutputStream(); + HttpParser parser = new HttpParser(new ByteArrayBuffer(responseBytes), new HttpParser.EventHandler() + { + @Override + public void startRequest(Buffer method, Buffer uri, Buffer httpVersion) throws IOException + { + } + + @Override + public void startResponse(Buffer httpVersion, int statusCode, Buffer statusMessage) throws IOException + { + RHTTPResponse.this.code = statusCode; + RHTTPResponse.this.message = statusMessage.toString("UTF-8"); + RHTTPResponse.this.headers = new LinkedHashMap(); + } + + @Override + public void parsedHeader(Buffer name, Buffer value) throws IOException + { + RHTTPResponse.this.headers.put(name.toString("UTF-8"), value.toString("UTF-8")); + } + + @Override + public void content(Buffer content) throws IOException + { + content.writeTo(body); + } + }); + parser.parse(); + this.body = body.toByteArray(); + } + catch (IOException x) + { + // Cannot happen: we're parsing from a byte[], not from an I/O stream + throw new AssertionError(x); + } + } + + public int getId() + { + return id; + } + + public byte[] getResponseBytes() + { + return responseBytes; + } + + public byte[] getFrameBytes() + { + return frameBytes; + } + + public int getStatusCode() + { + if (code == 0) + initialize(); + return code; + } + + public String getStatusMessage() + { + if (message == null) + initialize(); + return message; + } + + public Map getHeaders() + { + if (headers == null) + initialize(); + return headers; + } + + public byte[] getBody() + { + if (body == null) + initialize(); + return body; + } + + private byte[] toResponseBytes() + { + try + { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write("HTTP/1.1".getBytes("UTF-8")); + bytes.write(' '); + bytes.write(String.valueOf(code).getBytes("UTF-8")); + bytes.write(' '); + bytes.write(message.getBytes("UTF-8")); + bytes.write(CRLF_BYTES); + for (Map.Entry entry : headers.entrySet()) + { + bytes.write(entry.getKey().getBytes("UTF-8")); + bytes.write(':'); + bytes.write(' '); + bytes.write(entry.getValue().getBytes("UTF-8")); + bytes.write(CRLF_BYTES); + } + bytes.write(CRLF_BYTES); + bytes.write(body); + bytes.close(); + return bytes.toByteArray(); + } + catch (IOException x) + { + throw new AssertionError(x); + } + } + + private byte[] toFrameBytes(byte[] responseBytes) + { + try + { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write(String.valueOf(id).getBytes("UTF-8")); + bytes.write(' '); + bytes.write(String.valueOf(responseBytes.length).getBytes("UTF-8")); + bytes.write(CRLF_BYTES); + bytes.write(responseBytes); + return bytes.toByteArray(); + } + catch (IOException x) + { + throw new AssertionError(x); + } + } + + @Override + public String toString() + { + // Use fields to avoid initialization + StringBuilder builder = new StringBuilder(); + builder.append(id).append(" "); + builder.append(code).append(" "); + builder.append(message).append(" "); + builder.append(responseBytes.length).append("/"); + builder.append(frameBytes.length); + return builder.toString(); + } + + public String toLongString() + { + // Use getters to trigger initialization + StringBuilder builder = new StringBuilder(); + builder.append(id).append(" "); + builder.append(getStatusCode()).append(" "); + builder.append(getStatusMessage()).append(CRLF); + for (Map.Entry header : getHeaders().entrySet()) + builder.append(header.getKey()).append(": ").append(header.getValue()).append(CRLF); + builder.append(getBody().length).append(" body bytes").append(CRLF); + return builder.toString(); + } +} diff --git a/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RetryingApacheClient.java b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RetryingApacheClient.java new file mode 100644 index 00000000000..b461abbad11 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-client/src/main/java/org/eclipse/jetty/rhttp/client/RetryingApacheClient.java @@ -0,0 +1,112 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.client; + +import java.io.IOException; + +import org.apache.http.client.HttpClient; + +/** + * @version $Revision$ $Date$ + */ +public class RetryingApacheClient extends ApacheClient +{ + public RetryingApacheClient(HttpClient httpClient, String gatewayURI, String targetId) + { + super(httpClient, gatewayURI, targetId); + addClientListener(new RetryClientListener()); + } + + @Override + protected void syncHandshake() throws IOException + { + while (true) + { + try + { + super.syncHandshake(); + break; + } + catch (IOException x) + { + getLogger().debug("Handshake failed, backing off and retrying"); + try + { + Thread.sleep(1000); + } + catch (InterruptedException xx) + { + throw (IOException)new IOException().initCause(xx); + } + } + } + } + + private class RetryClientListener implements ClientListener + { + public void connectRequired() + { + getLogger().debug("Connect requested by server"); + try + { + connect(); + } + catch (IOException x) + { + // The connect() method is retried, so if it fails, it's a hard failure + getLogger().debug("Connect failed after server required connect, giving up"); + } + } + + public void connectClosed() + { + connectException(); + } + + public void connectException() + { + getLogger().debug("Connect failed, backing off and retrying"); + try + { + Thread.sleep(1000); + asyncConnect(); + } + catch (InterruptedException x) + { + // Ignore and stop retrying + Thread.currentThread().interrupt(); + } + } + + public void deliverException(RHTTPResponse response) + { + getLogger().debug("Deliver failed, backing off and retrying"); + try + { + Thread.sleep(1000); + asyncDeliver(response); + } + catch (InterruptedException x) + { + // Ignore and stop retrying + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ApacheClientTest.java b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ApacheClientTest.java new file mode 100644 index 00000000000..478d4d2cd42 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ApacheClientTest.java @@ -0,0 +1,75 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.client; + +import java.io.IOException; + +import org.apache.http.HttpHost; +import org.apache.http.client.HttpRequestRetryHandler; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpParams; +import org.apache.http.protocol.HttpContext; +import org.eclipse.jetty.rhttp.client.ApacheClient; +import org.eclipse.jetty.rhttp.client.RHTTPClient; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.StdErrLog; + + +/** + * @version $Revision$ $Date$ + */ +public class ApacheClientTest extends ClientTest +{ + { + ((StdErrLog)Log.getLog()).setHideStacks(!Log.getLog().isDebugEnabled()); + } + + private ClientConnectionManager connectionManager; + + protected RHTTPClient createClient(int port, String targetId) throws Exception + { + SchemeRegistry schemeRegistry = new SchemeRegistry(); + schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), port)); + connectionManager = new ThreadSafeClientConnManager(new BasicHttpParams(), schemeRegistry); + HttpParams httpParams = new BasicHttpParams(); + httpParams.setParameter("http.default-host", new HttpHost("localhost", port)); + DefaultHttpClient httpClient = new DefaultHttpClient(connectionManager, httpParams); + httpClient.setHttpRequestRetryHandler(new NoRetryHandler()); + return new ApacheClient(httpClient, "", targetId); + } + + protected void destroyClient(RHTTPClient client) throws Exception + { + connectionManager.shutdown(); + } + + private class NoRetryHandler implements HttpRequestRetryHandler + { + public boolean retryRequest(IOException x, int failedAttempts, HttpContext httpContext) + { + return false; + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ClientTest.java b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ClientTest.java new file mode 100644 index 00000000000..dae49dd7be3 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ClientTest.java @@ -0,0 +1,299 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.client; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import junit.framework.TestCase; + +import org.eclipse.jetty.rhttp.client.ClientListener; +import org.eclipse.jetty.rhttp.client.RHTTPClient; +import org.eclipse.jetty.rhttp.client.RHTTPResponse; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.bio.SocketConnector; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.server.nio.SelectChannelConnector; +import org.eclipse.jetty.util.log.Log; + + +/** + * @version $Revision$ $Date$ + */ +public abstract class ClientTest extends TestCase +{ + protected abstract RHTTPClient createClient(int port, String targetId) throws Exception; + + protected abstract void destroyClient(RHTTPClient client) throws Exception; + + public void testConnectNoServer() throws Exception + { + RHTTPClient client = createClient(8080, "test1"); + try + { + client.connect(); + fail(); + } + catch (IOException x) + { + } + finally + { + destroyClient(client); + } + } + + public void testServerExceptionOnHandshake() throws Exception + { + final CountDownLatch serverLatch = new CountDownLatch(1); + + Server server = new Server(); + Connector connector = new SelectChannelConnector(); + server.addConnector(connector); + server.setHandler(new AbstractHandler() + { + public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException + { + request.setHandled(true); + if (target.endsWith("/handshake")) + { + serverLatch.countDown(); + throw new TestException(); + } + } + }); + server.start(); + try + { + RHTTPClient client = createClient(connector.getLocalPort(), "test2"); + try + { + try + { + client.connect(); + fail(); + } + catch (IOException x) + { + } + + assertTrue(serverLatch.await(1000, TimeUnit.MILLISECONDS)); + } + finally + { + destroyClient(client); + } + } + finally + { + server.stop(); + } + } + + public void testServerExceptionOnConnect() throws Exception + { + final CountDownLatch serverLatch = new CountDownLatch(1); + + Server server = new Server(); + Connector connector = new SelectChannelConnector(); + server.addConnector(connector); + server.setHandler(new AbstractHandler() + { + public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException + { + request.setHandled(true); + if (target.endsWith("/connect")) + { + serverLatch.countDown(); + throw new TestException(); + } + } + }); + server.start(); + try + { + RHTTPClient client = createClient(connector.getLocalPort(), "test3"); + try + { + final CountDownLatch connectLatch = new CountDownLatch(1); + client.addClientListener(new ClientListener.Adapter() + { + @Override + public void connectException() + { + connectLatch.countDown(); + } + }); + client.connect(); + + assertTrue(serverLatch.await(1000, TimeUnit.MILLISECONDS)); + assertTrue(connectLatch.await(1000, TimeUnit.MILLISECONDS)); + } + finally + { + destroyClient(client); + } + } + finally + { + server.stop(); + } + } + + public void testServerExceptionOnDeliver() throws Exception + { + final CountDownLatch serverLatch = new CountDownLatch(1); + + Server server = new Server(); + Connector connector = new SelectChannelConnector(); + server.addConnector(connector); + server.setHandler(new AbstractHandler() + { + public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException + { + request.setHandled(true); + if (target.endsWith("/connect")) + { + serverLatch.countDown(); + try + { + // Simulate a long poll timeout + Thread.sleep(10000); + } + catch (InterruptedException x) + { + Thread.currentThread().interrupt(); + } + } + else if (target.endsWith("/deliver")) + { + // Throw an exception on deliver + throw new TestException(); + } + } + }); + server.start(); + try + { + RHTTPClient client = createClient(connector.getLocalPort(), "test4"); + try + { + final CountDownLatch deliverLatch = new CountDownLatch(1); + client.addClientListener(new ClientListener.Adapter() + { + @Override + public void deliverException(RHTTPResponse response) + { + deliverLatch.countDown(); + } + }); + client.connect(); + + assertTrue(serverLatch.await(1000, TimeUnit.MILLISECONDS)); + + client.deliver(new RHTTPResponse(1, 200, "OK", new LinkedHashMap(), new byte[0])); + + assertTrue(deliverLatch.await(1000, TimeUnit.MILLISECONDS)); + } + finally + { + destroyClient(client); + } + } + finally + { + server.stop(); + } + } + + public void testServerShutdownAfterConnect() throws Exception + { + final CountDownLatch connectLatch = new CountDownLatch(1); + final CountDownLatch stopLatch = new CountDownLatch(1); + + Server server = new Server(); + Connector connector = new SocketConnector(); + server.addConnector(connector); + server.setHandler(new AbstractHandler() + { + public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException + { + request.setHandled(true); + if (target.endsWith("/connect")) + { + connectLatch.countDown(); + try + { + Thread.sleep(10000); + } + catch (InterruptedException e) + { + stopLatch.countDown(); + } + } + } + }); + server.start(); + try + { + RHTTPClient client = createClient(connector.getLocalPort(), "test5"); + try + { + final CountDownLatch serverLatch = new CountDownLatch(1); + client.addClientListener(new ClientListener.Adapter() + { + @Override + public void connectClosed() + { + serverLatch.countDown(); + } + }); + client.connect(); + + assertTrue(connectLatch.await(2000, TimeUnit.MILLISECONDS)); + + server.stop(); + assertTrue(stopLatch.await(2000, TimeUnit.MILLISECONDS)); + + assertTrue(serverLatch.await(2000, TimeUnit.MILLISECONDS)); + } + finally + { + destroyClient(client); + } + } + finally + { + server.stop(); + } + } + + public static class TestException extends NullPointerException + { + + } +} diff --git a/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/JettyClientTest.java b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/JettyClientTest.java new file mode 100644 index 00000000000..8ebe859303b --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/JettyClientTest.java @@ -0,0 +1,52 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.client; + +import org.eclipse.jetty.client.Address; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.rhttp.client.JettyClient; +import org.eclipse.jetty.rhttp.client.RHTTPClient; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.StdErrLog; + + +/** + * @version $Revision$ $Date$ + */ +public class JettyClientTest extends ClientTest +{ + { + ((StdErrLog)Log.getLog()).setHideStacks(!Log.getLog().isDebugEnabled()); + } + + private HttpClient httpClient; + + protected RHTTPClient createClient(int port, String targetId) throws Exception + { + ((StdErrLog)Log.getLog()).setSource(true); + httpClient = new HttpClient(); + httpClient.start(); + return new JettyClient(httpClient, new Address("localhost", port), "", targetId); + } + + protected void destroyClient(RHTTPClient client) throws Exception + { + httpClient.stop(); + } +} diff --git a/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/RequestTest.java b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/RequestTest.java new file mode 100644 index 00000000000..b34ab4931e1 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/RequestTest.java @@ -0,0 +1,85 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.client; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jetty.rhttp.client.RHTTPRequest; + +import junit.framework.TestCase; + +/** + * @version $Revision$ $Date$ + */ +public class RequestTest extends TestCase +{ + public void testRequestConversions() throws Exception + { + int id = 1; + String method = "GET"; + String uri = "/test"; + Map headers = new LinkedHashMap(); + headers.put("X", "X"); + headers.put("Y", "Y"); + headers.put("Z", "Z"); + byte[] body = "BODY".getBytes("UTF-8"); + headers.put("Content-Length", String.valueOf(body.length)); + RHTTPRequest request1 = new RHTTPRequest(id, method, uri, headers, body); + byte[] requestBytes1 = request1.getRequestBytes(); + RHTTPRequest request2 = RHTTPRequest.fromRequestBytes(id, requestBytes1); + assertEquals(id, request2.getId()); + assertEquals(method, request2.getMethod()); + assertEquals(uri, request2.getURI()); + assertEquals(headers, request2.getHeaders()); + assertTrue(Arrays.equals(request2.getBody(), body)); + + byte[] requestBytes2 = request2.getRequestBytes(); + assertTrue(Arrays.equals(requestBytes1, requestBytes2)); + } + + public void testFrameConversions() throws Exception + { + int id = 1; + String method = "GET"; + String uri = "/test"; + Map headers = new LinkedHashMap(); + headers.put("X", "X"); + headers.put("Y", "Y"); + headers.put("Z", "Z"); + byte[] body = "BODY".getBytes("UTF-8"); + headers.put("Content-Length", String.valueOf(body.length)); + RHTTPRequest request1 = new RHTTPRequest(id, method, uri, headers, body); + byte[] frameBytes1 = request1.getFrameBytes(); + List requests = RHTTPRequest.fromFrameBytes(frameBytes1); + assertNotNull(requests); + assertEquals(1, requests.size()); + RHTTPRequest request2 = requests.get(0); + assertEquals(id, request2.getId()); + assertEquals(method, request2.getMethod()); + assertEquals(uri, request2.getURI()); + assertEquals(headers, request2.getHeaders()); + assertTrue(Arrays.equals(request2.getBody(), body)); + + byte[] frameBytes2 = request2.getFrameBytes(); + assertTrue(Arrays.equals(frameBytes1, frameBytes2)); + } +} diff --git a/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ResponseTest.java b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ResponseTest.java new file mode 100644 index 00000000000..4873369c379 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-client/src/test/java/org/eclipse/jetty/rhttp/client/ResponseTest.java @@ -0,0 +1,85 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.client; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.eclipse.jetty.rhttp.client.RHTTPResponse; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.StdErrLog; + +import junit.framework.TestCase; + +/** + * @version $Revision$ $Date$ + */ +public class ResponseTest extends TestCase +{ + { + ((StdErrLog)Log.getLog()).setHideStacks(!Log.getLog().isDebugEnabled()); + } + + public void testResponseConversions() throws Exception + { + int id = 1; + int statusCode = 200; + String statusMessage = "OK"; + Map headers = new LinkedHashMap(); + headers.put("X", "X"); + headers.put("Y", "Y"); + headers.put("Z", "Z"); + byte[] body = "BODY".getBytes("UTF-8"); + RHTTPResponse response1 = new RHTTPResponse(id, statusCode, statusMessage, headers, body); + byte[] responseBytes1 = response1.getResponseBytes(); + RHTTPResponse response2 = RHTTPResponse.fromResponseBytes(id, responseBytes1); + assertEquals(id, response2.getId()); + assertEquals(statusCode, response2.getStatusCode()); + assertEquals(statusMessage, response2.getStatusMessage()); + assertEquals(headers, response2.getHeaders()); + assertTrue(Arrays.equals(response2.getBody(), body)); + + byte[] responseBytes2 = response2.getResponseBytes(); + assertTrue(Arrays.equals(responseBytes1, responseBytes2)); + } + + public void testFrameConversions() throws Exception + { + int id = 1; + int statusCode = 200; + String statusMessage = "OK"; + Map headers = new LinkedHashMap(); + headers.put("X", "X"); + headers.put("Y", "Y"); + headers.put("Z", "Z"); + byte[] body = "BODY".getBytes("UTF-8"); + RHTTPResponse response1 = new RHTTPResponse(id, statusCode, statusMessage, headers, body); + byte[] frameBytes1 = response1.getFrameBytes(); + RHTTPResponse response2 = RHTTPResponse.fromFrameBytes(frameBytes1); + assertEquals(id, response2.getId()); + assertEquals(statusCode, response2.getStatusCode()); + assertEquals(response2.getStatusMessage(), statusMessage); + assertEquals(headers, response2.getHeaders()); + assertTrue(Arrays.equals(response2.getBody(), body)); + + byte[] frameBytes2 = response2.getFrameBytes(); + assertTrue(Arrays.equals(frameBytes1, frameBytes2)); + } +} diff --git a/jetty-rhttp/jetty-rhttp-connector/pom.xml b/jetty-rhttp/jetty-rhttp-connector/pom.xml new file mode 100644 index 00000000000..c507ebd4fdf --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-connector/pom.xml @@ -0,0 +1,62 @@ + + + + org.eclipse.jetty.rhttp + jetty-rhttp-project + 9.0.0-SNAPSHOT + + + 4.0.0 + reverse-http-connector + jar + Jetty :: Reverse HTTP :: Connector + + + + + org.codehaus.mojo + exec-maven-plugin + + test + org.eclipse.jetty.rhttp.connector.TestReverseServer + + + + + + + + org.eclipse.jetty + reverse-http-client + ${project.version} + + + org.eclipse.jetty + jetty-util + + + org.eclipse.jetty + jetty-io + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + example-jetty-embedded + ${project.version} + test + + + junit + junit + + + javax.servlet + servlet-api + test + + + + diff --git a/jetty-rhttp/jetty-rhttp-connector/src/main/config/etc/jetty-rhttp.xml b/jetty-rhttp/jetty-rhttp-connector/src/main/config/etc/jetty-rhttp.xml new file mode 100644 index 00000000000..ba7055f5524 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-connector/src/main/config/etc/jetty-rhttp.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + http://localhost:8888/ + nodeA + + + + + diff --git a/jetty-rhttp/jetty-rhttp-connector/src/main/java/org/eclipse/jetty/rhttp/connector/ReverseHTTPConnector.java b/jetty-rhttp/jetty-rhttp-connector/src/main/java/org/eclipse/jetty/rhttp/connector/ReverseHTTPConnector.java new file mode 100644 index 00000000000..d95e4995dea --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-connector/src/main/java/org/eclipse/jetty/rhttp/connector/ReverseHTTPConnector.java @@ -0,0 +1,170 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.connector; + +import java.io.IOException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import org.eclipse.jetty.io.ByteArrayEndPoint; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.rhttp.client.RHTTPClient; +import org.eclipse.jetty.rhttp.client.RHTTPListener; +import org.eclipse.jetty.rhttp.client.RHTTPRequest; +import org.eclipse.jetty.rhttp.client.RHTTPResponse; +import org.eclipse.jetty.server.AbstractConnector; +import org.eclipse.jetty.server.AbstractHttpConnection; +import org.eclipse.jetty.server.BlockingHttpConnection; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * An implementation of a Jetty connector that uses a {@link RHTTPClient} connected + * to a gateway server to receive requests, feed them to the Jetty server, and + * forward responses from the Jetty server to the gateway server. + * + * @version $Revision$ $Date$ + */ +public class ReverseHTTPConnector extends AbstractConnector implements RHTTPListener +{ + private static final Logger LOG = Log.getLogger(ReverseHTTPConnector.class); + + private final BlockingQueue requests = new LinkedBlockingQueue(); + private final RHTTPClient client; + + public ReverseHTTPConnector(RHTTPClient client) + { + this.client = client; + super.setHost(client.getHost()); + super.setPort(client.getPort()); + } + + @Override + public void setHost(String host) + { + throw new UnsupportedOperationException(); + } + + @Override + public void setPort(int port) + { + throw new UnsupportedOperationException(); + } + + @Override + protected void doStart() throws Exception + { + if (client instanceof LifeCycle) + ((LifeCycle)client).start(); + super.doStart(); + client.connect(); + } + + @Override + protected void doStop() throws Exception + { + client.disconnect(); + super.doStop(); + if (client instanceof LifeCycle) + ((LifeCycle)client).stop(); + } + + public void open() + { + client.addListener(this); + } + + public void close() + { + client.removeListener(this); + } + + public int getLocalPort() + { + return -1; + } + + public Object getConnection() + { + return this; + } + + @Override + protected void accept(int acceptorId) throws IOException, InterruptedException + { + RHTTPRequest request = requests.take(); + IncomingRequest incomingRequest = new IncomingRequest(request); + getThreadPool().dispatch(incomingRequest); + } + + @Override + public void persist(EndPoint endpoint) throws IOException + { + // Signals that the connection should not be closed + // Do nothing in this case, as we run from memory + } + + public void onRequest(RHTTPRequest request) throws Exception + { + requests.add(request); + } + + private class IncomingRequest implements Runnable + { + private final RHTTPRequest request; + + private IncomingRequest(RHTTPRequest request) + { + this.request = request; + } + + public void run() + { + byte[] requestBytes = request.getRequestBytes(); + + ByteArrayEndPoint endPoint = new ByteArrayEndPoint(requestBytes, 1024); + endPoint.setGrowOutput(true); + + AbstractHttpConnection connection = new BlockingHttpConnection(ReverseHTTPConnector.this, endPoint, getServer()); + + connectionOpened(connection); + try + { + // Loop over the whole content, since handle() only + // reads up to the connection buffer's capacities + while (endPoint.getIn().length() > 0) + connection.handle(); + + byte[] responseBytes = endPoint.getOut().asArray(); + RHTTPResponse response = RHTTPResponse.fromResponseBytes(request.getId(), responseBytes); + client.deliver(response); + } + catch (Exception x) + { + LOG.debug(x); + } + finally + { + connectionClosed(connection); + } + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-connector/src/test/java/org/eclipse/jetty/rhttp/connector/ReverseHTTPConnectorTest.java b/jetty-rhttp/jetty-rhttp-connector/src/test/java/org/eclipse/jetty/rhttp/connector/ReverseHTTPConnectorTest.java new file mode 100644 index 00000000000..900fdee44ef --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-connector/src/test/java/org/eclipse/jetty/rhttp/connector/ReverseHTTPConnectorTest.java @@ -0,0 +1,187 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.connector; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import junit.framework.TestCase; + +import org.eclipse.jetty.rhttp.client.ClientListener; +import org.eclipse.jetty.rhttp.client.RHTTPClient; +import org.eclipse.jetty.rhttp.client.RHTTPListener; +import org.eclipse.jetty.rhttp.client.RHTTPRequest; +import org.eclipse.jetty.rhttp.client.RHTTPResponse; +import org.eclipse.jetty.rhttp.connector.ReverseHTTPConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; + +/** + * @version $Revision$ $Date$ + */ +public class ReverseHTTPConnectorTest extends TestCase +{ + public void testGatewayConnectorWithoutRequestBody() throws Exception + { + testGatewayConnector(false); + } + + public void testGatewayConnectorWithRequestBody() throws Exception + { + testGatewayConnector(true); + } + + private void testGatewayConnector(boolean withRequestBody) throws Exception + { + Server server = new Server(); + final CountDownLatch handlerLatch = new CountDownLatch(1); + CountDownLatch clientLatch = new CountDownLatch(1); + AtomicReference responseRef = new AtomicReference(); + ReverseHTTPConnector connector = new ReverseHTTPConnector(new TestClient(clientLatch, responseRef)); + server.addConnector(connector); + final String method = "POST"; + final String uri = "/test"; + final byte[] requestBody = withRequestBody ? "REQUEST-BODY".getBytes("UTF-8") : new byte[0]; + final int statusCode = HttpServletResponse.SC_CREATED; + final String headerName = "foo"; + final String headerValue = "bar"; + final byte[] responseBody = "RESPONSE-BODY".getBytes("UTF-8"); + server.setHandler(new AbstractHandler() + { + public void handle(String pathInfo, org.eclipse.jetty.server.Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException + { + assertEquals(method, httpRequest.getMethod()); + assertEquals(uri, httpRequest.getRequestURI()); + assertEquals(headerValue, httpRequest.getHeader(headerName)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + InputStream input = httpRequest.getInputStream(); + int read; + while ((read = input.read()) >= 0) + baos.write(read); + baos.close(); + assertTrue(Arrays.equals(requestBody, baos.toByteArray())); + + httpResponse.setStatus(statusCode); + httpResponse.setHeader(headerName, headerValue); + OutputStream output = httpResponse.getOutputStream(); + output.write(responseBody); + output.flush(); + request.setHandled(true); + handlerLatch.countDown(); + } + }); + server.start(); + + HashMap headers = new HashMap(); + headers.put("Host", "localhost"); + headers.put(headerName, headerValue); + headers.put("Content-Length", String.valueOf(requestBody.length)); + RHTTPRequest request = new RHTTPRequest(1, method, uri, headers, requestBody); + request = RHTTPRequest.fromRequestBytes(request.getId(), request.getRequestBytes()); + connector.onRequest(request); + + assertTrue(handlerLatch.await(1000, TimeUnit.MILLISECONDS)); + assertTrue(clientLatch.await(1000, TimeUnit.MILLISECONDS)); + RHTTPResponse response = responseRef.get(); + assertEquals(request.getId(), response.getId()); + assertEquals(statusCode, response.getStatusCode()); + assertEquals(headerValue, response.getHeaders().get(headerName)); + assertTrue(Arrays.equals(response.getBody(), responseBody)); + } + + private class TestClient implements RHTTPClient + { + private final CountDownLatch latch; + private final AtomicReference responseRef; + + private TestClient(CountDownLatch latch, AtomicReference response) + { + this.latch = latch; + this.responseRef = response; + } + + public String getTargetId() + { + return null; + } + + public void connect() throws IOException + { + } + + public void disconnect() throws IOException + { + } + + public void deliver(RHTTPResponse response) throws IOException + { + responseRef.set(response); + latch.countDown(); + } + + public void addListener(RHTTPListener listener) + { + } + + public void removeListener(RHTTPListener listener) + { + } + + public void addClientListener(ClientListener listener) + { + } + + public void removeClientListener(ClientListener listener) + { + } + + public String getHost() + { + return null; + } + + public int getPort() + { + return 0; + } + + public String getGatewayURI() + { + // TODO Auto-generated method stub + return null; + } + + public String getPath() + { + // TODO Auto-generated method stub + return null; + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-connector/src/test/java/org/eclipse/jetty/rhttp/connector/TestReverseServer.java b/jetty-rhttp/jetty-rhttp-connector/src/test/java/org/eclipse/jetty/rhttp/connector/TestReverseServer.java new file mode 100644 index 00000000000..628768c648a --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-connector/src/test/java/org/eclipse/jetty/rhttp/connector/TestReverseServer.java @@ -0,0 +1,58 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.connector; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.embedded.HelloHandler; +import org.eclipse.jetty.rhttp.client.JettyClient; +import org.eclipse.jetty.rhttp.client.RHTTPClient; +import org.eclipse.jetty.rhttp.connector.ReverseHTTPConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.log.Log; + +/** + * A Test content server that uses a {@link ReverseHTTPConnector}. + * The main of this class starts 3 TestReversionServers with IDs A, B and C. + */ +public class TestReverseServer extends Server +{ + TestReverseServer(String targetId) + { + setHandler(new HelloHandler("Hello "+targetId,"Hi from "+targetId)); + + HttpClient httpClient = new HttpClient(); + RHTTPClient client = new JettyClient(httpClient,"http://localhost:8080/__rhttp",targetId); + ReverseHTTPConnector connector = new ReverseHTTPConnector(client); + + addConnector(connector); + } + + public static void main(String... args) throws Exception + { + Log.getLogger("org.mortbay.jetty.rhttp.client").setDebugEnabled(true); + + TestReverseServer[] node = new TestReverseServer[] { new TestReverseServer("A"),new TestReverseServer("B"),new TestReverseServer("C") }; + + for (TestReverseServer s : node) + s.start(); + + for (TestReverseServer s : node) + s.join(); + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/pom.xml b/jetty-rhttp/jetty-rhttp-gateway/pom.xml new file mode 100644 index 00000000000..5905e02a58c --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/pom.xml @@ -0,0 +1,65 @@ + + + + org.eclipse.jetty.rhttp + jetty-rhttp-project + 9.0.0-SNAPSHOT + + + 4.0.0 + reverse-http-gateway + jar + Jetty :: Reverse HTTP :: Gateway + + + + + org.codehaus.mojo + exec-maven-plugin + + org.mortbay.jetty.rhttp.gateway.Main + + + + + + + + + + org.eclipse.jetty + reverse-http-client + ${project.version} + + + javax.servlet + servlet-api + + + org.eclipse.jetty + jetty-io + + + org.eclipse.jetty + jetty-continuation + ${project.version} + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-servlet + ${project.version} + + + org.eclipse.jetty + jetty-client + + + junit + junit + + + diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ClientDelegate.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ClientDelegate.java new file mode 100644 index 00000000000..a57ed09cc32 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ClientDelegate.java @@ -0,0 +1,92 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.io.IOException; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jetty.rhttp.client.RHTTPRequest; + + +/** + *

A ClientDelegate is the server-side counterpart of a gateway client.

+ *

The gateway client, the comet protocol and the ClientDelegate form the + * Half-Object plus Protocol pattern that is used between the gateway server + * and the gateway client.

+ *

ClientDelegate offers a server-side API on top of the comet communication.
+ * The API allows to enqueue server-side events to the gateway client, allows to + * flush them to the gateway client, and allows to close and dispose server-side + * resources when the gateway client disconnects.

+ * + * @version $Revision$ $Date$ + */ +public interface ClientDelegate +{ + /** + * @return the targetId that uniquely identifies this client delegate. + */ + public String getTargetId(); + + /** + *

Enqueues the given request to the delivery queue so that it will be sent to the + * gateway client on the first flush occasion.

+ *

Requests may fail to be queued, for example because the gateway client disconnected + * concurrently.

+ * + * @param request the request to add to the delivery queue + * @return whether the request has been queued or not + * @see #process(HttpServletRequest) + */ + public boolean enqueue(RHTTPRequest request); + + /** + *

Flushes the requests that have been {@link #enqueue(RHTTPRequest) enqueued}.

+ *

If no requests have been enqueued, then this method may suspend the current request for + * the long poll timeout.
+ * The request is suspended only if all these conditions holds true: + *

    + *
  • it is not the first time that this method is called for this client delegate
  • + *
  • no requests have been enqueued
  • + *
  • this client delegate is not closed
  • + *
  • the previous call to this method did not suspend the request
  • + *
+ * In all other cases, a response if sent to the gateway client, possibly containing no requests. + * + * @param httpRequest the HTTP request for the long poll request from the gateway client + * @return the list of requests to send to the gateway client, or null if no response should be sent + * to the gateway client + * @throws IOException in case of I/O exception while flushing content to the gateway client + * @see #enqueue(RHTTPRequest) + */ + public List process(HttpServletRequest httpRequest) throws IOException; + + /** + *

Closes this client delegate, in response to a gateway client request to disconnect.

+ * @see #isClosed() + */ + public void close(); + + /** + * @return whether this delegate client is closed + * @see #close() + */ + public boolean isClosed(); +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ConnectorServlet.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ConnectorServlet.java new file mode 100644 index 00000000000..6faf5e08783 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ConnectorServlet.java @@ -0,0 +1,224 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.rhttp.client.RHTTPRequest; +import org.eclipse.jetty.rhttp.client.RHTTPResponse; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * The servlet that handles the communication with the gateway clients. + * @version $Revision$ $Date$ + */ +public class ConnectorServlet extends HttpServlet +{ + private final Logger logger = Log.getLogger(getClass().toString()); + private final TargetIdRetriever targetIdRetriever = new StandardTargetIdRetriever(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final ConcurrentMap> expirations = new ConcurrentHashMap>(); + private final Gateway gateway; + private long clientTimeout=15000; + + public ConnectorServlet(Gateway gateway) + { + this.gateway = gateway; + } + + @Override + public void init() throws ServletException + { + String t = getInitParameter("clientTimeout"); + if (t!=null && !"".equals(t)) + clientTimeout=Long.parseLong(t); + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + String targetId = targetIdRetriever.retrieveTargetId(request); + + String uri = request.getRequestURI(); + String path = uri.substring(request.getServletPath().length()); + String[] segments = path.split("/"); + if (segments.length < 3) + throw new ServletException("Invalid request to " + getClass().getSimpleName() + ": " + uri); + + String action = segments[2]; + if ("handshake".equals(action)) + serviceHandshake(targetId, request, response); + else if ("connect".equals(action)) + serviceConnect(targetId, request, response); + else if ("deliver".equals(action)) + serviceDeliver(targetId, request, response); + else if ("disconnect".equals(action)) + serviceDisconnect(targetId, request, response); + else + throw new ServletException("Invalid request to " + getClass().getSimpleName() + ": " + uri); + } + + private void serviceHandshake(String targetId, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException + { + ClientDelegate client = gateway.getClientDelegate(targetId); + if (client != null) + throw new IOException("Client with targetId " + targetId + " is already connected"); + + client = gateway.newClientDelegate(targetId); + ClientDelegate existing = gateway.addClientDelegate(targetId, client); + if (existing != null) + throw new IOException("Client with targetId " + targetId + " is already connected"); + + flush(client, httpRequest, httpResponse); + } + + private void flush(ClientDelegate client, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException + { + List requests = client.process(httpRequest); + if (requests != null) + { + // Schedule before sending the requests, to avoid that the remote client + // reconnects before we have scheduled the expiration timeout. + if (!client.isClosed()) + schedule(client); + + ServletOutputStream output = httpResponse.getOutputStream(); + for (RHTTPRequest request : requests) + output.write(request.getFrameBytes()); + // I could count the framed bytes of all requests and set a Content-Length header, + // but the implementation of ServletOutputStream takes care of everything: + // if the request was HTTP/1.1, then flushing result in a chunked response, but the + // client know how to handle it; if the request was HTTP/1.0, then no chunking. + // To avoid chunking in HTTP/1.1 I must set the Content-Length header. + output.flush(); + logger.debug("Delivered to device {} requests {} ", client.getTargetId(), requests); + } + } + + private void schedule(ClientDelegate client) + { + Future task = scheduler.schedule(new ClientExpirationTask(client), clientTimeout, TimeUnit.MILLISECONDS); + Future existing = expirations.put(client.getTargetId(), task); + assert existing == null; + } + + private void unschedule(String targetId) + { + Future task = expirations.remove(targetId); + if (task != null) + task.cancel(false); + } + + private void serviceConnect(String targetId, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException + { + unschedule(targetId); + + ClientDelegate client = gateway.getClientDelegate(targetId); + if (client == null) + { + // Expired client tries to connect without handshake + httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + + flush(client, httpRequest, httpResponse); + + if (client.isClosed()) + gateway.removeClientDelegate(targetId); + } + + private void expireConnect(ClientDelegate client, long time) + { + String targetId = client.getTargetId(); + logger.info("Client with targetId {} missing, last seen {} ms ago, closing it", targetId, System.currentTimeMillis() - time); + client.close(); + // If the client expired, means that it did not connect, + // so there no request to resume, and we cleanup here + // (while normally this cleanup is done in serviceConnect()) + unschedule(targetId); + gateway.removeClientDelegate(targetId); + } + + private void serviceDeliver(String targetId, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException, IOException + { + if (gateway.getClientDelegate(targetId) == null) + { + // Expired client tries to deliver without handshake + httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + + byte[] body = Utils.read(httpRequest.getInputStream()); + + RHTTPResponse response = RHTTPResponse.fromFrameBytes(body); + + ExternalRequest externalRequest = gateway.removeExternalRequest(response.getId()); + if (externalRequest != null) + { + externalRequest.respond(response); + logger.debug("Deliver request from device {}, gateway request {}, response {}", new Object[] {targetId, externalRequest, response}); + } + else + { + // We can arrive here for a race with the continuation expiration, which expired just before + // the gateway client responded with a valid response; log this case ignore it. + logger.debug("Deliver request from device {}, missing gateway request, response {}", targetId, response); + } + } + + private void serviceDisconnect(String targetId, HttpServletRequest request, HttpServletResponse response) + { + // Do not remove the ClientDelegate from the gateway here, + // since closing the ClientDelegate will resume the connect request + // and we remove the ClientDelegate from the gateway there + ClientDelegate client = gateway.getClientDelegate(targetId); + if (client != null) + client.close(); + } + + private class ClientExpirationTask implements Runnable + { + private final long time = System.currentTimeMillis(); + private final ClientDelegate client; + + public ClientExpirationTask(ClientDelegate client) + { + this.client = client; + } + + public void run() + { + expireConnect(client, time); + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ExternalRequest.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ExternalRequest.java new file mode 100644 index 00000000000..9b039bac165 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ExternalRequest.java @@ -0,0 +1,54 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.io.IOException; + +import org.eclipse.jetty.rhttp.client.RHTTPRequest; +import org.eclipse.jetty.rhttp.client.RHTTPResponse; + + +/** + *

ExternalRequest represent an external request made to the gateway server.

+ *

ExternalRequests that arrive to the gateway server are suspended, waiting + * for a response from the corresponding gateway client.

+ * + * @version $Revision$ $Date$ + */ +public interface ExternalRequest +{ + /** + *

Suspends this ExternalRequest waiting for a response from the gateway client.

+ * @return true if the ExternalRequest has been suspended, false if the + * ExternalRequest has already been responded. + */ + public boolean suspend(); + + /** + *

Responds to the original external request with the response arrived from the gateway client.

+ * @param response the response arrived from the gateway client + * @throws IOException if responding to the original external request fails + */ + public void respond(RHTTPResponse response) throws IOException; + + /** + * @return the request to be sent to the gateway client + */ + public RHTTPRequest getRequest(); +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ExternalServlet.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ExternalServlet.java new file mode 100644 index 00000000000..a3585dfcc3b --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/ExternalServlet.java @@ -0,0 +1,88 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.rhttp.client.RHTTPRequest; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * The servlet that handles external requests. + * + * @version $Revision$ $Date$ + */ +public class ExternalServlet extends HttpServlet +{ + private final Logger logger = Log.getLogger(getClass().toString()); + private final Gateway gateway; + private TargetIdRetriever targetIdRetriever; + + public ExternalServlet(Gateway gateway, TargetIdRetriever targetIdRetriever) + { + this.gateway = gateway; + this.targetIdRetriever = targetIdRetriever; + } + + public TargetIdRetriever getTargetIdRetriever() + { + return targetIdRetriever; + } + + public void setTargetIdRetriever(TargetIdRetriever targetIdRetriever) + { + this.targetIdRetriever = targetIdRetriever; + } + + @Override + protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException, IOException + { + logger.debug("External http request: {}", httpRequest.getRequestURL()); + + String targetId = targetIdRetriever.retrieveTargetId(httpRequest); + if (targetId == null) + throw new ServletException("Invalid request to " + getClass().getSimpleName() + ": " + httpRequest.getRequestURI()); + + ClientDelegate client = gateway.getClientDelegate(targetId); + if (client == null) throw new ServletException("Client with targetId " + targetId + " is not connected"); + + ExternalRequest externalRequest = gateway.newExternalRequest(httpRequest, httpResponse); + RHTTPRequest request = externalRequest.getRequest(); + ExternalRequest existing = gateway.addExternalRequest(request.getId(), externalRequest); + assert existing == null; + logger.debug("External request {} for device {}", request, targetId); + + boolean delivered = client.enqueue(request); + if (delivered) + { + externalRequest.suspend(); + } + else + { + // TODO: improve this: we can temporarly queue this request elsewhere and wait for the client to reconnect ? + throw new ServletException("Could not enqueue request to client with targetId " + targetId); + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Gateway.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Gateway.java new file mode 100644 index 00000000000..79b0eec46dd --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Gateway.java @@ -0,0 +1,99 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + *

Gateway instances are responsible of holding the state of the gateway server.

+ *

The state is composed by: + *

    + *
  • {@link ExternalRequest external requests} that are suspended waiting for the response
  • + *
  • {@link ClientDelegate gateway clients} that are connected with the gateway server
  • + *

+ *

Instances of this class are created by the {@link GatewayServer}.

+ * + * @version $Revision$ $Date$ + */ +public interface Gateway +{ + /** + *

Returns the {@link ClientDelegate} with the given targetId.
+ * If there is no such ClientDelegate returns null.

+ * + * @param targetId the targetId of the ClientDelegate to return + * @return the ClientDelegate associated with the given targetId + */ + public ClientDelegate getClientDelegate(String targetId); + + /** + *

Creates and configures a new {@link ClientDelegate} with the given targetId.

+ * @param targetId the targetId of the ClientDelegate to create + * @return a newly created ClientDelegate + * @see #addClientDelegate(String, ClientDelegate) + */ + public ClientDelegate newClientDelegate(String targetId); + + /** + *

Maps the given ClientDelegate to the given targetId.

+ * @param targetId the targetId of the given ClientDelegate + * @param client the ClientDelegate to map + * @return the previously existing ClientDelegate mapped to the same targetId + * @see #removeClientDelegate(String) + */ + public ClientDelegate addClientDelegate(String targetId, ClientDelegate client); + + /** + *

Removes the {@link ClientDelegate} associated with the given targetId.

+ * @param targetId the targetId of the ClientDelegate to remove + * @return the removed ClientDelegate, or null if no ClientDelegate was removed + * @see #addClientDelegate(String, ClientDelegate) + */ + public ClientDelegate removeClientDelegate(String targetId); + + /** + *

Creates a new {@link ExternalRequest} from the given HTTP request and HTTP response.

+ * @param httpRequest the HTTP request of the external request + * @param httpResponse the HTTP response of the external request + * @return a newly created ExternalRequest + * @throws IOException in case of failures creating the ExternalRequest + * @see #addExternalRequest(int, ExternalRequest) + */ + public ExternalRequest newExternalRequest(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException; + + /** + * Maps the given ExternalRequest with the given requestId into the gateway state. + * @param requestId the id of the ExternalRequest + * @param externalRequest the ExternalRequest to map + * @return the previously existing ExternalRequest mapped to the same requestId + * @see #removeExternalRequest(int) + */ + public ExternalRequest addExternalRequest(int requestId, ExternalRequest externalRequest); + + /** + * Removes the ExternalRequest mapped to the given requestId from the gateway state. + * @param requestId the id of the ExternalRequest + * @return the removed ExternalRequest + * @see #addExternalRequest(int, ExternalRequest) + */ + public ExternalRequest removeExternalRequest(int requestId); +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/GatewayProxyServer.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/GatewayProxyServer.java new file mode 100644 index 00000000000..6931636ad9e --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/GatewayProxyServer.java @@ -0,0 +1,228 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.client.Address; +import org.eclipse.jetty.client.ContentExchange; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpExchange; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.rhttp.client.JettyClient; +import org.eclipse.jetty.rhttp.client.RHTTPClient; +import org.eclipse.jetty.rhttp.client.RHTTPListener; +import org.eclipse.jetty.rhttp.client.RHTTPRequest; +import org.eclipse.jetty.rhttp.client.RHTTPResponse; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.nio.SelectChannelConnector; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + *

This class combines a gateway server and a gateway client to obtain the functionality of a simple proxy server.

+ *

This gateway proxy server starts on port 8080 and can be set as http proxy in browsers such as Firefox, and used + * to browse the internet.

+ *

Its functionality is limited (for example, it only supports http, and not https).

+ * @version $Revision$ $Date$ + */ +public class GatewayProxyServer +{ + private static final Logger logger = Log.getLogger(GatewayProxyServer.class.toString()); + + public static void main(String[] args) throws Exception + { + GatewayServer server = new GatewayServer(); + + Connector plainConnector = new SelectChannelConnector(); + plainConnector.setPort(8080); + server.addConnector(plainConnector); + + ((StandardGateway)server.getGateway()).setExternalTimeout(180000); + ((StandardGateway)server.getGateway()).setGatewayTimeout(20000); + server.setTargetIdRetriever(new ProxyTargetIdRetriever()); + server.start(); + + HttpClient httpClient = new HttpClient(); + httpClient.setConnectorType(HttpClient.CONNECTOR_SOCKET); + httpClient.start(); + + RHTTPClient client = new JettyClient(httpClient, new Address("localhost", plainConnector.getPort()), server.getContext().getContextPath() + "/gw", "proxy"); + client.addListener(new ProxyListener(httpClient, client)); + client.connect(); + + Runtime.getRuntime().addShutdownHook(new Shutdown(server, httpClient, client)); + logger.info("{} started", GatewayProxyServer.class.getSimpleName()); + } + + private static class Shutdown extends Thread + { + private final GatewayServer server; + private final HttpClient httpClient; + private final RHTTPClient client; + + public Shutdown(GatewayServer server, HttpClient httpClient, RHTTPClient client) + { + this.server = server; + this.httpClient = httpClient; + this.client = client; + } + + @Override + public void run() + { + try + { + client.disconnect(); + httpClient.stop(); + server.stop(); + logger.info("{} stopped", GatewayProxyServer.class.getSimpleName()); + } + catch (Exception x) + { + logger.debug("Exception while stopping " + GatewayProxyServer.class.getSimpleName(), x); + } + } + } + + private static class ProxyListener implements RHTTPListener + { + private final HttpClient httpClient; + private final RHTTPClient client; + + private ProxyListener(HttpClient httpClient, RHTTPClient client) + { + this.httpClient = httpClient; + this.client = client; + } + + public void onRequest(RHTTPRequest request) throws Exception + { + ProxyExchange exchange = new ProxyExchange(); + Address address = Address.from(request.getHeaders().get("Host")); + if (address.getPort() == 0) address = new Address(address.getHost(), 80); + exchange.setAddress(address); + exchange.setMethod(request.getMethod()); + exchange.setURI(request.getURI()); + for (Map.Entry header : request.getHeaders().entrySet()) + exchange.setRequestHeader(header.getKey(), header.getValue()); + exchange.setRequestContent(new ByteArrayBuffer(request.getBody())); + int status = syncSend(exchange); + if (status == HttpExchange.STATUS_COMPLETED) + { + int statusCode = exchange.getResponseStatus(); + String statusMessage = exchange.getResponseMessage(); + Map responseHeaders = exchange.getResponseHeaders(); + byte[] responseBody = exchange.getResponseBody(); + RHTTPResponse response = new RHTTPResponse(request.getId(), statusCode, statusMessage, responseHeaders, responseBody); + client.deliver(response); + } + else + { + int statusCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE; + String statusMessage = "Gateway error"; + HashMap responseHeaders = new HashMap(); + responseHeaders.put("Connection", "close"); + byte[] responseBody = new byte[0]; + RHTTPResponse response = new RHTTPResponse(request.getId(), statusCode, statusMessage, responseHeaders, responseBody); + client.deliver(response); + } + } + + private int syncSend(ProxyExchange exchange) throws Exception + { + long start = System.nanoTime(); + httpClient.send(exchange); + int status = exchange.waitForDone(); + long end = System.nanoTime(); + long millis = TimeUnit.NANOSECONDS.toMillis(end - start); + long micros = TimeUnit.NANOSECONDS.toMicros(end - start - TimeUnit.MILLISECONDS.toNanos(millis)); + logger.debug("Proxied request took {}.{} ms", millis, micros); + return status; + } + } + + private static class ProxyExchange extends ContentExchange + { + private String responseMessage; + private Map responseHeaders = new HashMap(); + private ByteArrayOutputStream responseBody = new ByteArrayOutputStream(); + + private ProxyExchange() + { + super(true); + } + + public String getResponseMessage() + { + return responseMessage; + } + + public Map getResponseHeaders() + { + return responseHeaders; + } + + public byte[] getResponseBody() + { + return responseBody.toByteArray(); + } + + @Override + protected void onResponseStatus(Buffer version, int code, Buffer message) throws IOException + { + super.onResponseStatus(version, code, message); + this.responseMessage = message.toString("UTF-8"); + } + + @Override + protected void onResponseHeader(Buffer nameBuffer, Buffer valueBuffer) throws IOException + { + super.onResponseHeader(nameBuffer, valueBuffer); + String name = nameBuffer.toString("UTF-8"); + String value = valueBuffer.toString("UTF-8"); + // Skip chunked header, since we read the whole body and will not re-chunk it + if (!name.equalsIgnoreCase("Transfer-Encoding") || !value.equalsIgnoreCase("chunked")) + responseHeaders.put(name, value); + } + + @Override + protected void onResponseContent(Buffer buffer) throws IOException + { + responseBody.write(buffer.asArray()); + super.onResponseContent(buffer); + } + } + + public static class ProxyTargetIdRetriever implements TargetIdRetriever + { + public String retrieveTargetId(HttpServletRequest httpRequest) + { + return "proxy"; + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/GatewayServer.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/GatewayServer.java new file mode 100644 index 00000000000..b9112d37fc2 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/GatewayServer.java @@ -0,0 +1,171 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.rhttp.client.RHTTPClient; +import org.eclipse.jetty.rhttp.client.RHTTPRequest; +import org.eclipse.jetty.rhttp.client.RHTTPResponse; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.HandlerCollection; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + *

The gateway server is a server component that acts as intermediary between + * external clients which perform requests for resources, and the + * resource providers.

+ *

The particularity of the gateway server is that the resource providers + * connect to the gateway using a comet protocol.
+ * The comet procotol functionality is implemented by a gateway client.
+ * This is quite different from a normal proxy server where it is the proxy that + * connects to the resource providers.

+ *

Schematically, this is how the gateway server works:

+ *
+ * External Client       Gateway Server         Gateway Client         Resource Provider
+ *                              |                      |
+ *                              | <-- comet req. 1 --- |
+ *        | --- ext. req. 1 --> |                      |
+ *        |                     | --- comet res. 1 --> |
+ *        |                     | <-- comet req. 2 --- |
+ *        |                                            | --- ext. req. 1 --> |
+ *                                                                           |
+ *        |                                            | <-- ext. res. 1 --- |
+ *        |                     | <-- ext.  res. 1 --- |
+ *        | <-- ext. res. 1 --- |
+ *
+ *        | --- ext. req. 2 --> |
+ *        |                     | --- comet res. 2 --> |
+ *        .                     .                      .
+ * 
+ *

The gateway server is made of two servlets: + *

    + *
  • the external servlet, that handles external requests
  • + *
  • the gateway servlet, that handles the communication with the gateway client
  • + *
+ *

+ *

External requests are suspended using Jetty continuations until a response for + * that request arrives from the resource provider, or a + * {@link #getExternalTimeout() configurable timeout} expires.
+ * Comet requests made by the gateway client also expires after a (different) + * {@link #getGatewayTimeout() configurable timeout}.

+ *

External requests are packed into {@link RHTTPRequest} objects, converted into an + * opaque byte array and sent as the body of the comet reponse to the gateway + * {@link RHTTPClient}.

+ *

The gateway client uses a notification mechanism to alert listeners interested + * in external requests that have been forwarded through the gateway. It is up to the + * listeners to connect to the resource provider however they like.

+ *

When the gateway client receives a response from the resource provider, it packs + * the response into a {@link RHTTPResponse} object, converts it into an opaque byte array + * and sends it as the body of a normal HTTP request to the gateway server.

+ *

It is possible to connect more than one gateway client to a gateway server; each + * gateway client is identified by a unique targetId.
+ * External requests must specify a targetId that allows the gateway server to forward + * the requests to the specific gateway client; how the targetId is retrieved from an + * external request is handled by {@link TargetIdRetriever} implementations.

+ * + * @version $Revision$ $Date$ + */ +public class GatewayServer extends Server +{ + public final static String DFT_EXT_PATH="/gw"; + public final static String DFT_CONNECT_PATH="/__rhttp"; + private final Logger logger = Log.getLogger(getClass().toString()); + private final Gateway gateway; + private final ServletHolder externalServletHolder; + private final ServletHolder connectorServletHolder; + private final ServletContextHandler context; + + public GatewayServer() + { + this("",DFT_EXT_PATH,DFT_CONNECT_PATH,new StandardTargetIdRetriever()); + } + + public GatewayServer(String contextPath, String externalServletPath,String gatewayServletPath, TargetIdRetriever targetIdRetriever) + { + HandlerCollection handlers = new HandlerCollection(); + setHandler(handlers); + context = new ServletContextHandler(handlers, contextPath, ServletContextHandler.SESSIONS); + + // Setup the gateway + gateway = createGateway(); + + // Setup external servlet + ExternalServlet externalServlet = new ExternalServlet(gateway, targetIdRetriever); + externalServletHolder = new ServletHolder(externalServlet); + context.addServlet(externalServletHolder, externalServletPath + "/*"); + logger.debug("External servlet mapped to {}/*", externalServletPath); + + // Setup gateway servlet + ConnectorServlet gatewayServlet = new ConnectorServlet(gateway); + connectorServletHolder = new ServletHolder(gatewayServlet); + connectorServletHolder.setInitParameter("clientTimeout", "15000"); + context.addServlet(connectorServletHolder, gatewayServletPath + "/*"); + logger.debug("Gateway servlet mapped to {}/*", gatewayServletPath); + } + + /** + * Creates and configures a {@link Gateway} object. + * @return the newly created and configured Gateway object. + */ + protected Gateway createGateway() + { + StandardGateway gateway = new StandardGateway(); + return gateway; + } + + public ServletContextHandler getContext() + { + return context; + } + + public Gateway getGateway() + { + return gateway; + } + + public ServletHolder getExternalServlet() + { + return externalServletHolder; + } + + public ServletHolder getConnectorServlet() + { + return connectorServletHolder; + } + + public void setTargetIdRetriever(TargetIdRetriever retriever) + { + ((ExternalServlet)externalServletHolder.getServletInstance()).setTargetIdRetriever(retriever); + } + + public TargetIdRetriever getTargetIdRetriever() + { + return ((ExternalServlet)externalServletHolder.getServletInstance()).getTargetIdRetriever(); + } + +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/HostTargetIdRetriever.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/HostTargetIdRetriever.java new file mode 100644 index 00000000000..01c107fad37 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/HostTargetIdRetriever.java @@ -0,0 +1,54 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import javax.servlet.http.HttpServletRequest; + +/** + * @version $Revision$ $Date$ + */ +public class HostTargetIdRetriever implements TargetIdRetriever +{ + private final String suffix; + + public HostTargetIdRetriever(String suffix) + { + this.suffix = suffix; + } + + public String retrieveTargetId(HttpServletRequest httpRequest) + { + String host = httpRequest.getHeader("Host"); + if (host != null) + { + // Strip the port + int colon = host.indexOf(':'); + if (colon > 0) + { + host = host.substring(0, colon); + } + + if (suffix != null && host.endsWith(suffix)) + { + return host.substring(0, host.length() - suffix.length()); + } + } + return host; + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Main.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Main.java new file mode 100644 index 00000000000..bfb7b0804ae --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Main.java @@ -0,0 +1,128 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.nio.SelectChannelConnector; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ServletHolder; + +/** + *

Main class that starts the gateway server.

+ *

This class supports the following arguments:

+ *
    + *
  • --port=<port> specifies the port on which the gateway server listens to, by default 8080
  • + *
  • --retriever=<retriever> specifies the + * {@link GatewayServer#setTargetIdRetriever(TargetIdRetriever) target id retriever}
  • + *
  • --resources=<resources file path> specifies the resource file path for the gateway
  • + *
+ *

Examples

+ *

java --port=8080

+ *

java --port=8080 --resources=/tmp/gateway-resources

+ *

java --port=8080 --retriever=standard

+ *

java --port=8080 --retriever=host,.rhttp.example.com

+ *

The latter example specifies the {@link HostTargetIdRetriever} with a suffix of .rhttp.example.com

+ * + * @see GatewayServer + * @version $Revision$ $Date$ + */ +public class Main +{ + private static final String PORT_ARG = "port"; + private static final String RESOURCES_ARG = "resources"; + private static final String RETRIEVER_ARG = "retriever"; + + public static void main(String[] args) throws Exception + { + Map arguments = parse(args); + + int port = 8080; + if (arguments.containsKey(PORT_ARG)) + port = (Integer)arguments.get(PORT_ARG); + + String resources = null; + if (arguments.containsKey(RESOURCES_ARG)) + resources = (String)arguments.get(RESOURCES_ARG); + + TargetIdRetriever retriever = null; + if (arguments.containsKey(RETRIEVER_ARG)) + retriever = (TargetIdRetriever)arguments.get(RETRIEVER_ARG); + + GatewayServer server = new GatewayServer(); + + Connector connector = new SelectChannelConnector(); + connector.setPort(port); + server.addConnector(connector); + + if (resources != null) + { + server.getContext().setResourceBase(resources); + ServletHolder resourcesServletHolder = server.getContext().addServlet(DefaultServlet.class,"__r/*"); + resourcesServletHolder.setInitParameter("dirAllowed", "true"); + } + + if (retriever != null) + server.setTargetIdRetriever(retriever); + + server.start(); + } + + private static Map parse(String[] args) + { + Map result = new HashMap(); + + Pattern pattern = Pattern.compile("--([^=]+)=(.+)"); + for (String arg : args) + { + Matcher matcher = pattern.matcher(arg); + if (matcher.matches()) + { + String argName = matcher.group(1); + if (PORT_ARG.equals(argName)) + { + result.put(PORT_ARG, Integer.parseInt(matcher.group(2))); + } + else if (RESOURCES_ARG.equals(argName)) + { + String argValue = matcher.group(2); + result.put(RESOURCES_ARG, argValue); + } + else if (RETRIEVER_ARG.equals(argName)) + { + String argValue = matcher.group(2); + if (argValue.startsWith("host,")) + { + String[] typeAndSuffix = argValue.split(","); + if (typeAndSuffix.length != 2) + throw new IllegalArgumentException("Invalid option " + arg + ", must be of the form --" + RETRIEVER_ARG + "=host,suffix"); + + result.put(RETRIEVER_ARG, new HostTargetIdRetriever(typeAndSuffix[1])); + } + } + } + } + + return result; + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardClientDelegate.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardClientDelegate.java new file mode 100644 index 00000000000..1afd85d7652 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardClientDelegate.java @@ -0,0 +1,172 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jetty.continuation.Continuation; +import org.eclipse.jetty.continuation.ContinuationSupport; +import org.eclipse.jetty.rhttp.client.RHTTPRequest; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + *

Default implementation of {@link ClientDelegate}.

+ * + * @version $Revision$ $Date$ + */ +public class StandardClientDelegate implements ClientDelegate +{ + private final Logger logger = Log.getLogger(getClass().toString()); + private final Object lock = new Object(); + private final List requests = new ArrayList(); + private final String targetId; + private volatile boolean firstFlush = true; + private volatile long timeout; + private volatile boolean closed; + private Continuation continuation; + + public StandardClientDelegate(String targetId) + { + this.targetId = targetId; + } + + public String getTargetId() + { + return targetId; + } + + public long getTimeout() + { + return timeout; + } + + public void setTimeout(long timeout) + { + this.timeout = timeout; + } + + public boolean enqueue(RHTTPRequest request) + { + if (isClosed()) + return false; + + synchronized (lock) + { + requests.add(request); + resume(); + } + + return true; + } + + private void resume() + { + synchronized (lock) + { + // Continuation may be null in several cases: + // 1. there always is something to deliver so we never suspend + // 2. concurrent calls to add() and close() + // 3. concurrent close() with a long poll that expired + // 4. concurrent close() with a long poll that resumed + if (continuation != null) + { + continuation.resume(); + // Null the continuation, as there is no point is resuming multiple times + continuation = null; + } + } + } + + public List process(HttpServletRequest httpRequest) throws IOException + { + // We want to respond in the following cases: + // 1. It's the first time we process: the client will wait for a response before issuing another connect. + // 2. The client disconnected, so we want to return from this connect before it times out. + // 3. We've been woken up because there are responses to send. + // 4. The continuation was suspended but timed out. + // The timeout case is different from a non-first connect, in that we want to return + // a (most of the times empty) response and we do not want to wait again. + // The order of these if statements is important, as the continuation timed out only if + // the client is not closed and there are no responses to send + List result = Collections.emptyList(); + if (firstFlush) + { + firstFlush = false; + logger.debug("Connect request (first) from device {}, delivering requests {}", targetId, result); + } + else + { + // Synchronization is crucial here, since we don't want to suspend if there is something to deliver + synchronized (lock) + { + int size = requests.size(); + if (size > 0) + { + assert continuation == null; + result = new ArrayList(size); + result.addAll(requests); + requests.clear(); + logger.debug("Connect request (resumed) from device {}, delivering requests {}", targetId, result); + } + else + { + if (continuation != null) + { + continuation = null; + logger.debug("Connect request (expired) from device {}, delivering requests {}", targetId, result); + } + else + { + if (isClosed()) + { + logger.debug("Connect request (closed) from device {}, delivering requests {}", targetId, result); + } + else + { + // Here we need to suspend + continuation = ContinuationSupport.getContinuation(httpRequest); + continuation.setTimeout(getTimeout()); + continuation.suspend(); + result = null; + logger.debug("Connect request (suspended) from device {}", targetId); + } + } + } + } + } + return result; + } + + public void close() + { + closed = true; + resume(); + } + + public boolean isClosed() + { + return closed; + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardExternalRequest.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardExternalRequest.java new file mode 100644 index 00000000000..4f905b87e4a --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardExternalRequest.java @@ -0,0 +1,189 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.continuation.Continuation; +import org.eclipse.jetty.continuation.ContinuationListener; +import org.eclipse.jetty.continuation.ContinuationSupport; +import org.eclipse.jetty.rhttp.client.RHTTPRequest; +import org.eclipse.jetty.rhttp.client.RHTTPResponse; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * Default implementation of {@link ExternalRequest}. + * + * @version $Revision$ $Date$ + */ +public class StandardExternalRequest implements ExternalRequest +{ + private final Logger logger = Log.getLogger(getClass().toString()); + private final RHTTPRequest request; + private final HttpServletRequest httpRequest; + private final HttpServletResponse httpResponse; + private final Gateway gateway; + private final Object lock = new Object(); + private volatile long timeout; + private Continuation continuation; + private boolean responded; + + public StandardExternalRequest(RHTTPRequest request, HttpServletRequest httpRequest, HttpServletResponse httpResponse, Gateway gateway) + { + this.request = request; + this.httpRequest = httpRequest; + this.httpResponse = httpResponse; + this.gateway = gateway; + } + + public long getTimeout() + { + return timeout; + } + + public void setTimeout(long timeout) + { + this.timeout = timeout; + } + + public boolean suspend() + { + synchronized (lock) + { + // We suspend only if we have no responded yet + if (!responded) + { + assert continuation == null; + continuation = ContinuationSupport.getContinuation(httpRequest); + continuation.setTimeout(getTimeout()); + continuation.addContinuationListener(new TimeoutListener()); + continuation.suspend(httpResponse); + logger.debug("Request {} suspended", getRequest()); + } + else + { + logger.debug("Request {} already responded", getRequest()); + } + return !responded; + } + } + + public void respond(RHTTPResponse response) throws IOException + { + responseCompleted(response); + } + + private void responseCompleted(RHTTPResponse response) throws IOException + { + synchronized (lock) + { + // Could be that we complete exactly when the response is being expired + if (!responded) + { + httpResponse.setStatus(response.getStatusCode()); + + for (Map.Entry header : response.getHeaders().entrySet()) + httpResponse.setHeader(header.getKey(), header.getValue()); + + ServletOutputStream output = httpResponse.getOutputStream(); + output.write(response.getBody()); + output.flush(); + + // It may happen that the continuation is null, + // because the response arrived before we had the chance to suspend + if (continuation != null) + { + continuation.complete(); + continuation = null; + } + + // Mark as responded, so we know we don't have to suspend + // or respond with an expired response + responded = true; + + if (logger.isDebugEnabled()) + { + String eol = System.getProperty("line.separator"); + logger.debug("Request {} responded {}{}{}{}{}", new Object[]{request, response, eol, request.toLongString(), eol, response.toLongString()}); + } + } + } + } + + private void responseExpired() throws IOException + { + synchronized (lock) + { + // Could be that we expired exactly when the response is being completed + if (!responded) + { + httpResponse.sendError(HttpServletResponse.SC_GATEWAY_TIMEOUT, "Gateway Time-out"); + + continuation.complete(); + continuation = null; + + // Mark as responded, so we know we don't have to respond with a completed response + responded = true; + + logger.debug("Request {} expired", getRequest()); + } + } + } + + public RHTTPRequest getRequest() + { + return request; + } + + @Override + public String toString() + { + return request.toString(); + } + + private class TimeoutListener implements ContinuationListener + { + public void onComplete(Continuation continuation) + { + } + + public void onTimeout(Continuation continuation) + { + ExternalRequest externalRequest = gateway.removeExternalRequest(getRequest().getId()); + // The gateway request can be null for a race with delivery + if (externalRequest != null) + { + try + { + responseExpired(); + } + catch (Exception x) + { + logger.warn("Request " + getRequest() + " expired but failed", x); + } + } + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardGateway.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardGateway.java new file mode 100644 index 00000000000..871aea6c85a --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardGateway.java @@ -0,0 +1,131 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.rhttp.client.RHTTPRequest; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * Default implementation of {@link Gateway}. + * + * @version $Revision$ $Date$ + */ +public class StandardGateway implements Gateway +{ + private final Logger logger = Log.getLogger(getClass().toString()); + private final ConcurrentMap clients = new ConcurrentHashMap(); + private final ConcurrentMap requests = new ConcurrentHashMap(); + private final AtomicInteger requestIds = new AtomicInteger(); + private volatile long gatewayTimeout=20000; + private volatile long externalTimeout=60000; + + public long getGatewayTimeout() + { + return gatewayTimeout; + } + + public void setGatewayTimeout(long timeout) + { + this.gatewayTimeout = timeout; + } + + public long getExternalTimeout() + { + return externalTimeout; + } + + public void setExternalTimeout(long externalTimeout) + { + this.externalTimeout = externalTimeout; + } + + public ClientDelegate getClientDelegate(String targetId) + { + return clients.get(targetId); + } + + public ClientDelegate newClientDelegate(String targetId) + { + StandardClientDelegate client = new StandardClientDelegate(targetId); + client.setTimeout(getGatewayTimeout()); + return client; + } + + public ClientDelegate addClientDelegate(String targetId, ClientDelegate client) + { + return clients.putIfAbsent(targetId, client); + } + + public ClientDelegate removeClientDelegate(String targetId) + { + return clients.remove(targetId); + } + + public ExternalRequest newExternalRequest(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException + { + int requestId = requestIds.incrementAndGet(); + RHTTPRequest request = convertHttpRequest(requestId, httpRequest); + StandardExternalRequest gatewayRequest = new StandardExternalRequest(request, httpRequest, httpResponse, this); + gatewayRequest.setTimeout(getExternalTimeout()); + return gatewayRequest; + } + + protected RHTTPRequest convertHttpRequest(int requestId, HttpServletRequest httpRequest) throws IOException + { + Map headers = new HashMap(); + for (Enumeration headerNames = httpRequest.getHeaderNames(); headerNames.hasMoreElements();) + { + String name = (String)headerNames.nextElement(); + // TODO: improve by supporting getHeaders(name) + String value = httpRequest.getHeader(name); + headers.put(name, value); + } + + byte[] body = Utils.read(httpRequest.getInputStream()); + return new RHTTPRequest(requestId, httpRequest.getMethod(), httpRequest.getRequestURI(), headers, body); + } + + public ExternalRequest addExternalRequest(int requestId, ExternalRequest externalRequest) + { + ExternalRequest existing = requests.putIfAbsent(requestId, externalRequest); + if (existing == null) + logger.debug("Added external request {}/{} - {}", new Object[]{requestId, requests.size(), externalRequest}); + return existing; + } + + public ExternalRequest removeExternalRequest(int requestId) + { + ExternalRequest externalRequest = requests.remove(requestId); + if (externalRequest != null) + logger.debug("Removed external request {}/{} - {}", new Object[]{requestId, requests.size(), externalRequest}); + return externalRequest; + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardTargetIdRetriever.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardTargetIdRetriever.java new file mode 100644 index 00000000000..114747f2f5a --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/StandardTargetIdRetriever.java @@ -0,0 +1,40 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import javax.servlet.http.HttpServletRequest; + +/** + *

This implementation retrieves the targetId from the request URI following this pattern:

+ *
+ * /contextPath/servletPath/<targetId>/other/paths
+ * 
+ * @version $Revision$ $Date$ + */ +public class StandardTargetIdRetriever implements TargetIdRetriever +{ + public String retrieveTargetId(HttpServletRequest httpRequest) + { + String uri = httpRequest.getRequestURI(); + String path = uri.substring(httpRequest.getServletPath().length()); + String[] segments = path.split("/"); + if (segments.length < 2) return null; + return segments[1]; + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/TargetIdRetriever.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/TargetIdRetriever.java new file mode 100644 index 00000000000..2348d3c1993 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/TargetIdRetriever.java @@ -0,0 +1,40 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import javax.servlet.http.HttpServletRequest; + +/** + *

Implementations should retrieve a targetId from an external request.

+ *

Implementations of this class may return a fixed value, or inspect the request + * looking for URL patterns (e.g. "/<targetId>/resource.jsp"), or looking for request + * parameters (e.g. "/resource.jsp?targetId=<targetId>), or looking for virtual host + * naming patterns (e.g. "http://<targetId>.host.com/resource.jsp"), etc.

+ * + * @version $Revision$ $Date$ + */ +public interface TargetIdRetriever +{ + /** + * Extracts and returns the targetId. + * @param httpRequest the external request from where the targetId could be extracted + * @return the extracted targetId + */ + public String retrieveTargetId(HttpServletRequest httpRequest); +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Utils.java b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Utils.java new file mode 100644 index 00000000000..b9361fe750b --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/main/java/org/eclipse/jetty/rhttp/gateway/Utils.java @@ -0,0 +1,40 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * @version $Revision$ $Date$ + */ +class Utils +{ + static byte[] read(InputStream input) throws IOException + { + ByteArrayOutputStream body = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int read; + while ((read = input.read(buffer)) >= 0) + body.write(buffer, 0, read); + body.close(); + return body.toByteArray(); + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ClientTimeoutTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ClientTimeoutTest.java new file mode 100644 index 00000000000..1021d628fbf --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ClientTimeoutTest.java @@ -0,0 +1,115 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import junit.framework.TestCase; + +import org.eclipse.jetty.client.Address; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.rhttp.client.ClientListener; +import org.eclipse.jetty.rhttp.client.JettyClient; +import org.eclipse.jetty.rhttp.client.RHTTPClient; +import org.eclipse.jetty.rhttp.gateway.GatewayServer; +import org.eclipse.jetty.rhttp.gateway.StandardGateway; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.nio.SelectChannelConnector; + + +/** + * @version $Revision$ $Date$ + */ +public class ClientTimeoutTest extends TestCase +{ + public void testClientTimeout() throws Exception + { + GatewayServer server = new GatewayServer(); + Connector connector = new SelectChannelConnector(); + server.addConnector(connector); + final long clientTimeout = 2000L; + server.getConnectorServlet().setInitParameter("clientTimeout",""+clientTimeout); + final long gatewayTimeout = 4000L; + ((StandardGateway)server.getGateway()).setGatewayTimeout(gatewayTimeout); + server.start(); + try + { + Address address = new Address("localhost", connector.getLocalPort()); + + HttpClient httpClient = new HttpClient(); + httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); + httpClient.start(); + try + { + String targetId = "1"; + final RHTTPClient client = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId) + { + private final AtomicInteger connects = new AtomicInteger(); + + @Override + protected void asyncConnect() + { + if (connects.incrementAndGet() == 2) + { + try + { + // Wait here instead of connecting, so that the client expires on the server + Thread.sleep(clientTimeout * 2); + } + catch (InterruptedException x) + { + throw new RuntimeException(x); + } + } + super.asyncConnect(); + } + }; + + final CountDownLatch connectLatch = new CountDownLatch(1); + client.addClientListener(new ClientListener.Adapter() + { + @Override + public void connectRequired() + { + connectLatch.countDown(); + } + }); + client.connect(); + try + { + assertTrue(connectLatch.await(gatewayTimeout + clientTimeout * 3, TimeUnit.MILLISECONDS)); + } + finally + { + client.disconnect(); + } + } + finally + { + httpClient.stop(); + } + } + finally + { + server.stop(); + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/DisconnectClientTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/DisconnectClientTest.java new file mode 100644 index 00000000000..6c8f9d03701 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/DisconnectClientTest.java @@ -0,0 +1,93 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import junit.framework.TestCase; + +import org.eclipse.jetty.client.Address; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.rhttp.client.JettyClient; +import org.eclipse.jetty.rhttp.client.RHTTPClient; +import org.eclipse.jetty.rhttp.gateway.GatewayServer; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.nio.SelectChannelConnector; + + +/** + * @version $Revision$ $Date$ + */ +public class DisconnectClientTest extends TestCase +{ + public void testDifferentClientDisconnects() throws Exception + { + GatewayServer server = new GatewayServer(); + Connector connector = new SelectChannelConnector(); + server.addConnector(connector); + server.start(); + try + { + Address address = new Address("localhost", connector.getLocalPort()); + + HttpClient httpClient = new HttpClient(); + httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); + httpClient.start(); + try + { + final CountDownLatch latch = new CountDownLatch(1); + String targetId = "1"; + final RHTTPClient client1 = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId) + { + @Override + protected void connectComplete(byte[] responseContent) throws IOException + { + // If the other client can disconnect this one, this method is called soon after it disconnected + latch.countDown(); + super.connectComplete(responseContent); + } + }; + client1.connect(); + try + { + final RHTTPClient client2 = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId); + // Disconnect client 2, this should not disconnect client1 + client2.disconnect(); + + // We want the await() to expire, it means it has not disconnected + assertFalse(latch.await(1000, TimeUnit.MILLISECONDS)); + } + finally + { + client1.disconnect(); + } + } + finally + { + httpClient.stop(); + } + } + finally + { + server.stop(); + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/DuplicateClientTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/DuplicateClientTest.java new file mode 100644 index 00000000000..50eb45ad0bc --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/DuplicateClientTest.java @@ -0,0 +1,84 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.io.IOException; + +import junit.framework.TestCase; + +import org.eclipse.jetty.client.Address; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.rhttp.client.JettyClient; +import org.eclipse.jetty.rhttp.client.RHTTPClient; +import org.eclipse.jetty.rhttp.gateway.GatewayServer; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.nio.SelectChannelConnector; + + +/** + * @version $Revision$ $Date$ + */ +public class DuplicateClientTest extends TestCase +{ + public void testDuplicateClient() throws Exception + { + GatewayServer server = new GatewayServer(); + Connector connector = new SelectChannelConnector(); + server.addConnector(connector); + server.start(); + try + { + Address address = new Address("localhost", connector.getLocalPort()); + + HttpClient httpClient = new HttpClient(); + httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); + httpClient.start(); + try + { + String targetId = "1"; + final RHTTPClient client1 = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId); + client1.connect(); + try + { + final RHTTPClient client2 = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId); + try + { + client2.connect(); + fail(); + } + catch (IOException x) + { + } + } + finally + { + client1.disconnect(); + } + } + finally + { + httpClient.stop(); + } + } + finally + { + server.stop(); + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ExternalRequestNotSuspendedTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ExternalRequestNotSuspendedTest.java new file mode 100644 index 00000000000..0cd05d3c0f5 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ExternalRequestNotSuspendedTest.java @@ -0,0 +1,186 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.io.IOException; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import junit.framework.TestCase; + +import org.eclipse.jetty.client.Address; +import org.eclipse.jetty.client.ContentExchange; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpExchange; +import org.eclipse.jetty.http.HttpMethods; +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.rhttp.client.JettyClient; +import org.eclipse.jetty.rhttp.client.RHTTPClient; +import org.eclipse.jetty.rhttp.client.RHTTPListener; +import org.eclipse.jetty.rhttp.client.RHTTPRequest; +import org.eclipse.jetty.rhttp.client.RHTTPResponse; +import org.eclipse.jetty.rhttp.gateway.ExternalRequest; +import org.eclipse.jetty.rhttp.gateway.Gateway; +import org.eclipse.jetty.rhttp.gateway.GatewayServer; +import org.eclipse.jetty.rhttp.gateway.StandardGateway; +import org.eclipse.jetty.server.nio.SelectChannelConnector; + + +/** + * @version $Revision$ $Date$ + */ +public class ExternalRequestNotSuspendedTest extends TestCase +{ + public void testExternalRequestNotSuspended() throws Exception + { + final CountDownLatch respondLatch = new CountDownLatch(1); + final CountDownLatch suspendLatch = new CountDownLatch(1); + final AtomicBoolean suspended = new AtomicBoolean(true); + GatewayServer server = new GatewayServer() + { + @Override + protected Gateway createGateway() + { + StandardGateway gateway = new StandardGateway() + { + @Override + public ExternalRequest newExternalRequest(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException + { + return new SlowToSuspendExternalRequest(super.newExternalRequest(httpRequest, httpResponse), respondLatch, suspendLatch, suspended); + } + }; + return gateway; + } + }; + SelectChannelConnector connector = new SelectChannelConnector(); + server.addConnector(connector); + server.start(); + try + { + Address address = new Address("localhost", connector.getLocalPort()); + + HttpClient httpClient = new HttpClient(); + httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); + httpClient.start(); + try + { + String targetId = "1"; + final RHTTPClient client = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId); + final AtomicReference exception = new AtomicReference(); + client.addListener(new RHTTPListener() + { + public void onRequest(RHTTPRequest request) + { + try + { + RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap(), request.getBody()); + client.deliver(response); + } + catch (Exception x) + { + exception.set(x); + } + } + }); + + client.connect(); + try + { + // Make a request to the gateway and check response + ContentExchange exchange = new ContentExchange(true); + exchange.setMethod(HttpMethods.POST); + exchange.setAddress(address); + exchange.setURI(server.getContext().getContextPath()+GatewayServer.DFT_EXT_PATH + "/" + URLEncoder.encode(targetId, "UTF-8")); + String requestContent = "body"; + exchange.setRequestContent(new ByteArrayBuffer(requestContent.getBytes("UTF-8"))); + httpClient.send(exchange); + + int status = exchange.waitForDone(); + assertEquals(HttpExchange.STATUS_COMPLETED, status); + assertEquals(HttpServletResponse.SC_OK, exchange.getResponseStatus()); + assertNull(exception.get()); + + suspendLatch.await(); + assertFalse(suspended.get()); + } + finally + { + client.disconnect(); + } + } + finally + { + httpClient.stop(); + } + } + finally + { + server.stop(); + } + } + + private class SlowToSuspendExternalRequest implements ExternalRequest + { + private final ExternalRequest delegate; + private final CountDownLatch respondLatch; + private final CountDownLatch suspendLatch; + private final AtomicBoolean suspended; + + private SlowToSuspendExternalRequest(ExternalRequest delegate, CountDownLatch respondLatch, CountDownLatch suspendLatch, AtomicBoolean suspended) + { + this.delegate = delegate; + this.respondLatch = respondLatch; + this.suspendLatch = suspendLatch; + this.suspended = suspended; + } + + public boolean suspend() + { + try + { + respondLatch.await(); + boolean result = delegate.suspend(); + suspended.set(result); + suspendLatch.countDown(); + return result; + } + catch (InterruptedException x) + { + throw new AssertionError(x); + } + } + + public void respond(RHTTPResponse response) throws IOException + { + delegate.respond(response); + respondLatch.countDown(); + } + + public RHTTPRequest getRequest() + { + return delegate.getRequest(); + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ExternalTimeoutTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ExternalTimeoutTest.java new file mode 100644 index 00000000000..4b88dc3e45a --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/ExternalTimeoutTest.java @@ -0,0 +1,126 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicReference; + +import javax.servlet.http.HttpServletResponse; + +import junit.framework.TestCase; + +import org.eclipse.jetty.client.Address; +import org.eclipse.jetty.client.ContentExchange; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpExchange; +import org.eclipse.jetty.http.HttpMethods; +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.rhttp.client.JettyClient; +import org.eclipse.jetty.rhttp.client.RHTTPClient; +import org.eclipse.jetty.rhttp.client.RHTTPListener; +import org.eclipse.jetty.rhttp.client.RHTTPRequest; +import org.eclipse.jetty.rhttp.client.RHTTPResponse; +import org.eclipse.jetty.rhttp.gateway.GatewayServer; +import org.eclipse.jetty.rhttp.gateway.StandardGateway; +import org.eclipse.jetty.server.nio.SelectChannelConnector; + + +/** + * @version $Revision$ $Date$ + */ +public class ExternalTimeoutTest extends TestCase +{ + public void testExternalTimeout() throws Exception + { + GatewayServer server = new GatewayServer(); + SelectChannelConnector connector = new SelectChannelConnector(); + server.addConnector(connector); + final long externalTimeout = 5000L; + ((StandardGateway)server.getGateway()).setExternalTimeout(externalTimeout); + server.start(); + try + { + Address address = new Address("localhost", connector.getLocalPort()); + + HttpClient httpClient = new HttpClient(); + httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); + httpClient.start(); + try + { + String targetId = "1"; + final RHTTPClient client = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId); + final AtomicReference requestId = new AtomicReference(); + final AtomicReference exceptionRef = new AtomicReference(); + client.addListener(new RHTTPListener() + { + public void onRequest(RHTTPRequest request) + { + try + { + // Save the request id + requestId.set(request.getId()); + + // Wait until external timeout expires + Thread.sleep(externalTimeout + 1000L); + RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap(), request.getBody()); + client.deliver(response); + } + catch (Exception x) + { + exceptionRef.set(x); + } + } + }); + + client.connect(); + try + { + // Make a request to the gateway and check response + ContentExchange exchange = new ContentExchange(true); + exchange.setMethod(HttpMethods.POST); + exchange.setAddress(address); + exchange.setURI(server.getContext().getContextPath()+GatewayServer.DFT_EXT_PATH + "/" + URLEncoder.encode(targetId, "UTF-8")); + String requestContent = "body"; + exchange.setRequestContent(new ByteArrayBuffer(requestContent.getBytes("UTF-8"))); + httpClient.send(exchange); + + int status = exchange.waitForDone(); + assertEquals(HttpExchange.STATUS_COMPLETED, status); + assertEquals(HttpServletResponse.SC_GATEWAY_TIMEOUT, exchange.getResponseStatus()); + assertNull(exceptionRef.get()); + + assertNull(server.getGateway().removeExternalRequest(requestId.get())); + } + finally + { + client.disconnect(); + } + } + finally + { + httpClient.stop(); + } + } + finally + { + server.stop(); + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayEchoServer.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayEchoServer.java new file mode 100644 index 00000000000..a3862b5f522 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayEchoServer.java @@ -0,0 +1,109 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.util.HashMap; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jetty.client.Address; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.rhttp.client.JettyClient; +import org.eclipse.jetty.rhttp.client.RHTTPClient; +import org.eclipse.jetty.rhttp.client.RHTTPListener; +import org.eclipse.jetty.rhttp.client.RHTTPRequest; +import org.eclipse.jetty.rhttp.client.RHTTPResponse; +import org.eclipse.jetty.rhttp.gateway.GatewayServer; +import org.eclipse.jetty.rhttp.gateway.TargetIdRetriever; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.nio.SelectChannelConnector; + + +/** + * @version $Revision$ $Date$ + */ +public class GatewayEchoServer +{ + private volatile GatewayServer server; + private volatile Address address; + private volatile String uri; + private volatile HttpClient httpClient; + private volatile RHTTPClient client; + + public void start() throws Exception + { + server = new GatewayServer(); + Connector connector = new SelectChannelConnector(); + server.addConnector(connector); + server.setTargetIdRetriever(new EchoTargetIdRetriever()); + server.start(); + server.dumpStdErr(); + address = new Address("localhost", connector.getLocalPort()); + uri = server.getContext().getContextPath()+GatewayServer.DFT_EXT_PATH; + + httpClient = new HttpClient(); + httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); + httpClient.start(); + + client = new JettyClient(httpClient, new Address("localhost", connector.getLocalPort()), server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, "echo"); + client.addListener(new EchoListener(client)); + client.connect(); + } + + public void stop() throws Exception + { + client.disconnect(); + httpClient.stop(); + server.stop(); + } + + public Address getAddress() + { + return address; + } + + public String getURI() + { + return uri; + } + + public static class EchoTargetIdRetriever implements TargetIdRetriever + { + public String retrieveTargetId(HttpServletRequest httpRequest) + { + return "echo"; + } + } + + private static class EchoListener implements RHTTPListener + { + private final RHTTPClient client; + + public EchoListener(RHTTPClient client) + { + this.client = client; + } + + public void onRequest(RHTTPRequest request) throws Exception + { + RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap(), request.getBody()); + client.deliver(response); + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayEchoTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayEchoTest.java new file mode 100644 index 00000000000..d4c34be2293 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayEchoTest.java @@ -0,0 +1,77 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import javax.servlet.http.HttpServletResponse; + +import junit.framework.TestCase; + +import org.eclipse.jetty.client.ContentExchange; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpExchange; +import org.eclipse.jetty.http.HttpMethods; +import org.eclipse.jetty.io.ByteArrayBuffer; + +/** + * @version $Revision$ $Date$ + */ +public class GatewayEchoTest extends TestCase +{ + /** + * Tests that the basic functionality of the gateway works, + * by issuing a request and by replying with the same body. + * + * @throws Exception in case of test exceptions + */ + public void testEcho() throws Exception + { + GatewayEchoServer server = new GatewayEchoServer(); + server.start(); + try + { + HttpClient httpClient = new HttpClient(); + httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); + httpClient.start(); + try + { + // Make a request to the gateway and check response + ContentExchange exchange = new ContentExchange(true); + exchange.setMethod(HttpMethods.POST); + exchange.setAddress(server.getAddress()); + exchange.setURI(server.getURI() + "/"); + String requestBody = "body"; + exchange.setRequestContent(new ByteArrayBuffer(requestBody.getBytes("UTF-8"))); + httpClient.send(exchange); + int status = exchange.waitForDone(); + assertEquals(HttpExchange.STATUS_COMPLETED, status); + assertEquals(HttpServletResponse.SC_OK, exchange.getResponseStatus()); + String responseContent = exchange.getResponseContent(); + assertEquals(responseContent, requestBody); + } + finally + { + httpClient.stop(); + } + } + finally + { + server.stop(); + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayLoadTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayLoadTest.java new file mode 100644 index 00000000000..7c65db22403 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayLoadTest.java @@ -0,0 +1,202 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import javax.servlet.http.HttpServletResponse; + +import junit.framework.TestCase; + +import org.eclipse.jetty.client.ContentExchange; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpMethods; +import org.eclipse.jetty.io.ByteArrayBuffer; + +/** + * @version $Revision$ $Date$ + */ +public class GatewayLoadTest extends TestCase +{ + private final ConcurrentMap latencies = new ConcurrentHashMap(); + private final AtomicLong responses = new AtomicLong(0L); + private final AtomicLong failures = new AtomicLong(0L); + private final AtomicLong minLatency = new AtomicLong(Long.MAX_VALUE); + private final AtomicLong maxLatency = new AtomicLong(0L); + private final AtomicLong totLatency = new AtomicLong(0L); + + public void testEcho() throws Exception + { + GatewayEchoServer server = new GatewayEchoServer(); + server.start(); + + HttpClient httpClient = new HttpClient(); + httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); + httpClient.start(); + + String uri = server.getURI() + "/"; + + char[] chars = new char[1024]; + Arrays.fill(chars, 'x'); + String requestBody = new String(chars); + + int count = 1000; + CountDownLatch latch = new CountDownLatch(count); + for (int i = 0; i < count; ++i) + { + GatewayLoadTestExchange exchange = new GatewayLoadTestExchange(latch); + exchange.setMethod(HttpMethods.POST); + exchange.setAddress(server.getAddress()); + exchange.setURI(uri + i); + exchange.setRequestContent(new ByteArrayBuffer(requestBody.getBytes("UTF-8"))); + exchange.setStartNanos(System.nanoTime()); + httpClient.send(exchange); + Thread.sleep(5); + } + latch.await(); + printLatencies(count); + assertEquals(count, responses.get() + failures.get()); + } + + private void updateLatencies(long start, long end) + { + long latency = end - start; + + // Update the latencies using a non-blocking algorithm + long oldMinLatency = minLatency.get(); + while (latency < oldMinLatency) + { + if (minLatency.compareAndSet(oldMinLatency, latency)) break; + oldMinLatency = minLatency.get(); + } + long oldMaxLatency = maxLatency.get(); + while (latency > oldMaxLatency) + { + if (maxLatency.compareAndSet(oldMaxLatency, latency)) break; + oldMaxLatency = maxLatency.get(); + } + totLatency.addAndGet(latency); + + latencies.putIfAbsent(latency, new AtomicLong(0L)); + latencies.get(latency).incrementAndGet(); + } + + public void printLatencies(long expectedCount) + { + if (latencies.size() > 1) + { + long maxLatencyBucketFrequency = 0L; + long[] latencyBucketFrequencies = new long[20]; + long latencyRange = maxLatency.get() - minLatency.get(); + for (Iterator> entries = latencies.entrySet().iterator(); entries.hasNext();) + { + Map.Entry entry = entries.next(); + long latency = entry.getKey(); + Long bucketIndex = (latency - minLatency.get()) * latencyBucketFrequencies.length / latencyRange; + int index = bucketIndex.intValue() == latencyBucketFrequencies.length ? latencyBucketFrequencies.length - 1 : bucketIndex.intValue(); + long value = entry.getValue().get(); + latencyBucketFrequencies[index] += value; + if (latencyBucketFrequencies[index] > maxLatencyBucketFrequency) maxLatencyBucketFrequency = latencyBucketFrequencies[index]; + entries.remove(); + } + + System.out.println("Messages - Latency Distribution Curve (X axis: Frequency, Y axis: Latency):"); + for (int i = 0; i < latencyBucketFrequencies.length; i++) + { + long latencyBucketFrequency = latencyBucketFrequencies[i]; + int value = Math.round(latencyBucketFrequency * (float) latencyBucketFrequencies.length / maxLatencyBucketFrequency); + if (value == latencyBucketFrequencies.length) value = value - 1; + for (int j = 0; j < value; ++j) System.out.print(" "); + System.out.print("@"); + for (int j = value + 1; j < latencyBucketFrequencies.length; ++j) System.out.print(" "); + System.out.print(" _ "); + System.out.print(TimeUnit.NANOSECONDS.toMillis((latencyRange * (i + 1) / latencyBucketFrequencies.length) + minLatency.get())); + System.out.print(" (" + latencyBucketFrequency + ")"); + System.out.println(" ms"); + } + } + + long responseCount = responses.get(); + System.out.print("Messages success/failed/expected = "); + System.out.print(responseCount); + System.out.print("/"); + System.out.print(failures.get()); + System.out.print("/"); + System.out.print(expectedCount); + System.out.print(" - Latency min/ave/max = "); + System.out.print(TimeUnit.NANOSECONDS.toMillis(minLatency.get()) + "/"); + System.out.print(responseCount == 0 ? "-/" : TimeUnit.NANOSECONDS.toMillis(totLatency.get() / responseCount) + "/"); + System.out.println(TimeUnit.NANOSECONDS.toMillis(maxLatency.get()) + " ms"); + } + + private class GatewayLoadTestExchange extends ContentExchange + { + private final CountDownLatch latch; + private volatile long start; + + private GatewayLoadTestExchange(CountDownLatch latch) + { + super(true); + this.latch = latch; + } + + @Override + protected void onResponseComplete() throws IOException + { + if (getResponseStatus() == HttpServletResponse.SC_OK) + { + long end = System.nanoTime(); + responses.incrementAndGet(); + updateLatencies(start, end); + } + else + { + failures.incrementAndGet(); + } + latch.countDown(); + } + + @Override + protected void onException(Throwable throwable) + { + failures.incrementAndGet(); + latch.countDown(); + } + + @Override + protected void onExpire() + { + failures.incrementAndGet(); + latch.countDown(); + } + + public void setStartNanos(long value) + { + start = value; + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayTimeoutTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayTimeoutTest.java new file mode 100644 index 00000000000..1c6b1f8fd39 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/GatewayTimeoutTest.java @@ -0,0 +1,130 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicReference; + +import javax.servlet.http.HttpServletResponse; + +import junit.framework.TestCase; + +import org.eclipse.jetty.client.Address; +import org.eclipse.jetty.client.ContentExchange; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpExchange; +import org.eclipse.jetty.http.HttpMethods; +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.rhttp.client.JettyClient; +import org.eclipse.jetty.rhttp.client.RHTTPClient; +import org.eclipse.jetty.rhttp.client.RHTTPListener; +import org.eclipse.jetty.rhttp.client.RHTTPRequest; +import org.eclipse.jetty.rhttp.client.RHTTPResponse; +import org.eclipse.jetty.rhttp.gateway.GatewayServer; +import org.eclipse.jetty.rhttp.gateway.StandardGateway; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.nio.SelectChannelConnector; + + +/** + * @version $Revision$ $Date$ + */ +public class GatewayTimeoutTest extends TestCase +{ + /** + * Tests a forwarded request that lasts longer than the gateway timeout. + * The gateway client will perform 2 long polls before the forwarded request's response is returned. + * + * @throws Exception in case of test exceptions + */ + public void testGatewayTimeout() throws Exception + { + GatewayServer server = new GatewayServer(); + Connector connector = new SelectChannelConnector(); + server.addConnector(connector); + final long gatewayTimeout = 5000L; + ((StandardGateway)server.getGateway()).setGatewayTimeout(gatewayTimeout); + final long externalTimeout = gatewayTimeout * 2; + ((StandardGateway)server.getGateway()).setExternalTimeout(externalTimeout); + server.start(); + try + { + Address address = new Address("localhost", connector.getLocalPort()); + + HttpClient httpClient = new HttpClient(); + httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); + httpClient.start(); + try + { + String targetId = "1"; + final RHTTPClient client = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId); + final AtomicReference exceptionRef = new AtomicReference(); + client.addListener(new RHTTPListener() + { + public void onRequest(RHTTPRequest request) + { + try + { + // Wait until gateway timeout expires + Thread.sleep(gatewayTimeout + 1000L); + RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap(), request.getBody()); + client.deliver(response); + } + catch (Exception x) + { + exceptionRef.set(x); + } + } + }); + client.connect(); + try + { + // Make a request to the gateway and check response + ContentExchange exchange = new ContentExchange(true); + exchange.setMethod(HttpMethods.POST); + exchange.setAddress(address); + exchange.setURI(server.getContext().getContextPath()+GatewayServer.DFT_EXT_PATH + "/" + URLEncoder.encode(targetId, "UTF-8")); + String requestContent = "body"; + exchange.setRequestContent(new ByteArrayBuffer(requestContent.getBytes("UTF-8"))); + httpClient.send(exchange); + + int status = exchange.waitForDone(); + assertEquals(HttpExchange.STATUS_COMPLETED, status); + assertEquals(HttpServletResponse.SC_OK, exchange.getResponseStatus()); + assertNull(exceptionRef.get()); + String responseContent = exchange.getResponseContent(); + assertEquals(responseContent, requestContent); + } + finally + { + client.disconnect(); + } + } + finally + { + httpClient.stop(); + } + } + finally + { + server.stop(); + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/HandshakeClientTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/HandshakeClientTest.java new file mode 100644 index 00000000000..bee181319b2 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/HandshakeClientTest.java @@ -0,0 +1,75 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import junit.framework.TestCase; + +import org.eclipse.jetty.client.Address; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.rhttp.client.JettyClient; +import org.eclipse.jetty.rhttp.client.RHTTPClient; +import org.eclipse.jetty.rhttp.gateway.GatewayServer; +import org.eclipse.jetty.rhttp.gateway.StandardGateway; +import org.eclipse.jetty.server.nio.SelectChannelConnector; + + +/** + * @version $Revision$ $Date$ + */ +public class HandshakeClientTest extends TestCase +{ + public void testConnectReturnsImmediately() throws Exception + { + GatewayServer server = new GatewayServer(); + SelectChannelConnector connector = new SelectChannelConnector(); + server.addConnector(connector); + long gwt=5000L; + ((StandardGateway)server.getGateway()).setGatewayTimeout(gwt); + server.start(); + try + { + HttpClient httpClient = new HttpClient(); + httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); + httpClient.start(); + try + { + RHTTPClient client = new JettyClient(httpClient, new Address("localhost", connector.getLocalPort()), server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, "test1"); + long start = System.currentTimeMillis(); + client.connect(); + try + { + long end = System.currentTimeMillis(); + assertTrue(end - start < gwt / 2); + } + finally + { + client.disconnect(); + } + } + finally + { + httpClient.stop(); + } + } + finally + { + server.stop(); + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/HostTargetIdRetrieverTest.java b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/HostTargetIdRetrieverTest.java new file mode 100644 index 00000000000..911d1cb2fdb --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/java/org/eclipse/jetty/rhttp/gateway/HostTargetIdRetrieverTest.java @@ -0,0 +1,107 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.gateway; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jetty.rhttp.gateway.HostTargetIdRetriever; + +import junit.framework.TestCase; + +/** + * @version $Revision$ $Date$ + */ +public class HostTargetIdRetrieverTest extends TestCase +{ + public void testHostTargetIdRetrieverNoSuffix() + { + String host = "test"; + Class klass = HttpServletRequest.class; + HttpServletRequest request = (HttpServletRequest)Proxy.newProxyInstance(klass.getClassLoader(), new Class[]{klass}, new Request(host)); + + HostTargetIdRetriever retriever = new HostTargetIdRetriever(null); + String result = retriever.retrieveTargetId(request); + + assertEquals(host, result); + } + + public void testHostTargetIdRetrieverWithSuffix() + { + String suffix = ".rhttp.example.com"; + String host = "test"; + Class klass = HttpServletRequest.class; + HttpServletRequest request = (HttpServletRequest)Proxy.newProxyInstance(klass.getClassLoader(), new Class[]{klass}, new Request(host + suffix)); + + HostTargetIdRetriever retriever = new HostTargetIdRetriever(suffix); + String result = retriever.retrieveTargetId(request); + + assertEquals(host, result); + } + + public void testHostTargetIdRetrieverWithSuffixAndPort() + { + String suffix = ".rhttp.example.com"; + String host = "test"; + Class klass = HttpServletRequest.class; + HttpServletRequest request = (HttpServletRequest)Proxy.newProxyInstance(klass.getClassLoader(), new Class[]{klass}, new Request(host + suffix + ":8080")); + + HostTargetIdRetriever retriever = new HostTargetIdRetriever(suffix); + String result = retriever.retrieveTargetId(request); + + assertEquals(host, result); + } + + public void testHostTargetIdRetrieverNullHost() + { + Class klass = HttpServletRequest.class; + HttpServletRequest request = (HttpServletRequest)Proxy.newProxyInstance(klass.getClassLoader(), new Class[]{klass}, new Request(null)); + + HostTargetIdRetriever retriever = new HostTargetIdRetriever(".rhttp.example.com"); + String result = retriever.retrieveTargetId(request); + + assertNull(result); + } + + private static class Request implements InvocationHandler + { + private final String host; + + private Request(String host) + { + this.host = host; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable + { + if ("getHeader".equals(method.getName())) + { + if (args.length == 1 && "Host".equals(args[0])) + { + return host; + } + } + return null; + } + } + +} diff --git a/jetty-rhttp/jetty-rhttp-gateway/src/test/resources/log4j.properties b/jetty-rhttp/jetty-rhttp-gateway/src/test/resources/log4j.properties new file mode 100644 index 00000000000..8e6eddb704b --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-gateway/src/test/resources/log4j.properties @@ -0,0 +1,13 @@ +# LOG4J levels: OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL +# +log4j.rootLogger=ALL,CONSOLE + +log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender +#log4j.appender.CONSOLE.threshold=INFO +log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout +#log4j.appender.CONSOLE.layout.ConversionPattern=%d %t [%5p][%c{1}] %m%n +log4j.appender.CONSOLE.layout.ConversionPattern=%d [%5p][%c] %m%n + +# Level tuning +log4j.logger.org.eclipse.jetty=INFO +log4j.logger.org.mortbay.jetty.rhttp=INFO diff --git a/jetty-rhttp/jetty-rhttp-loadtest/pom.xml b/jetty-rhttp/jetty-rhttp-loadtest/pom.xml new file mode 100644 index 00000000000..bd36d4ebdec --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-loadtest/pom.xml @@ -0,0 +1,58 @@ + + + + org.eclipse.jetty.rhttp + jetty-rhttp-project + 9.0.0-SNAPSHOT + + + 4.0.0 + reverse-http-loadtest + jar + Jetty :: Reverse HTTP :: Load Test + + + + loader + + true + + + + + org.codehaus.mojo + exec-maven-plugin + + org.eclipse.jetty.rhttp.loadtest.Loader + runtime + + + + + + + server + + + + org.codehaus.mojo + exec-maven-plugin + + org.eclipse.jetty.rhttp.loadtest.Server + runtime + + + + + + + + + + org.eclipse.jetty + reverse-http-gateway + ${project.version} + + + + diff --git a/jetty-rhttp/jetty-rhttp-loadtest/src/main/java/org/eclipse/jetty/rhttp/loadtest/Loader.java b/jetty-rhttp/jetty-rhttp-loadtest/src/main/java/org/eclipse/jetty/rhttp/loadtest/Loader.java new file mode 100644 index 00000000000..383ddd7ecd1 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-loadtest/src/main/java/org/eclipse/jetty/rhttp/loadtest/Loader.java @@ -0,0 +1,429 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.loadtest; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import org.eclipse.jetty.client.Address; +import org.eclipse.jetty.client.ContentExchange; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.mortbay.jetty.rhttp.client.RHTTPClient; +import org.mortbay.jetty.rhttp.client.JettyClient; +import org.mortbay.jetty.rhttp.client.RHTTPListener; +import org.mortbay.jetty.rhttp.client.RHTTPRequest; +import org.mortbay.jetty.rhttp.client.RHTTPResponse; + +/** + * @version $Revision$ $Date$ + */ +public class Loader +{ + private final List clients = new ArrayList(); + private final AtomicLong start = new AtomicLong(); + private final AtomicLong end = new AtomicLong(); + private final AtomicLong responses = new AtomicLong(); + private final AtomicLong failures = new AtomicLong(); + private final AtomicLong minLatency = new AtomicLong(); + private final AtomicLong maxLatency = new AtomicLong(); + private final AtomicLong totLatency = new AtomicLong(); + private final ConcurrentMap latencies = new ConcurrentHashMap(); + private final String nodeName; + + public static void main(String[] args) throws Exception + { + String nodeName = ""; + if (args.length > 0) + nodeName = args[0]; + + Loader loader = new Loader(nodeName); + loader.run(); + } + + public Loader(String nodeName) + { + this.nodeName = nodeName; + } + + private void run() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.setMaxConnectionsPerAddress(40000); + QueuedThreadPool threadPool = new QueuedThreadPool(); + threadPool.setMaxThreads(500); + threadPool.setDaemon(true); + httpClient.setThreadPool(threadPool); + httpClient.setIdleTimeout(5000); + httpClient.start(); + + Random random = new Random(); + + BufferedReader console = new BufferedReader(new InputStreamReader(System.in)); + + System.err.print("server [localhost]: "); + String value = console.readLine().trim(); + if (value.length() == 0) + value = "localhost"; + String host = value; + + System.err.print("port [8080]: "); + value = console.readLine().trim(); + if (value.length() == 0) + value = "8080"; + int port = Integer.parseInt(value); + + System.err.print("context []: "); + value = console.readLine().trim(); + if (value.length() == 0) + value = ""; + String context = value; + + System.err.print("external path [/]: "); + value = console.readLine().trim(); + if (value.length() == 0) + value = "/"; + String externalPath = value; + + System.err.print("gateway path [/__gateway]: "); + value = console.readLine().trim(); + if (value.length() == 0) + value = "/__gateway"; + String gatewayPath = value; + + int clients = 100; + int batchCount = 1000; + int batchSize = 5; + long batchPause = 5; + int requestSize = 50; + + while (true) + { + System.err.println("-----"); + + System.err.print("clients [" + clients + "]: "); + value = console.readLine(); + if (value == null) + break; + value = value.trim(); + if (value.length() == 0) + value = "" + clients; + clients = Integer.parseInt(value); + + System.err.println("Waiting for clients to be ready..."); + + Address gatewayAddress = new Address(host, port); + String gatewayURI = context + gatewayPath; + + // Create or remove the necessary clients + int currentClients = this.clients.size(); + if (currentClients < clients) + { + for (int i = 0; i < clients - currentClients; ++i) + { + final RHTTPClient client = new JettyClient(httpClient, gatewayAddress, gatewayURI, nodeName + (currentClients + i)); + client.addListener(new EchoListener(client)); + client.connect(); + this.clients.add(client); + + // Give some time to the server to accept connections and + // reply to handshakes and connects + if (i % 10 == 0) + { + Thread.sleep(100); + } + } + } + else if (currentClients > clients) + { + for (int i = 0; i < currentClients - clients; ++i) + { + RHTTPClient client = this.clients.remove(currentClients - i - 1); + client.disconnect(); + } + } + + System.err.println("Clients ready"); + + currentClients = this.clients.size(); + if (currentClients > 0) + { + System.err.print("batch count [" + batchCount + "]: "); + value = console.readLine().trim(); + if (value.length() == 0) + value = "" + batchCount; + batchCount = Integer.parseInt(value); + + System.err.print("batch size [" + batchSize + "]: "); + value = console.readLine().trim(); + if (value.length() == 0) + value = "" + batchSize; + batchSize = Integer.parseInt(value); + + System.err.print("batch pause [" + batchPause + "]: "); + value = console.readLine().trim(); + if (value.length() == 0) + value = "" + batchPause; + batchPause = Long.parseLong(value); + + System.err.print("request size [" + requestSize + "]: "); + value = console.readLine().trim(); + if (value.length() == 0) + value = "" + requestSize; + requestSize = Integer.parseInt(value); + String requestBody = ""; + for (int i = 0; i < requestSize; i++) + requestBody += "x"; + + String externalURL = "http://" + host + ":" + port + context + externalPath; + if (!externalURL.endsWith("/")) + externalURL += "/"; + + reset(); + + long start = System.nanoTime(); + long expected = 0; + for (int i = 0; i < batchCount; ++i) + { + for (int j = 0; j < batchSize; ++j) + { + int clientIndex = random.nextInt(this.clients.size()); + RHTTPClient client = this.clients.get(clientIndex); + String targetId = client.getTargetId(); + String url = externalURL + targetId; + + ExternalExchange exchange = new ExternalExchange(); + exchange.setMethod("GET"); + exchange.setURL(url); + exchange.setRequestContent(new ByteArrayBuffer(requestBody, "UTF-8")); + exchange.send(httpClient); + ++expected; + } + + if (batchPause > 0) + Thread.sleep(batchPause); + } + long end = System.nanoTime(); + long elapsedNanos = end - start; + if (elapsedNanos > 0) + { + System.err.print("Messages - Elapsed | Rate = "); + System.err.print(TimeUnit.NANOSECONDS.toMillis(elapsedNanos)); + System.err.print(" ms | "); + System.err.print(expected * 1000 * 1000 * 1000 / elapsedNanos); + System.err.println(" requests/s "); + } + + waitForResponses(expected); + printReport(expected); + } + } + } + + private void reset() + { + start.set(0L); + end.set(0L); + responses.set(0L); + failures.set(0L); + minLatency.set(Long.MAX_VALUE); + maxLatency.set(0L); + totLatency.set(0L); + } + + private void updateLatencies(long start, long end) + { + long latency = end - start; + + // Update the latencies using a non-blocking algorithm + long oldMinLatency = minLatency.get(); + while (latency < oldMinLatency) + { + if (minLatency.compareAndSet(oldMinLatency, latency)) break; + oldMinLatency = minLatency.get(); + } + long oldMaxLatency = maxLatency.get(); + while (latency > oldMaxLatency) + { + if (maxLatency.compareAndSet(oldMaxLatency, latency)) break; + oldMaxLatency = maxLatency.get(); + } + totLatency.addAndGet(latency); + + latencies.putIfAbsent(latency, new AtomicLong(0L)); + latencies.get(latency).incrementAndGet(); + } + + private boolean waitForResponses(long expected) throws InterruptedException + { + long arrived = responses.get() + failures.get(); + long lastArrived = 0; + int maxRetries = 20; + int retries = maxRetries; + while (arrived < expected) + { + System.err.println("Waiting for responses to arrive " + arrived + "/" + expected); + Thread.sleep(500); + if (lastArrived == arrived) + { + --retries; + if (retries == 0) break; + } + else + { + lastArrived = arrived; + retries = maxRetries; + } + arrived = responses.get() + failures.get(); + } + if (arrived < expected) + { + System.err.println("Interrupting wait for responses " + arrived + "/" + expected); + return false; + } + else + { + System.err.println("All responses arrived " + arrived + "/" + expected); + return true; + } + } + + public void printReport(long expectedCount) + { + long responseCount = responses.get() + failures.get(); + System.err.print("Messages - Success/Failures/Expected = "); + System.err.print(responses.get()); + System.err.print("/"); + System.err.print(failures.get()); + System.err.print("/"); + System.err.println(expectedCount); + + long elapsedNanos = end.get() - start.get(); + if (elapsedNanos > 0) + { + System.err.print("Messages - Elapsed | Rate = "); + System.err.print(TimeUnit.NANOSECONDS.toMillis(elapsedNanos)); + System.err.print(" ms | "); + System.err.print(responseCount * 1000 * 1000 * 1000 / elapsedNanos); + System.err.println(" responses/s "); + } + + if (latencies.size() > 1) + { + long maxLatencyBucketFrequency = 0L; + long[] latencyBucketFrequencies = new long[20]; + long latencyRange = maxLatency.get() - minLatency.get(); + for (Iterator> entries = latencies.entrySet().iterator(); entries.hasNext();) + { + Map.Entry entry = entries.next(); + long latency = entry.getKey(); + Long bucketIndex = (latency - minLatency.get()) * latencyBucketFrequencies.length / latencyRange; + int index = bucketIndex.intValue() == latencyBucketFrequencies.length ? latencyBucketFrequencies.length - 1 : bucketIndex.intValue(); + long value = entry.getValue().get(); + latencyBucketFrequencies[index] += value; + if (latencyBucketFrequencies[index] > maxLatencyBucketFrequency) maxLatencyBucketFrequency = latencyBucketFrequencies[index]; + entries.remove(); + } + + System.err.println("Messages - Latency Distribution Curve (X axis: Frequency, Y axis: Latency):"); + for (int i = 0; i < latencyBucketFrequencies.length; i++) + { + long latencyBucketFrequency = latencyBucketFrequencies[i]; + int value = Math.round(latencyBucketFrequency * (float) latencyBucketFrequencies.length / maxLatencyBucketFrequency); + if (value == latencyBucketFrequencies.length) value = value - 1; + for (int j = 0; j < value; ++j) System.err.print(" "); + System.err.print("@"); + for (int j = value + 1; j < latencyBucketFrequencies.length; ++j) System.err.print(" "); + System.err.print(" _ "); + System.err.print(TimeUnit.NANOSECONDS.toMillis((latencyRange * (i + 1) / latencyBucketFrequencies.length) + minLatency.get())); + System.err.println(" ms (" + latencyBucketFrequency + ")"); + } + } + + System.err.print("Messages - Latency Min/Ave/Max = "); + System.err.print(TimeUnit.NANOSECONDS.toMillis(minLatency.get()) + "/"); + System.err.print(responseCount == 0 ? "-/" : TimeUnit.NANOSECONDS.toMillis(totLatency.get() / responseCount) + "/"); + System.err.println(TimeUnit.NANOSECONDS.toMillis(maxLatency.get()) + " ms"); + } + + private class ExternalExchange extends ContentExchange + { + private volatile long sendTime; + + private ExternalExchange() + { + super(true); + } + + private void send(HttpClient httpClient) throws IOException + { + this.sendTime = System.nanoTime(); + httpClient.send(this); + } + + @Override + protected void onResponseComplete() throws IOException + { + if (getResponseStatus() == 200) + responses.incrementAndGet(); + else + failures.incrementAndGet(); + + long arrivalTime = System.nanoTime(); + if (start.get() == 0L) + start.set(arrivalTime); + end.set(arrivalTime); + updateLatencies(sendTime, arrivalTime); + } + + @Override + protected void onException(Throwable x) + { + failures.incrementAndGet(); + } + } + + private static class EchoListener implements RHTTPListener + { + private final RHTTPClient client; + + public EchoListener(RHTTPClient client) + { + this.client = client; + } + + public void onRequest(RHTTPRequest request) throws Exception + { + RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap(), request.getBody()); + client.deliver(response); + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-loadtest/src/main/java/org/eclipse/jetty/rhttp/loadtest/Server.java b/jetty-rhttp/jetty-rhttp-loadtest/src/main/java/org/eclipse/jetty/rhttp/loadtest/Server.java new file mode 100644 index 00000000000..20fcf1fa191 --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-loadtest/src/main/java/org/eclipse/jetty/rhttp/loadtest/Server.java @@ -0,0 +1,69 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.rhttp.loadtest; + +import org.eclipse.jetty.rhttp.gateway.GatewayServer; +import org.eclipse.jetty.rhttp.gateway.StandardTargetIdRetriever; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.nio.SelectChannelConnector; + +/** + * @version $Revision$ $Date$ + */ +public class Server +{ + public static void main(String[] args) throws Exception + { + int port = 8080; + if (args.length > 0) + port = Integer.parseInt(args[0]); + + GatewayServer server = new GatewayServer(); + Connector connector = new SelectChannelConnector(); + connector.setLowResourceMaxIdleTime(connector.getMaxIdleTime()); + connector.setPort(port); + server.addConnector(connector); + server.setTargetIdRetriever(new StandardTargetIdRetriever()); + server.start(); + server.getServer().dumpStdErr(); + Runtime.getRuntime().addShutdownHook(new Shutdown(server)); + } + + private static class Shutdown extends Thread + { + private final GatewayServer server; + + public Shutdown(GatewayServer server) + { + this.server = server; + } + + @Override + public void run() + { + try + { + server.stop(); + } + catch (Exception ignored) + { + } + } + } +} diff --git a/jetty-rhttp/jetty-rhttp-loadtest/src/main/resources/log4j.properties b/jetty-rhttp/jetty-rhttp-loadtest/src/main/resources/log4j.properties new file mode 100644 index 00000000000..8e6eddb704b --- /dev/null +++ b/jetty-rhttp/jetty-rhttp-loadtest/src/main/resources/log4j.properties @@ -0,0 +1,13 @@ +# LOG4J levels: OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL +# +log4j.rootLogger=ALL,CONSOLE + +log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender +#log4j.appender.CONSOLE.threshold=INFO +log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout +#log4j.appender.CONSOLE.layout.ConversionPattern=%d %t [%5p][%c{1}] %m%n +log4j.appender.CONSOLE.layout.ConversionPattern=%d [%5p][%c] %m%n + +# Level tuning +log4j.logger.org.eclipse.jetty=INFO +log4j.logger.org.mortbay.jetty.rhttp=INFO diff --git a/jetty-rhttp/pom.xml b/jetty-rhttp/pom.xml new file mode 100644 index 00000000000..307e73846e6 --- /dev/null +++ b/jetty-rhttp/pom.xml @@ -0,0 +1,108 @@ + + + org.eclipse.jetty + jetty-project + 9.0.0-SNAPSHOT + + + 4.0.0 + org.eclipse.jetty.rhttp + jetty-rhttp-project + pom + Jetty :: Reverse HTTP + + + + + + reverse-http-client + reverse-http-connector + reverse-http-gateway + reverse-http-loadtest + + + + + + org.sonatype.maven.plugin + emma-maven-plugin + + + process-classes + + instrument + + + + + + maven-surefire-plugin + + ${project.build.directory}/generated-classes/emma/classes + + + emma.coverage.out.file + ${project.build.directory}/coverage.ec + + + + + + org.sonatype.maven.plugin + emma4it-maven-plugin + + + report + post-integration-test + + report + + + + + ${project.build.sourceDirectory} + + + + + + + + + + + + + javax.servlet + servlet-api + + + org.eclipse.jetty + jetty-server + ${project.version} + + + org.eclipse.jetty + jetty-client + ${project.version} + + + org.eclipse.jetty + jetty-io + ${project.version} + + + org.eclipse.jetty + jetty-util + ${project.version} + + + junit + junit + test + + + + + org.eclipse.jetty.rhttp +