pluggable HTTP protocols, add netty-http-rx, adapted from https://github.com/ReactiveX/RxNetty

This commit is contained in:
Jörg Prante 2019-09-23 10:02:20 +02:00
parent 833e502a7c
commit 59ac22d492
378 changed files with 44782 additions and 1046 deletions

View file

@ -1,13 +1,13 @@
group = org.xbib group = org.xbib
name = netty-http name = netty-http
version = 4.1.41.0 version = 4.1.41.1
# netty # netty
netty.version = 4.1.41.Final netty.version = 4.1.41.Final
tcnative.version = 2.0.25.Final tcnative.version = 2.0.25.Final
# for netty-http-common # for netty-http-common
xbib-net-url.version = 2.0.1 xbib-net-url.version = 2.0.2
# for netty-http-server # for netty-http-server
bouncycastle.version = 1.62 bouncycastle.version = 1.62
@ -18,11 +18,16 @@ reactivestreams.version = 1.0.2
# for netty-http-server-rest # for netty-http-server-rest
xbib-guice.version = 4.0.4 xbib-guice.version = 4.0.4
# for rx
reactivex.version = 1.2.+
# test # test
junit.version = 5.5.1 junit.version = 5.5.1
junit4.version = 4.12 junit4.version = 4.12
conscrypt.version = 2.2.1 conscrypt.version = 2.2.1
jackson.version = 2.9.9 jackson.version = 2.9.9
hamcrest.version = 1.3
mockito.version = 1.10.19
# doc # doc
asciidoclet.version = 1.5.4 asciidoclet.version = 1.5.4

View file

@ -0,0 +1,3 @@
dependencies {
compile project(":netty-http-common")
}

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.client.retry; package org.xbib.netty.http.client.api;
/** /**
* Back-off policy when retrying an operation. * Back-off policy when retrying an operation.

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.client.listener; package org.xbib.netty.http.client.api;
@FunctionalInterface @FunctionalInterface
public interface ExceptionListener { public interface ExceptionListener {

View file

@ -0,0 +1,10 @@
package org.xbib.netty.http.client.api;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
public interface HttpChannelInitializer extends ChannelHandler {
void initChannel(Channel channel);
}

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.client.pool; package org.xbib.netty.http.client.api;
import java.io.Closeable; import java.io.Closeable;

View file

@ -0,0 +1,10 @@
package org.xbib.netty.http.client.api;
public interface ProtocolProvider<C extends HttpChannelInitializer, T extends Transport> {
boolean supportsMajorVersion(int majorVersion);
Class<C> initializerClass();
Class<T> transportClass();
}

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.client; package org.xbib.netty.http.client.api;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufAllocator;
@ -13,21 +13,17 @@ import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.QueryStringEncoder; import io.netty.handler.codec.http.QueryStringEncoder;
import io.netty.handler.codec.http2.HttpConversionUtil; import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.util.AsciiString; import io.netty.util.AsciiString;
import org.xbib.net.PercentEncoder; import org.xbib.net.PercentEncoder;
import org.xbib.net.PercentEncoders; import org.xbib.net.PercentEncoders;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.netty.http.client.listener.CookieListener;
import org.xbib.netty.http.client.listener.ResponseListener;
import org.xbib.netty.http.client.listener.StatusListener;
import org.xbib.netty.http.client.retry.BackOff;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.common.HttpParameters; import org.xbib.netty.http.common.HttpParameters;
import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.common.HttpResponse;
import org.xbib.netty.http.common.cookie.Cookie; import org.xbib.netty.http.common.cookie.Cookie;
import java.nio.charset.Charset;
import java.nio.charset.MalformedInputException; import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.charset.UnmappableCharacterException; import java.nio.charset.UnmappableCharacterException;
@ -45,7 +41,7 @@ import java.util.concurrent.CompletableFuture;
/** /**
* HTTP client request. * HTTP client request.
*/ */
public class Request { public final class Request {
private final URL url; private final URL url;
@ -73,18 +69,14 @@ public class Request {
private final BackOff backOff; private final BackOff backOff;
private CompletableFuture<?> completableFuture; private CompletableFuture<Request> completableFuture;
private ResponseListener<HttpResponse> responseListener; private ResponseListener<HttpResponse> responseListener;
private CookieListener cookieListener;
private StatusListener statusListener;
private Request(URL url, String uri, HttpVersion httpVersion, HttpMethod httpMethod, private Request(URL url, String uri, HttpVersion httpVersion, HttpMethod httpMethod,
HttpHeaders headers, Collection<Cookie> cookies, ByteBuf content, HttpHeaders headers, Collection<Cookie> cookies, ByteBuf content,
long timeoutInMillis, boolean followRedirect, int maxRedirect, int redirectCount, long timeoutInMillis, boolean followRedirect, int maxRedirect, int redirectCount,
boolean isBackOff, BackOff backOff) { boolean isBackOff, BackOff backOff, ResponseListener<HttpResponse> responseListener) {
this.url = url; this.url = url;
this.uri = uri; this.uri = uri;
this.httpVersion = httpVersion; this.httpVersion = httpVersion;
@ -98,6 +90,7 @@ public class Request {
this.redirectCount = redirectCount; this.redirectCount = redirectCount;
this.isBackOff = isBackOff; this.isBackOff = isBackOff;
this.backOff = backOff; this.backOff = backOff;
this.responseListener = responseListener;
} }
public URL url() { public URL url() {
@ -182,41 +175,26 @@ public class Request {
"]"; "]";
} }
public Request setCompletableFuture(CompletableFuture<?> completableFuture) { public Request setCompletableFuture(CompletableFuture<Request> completableFuture) {
this.completableFuture = completableFuture; this.completableFuture = completableFuture;
return this; return this;
} }
public CompletableFuture<?> getCompletableFuture() { public CompletableFuture<Request> getCompletableFuture() {
return completableFuture; return completableFuture;
} }
public void setResponseListener(ResponseListener<HttpResponse> responseListener) {
public Request setCookieListener(CookieListener cookieListener) {
this.cookieListener = cookieListener;
return this;
}
public CookieListener getCookieListener() {
return cookieListener;
}
public Request setStatusListener(StatusListener statusListener) {
this.statusListener = statusListener;
return this;
}
public StatusListener getStatusListener() {
return statusListener;
}
public Request setResponseListener(ResponseListener<HttpResponse> responseListener) {
this.responseListener = responseListener; this.responseListener = responseListener;
return this;
} }
public ResponseListener<HttpResponse> getResponseListener() { public void onResponse(HttpResponse httpResponse) {
return responseListener; if (responseListener != null) {
responseListener.onResponse(httpResponse);
}
if (completableFuture != null) {
completableFuture.complete(this);
}
} }
public static Builder get() { public static Builder get() {
@ -259,6 +237,15 @@ public class Request {
return builder(PooledByteBufAllocator.DEFAULT, httpMethod); return builder(PooledByteBufAllocator.DEFAULT, httpMethod);
} }
public static Builder builder(HttpMethod httpMethod, Request request) {
return builder(PooledByteBufAllocator.DEFAULT, httpMethod)
.setVersion(request.httpVersion)
.uri(request.uri)
.setHeaders(request.headers)
.content(request.content)
.setResponseListener(request.responseListener);
}
public static Builder builder(ByteBufAllocator allocator, HttpMethod httpMethod) { public static Builder builder(ByteBufAllocator allocator, HttpMethod httpMethod) {
return new Builder(allocator).setMethod(httpMethod); return new Builder(allocator).setMethod(httpMethod);
} }
@ -293,7 +280,7 @@ public class Request {
private final Collection<Cookie> cookies; private final Collection<Cookie> cookies;
private final PercentEncoder encoder; private PercentEncoder encoder;
private HttpMethod httpMethod; private HttpMethod httpMethod;
@ -311,6 +298,8 @@ public class Request {
private String uri; private String uri;
private CharSequence contentType;
private HttpParameters uriParameters; private HttpParameters uriParameters;
private HttpParameters formParameters; private HttpParameters formParameters;
@ -327,6 +316,8 @@ public class Request {
private BackOff backOff; private BackOff backOff;
private ResponseListener<HttpResponse> responseListener;
Builder(ByteBufAllocator allocator) { Builder(ByteBufAllocator allocator) {
this.allocator = allocator; this.allocator = allocator;
this.httpMethod = DEFAULT_METHOD; this.httpMethod = DEFAULT_METHOD;
@ -341,9 +332,8 @@ public class Request {
this.headers = new DefaultHttpHeaders(); this.headers = new DefaultHttpHeaders();
this.removeHeaders = new ArrayList<>(); this.removeHeaders = new ArrayList<>();
this.cookies = new HashSet<>(); this.cookies = new HashSet<>();
this.encoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8);
this.uriParameters = new HttpParameters(); this.uriParameters = new HttpParameters();
this.formParameters = new HttpParameters(DEFAULT_FORM_CONTENT_TYPE); charset(StandardCharsets.UTF_8);
} }
public Builder setMethod(HttpMethod httpMethod) { public Builder setMethod(HttpMethod httpMethod) {
@ -420,22 +410,57 @@ public class Request {
return this; return this;
} }
public Builder addParameter(String name, String value) { public Builder charset(Charset charset) {
try { this.encoder = PercentEncoders.getQueryEncoder(charset);
uriParameters.add(encoder.encode(name), encoder.encode(value)); this.formParameters = new HttpParameters(DEFAULT_FORM_CONTENT_TYPE);
} catch (MalformedInputException | UnmappableCharacterException e) { return this;
throw new IllegalArgumentException(e);
} }
public Builder contentType(CharSequence contentType) {
Objects.requireNonNull(contentType);
this.contentType = contentType;
addHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
return this;
}
public Builder contentType(CharSequence contentType, Charset charset) {
Objects.requireNonNull(contentType);
Objects.requireNonNull(charset);
this.contentType = contentType;
charset(charset);
addHeader(HttpHeaderNames.CONTENT_TYPE, contentType + "; charset=" + charset.name().toLowerCase());
return this;
}
public Builder addParameter(String name, String value) {
Objects.requireNonNull(name);
Objects.requireNonNull(value);
uriParameters.add(encode(contentType, name), encode(contentType, value));
return this; return this;
} }
public Builder addFormParameter(String name, String value) { public Builder addFormParameter(String name, String value) {
Objects.requireNonNull(name);
Objects.requireNonNull(value);
formParameters.add(encode(contentType, name), encode(contentType, value));
return this;
}
private String encode(CharSequence contentType, String value) {
if (value == null) {
return null;
}
try { try {
formParameters.add(encoder.encode(name), encoder.encode(value)); String encodedValue = encoder.encode(value);
// https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4
if (HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.equals(contentType)) {
encodedValue = encodedValue.replace("%20", "+");
}
return encodedValue;
} catch (MalformedInputException | UnmappableCharacterException e) { } catch (MalformedInputException | UnmappableCharacterException e) {
// should never be reached
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} }
return this;
} }
public Builder addCookie(Cookie cookie) { public Builder addCookie(Cookie cookie) {
@ -443,11 +468,6 @@ public class Request {
return this; return this;
} }
public Builder contentType(String contentType) {
addHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
return this;
}
public Builder acceptGzip(boolean gzip) { public Builder acceptGzip(boolean gzip) {
this.gzip = gzip; this.gzip = gzip;
return this; return this;
@ -513,12 +533,17 @@ public class Request {
return this; return this;
} }
public Builder content(CharSequence charSequence, String contentType) { public Builder content(CharSequence charSequence, CharSequence contentType) {
content(charSequence.toString().getBytes(HttpUtil.getCharset(contentType, StandardCharsets.UTF_8)), content(charSequence.toString().getBytes(HttpUtil.getCharset(contentType, StandardCharsets.UTF_8)),
AsciiString.of(contentType)); AsciiString.of(contentType));
return this; return this;
} }
public Builder content(CharSequence charSequence, CharSequence contentType, Charset charset) {
content(charSequence.toString().getBytes(charset), AsciiString.of(contentType));
return this;
}
public Builder content(byte[] buf, String contentType) { public Builder content(byte[] buf, String contentType) {
content(buf, AsciiString.of(contentType)); content(buf, AsciiString.of(contentType));
return this; return this;
@ -529,6 +554,11 @@ public class Request {
return this; return this;
} }
public Builder setResponseListener(ResponseListener<HttpResponse> responseListener) {
this.responseListener = responseListener;
return this;
}
public Request build() { public Request build() {
DefaultHttpHeaders validatedHeaders = new DefaultHttpHeaders(true); DefaultHttpHeaders validatedHeaders = new DefaultHttpHeaders(true);
if (url != null) { if (url != null) {
@ -602,7 +632,8 @@ public class Request {
validatedHeaders.remove(headerName); validatedHeaders.remove(headerName);
} }
return new Request(url, uri, httpVersion, httpMethod, validatedHeaders, cookies, content, return new Request(url, uri, httpVersion, httpMethod, validatedHeaders, cookies, content,
timeoutInMillis, followRedirect, maxRedirects, 0, enableBackOff, backOff); timeoutInMillis, followRedirect, maxRedirects, 0, enableBackOff, backOff,
responseListener);
} }
private void addHeader(AsciiString name, Object value) { private void addHeader(AsciiString name, Object value) {

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.client.listener; package org.xbib.netty.http.client.api;
import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.common.HttpResponse;

View file

@ -1,11 +1,10 @@
package org.xbib.netty.http.client.transport; package org.xbib.netty.http.client.api;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.Http2Settings;
import io.netty.util.AttributeKey; import io.netty.util.AttributeKey;
import org.xbib.netty.http.client.Request;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.common.HttpResponse;
import org.xbib.netty.http.common.cookie.CookieBox; import org.xbib.netty.http.common.cookie.CookieBox;

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.client; package org.xbib.netty.http.client.api;
import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.Bootstrap;

View file

@ -0,0 +1,4 @@
/**
* Listeners for Netty HTTP client.
*/
package org.xbib.netty.http.client.api;

View file

@ -6,7 +6,7 @@ import io.netty.handler.codec.http.HttpMethod;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.common.HttpResponse;
import java.io.IOException; import java.io.IOException;
@ -95,7 +95,7 @@ public class RestClient {
requestBuilder.content(byteBuf); requestBuilder.content(byteBuf);
try { try {
client.newTransport(HttpAddress.http1(url)) client.newTransport(HttpAddress.http1(url))
.execute(requestBuilder.build().setResponseListener(restClient::setResponse)).close(); .execute(requestBuilder.setResponseListener(restClient::setResponse).build()).close();
} catch (Exception e) { } catch (Exception e) {
throw new IOException(e); throw new IOException(e);
} }

View file

@ -1,6 +1,6 @@
dependencies { dependencies {
compile project(":netty-http-common") compile project(":netty-http-client-api")
compile "io.netty:netty-handler-proxy:${project.property('netty.version')}" compile "io.netty:netty-handler-proxy:${project.property('netty.version')}"
compile "io.netty:netty-transport-native-epoll:${project.property('netty.version')}" compile "io.netty:netty-transport-native-epoll:${project.property('netty.version')}"
testCompile "io.netty:netty-tcnative-boringssl-static:${project.property('tcnative.version')}" testCompile "io.netty:netty-tcnative-boringssl-static:${project.property('tcnative.version')}"

View file

@ -3,7 +3,6 @@ package org.xbib.netty.http.client;
import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption; import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup; import io.netty.channel.EventLoopGroup;
import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.WriteBufferWaterMark;
@ -25,12 +24,11 @@ import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler; import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.xbib.netty.http.client.handler.http.HttpChannelInitializer; import org.xbib.netty.http.client.api.HttpChannelInitializer;
import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer; import org.xbib.netty.http.client.api.ProtocolProvider;
import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.client.pool.BoundedChannelPool; import org.xbib.netty.http.client.pool.BoundedChannelPool;
import org.xbib.netty.http.client.transport.Http2Transport; import org.xbib.netty.http.client.api.Transport;
import org.xbib.netty.http.client.transport.HttpTransport;
import org.xbib.netty.http.client.transport.Transport;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.common.HttpResponse;
import org.xbib.netty.http.common.NetworkUtils; import org.xbib.netty.http.common.NetworkUtils;
@ -44,6 +42,7 @@ import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.TrustManagerFactory;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.security.KeyStoreException; import java.security.KeyStoreException;
import java.security.Provider; import java.security.Provider;
@ -52,6 +51,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Queue; import java.util.Queue;
import java.util.ServiceLoader;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore; import java.util.concurrent.Semaphore;
@ -96,6 +96,8 @@ public final class Client implements AutoCloseable {
private final Queue<Transport> transports; private final Queue<Transport> transports;
private final List<ProtocolProvider<HttpChannelInitializer, Transport>> protocolProviders;
private BoundedChannelPool<HttpAddress> pool; private BoundedChannelPool<HttpAddress> pool;
public Client() { public Client() {
@ -106,10 +108,16 @@ public final class Client implements AutoCloseable {
this(clientConfig, null, null, null); this(clientConfig, null, null, null);
} }
@SuppressWarnings("unchecked")
public Client(ClientConfig clientConfig, ByteBufAllocator byteBufAllocator, public Client(ClientConfig clientConfig, ByteBufAllocator byteBufAllocator,
EventLoopGroup eventLoopGroup, Class<? extends SocketChannel> socketChannelClass) { EventLoopGroup eventLoopGroup, Class<? extends SocketChannel> socketChannelClass) {
Objects.requireNonNull(clientConfig); Objects.requireNonNull(clientConfig);
this.clientConfig = clientConfig; this.clientConfig = clientConfig;
this.protocolProviders = new ArrayList<>();
for (ProtocolProvider<HttpChannelInitializer, Transport> provider : ServiceLoader.load(ProtocolProvider.class)) {
protocolProviders.add(provider);
logger.log(Level.INFO, "protocol provider up: " + provider.transportClass() );
}
initializeTrustManagerFactory(clientConfig); initializeTrustManagerFactory(clientConfig);
this.byteBufAllocator = byteBufAllocator != null ? this.byteBufAllocator = byteBufAllocator != null ?
byteBufAllocator : ByteBufAllocator.DEFAULT; byteBufAllocator : ByteBufAllocator.DEFAULT;
@ -162,6 +170,10 @@ public final class Client implements AutoCloseable {
return new Builder(); return new Builder();
} }
public List<ProtocolProvider<HttpChannelInitializer, Transport>> getProtocolProviders() {
return protocolProviders;
}
public ClientConfig getClientConfig() { public ClientConfig getClientConfig() {
return clientConfig; return clientConfig;
} }
@ -200,18 +212,36 @@ public final class Client implements AutoCloseable {
} }
public Transport newTransport(HttpAddress httpAddress) { public Transport newTransport(HttpAddress httpAddress) {
Transport transport; Transport transport = null;
if (httpAddress != null) { if (httpAddress != null) {
if (httpAddress.getVersion().majorVersion() == 1) { for (ProtocolProvider<HttpChannelInitializer, Transport> protocolProvider : protocolProviders) {
transport = new HttpTransport(this, httpAddress); if (protocolProvider.supportsMajorVersion(httpAddress.getVersion().majorVersion())) {
} else { try {
transport = new Http2Transport(this, httpAddress); transport = protocolProvider.transportClass()
.getConstructor(Client.class, HttpAddress.class).newInstance(this, httpAddress);
break;
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new IllegalStateException();
}
}
}
if (transport == null) {
throw new UnsupportedOperationException("no protocol support for " + httpAddress);
} }
} else if (hasPooledConnections()) { } else if (hasPooledConnections()) {
if (pool.getVersion().majorVersion() == 1) { for (ProtocolProvider<HttpChannelInitializer, Transport> protocolProvider : protocolProviders) {
transport = new HttpTransport(this, null); if (protocolProvider.supportsMajorVersion(pool.getVersion().majorVersion())) {
} else { try {
transport = new Http2Transport(this, null); transport = protocolProvider.transportClass()
.getConstructor(Client.class, HttpAddress.class).newInstance(this, null);
break;
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new IllegalStateException();
}
}
}
if (transport == null) {
throw new UnsupportedOperationException("no pool protocol support for " + pool.getVersion().majorVersion());
} }
} else { } else {
throw new IllegalStateException("no address given to connect to"); throw new IllegalStateException("no address given to connect to");
@ -226,13 +256,10 @@ public final class Client implements AutoCloseable {
HttpVersion httpVersion = httpAddress.getVersion(); HttpVersion httpVersion = httpAddress.getVersion();
SslContext sslContext = newSslContext(clientConfig, httpAddress.getVersion()); SslContext sslContext = newSslContext(clientConfig, httpAddress.getVersion());
SslHandlerFactory sslHandlerFactory = new SslHandlerFactory(sslContext, clientConfig, httpAddress, byteBufAllocator); SslHandlerFactory sslHandlerFactory = new SslHandlerFactory(sslContext, clientConfig, httpAddress, byteBufAllocator);
ChannelInitializer<Channel> initializer; HttpChannelInitializer initializerTwo =
if (httpVersion.majorVersion() == 1) { findChannelInitializer(2, httpAddress, sslHandlerFactory, null);
initializer = new HttpChannelInitializer(clientConfig, httpAddress, sslHandlerFactory, HttpChannelInitializer initializer =
new Http2ChannelInitializer(clientConfig, httpAddress, sslHandlerFactory)); findChannelInitializer(httpVersion.majorVersion(), httpAddress, sslHandlerFactory, initializerTwo);
} else {
initializer = new Http2ChannelInitializer(clientConfig, httpAddress, sslHandlerFactory);
}
try { try {
channel = bootstrap.handler(initializer) channel = bootstrap.handler(initializer)
.connect(httpAddress.getInetSocketAddress()).sync().await().channel(); .connect(httpAddress.getInetSocketAddress()).sync().await().channel();
@ -273,6 +300,15 @@ public final class Client implements AutoCloseable {
.execute(request); .execute(request);
} }
/**
* Execute a request and return a {@link CompletableFuture}.
*
* @param request the request
* @param supplier the function for the response
* @param <T> the result of the function for the response
* @return the completable future
* @throws IOException if the request fails to be executed.
*/
public <T> CompletableFuture<T> execute(Request request, public <T> CompletableFuture<T> execute(Request request,
Function<HttpResponse, T> supplier) throws IOException { Function<HttpResponse, T> supplier) throws IOException {
return newTransport(HttpAddress.of(request.url(), request.httpVersion())) return newTransport(HttpAddress.of(request.url(), request.httpVersion()))
@ -294,7 +330,7 @@ public final class Client implements AutoCloseable {
} }
/** /**
* Retry request by following a back-off strategy. * Retry request.
* *
* @param transport the transport to retry * @param transport the transport to retry
* @param request the request to retry * @param request the request to retry
@ -345,6 +381,24 @@ public final class Client implements AutoCloseable {
} }
} }
private HttpChannelInitializer findChannelInitializer(int majorVersion,
HttpAddress httpAddress,
SslHandlerFactory sslHandlerFactory,
HttpChannelInitializer helper) {
for (ProtocolProvider<HttpChannelInitializer, Transport> protocolProvider : protocolProviders) {
if (protocolProvider.supportsMajorVersion(majorVersion)) {
try {
return protocolProvider.initializerClass()
.getConstructor(ClientConfig.class, HttpAddress.class, SslHandlerFactory.class, HttpChannelInitializer.class)
.newInstance(clientConfig, httpAddress, sslHandlerFactory, helper);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new IllegalStateException();
}
}
}
throw new IllegalStateException("no channel initializer found for major version " + majorVersion);
}
/** /**
* Initialize trust manager factory once per client lifecycle. * Initialize trust manager factory once per client lifecycle.
* @param clientConfig the client config * @param clientConfig the client config
@ -360,40 +414,8 @@ public final class Client implements AutoCloseable {
} }
} }
private static SslHandler newSslHandler(SslContext sslContext,
ClientConfig clientConfig, ByteBufAllocator allocator, HttpAddress httpAddress) {
InetSocketAddress peer = httpAddress.getInetSocketAddress();
SslHandler sslHandler = sslContext.newHandler(allocator, peer.getHostName(), peer.getPort());
SSLEngine engine = sslHandler.engine();
List<String> serverNames = clientConfig.getServerNamesForIdentification();
if (serverNames.isEmpty()) {
serverNames = Collections.singletonList(peer.getHostName());
}
SSLParameters params = engine.getSSLParameters();
// use sslContext.newHandler(allocator, peerHost, peerPort) when using params.setEndpointIdentificationAlgorithm
params.setEndpointIdentificationAlgorithm("HTTPS");
List<SNIServerName> sniServerNames = new ArrayList<>();
for (String serverName : serverNames) {
sniServerNames.add(new SNIHostName(serverName));
}
params.setServerNames(sniServerNames);
engine.setSSLParameters(params);
switch (clientConfig.getClientAuthMode()) {
case NEED:
engine.setNeedClientAuth(true);
break;
case WANT:
engine.setWantClientAuth(true);
break;
default:
break;
}
engine.setEnabledProtocols(clientConfig.getProtocols());
return sslHandler;
}
private static SslContext newSslContext(ClientConfig clientConfig, HttpVersion httpVersion) throws SSLException { private static SslContext newSslContext(ClientConfig clientConfig, HttpVersion httpVersion) throws SSLException {
// Conscrypt? // Conscrypt support?
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient() SslContextBuilder sslContextBuilder = SslContextBuilder.forClient()
.sslProvider(clientConfig.getSslProvider()) .sslProvider(clientConfig.getSslProvider())
.ciphers(clientConfig.getCiphers(), clientConfig.getCipherSuiteFilter()) .ciphers(clientConfig.getCiphers(), clientConfig.getCipherSuiteFilter())
@ -449,16 +471,11 @@ public final class Client implements AutoCloseable {
SslContext sslContext = newSslContext(clientConfig, httpAddress.getVersion()); SslContext sslContext = newSslContext(clientConfig, httpAddress.getVersion());
SslHandlerFactory sslHandlerFactory = new SslHandlerFactory(sslContext, SslHandlerFactory sslHandlerFactory = new SslHandlerFactory(sslContext,
clientConfig, httpAddress, byteBufAllocator); clientConfig, httpAddress, byteBufAllocator);
Http2ChannelInitializer http2ChannelInitializer = HttpChannelInitializer initializerTwo =
new Http2ChannelInitializer(clientConfig, httpAddress, sslHandlerFactory); findChannelInitializer(2, httpAddress, sslHandlerFactory, null);
if (httpVersion.majorVersion() == 1) {
HttpChannelInitializer initializer = HttpChannelInitializer initializer =
new HttpChannelInitializer(clientConfig, httpAddress, findChannelInitializer(httpVersion.majorVersion(), httpAddress, sslHandlerFactory, initializerTwo);
sslHandlerFactory, http2ChannelInitializer);
initializer.initChannel(channel); initializer.initChannel(channel);
} else {
http2ChannelInitializer.initChannel(channel);
}
} }
} }
@ -481,7 +498,34 @@ public final class Client implements AutoCloseable {
} }
public SslHandler create() { public SslHandler create() {
return newSslHandler(sslContext, clientConfig, allocator, httpAddress); InetSocketAddress peer = httpAddress.getInetSocketAddress();
SslHandler sslHandler = sslContext.newHandler(allocator, peer.getHostName(), peer.getPort());
SSLEngine engine = sslHandler.engine();
List<String> serverNames = clientConfig.getServerNamesForIdentification();
if (serverNames.isEmpty()) {
serverNames = Collections.singletonList(peer.getHostName());
}
SSLParameters params = engine.getSSLParameters();
// use sslContext.newHandler(allocator, peerHost, peerPort) when using params.setEndpointIdentificationAlgorithm
params.setEndpointIdentificationAlgorithm("HTTPS");
List<SNIServerName> sniServerNames = new ArrayList<>();
for (String serverName : serverNames) {
sniServerNames.add(new SNIHostName(serverName));
}
params.setServerNames(sniServerNames);
engine.setSSLParameters(params);
switch (clientConfig.getClientAuthMode()) {
case NEED:
engine.setNeedClientAuth(true);
break;
case WANT:
engine.setWantClientAuth(true);
break;
default:
break;
}
engine.setEnabledProtocols(clientConfig.getProtocols());
return sslHandler;
} }
} }

View file

@ -8,9 +8,8 @@ import io.netty.handler.logging.LogLevel;
import io.netty.handler.proxy.HttpProxyHandler; import io.netty.handler.proxy.HttpProxyHandler;
import io.netty.handler.ssl.CipherSuiteFilter; import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.SslProvider;
import org.xbib.netty.http.client.pool.BoundedChannelPool; import org.xbib.netty.http.client.api.Pool;
import org.xbib.netty.http.client.pool.Pool; import org.xbib.netty.http.client.api.BackOff;
import org.xbib.netty.http.client.retry.BackOff;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.common.security.SecurityUtil; import org.xbib.netty.http.common.security.SecurityUtil;
@ -20,7 +19,6 @@ import java.security.KeyStore;
import java.security.Provider; import java.security.Provider;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
public class ClientConfig { public class ClientConfig {

View file

@ -0,0 +1,23 @@
package org.xbib.netty.http.client;
import org.xbib.netty.http.client.api.ProtocolProvider;
import org.xbib.netty.http.client.handler.http.Http1ChannelInitializer;
import org.xbib.netty.http.client.transport.Http1Transport;
public class Http1Provider implements ProtocolProvider<Http1ChannelInitializer, Http1Transport> {
@Override
public boolean supportsMajorVersion(int majorVersion) {
return majorVersion == 1;
}
@Override
public Class<Http1ChannelInitializer> initializerClass() {
return Http1ChannelInitializer.class;
}
@Override
public Class<Http1Transport> transportClass() {
return Http1Transport.class;
}
}

View file

@ -0,0 +1,23 @@
package org.xbib.netty.http.client;
import org.xbib.netty.http.client.api.ProtocolProvider;
import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer;
import org.xbib.netty.http.client.transport.Http2Transport;
public class Http2Provider implements ProtocolProvider<Http2ChannelInitializer, Http2Transport> {
@Override
public boolean supportsMajorVersion(int majorVersion) {
return majorVersion == 2;
}
@Override
public Class<Http2ChannelInitializer> initializerClass() {
return Http2ChannelInitializer.class;
}
@Override
public Class<Http2Transport> transportClass() {
return Http2Transport.class;
}
}

View file

@ -13,15 +13,16 @@ import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
import io.netty.handler.ssl.SslHandler; import io.netty.handler.ssl.SslHandler;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.ClientConfig; import org.xbib.netty.http.client.ClientConfig;
import org.xbib.netty.http.client.api.HttpChannelInitializer;
import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer; import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
public class HttpChannelInitializer extends ChannelInitializer<Channel> { public class Http1ChannelInitializer extends ChannelInitializer<Channel> implements HttpChannelInitializer {
private static final Logger logger = Logger.getLogger(HttpChannelInitializer.class.getName()); private static final Logger logger = Logger.getLogger(Http1ChannelInitializer.class.getName());
private final ClientConfig clientConfig; private final ClientConfig clientConfig;
@ -33,14 +34,14 @@ public class HttpChannelInitializer extends ChannelInitializer<Channel> {
private final Http2ChannelInitializer http2ChannelInitializer; private final Http2ChannelInitializer http2ChannelInitializer;
public HttpChannelInitializer(ClientConfig clientConfig, public Http1ChannelInitializer(ClientConfig clientConfig,
HttpAddress httpAddress, HttpAddress httpAddress,
Client.SslHandlerFactory sslHandlerFactory, Client.SslHandlerFactory sslHandlerFactory,
Http2ChannelInitializer http2ChannelInitializer) { HttpChannelInitializer http2ChannelInitializer) {
this.clientConfig = clientConfig; this.clientConfig = clientConfig;
this.httpAddress = httpAddress; this.httpAddress = httpAddress;
this.sslHandlerFactory = sslHandlerFactory; this.sslHandlerFactory = sslHandlerFactory;
this.http2ChannelInitializer = http2ChannelInitializer; this.http2ChannelInitializer = (Http2ChannelInitializer) http2ChannelInitializer;
this.httpResponseHandler = new HttpResponseHandler(); this.httpResponseHandler = new HttpResponseHandler();
} }

View file

@ -4,7 +4,7 @@ import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse;
import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.client.api.Transport;
@ChannelHandler.Sharable @ChannelHandler.Sharable
public class HttpResponseHandler extends SimpleChannelInboundHandler<FullHttpResponse> { public class HttpResponseHandler extends SimpleChannelInboundHandler<FullHttpResponse> {

View file

@ -5,9 +5,6 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.codec.http2.DefaultHttp2SettingsFrame; import io.netty.handler.codec.http2.DefaultHttp2SettingsFrame;
import io.netty.handler.codec.http2.Http2ConnectionPrefaceAndSettingsFrameWrittenEvent; import io.netty.handler.codec.http2.Http2ConnectionPrefaceAndSettingsFrameWrittenEvent;
import io.netty.handler.codec.http2.Http2FrameLogger; import io.netty.handler.codec.http2.Http2FrameLogger;
@ -17,14 +14,15 @@ import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder;
import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LogLevel;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.ClientConfig; import org.xbib.netty.http.client.ClientConfig;
import org.xbib.netty.http.client.api.HttpChannelInitializer;
import org.xbib.netty.http.client.handler.http.TrafficLoggingHandler; import org.xbib.netty.http.client.handler.http.TrafficLoggingHandler;
import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.client.api.Transport;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
public class Http2ChannelInitializer extends ChannelInitializer<Channel> { public class Http2ChannelInitializer extends ChannelInitializer<Channel> implements HttpChannelInitializer {
private static final Logger logger = Logger.getLogger(Http2ChannelInitializer.class.getName()); private static final Logger logger = Logger.getLogger(Http2ChannelInitializer.class.getName());
@ -36,7 +34,8 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
public Http2ChannelInitializer(ClientConfig clientConfig, public Http2ChannelInitializer(ClientConfig clientConfig,
HttpAddress httpAddress, HttpAddress httpAddress,
Client.SslHandlerFactory sslHandlerFactory) { Client.SslHandlerFactory sslHandlerFactory,
HttpChannelInitializer unusedInitializer) {
this.clientConfig = clientConfig; this.clientConfig = clientConfig;
this.httpAddress = httpAddress; this.httpAddress = httpAddress;
this.sslHandlerFactory = sslHandlerFactory; this.sslHandlerFactory = sslHandlerFactory;

View file

@ -5,7 +5,7 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http2.HttpConversionUtil; import io.netty.handler.codec.http2.HttpConversionUtil;
import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.client.api.Transport;
@ChannelHandler.Sharable @ChannelHandler.Sharable
public class Http2ResponseHandler extends SimpleChannelInboundHandler<FullHttpResponse> { public class Http2ResponseHandler extends SimpleChannelInboundHandler<FullHttpResponse> {

View file

@ -1,9 +0,0 @@
package org.xbib.netty.http.client.listener;
import org.xbib.netty.http.common.cookie.Cookie;
@FunctionalInterface
public interface CookieListener {
void onCookie(Cookie cookie);
}

View file

@ -1,10 +0,0 @@
package org.xbib.netty.http.client.listener;
import org.xbib.netty.http.common.HttpStatus;
@FunctionalInterface
public interface StatusListener {
void onStatus(HttpStatus httpStatus);
}

View file

@ -1,4 +0,0 @@
/**
* Listeners for Netty HTTP client.
*/
package org.xbib.netty.http.client.listener;

View file

@ -11,6 +11,7 @@ import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.DefaultHttp2GoAwayFrame; import io.netty.handler.codec.http2.DefaultHttp2GoAwayFrame;
import io.netty.util.AttributeKey; import io.netty.util.AttributeKey;
import org.xbib.netty.http.client.api.Pool;
import org.xbib.netty.http.common.PoolKey; import org.xbib.netty.http.common.PoolKey;
import java.io.IOException; import java.io.IOException;
@ -65,7 +66,9 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
private PoolKeySelector<K> poolKeySelector; private PoolKeySelector<K> poolKeySelector;
/** /**
* @param semaphore the concurrency level * A bounded channel pool.
*
* @param semaphore the level of concurrency
* @param httpVersion the HTTP version of the pool connections * @param httpVersion the HTTP version of the pool connections
* @param nodes the endpoint nodes, any element may contain the port (followed after ":") * @param nodes the endpoint nodes, any element may contain the port (followed after ":")
* to override the defaultPort argument * to override the defaultPort argument

View file

@ -1,5 +1,7 @@
package org.xbib.netty.http.client.retry; package org.xbib.netty.http.client.retry;
import org.xbib.netty.http.client.api.BackOff;
/** /**
* Implementation of {@link BackOff} that increases the back off period for each retry attempt using * Implementation of {@link BackOff} that increases the back off period for each retry attempt using
* a randomization function that grows exponentially. * a randomization function that grows exponentially.

View file

@ -8,9 +8,10 @@ import org.xbib.net.PercentDecoder;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.net.URLSyntaxException; import org.xbib.net.URLSyntaxException;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.api.Transport;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.client.retry.BackOff; import org.xbib.netty.http.client.api.BackOff;
import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.common.HttpResponse;
import org.xbib.netty.http.common.cookie.Cookie; import org.xbib.netty.http.common.cookie.Cookie;
import org.xbib.netty.http.common.cookie.CookieBox; import org.xbib.netty.http.common.cookie.CookieBox;
@ -35,7 +36,7 @@ import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
abstract class BaseTransport implements Transport { public abstract class BaseTransport implements Transport {
private static final Logger logger = Logger.getLogger(BaseTransport.class.getName()); private static final Logger logger = Logger.getLogger(BaseTransport.class.getName());
@ -51,7 +52,7 @@ abstract class BaseTransport implements Transport {
private SSLSession sslSession; private SSLSession sslSession;
final Map<String, Flow> channelFlowMap; final Map<String, Flow> flowMap;
final SortedMap<String, Request> requests; final SortedMap<String, Request> requests;
@ -61,7 +62,7 @@ abstract class BaseTransport implements Transport {
this.client = client; this.client = client;
this.httpAddress = httpAddress; this.httpAddress = httpAddress;
this.channels = new ConcurrentHashMap<>(); this.channels = new ConcurrentHashMap<>();
this.channelFlowMap = new ConcurrentHashMap<>(); this.flowMap = new ConcurrentHashMap<>();
this.requests = new ConcurrentSkipListMap<>(); this.requests = new ConcurrentSkipListMap<>();
} }
@ -71,7 +72,8 @@ abstract class BaseTransport implements Transport {
} }
/** /**
* Experimental method for executing in a wrapping completable future. * Method for executing in a wrapping completable future.
*
* @param request request * @param request request
* @param supplier supplier * @param supplier supplier
* @param <T> supplier result * @param <T> supplier result
@ -98,7 +100,7 @@ abstract class BaseTransport implements Transport {
if (!channels.isEmpty()) { if (!channels.isEmpty()) {
get(); get();
} }
for (Flow flow : channelFlowMap.values()) { for (Flow flow : flowMap.values()) {
flow.close(); flow.close();
} }
channels.clear(); channels.clear();
@ -128,7 +130,7 @@ abstract class BaseTransport implements Transport {
} }
logger.log(Level.SEVERE, "failing: " + throwable.getMessage(), throwable); logger.log(Level.SEVERE, "failing: " + throwable.getMessage(), throwable);
this.throwable = throwable; this.throwable = throwable;
for (Flow flow : channelFlowMap.values()) { for (Flow flow : flowMap.values()) {
flow.fail(throwable); flow.fail(throwable);
} }
} }
@ -143,14 +145,15 @@ abstract class BaseTransport implements Transport {
if (channels.isEmpty()) { if (channels.isEmpty()) {
return this; return this;
} }
for (Map.Entry<String, Flow> entry : channelFlowMap.entrySet()) { for (Map.Entry<String, Flow> entry : flowMap.entrySet()) {
Flow flow = entry.getValue(); Flow flow = entry.getValue();
if (!flow.isClosed()) { if (!flow.isClosed()) {
for (Integer key : flow.keys()) { for (Integer key : flow.keys()) {
String requestKey = getRequestKey(entry.getKey(), key);
try { try {
flow.get(key).get(value, timeUnit); flow.get(key).get(value, timeUnit);
completeRequest(requestKey);
} catch (Exception e) { } catch (Exception e) {
String requestKey = getRequestKey(entry.getKey(), key);
if (requestKey != null) { if (requestKey != null) {
Request request = requests.get(requestKey); Request request = requests.get(requestKey);
if (request != null && request.getCompletableFuture() != null) { if (request != null && request.getCompletableFuture() != null) {
@ -180,7 +183,7 @@ abstract class BaseTransport implements Transport {
if (channels.isEmpty()) { if (channels.isEmpty()) {
return; return;
} }
for (Map.Entry<String, Flow> entry : channelFlowMap.entrySet()) { for (Map.Entry<String, Flow> entry : flowMap.entrySet()) {
Flow flow = entry.getValue(); Flow flow = entry.getValue();
for (Integer key : flow.keys()) { for (Integer key : flow.keys()) {
try { try {
@ -205,7 +208,7 @@ abstract class BaseTransport implements Transport {
logger.log(Level.WARNING, e.getMessage(), e); logger.log(Level.WARNING, e.getMessage(), e);
} }
}); });
channelFlowMap.clear(); flowMap.clear();
channels.clear(); channels.clear();
requests.clear(); requests.clear();
} }
@ -280,18 +283,13 @@ abstract class BaseTransport implements Transport {
logger.log(Level.FINE, "found redirect location: " + location); logger.log(Level.FINE, "found redirect location: " + location);
URL redirUrl = URL.base(request.url()).resolve(location); URL redirUrl = URL.base(request.url()).resolve(location);
HttpMethod method = httpResponse.getStatus().getCode() == 303 ? HttpMethod.GET : request.httpMethod(); HttpMethod method = httpResponse.getStatus().getCode() == 303 ? HttpMethod.GET : request.httpMethod();
Request.Builder newHttpRequestBuilder = Request.builder(method) Request.Builder newHttpRequestBuilder = Request.builder(method, request)
.url(redirUrl) .url(redirUrl);
.setVersion(request.httpVersion())
.setHeaders(request.headers())
.content(request.content());
request.url().getQueryParams().forEach(pair -> request.url().getQueryParams().forEach(pair ->
newHttpRequestBuilder.addParameter(pair.getFirst(), pair.getSecond()) newHttpRequestBuilder.addParameter(pair.getFirst(), pair.getSecond())
); );
request.cookies().forEach(newHttpRequestBuilder::addCookie); request.cookies().forEach(newHttpRequestBuilder::addCookie);
Request newHttpRequest = newHttpRequestBuilder.build(); Request newHttpRequest = newHttpRequestBuilder.build();
newHttpRequest.setResponseListener(request.getResponseListener());
newHttpRequest.setCookieListener(request.getCookieListener());
StringBuilder hostAndPort = new StringBuilder(); StringBuilder hostAndPort = new StringBuilder();
hostAndPort.append(redirUrl.getHost()); hostAndPort.append(redirUrl.getHost());
if (redirUrl.getPort() != null) { if (redirUrl.getPort() != null) {
@ -324,7 +322,8 @@ abstract class BaseTransport implements Transport {
return null; return null;
} }
if (request.isBackOff()) { if (request.isBackOff()) {
BackOff backOff = request.getBackOff() != null ? request.getBackOff() : BackOff backOff = request.getBackOff() != null ?
request.getBackOff() :
client.getClientConfig().getBackOff(); client.getClientConfig().getBackOff();
int status = httpResponse.getStatus ().getCode(); int status = httpResponse.getStatus ().getCode();
switch (status) { switch (status) {
@ -356,6 +355,24 @@ abstract class BaseTransport implements Transport {
return null; return null;
} }
private void completeRequest(String requestKey) {
if (requestKey != null) {
Request request = requests.get(requestKey);
if (request != null && request.getCompletableFuture() != null) {
request.getCompletableFuture().complete(request);
}
}
}
private void completeRequestExceptionally(String requestKey, Throwable throwable) {
if (requestKey != null) {
Request request = requests.get(requestKey);
if (request != null && request.getCompletableFuture() != null) {
request.getCompletableFuture().completeExceptionally(throwable);
}
}
}
@Override @Override
public void setCookieBox(CookieBox cookieBox) { public void setCookieBox(CookieBox cookieBox) {
this.cookieBox = cookieBox; this.cookieBox = cookieBox;

View file

@ -10,14 +10,13 @@ import io.netty.handler.codec.http2.Http2Settings;
import io.netty.handler.codec.http2.HttpConversionUtil; import io.netty.handler.codec.http2.HttpConversionUtil;
import org.xbib.net.URLSyntaxException; import org.xbib.net.URLSyntaxException;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.api.Transport;
import org.xbib.netty.http.client.cookie.ClientCookieDecoder; import org.xbib.netty.http.client.cookie.ClientCookieDecoder;
import org.xbib.netty.http.client.cookie.ClientCookieEncoder; import org.xbib.netty.http.client.cookie.ClientCookieEncoder;
import org.xbib.netty.http.client.listener.CookieListener;
import org.xbib.netty.http.client.listener.StatusListener;
import org.xbib.netty.http.common.DefaultHttpResponse; import org.xbib.netty.http.common.DefaultHttpResponse;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.client.listener.ResponseListener; import org.xbib.netty.http.client.api.ResponseListener;
import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.common.HttpResponse;
import org.xbib.netty.http.common.cookie.Cookie; import org.xbib.netty.http.common.cookie.Cookie;
@ -28,11 +27,11 @@ import java.util.concurrent.CompletableFuture;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
public class HttpTransport extends BaseTransport { public class Http1Transport extends BaseTransport {
private static final Logger logger = Logger.getLogger(HttpTransport.class.getName()); private static final Logger logger = Logger.getLogger(Http1Transport.class.getName());
public HttpTransport(Client client, HttpAddress httpAddress) { public Http1Transport(Client client, HttpAddress httpAddress) {
super(client, httpAddress); super(client, httpAddress);
} }
@ -43,7 +42,7 @@ public class HttpTransport extends BaseTransport {
return this; return this;
} }
final String channelId = channel.id().toString(); final String channelId = channel.id().toString();
channelFlowMap.putIfAbsent(channelId, new Flow()); flowMap.putIfAbsent(channelId, new Flow());
// Some HTTP 1 servers do not understand URIs in HTTP command line in spite of RFC 7230. // Some HTTP 1 servers do not understand URIs in HTTP command line in spite of RFC 7230.
// The "origin form" requires a "Host" header. // The "origin form" requires a "Host" header.
// Our algorithm is: use always "origin form" for HTTP 1, use absolute form for HTTP 2. // Our algorithm is: use always "origin form" for HTTP 1, use absolute form for HTTP 2.
@ -52,7 +51,7 @@ public class HttpTransport extends BaseTransport {
FullHttpRequest fullHttpRequest = request.content() == null ? FullHttpRequest fullHttpRequest = request.content() == null ?
new DefaultFullHttpRequest(request.httpVersion(), request.httpMethod(), uri) : new DefaultFullHttpRequest(request.httpVersion(), request.httpMethod(), uri) :
new DefaultFullHttpRequest(request.httpVersion(), request.httpMethod(), uri, request.content()); new DefaultFullHttpRequest(request.httpVersion(), request.httpMethod(), uri, request.content());
final Integer streamId = channelFlowMap.get(channelId).nextStreamId(); final Integer streamId = flowMap.get(channelId).nextStreamId();
if (streamId == null) { if (streamId == null) {
throw new IllegalStateException(); throw new IllegalStateException();
} }
@ -87,29 +86,24 @@ public class HttpTransport extends BaseTransport {
logger.log(Level.WARNING, "no request present for responding"); logger.log(Level.WARNING, "no request present for responding");
return; return;
} }
HttpResponse httpResponse = new DefaultHttpResponse(httpAddress, fullHttpResponse); String requestKey = requests.lastKey();
client.getResponseCounter().incrementAndGet(); Request request;
DefaultHttpResponse httpResponse = null;
try { try {
// streamID is expected to be null, last request on memory is expected to be current, remove request from memory // streamID is expected to be null, last request on memory is expected to be current, remove request from memory
Request request = requests.remove(requests.lastKey()); request = requests.get(requestKey);
if (request != null) { if (request != null) {
StatusListener statusListener = request.getStatusListener(); for (String cookieString : fullHttpResponse.headers().getAll(HttpHeaderNames.SET_COOKIE)) {
if (statusListener != null) {
statusListener.onStatus(httpResponse.getStatus());
}
for (String cookieString : httpResponse.getHeaders().getAllHeaders(HttpHeaderNames.SET_COOKIE)) {
Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString);
addCookie(cookie); addCookie(cookie);
CookieListener cookieListener = request.getCookieListener();
if (cookieListener != null) {
cookieListener.onCookie(cookie);
}
}
ResponseListener<HttpResponse> responseListener = request.getResponseListener();
if (responseListener != null) {
responseListener.onResponse(httpResponse);
} }
httpResponse = new DefaultHttpResponse(httpAddress, fullHttpResponse, getCookieBox());
request.onResponse(httpResponse);
client.getResponseCounter().incrementAndGet();
} else {
logger.log(Level.WARNING, "unable to find request for response");
} }
// check for retry / continue
try { try {
Request retryRequest = retry(request, httpResponse); Request retryRequest = retry(request, httpResponse);
if (retryRequest != null) { if (retryRequest != null) {
@ -125,8 +119,9 @@ public class HttpTransport extends BaseTransport {
} catch (URLSyntaxException | IOException e) { } catch (URLSyntaxException | IOException e) {
logger.log(Level.WARNING, e.getMessage(), e); logger.log(Level.WARNING, e.getMessage(), e);
} }
// acknowledge success
String channelId = channel.id().toString(); String channelId = channel.id().toString();
Flow flow = channelFlowMap.get(channelId); Flow flow = flowMap.get(channelId);
if (flow == null) { if (flow == null) {
return; return;
} }
@ -135,9 +130,14 @@ public class HttpTransport extends BaseTransport {
promise.complete(true); promise.complete(true);
} }
} finally { } finally {
if (requestKey != null) {
requests.remove(requestKey);
}
if (httpResponse != null) {
httpResponse.release(); httpResponse.release();
} }
} }
}
@Override @Override
public void settingsReceived(Http2Settings http2Settings) { public void settingsReceived(Http2Settings http2Settings) {

View file

@ -17,17 +17,14 @@ import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.util.AsciiString; import io.netty.util.AsciiString;
import org.xbib.net.URLSyntaxException; import org.xbib.net.URLSyntaxException;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.api.Transport;
import org.xbib.netty.http.client.cookie.ClientCookieDecoder; import org.xbib.netty.http.client.cookie.ClientCookieDecoder;
import org.xbib.netty.http.client.cookie.ClientCookieEncoder; import org.xbib.netty.http.client.cookie.ClientCookieEncoder;
import org.xbib.netty.http.client.handler.http2.Http2ResponseHandler; import org.xbib.netty.http.client.handler.http2.Http2ResponseHandler;
import org.xbib.netty.http.client.handler.http2.Http2StreamFrameToHttpObjectCodec; import org.xbib.netty.http.client.handler.http2.Http2StreamFrameToHttpObjectCodec;
import org.xbib.netty.http.client.listener.CookieListener;
import org.xbib.netty.http.client.listener.StatusListener;
import org.xbib.netty.http.common.DefaultHttpResponse; import org.xbib.netty.http.common.DefaultHttpResponse;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.client.listener.ResponseListener;
import org.xbib.netty.http.common.HttpResponse;
import org.xbib.netty.http.common.cookie.Cookie; import org.xbib.netty.http.common.cookie.Cookie;
import java.io.IOException; import java.io.IOException;
@ -74,7 +71,7 @@ public class Http2Transport extends BaseTransport {
return this; return this;
} }
final String channelId = channel.id().toString(); final String channelId = channel.id().toString();
channelFlowMap.putIfAbsent(channelId, new Flow()); flowMap.putIfAbsent(channelId, new Flow());
Http2StreamChannel childChannel = new Http2StreamChannelBootstrap(channel) Http2StreamChannel childChannel = new Http2StreamChannelBootstrap(channel)
.handler(initializer).open().syncUninterruptibly().getNow(); .handler(initializer).open().syncUninterruptibly().getNow();
AsciiString method = request.httpMethod().asciiName(); AsciiString method = request.httpMethod().asciiName();
@ -83,7 +80,7 @@ public class Http2Transport extends BaseTransport {
String path = request.relative().isEmpty() ? "/" : request.relative(); String path = request.relative().isEmpty() ? "/" : request.relative();
Http2Headers http2Headers = new DefaultHttp2Headers() Http2Headers http2Headers = new DefaultHttp2Headers()
.method(method).scheme(scheme).authority(authority).path(path); .method(method).scheme(scheme).authority(authority).path(path);
final Integer streamId = channelFlowMap.get(channelId).nextStreamId(); final Integer streamId = flowMap.get(channelId).nextStreamId();
if (streamId == null) { if (streamId == null) {
throw new IllegalStateException(); throw new IllegalStateException();
} }
@ -146,14 +143,14 @@ public class Http2Transport extends BaseTransport {
logger.log(Level.WARNING, "stream ID is null?"); logger.log(Level.WARNING, "stream ID is null?");
return; return;
} }
DefaultHttpResponse httpResponse = new DefaultHttpResponse(httpAddress, fullHttpResponse); DefaultHttpResponse httpResponse = null;
client.getResponseCounter().incrementAndGet(); client.getResponseCounter().incrementAndGet();
try { try {
// format of childchan channel ID is <parent channel ID> "/" <substream ID> // format of childchan channel ID is <parent channel ID> "/" <substream ID>
String channelId = channel.id().toString(); String channelId = channel.id().toString();
int pos = channelId.indexOf('/'); int pos = channelId.indexOf('/');
channelId = pos > 0 ? channelId.substring(0, pos) : channelId; channelId = pos > 0 ? channelId.substring(0, pos) : channelId;
Flow flow = channelFlowMap.get(channelId); Flow flow = flowMap.get(channelId);
if (flow == null) { if (flow == null) {
// should never happen since we keep the channelFlowMap around // should never happen since we keep the channelFlowMap around
if (logger.isLoggable(Level.WARNING)) { if (logger.isLoggable(Level.WARNING)) {
@ -172,24 +169,14 @@ public class Http2Transport extends BaseTransport {
promise.completeExceptionally(new IllegalStateException("no request")); promise.completeExceptionally(new IllegalStateException("no request"));
} }
} else { } else {
StatusListener statusListener = request.getStatusListener(); for (String cookieString : fullHttpResponse.headers().getAll(HttpHeaderNames.SET_COOKIE)) {
if (statusListener != null) {
statusListener.onStatus(httpResponse.getStatus());
}
for (String cookieString : httpResponse.getHeaders().getAllHeaders(HttpHeaderNames.SET_COOKIE)) {
Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString);
addCookie(cookie); addCookie(cookie);
CookieListener cookieListener = request.getCookieListener();
if (cookieListener != null) {
cookieListener.onCookie(cookie);
}
} }
httpResponse = new DefaultHttpResponse(httpAddress, fullHttpResponse, getCookieBox());
CompletableFuture<Boolean> promise = flow.get(streamId); CompletableFuture<Boolean> promise = flow.get(streamId);
try { try {
ResponseListener<HttpResponse> responseListener = request.getResponseListener(); request.onResponse(httpResponse);
if (responseListener != null) {
responseListener.onResponse(httpResponse);
}
Request retryRequest = retry(request, httpResponse); Request retryRequest = retry(request, httpResponse);
if (retryRequest != null) { if (retryRequest != null) {
// retry transport, wait for completion // retry transport, wait for completion
@ -218,14 +205,16 @@ public class Http2Transport extends BaseTransport {
} }
} }
} finally { } finally {
if (httpResponse != null) {
httpResponse.release(); httpResponse.release();
} }
} }
}
@Override @Override
public void pushPromiseReceived(Channel channel, Integer streamId, Integer promisedStreamId, Http2Headers headers) { public void pushPromiseReceived(Channel channel, Integer streamId, Integer promisedStreamId, Http2Headers headers) {
String channelId = channel.id().toString(); String channelId = channel.id().toString();
channelFlowMap.get(channelId).put(promisedStreamId, new CompletableFuture<>()); flowMap.get(channelId).put(promisedStreamId, new CompletableFuture<>());
String requestKey = getRequestKey(channel.id().toString(), streamId); String requestKey = getRequestKey(channel.id().toString(), streamId);
requests.put(requestKey, requests.get(requestKey)); requests.put(requestKey, requests.get(requestKey));
} }

View file

@ -0,0 +1,2 @@
org.xbib.netty.http.client.Http1Provider
org.xbib.netty.http.client.Http2Provider

View file

@ -2,7 +2,7 @@ package org.xbib.netty.http.client.test;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.common.HttpResponse;
import java.io.IOException; import java.io.IOException;

View file

@ -3,7 +3,7 @@ package org.xbib.netty.http.client.test;
import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpMethod;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.api.Request;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;

View file

@ -1,83 +0,0 @@
package org.xbib.netty.http.client.test;
import io.netty.handler.codec.http.HttpMethod;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;
@ExtendWith(NettyHttpTestExtension.class)
class SecureHttpTest {
private static final Logger logger = Logger.getLogger(SecureHttpTest.class.getName());
@Test
void testHttp1WithTlsV13() throws Exception {
Client client = Client.builder()
.setTlsProtocols(new String[] { "TLSv1.3" })
.build();
try {
Request request = Request.get().url("https://www.google.com/").build()
.setResponseListener(resp -> logger.log(Level.INFO, "got response: " +
resp.getHeaders() + resp.getBodyAsString(StandardCharsets.UTF_8) +
" status=" + resp.getStatus()));
client.execute(request).get();
} finally {
client.shutdownGracefully();
}
}
@Test
void testSequentialRequests() throws Exception {
Client client = Client.builder()
.build();
try {
Request request1 = Request.get().url("https://google.com").build()
.setResponseListener(resp -> logger.log(Level.INFO, "got HTTP 1.1 response: " +
resp.getBodyAsString(StandardCharsets.UTF_8)));
client.execute(request1).get();
// TODO decompression of frames
Request request2 = Request.get().url("https://google.com").setVersion("HTTP/2.0").build()
.setResponseListener(resp -> logger.log(Level.INFO, "got HTTP/2 response: " +
resp.getHeaders() + resp.getBodyAsString(StandardCharsets.UTF_8)));
client.execute(request2).get();
} finally {
client.shutdownGracefully();
}
}
@Test
void testParallelRequests() throws IOException {
Client client = Client.builder()
.build();
try {
Request request1 = Request.builder(HttpMethod.GET)
.url("https://google.com").setVersion("HTTP/1.1")
.build()
.setResponseListener(resp -> logger.log(Level.INFO, "got response: " +
resp.getHeaders() +
" status=" + resp.getStatus()));
Request request2 = Request.builder(HttpMethod.GET)
.url("https://google.com").setVersion("HTTP/1.1")
.build()
.setResponseListener(resp -> logger.log(Level.INFO, "got response: " +
resp.getHeaders() +
" status=" + resp.getStatus()));
for (int i = 0; i < 10; i++) {
client.execute(request1);
client.execute(request2);
}
} finally {
client.shutdownGracefully();
}
}
}

View file

@ -3,7 +3,7 @@ package org.xbib.netty.http.client.test.akamai;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.client.test.NettyHttpTestExtension; import org.xbib.netty.http.client.test.NettyHttpTestExtension;
import java.io.IOException; import java.io.IOException;
@ -38,11 +38,11 @@ public class AkamaiTest {
.url("https://http2.akamai.com/demo/h2_demo_frame.html") .url("https://http2.akamai.com/demo/h2_demo_frame.html")
//.url("https://http2.akamai.com/") //.url("https://http2.akamai.com/")
.setVersion("HTTP/2.0") .setVersion("HTTP/2.0")
.build()
.setResponseListener(resp -> { .setResponseListener(resp -> {
logger.log(Level.INFO, "status = " + resp.getStatus().getCode() + logger.log(Level.INFO, "status = " + resp.getStatus().getCode() +
resp.getHeaders() + " " + resp.getBodyAsString(StandardCharsets.UTF_8)); resp.getHeaders() + " " + resp.getBodyAsString(StandardCharsets.UTF_8));
}); })
.build();
client.execute(request).get(); client.execute(request).get();
} finally { } finally {
client.shutdownGracefully(); client.shutdownGracefully();

View file

@ -1,10 +1,11 @@
package org.xbib.netty.http.client.test; package org.xbib.netty.http.client.test.conscrypt;
import org.conscrypt.Conscrypt; import org.conscrypt.Conscrypt;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.client.test.NettyHttpTestExtension;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -34,11 +35,11 @@ class ConscryptTest {
Request request = Request.get() Request request = Request.get()
.url("https://google.com") .url("https://google.com")
.setVersion("HTTP/1.1") .setVersion("HTTP/1.1")
.build()
.setResponseListener(resp -> { .setResponseListener(resp -> {
logger.log(Level.INFO, "status = " + resp.getStatus() logger.log(Level.INFO, "status = " + resp.getStatus()
+ " response body = " + resp.getBodyAsString(StandardCharsets.UTF_8)); + " response body = " + resp.getBodyAsString(StandardCharsets.UTF_8));
}); })
.build();
client.execute(request).get(); client.execute(request).get();
} finally { } finally {
client.shutdownGracefully(); client.shutdownGracefully();

View file

@ -25,7 +25,7 @@ class ClientCookieDecoderTest {
void testDecodingSingleCookieV0() { void testDecodingSingleCookieV0() {
long millis = System.currentTimeMillis() + 50000; long millis = System.currentTimeMillis() + 50000;
String cookieString = "myCookie=myValue;expires=" + String cookieString = "myCookie=myValue;expires=" +
DateTimeUtil.formatMillis(millis) + DateTimeUtil.formatRfc1123(millis) +
";path=/apathsomewhere;domain=.adomainsomewhere;secure;"; ";path=/apathsomewhere;domain=.adomainsomewhere;secure;";
Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString);
assertNotNull(cookie); assertNotNull(cookie);

View file

@ -1,12 +1,16 @@
package org.xbib.netty.http.client.test; package org.xbib.netty.http.client.test.cookie;
import static org.junit.Assert.assertTrue;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.client.test.NettyHttpTestExtension;
import org.xbib.netty.http.common.cookie.Cookie;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -31,18 +35,25 @@ class CookieSetterHttpBinTest {
@Test @Test
void testHttpBinCookies() throws IOException { void testHttpBinCookies() throws IOException {
Client client = new Client(); Client client = new Client();
AtomicBoolean success = new AtomicBoolean();
try { try {
Request request = Request.get() Request request = Request.get()
.url("http://httpbin.org/cookies/set?name=value") .url("http://httpbin.org/cookies/set?name=value")
.build()
.setCookieListener(cookie -> logger.log(Level.INFO, "this is the cookie: " + cookie.toString()))
.setResponseListener(resp -> { .setResponseListener(resp -> {
logger.log(Level.INFO, "status = " + resp.getStatus() + logger.log(Level.INFO, "status = " + resp.getStatus() +
" response body = " + resp.getBodyAsString(StandardCharsets.UTF_8)); " response body = " + resp.getBodyAsString(StandardCharsets.UTF_8));
}); for (Cookie cookie : resp.getCookies().keySet()) {
logger.log(Level.INFO, "got cookie: " + cookie.toString());
if ("name".equals(cookie.name()) && ("value".equals(cookie.value()))) {
success.set(true);
}
}
})
.build();
client.execute(request).get(); client.execute(request).get();
} finally { } finally {
client.shutdownGracefully(); client.shutdownGracefully();
} }
assertTrue(success.get());
} }
} }

View file

@ -0,0 +1,100 @@
package org.xbib.netty.http.client.test.http1;
import io.netty.handler.codec.http.HttpMethod;
import static org.junit.Assert.assertTrue;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.client.test.NettyHttpTestExtension;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
@ExtendWith(NettyHttpTestExtension.class)
class GoogleTest {
private static final Logger logger = Logger.getLogger(GoogleTest.class.getName());
@Test
void testHttp1WithTlsV13() throws Exception {
AtomicBoolean success = new AtomicBoolean();
Client client = Client.builder()
.setTlsProtocols(new String[] { "TLSv1.3" })
.build();
try {
Request request = Request.get().url("https://www.google.com/")
.setResponseListener(resp -> {
logger.log(Level.INFO, "got response: " +
resp.getHeaders() + resp.getBodyAsString(StandardCharsets.UTF_8) +
" status=" + resp.getStatus());
success.set(true);
})
.build();
client.execute(request).get();
} finally {
client.shutdownGracefully();
}
assertTrue(success.get());
}
@Test
void testSequentialRequests() throws Exception {
AtomicBoolean success = new AtomicBoolean();
Client client = Client.builder()
.build();
try {
Request request1 = Request.get().url("https://google.com")
.setResponseListener(resp -> {
logger.log(Level.INFO, "got HTTP 1.1 response: " +
resp.getBodyAsString(StandardCharsets.UTF_8));
success.set(true);
})
.build();
client.execute(request1).get();
} finally {
client.shutdownGracefully();
}
assertTrue(success.get());
}
@Test
void testParallelRequests() throws IOException {
AtomicBoolean success1 = new AtomicBoolean();
AtomicBoolean success2 = new AtomicBoolean();
Client client = Client.builder()
.build();
try {
Request request1 = Request.builder(HttpMethod.GET)
.url("https://google.com").setVersion("HTTP/1.1")
.setResponseListener(resp -> {
logger.log(Level.INFO, "got response: " +
resp.getHeaders() +
" status=" + resp.getStatus());
success1.set(true);
})
.build();
Request request2 = Request.builder(HttpMethod.GET)
.url("https://google.com").setVersion("HTTP/1.1")
.setResponseListener(resp -> {
logger.log(Level.INFO, "got response: " +
resp.getHeaders() +
" status=" + resp.getStatus());
success2.set(true);
})
.build();
for (int i = 0; i < 10; i++) {
client.execute(request1);
client.execute(request2);
}
} finally {
client.shutdownGracefully();
}
assertTrue(success1.get());
assertTrue(success2.get());
}
}

View file

@ -1,10 +1,11 @@
package org.xbib.netty.http.client.test; package org.xbib.netty.http.client.test.http1;
import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpMethod;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.client.test.NettyHttpTestExtension;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -21,11 +22,12 @@ class Http1Test {
Client client = Client.builder() Client client = Client.builder()
.build(); .build();
try { try {
Request request = Request.get().url("http://xbib.org").build() Request request = Request.get().url("http://xbib.org")
.setResponseListener(resp -> logger.log(Level.FINE, .setResponseListener(resp -> logger.log(Level.FINE,
"got response: " + resp.getHeaders() + "got response: " + resp.getHeaders() +
resp.getBodyAsString(StandardCharsets.UTF_8) + resp.getBodyAsString(StandardCharsets.UTF_8) +
" status=" + resp.getStatus())); " status=" + resp.getStatus()))
.build();
client.execute(request).get(); client.execute(request).get();
} finally { } finally {
client.shutdownGracefully(); client.shutdownGracefully();
@ -37,14 +39,15 @@ class Http1Test {
Client client = Client.builder() Client client = Client.builder()
.build(); .build();
try { try {
Request request1 = Request.get().url("http://xbib.org").build() Request request1 = Request.get().url("http://xbib.org")
.setResponseListener(resp -> logger.log(Level.FINE, "got response: " + .setResponseListener(resp -> logger.log(Level.FINE, "got response: " +
resp.getBodyAsString(StandardCharsets.UTF_8))); resp.getBodyAsString(StandardCharsets.UTF_8)))
.build();
client.execute(request1).get(); client.execute(request1).get();
Request request2 = Request.get().url("http://google.com").setVersion("HTTP/1.1")
Request request2 = Request.get().url("http://google.com").setVersion("HTTP/1.1").build()
.setResponseListener(resp -> logger.log(Level.FINE, "got response: " + .setResponseListener(resp -> logger.log(Level.FINE, "got response: " +
resp.getBodyAsString(StandardCharsets.UTF_8))); resp.getBodyAsString(StandardCharsets.UTF_8)))
.build();
client.execute(request2).get(); client.execute(request2).get();
} finally { } finally {
client.shutdownGracefully(); client.shutdownGracefully();
@ -58,15 +61,14 @@ class Http1Test {
try { try {
Request request1 = Request.builder(HttpMethod.GET) Request request1 = Request.builder(HttpMethod.GET)
.url("http://xbib.org").setVersion("HTTP/1.1") .url("http://xbib.org").setVersion("HTTP/1.1")
.build()
.setResponseListener(resp -> logger.log(Level.FINE, "got response: " + .setResponseListener(resp -> logger.log(Level.FINE, "got response: " +
resp.getHeaders() + " status=" +resp.getStatus())); resp.getHeaders() + " status=" +resp.getStatus()))
.build();
Request request2 = Request.builder(HttpMethod.GET) Request request2 = Request.builder(HttpMethod.GET)
.url("http://xbib.org").setVersion("HTTP/1.1") .url("http://xbib.org").setVersion("HTTP/1.1")
.build()
.setResponseListener(resp -> logger.log(Level.FINE, "got response: " + .setResponseListener(resp -> logger.log(Level.FINE, "got response: " +
resp.getHeaders() + " status=" +resp.getStatus())); resp.getHeaders() + " status=" +resp.getStatus()))
.build();
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
client.execute(request1); client.execute(request1);
client.execute(request2); client.execute(request2);

View file

@ -1,9 +1,9 @@
package org.xbib.netty.http.client.test; package org.xbib.netty.http.client.test.http1;
import io.netty.handler.proxy.HttpProxyHandler; import io.netty.handler.proxy.HttpProxyHandler;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.common.HttpResponse;
import java.io.IOException; import java.io.IOException;
@ -23,11 +23,11 @@ class XbibTest {
Client client = new Client(); Client client = new Client();
try { try {
Request request = Request.get().url("http://xbib.org") Request request = Request.get().url("http://xbib.org")
.build()
.setResponseListener(resp -> { .setResponseListener(resp -> {
logger.log(Level.INFO, "status = " + resp.getStatus() + logger.log(Level.INFO, "status = " + resp.getStatus() +
" response = " + resp.getBodyAsString(StandardCharsets.UTF_8)); " response = " + resp.getBodyAsString(StandardCharsets.UTF_8));
}); })
.build();
client.execute(request); client.execute(request);
} finally { } finally {
client.shutdownGracefully(); client.shutdownGracefully();
@ -74,9 +74,9 @@ class XbibTest {
try { try {
httpClient.execute(Request.get() httpClient.execute(Request.get()
.url("http://xbib.org") .url("http://xbib.org")
.build()
.setResponseListener(resp -> logger.log(Level.INFO, "status = " + resp.getStatus() + .setResponseListener(resp -> logger.log(Level.INFO, "status = " + resp.getStatus() +
" response body = " + resp.getBodyAsString(StandardCharsets.UTF_8)))) " response body = " + resp.getBodyAsString(StandardCharsets.UTF_8)))
.build())
.get(); .get();
} finally { } finally {
httpClient.shutdownGracefully(); httpClient.shutdownGracefully();
@ -91,11 +91,10 @@ class XbibTest {
httpClient.execute(Request.get() httpClient.execute(Request.get()
.url("http://xbib.org") .url("http://xbib.org")
.setTimeoutInMillis(10) .setTimeoutInMillis(10)
.build()
.setResponseListener(resp -> .setResponseListener(resp ->
logger.log(Level.INFO, "status = " + resp.getStatus() + logger.log(Level.INFO, "status = " + resp.getStatus() +
" response body = " + resp.getBodyAsString(StandardCharsets.UTF_8)) " response body = " + resp.getBodyAsString(StandardCharsets.UTF_8)))
)) .build())
.get(); .get();
} finally { } finally {
httpClient.shutdownGracefully(); httpClient.shutdownGracefully();
@ -109,21 +108,20 @@ class XbibTest {
httpClient.execute(Request.get() httpClient.execute(Request.get()
.setVersion("HTTP/1.1") .setVersion("HTTP/1.1")
.url("http://xbib.org") .url("http://xbib.org")
.build()
.setResponseListener(resp -> { .setResponseListener(resp -> {
logger.log(Level.INFO, "status = " + resp.getStatus() + logger.log(Level.INFO, "status = " + resp.getStatus() +
" response body = " + resp.getBodyAsString(StandardCharsets.UTF_8)); " response body = " + resp.getBodyAsString(StandardCharsets.UTF_8));
})) })
.build())
.get(); .get();
httpClient.execute(Request.get() httpClient.execute(Request.get()
.setVersion("HTTP/1.1") .setVersion("HTTP/1.1")
.url("http://xbib.org") .url("http://xbib.org")
.build()
.setResponseListener(resp -> { .setResponseListener(resp -> {
logger.log(Level.INFO, "status = " + resp.getStatus() + logger.log(Level.INFO, "status = " + resp.getStatus() +
" response body = " + resp.getBodyAsString(StandardCharsets.UTF_8)); " response body = " + resp.getBodyAsString(StandardCharsets.UTF_8));
})) })
.build())
.get(); .get();
} finally { } finally {
httpClient.shutdownGracefully(); httpClient.shutdownGracefully();

View file

@ -0,0 +1,30 @@
package org.xbib.netty.http.client.test.http2;
import org.junit.jupiter.api.Test;
import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.api.Request;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;
public class GoogleTest {
private static final Logger logger = Logger.getLogger(GoogleTest.class.getName());
@Test
void testSequentialRequests() throws Exception {
Client client = Client.builder()
.build();
try {
// TODO decompression of frames
Request request2 = Request.get().url("https://google.com").setVersion("HTTP/2.0")
.setResponseListener(resp -> logger.log(Level.INFO, "got HTTP/2 response: " +
resp.getHeaders() + resp.getBodyAsString(StandardCharsets.UTF_8)))
.build();
client.execute(request2).get();
} finally {
client.shutdownGracefully();
}
}
}

View file

@ -1,11 +1,11 @@
package org.xbib.netty.http.client.test.htt2push; package org.xbib.netty.http.client.test.http2push;
import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpMethod;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.client.test.NettyHttpTestExtension; import org.xbib.netty.http.client.test.NettyHttpTestExtension;
import java.io.IOException; import java.io.IOException;
@ -27,9 +27,9 @@ class Http2PushTest {
try { try {
Request request = Request.builder(HttpMethod.GET) Request request = Request.builder(HttpMethod.GET)
.url(url).setVersion("HTTP/2.0") .url(url).setVersion("HTTP/2.0")
.build()
.setResponseListener(resp -> logger.log(Level.INFO, .setResponseListener(resp -> logger.log(Level.INFO,
"got response: " + resp.getHeaders() + " status=" + resp.getStatus())); "got response: " + resp.getHeaders() + " status=" + resp.getStatus()))
.build();
client.execute(request).get(); client.execute(request).get();
} finally { } finally {

View file

@ -22,7 +22,7 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.pool.Pool; import org.xbib.netty.http.client.api.Pool;
import org.xbib.netty.http.client.pool.BoundedChannelPool; import org.xbib.netty.http.client.pool.BoundedChannelPool;
import java.io.Closeable; import java.io.Closeable;

View file

@ -21,7 +21,7 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.pool.Pool; import org.xbib.netty.http.client.api.Pool;
import org.xbib.netty.http.client.pool.BoundedChannelPool; import org.xbib.netty.http.client.pool.BoundedChannelPool;
import java.io.Closeable; import java.io.Closeable;

View file

@ -13,7 +13,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.pool.BoundedChannelPool; import org.xbib.netty.http.client.pool.BoundedChannelPool;
import org.xbib.netty.http.client.pool.Pool; import org.xbib.netty.http.client.api.Pool;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -47,7 +47,7 @@ class PoolTest {
ServerBootstrap serverBootstrap = new ServerBootstrap() ServerBootstrap serverBootstrap = new ServerBootstrap()
.group(new NioEventLoopGroup()) .group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class) .channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<Channel>() { .childHandler(new ChannelInitializer<>() {
@Override @Override
protected void initChannel(Channel ch) { protected void initChannel(Channel ch) {
} }

View file

@ -5,10 +5,10 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.listener.ResponseListener; import org.xbib.netty.http.client.api.ResponseListener;
import org.xbib.netty.http.client.test.NettyHttpTestExtension; import org.xbib.netty.http.client.test.NettyHttpTestExtension;
import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.common.HttpResponse; import org.xbib.netty.http.common.HttpResponse;
import java.io.IOException; import java.io.IOException;
@ -49,8 +49,8 @@ class PooledClientTest {
for (int i = 0; i < loop; i++) { for (int i = 0; i < loop; i++) {
Request request = Request.get().setVersion(httpAddress.getVersion()) Request request = Request.get().setVersion(httpAddress.getVersion())
.url(url.toString()) .url(url.toString())
.build() .setResponseListener(responseListener)
.setResponseListener(responseListener); .build();
client.newTransport().execute(request).get(); client.newTransport().execute(request).get();
} }
logger.log(Level.INFO, "done " + Thread.currentThread()); logger.log(Level.INFO, "done " + Thread.currentThread());

View file

@ -1,7 +1,7 @@
package org.xbib.netty.http.client.test.retry; package org.xbib.netty.http.client.test.retry;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.xbib.netty.http.client.retry.BackOff; import org.xbib.netty.http.client.api.BackOff;
import org.xbib.netty.http.client.retry.ExponentialBackOff; import org.xbib.netty.http.client.retry.ExponentialBackOff;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;

View file

@ -1,8 +1,6 @@
package org.xbib.netty.http.client.test.retry; package org.xbib.netty.http.client.test.retry;
import org.xbib.netty.http.client.retry.BackOff; import org.xbib.netty.http.client.api.BackOff;
import java.io.IOException;
/** /**
* Mock for {@link BackOff} that always returns a fixed number. * Mock for {@link BackOff} that always returns a fixed number.

View file

@ -1,7 +1,7 @@
package org.xbib.netty.http.client.test.retry; package org.xbib.netty.http.client.test.retry;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.xbib.netty.http.client.retry.BackOff; import org.xbib.netty.http.client.api.BackOff;
import java.io.IOException; import java.io.IOException;

View file

@ -4,7 +4,7 @@ import io.netty.handler.codec.http.HttpMethod;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.client.test.NettyHttpTestExtension; import org.xbib.netty.http.client.test.NettyHttpTestExtension;
import java.io.IOException; import java.io.IOException;
@ -21,8 +21,9 @@ class WebtideTest {
Client client = Client.builder() Client client = Client.builder()
.build(); .build();
try { try {
Request request = Request.get().url("https://webtide.com").setVersion("HTTP/2.0").build() Request request = Request.get().url("https://webtide.com").setVersion("HTTP/2.0")
.setResponseListener(msg -> logger.log(Level.INFO, "got response: " + msg)); .setResponseListener(msg -> logger.log(Level.INFO, "got response: " + msg))
.build();
client.execute(request).get(); client.execute(request).get();
} finally { } finally {
client.shutdownGracefully(); client.shutdownGracefully();
@ -35,16 +36,14 @@ class WebtideTest {
try { try {
Request request1 = Request.builder(HttpMethod.GET) Request request1 = Request.builder(HttpMethod.GET)
.url("https://webtide.com").setVersion("HTTP/2.0") .url("https://webtide.com").setVersion("HTTP/2.0")
.build()
.setResponseListener(resp -> logger.log(Level.INFO, "got response: " + .setResponseListener(resp -> logger.log(Level.INFO, "got response: " +
resp.getHeaders() + " status=" + resp.getStatus())); resp.getHeaders() + " status=" + resp.getStatus()))
.build();
Request request2 = Request.builder(HttpMethod.GET) Request request2 = Request.builder(HttpMethod.GET)
.url("https://webtide.com/why-choose-jetty/").setVersion("HTTP/2.0") .url("https://webtide.com/why-choose-jetty/").setVersion("HTTP/2.0")
.build()
.setResponseListener(resp -> logger.log(Level.INFO, "got response: " + .setResponseListener(resp -> logger.log(Level.INFO, "got response: " +
resp.getHeaders() + " status=" +resp.getStatus())); resp.getHeaders() + " status=" +resp.getStatus()))
.build();
client.execute(request1).execute(request2); client.execute(request1).execute(request2);
} finally { } finally {
client.shutdownGracefully(); client.shutdownGracefully();

View file

@ -4,6 +4,7 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream; import io.netty.buffer.ByteBufInputStream;
import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse;
import org.xbib.netty.http.common.cookie.CookieBox;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -17,11 +18,16 @@ public class DefaultHttpResponse implements HttpResponse {
private final HttpHeaders httpHeaders; private final HttpHeaders httpHeaders;
public DefaultHttpResponse(HttpAddress httpAddress, FullHttpResponse fullHttpResponse) { private final CookieBox cookieBox;
public DefaultHttpResponse(HttpAddress httpAddress,
FullHttpResponse fullHttpResponse,
CookieBox cookieBox) {
this.httpAddress = httpAddress; this.httpAddress = httpAddress;
this.fullHttpResponse = fullHttpResponse.retain(); this.fullHttpResponse = fullHttpResponse.retain();
this.httpStatus = new HttpStatus(this.fullHttpResponse.status()); this.httpStatus = new HttpStatus(this.fullHttpResponse.status());
this.httpHeaders = new DefaultHttpHeaders(this.fullHttpResponse.headers()); this.httpHeaders = new DefaultHttpHeaders(this.fullHttpResponse.headers());
this.cookieBox = cookieBox;
} }
@Override @Override
@ -39,6 +45,11 @@ public class DefaultHttpResponse implements HttpResponse {
return httpHeaders; return httpHeaders;
} }
@Override
public CookieBox getCookies() {
return cookieBox;
}
@Override @Override
public ByteBuf getBody() { public ByteBuf getBody() {
return fullHttpResponse.content(); return fullHttpResponse.content();

View file

@ -10,6 +10,8 @@ import java.net.InetSocketAddress;
*/ */
public class HttpAddress implements PoolKey { public class HttpAddress implements PoolKey {
public static final HttpVersion HTTP_1_1 = HttpVersion.valueOf("HTTP/1.1");
public static final HttpVersion HTTP_2_0 = HttpVersion.valueOf("HTTP/2.0"); public static final HttpVersion HTTP_2_0 = HttpVersion.valueOf("HTTP/2.0");
private final String host; private final String host;
@ -23,19 +25,19 @@ public class HttpAddress implements PoolKey {
private InetSocketAddress inetSocketAddress; private InetSocketAddress inetSocketAddress;
public static HttpAddress http1(String host) { public static HttpAddress http1(String host) {
return new HttpAddress(host, 80, HttpVersion.HTTP_1_1, false); return new HttpAddress(host, 80, HTTP_1_1, false);
} }
public static HttpAddress http1(String host, int port) { public static HttpAddress http1(String host, int port) {
return new HttpAddress(host, port, HttpVersion.HTTP_1_1, false); return new HttpAddress(host, port, HTTP_1_1, false);
} }
public static HttpAddress secureHttp1(String host) { public static HttpAddress secureHttp1(String host) {
return new HttpAddress(host, 443, HttpVersion.HTTP_1_1, true); return new HttpAddress(host, 443, HTTP_1_1, true);
} }
public static HttpAddress secureHttp1(String host, int port) { public static HttpAddress secureHttp1(String host, int port) {
return new HttpAddress(host, port, HttpVersion.HTTP_1_1, true); return new HttpAddress(host, port, HTTP_1_1, true);
} }
public static HttpAddress http2(String host) { public static HttpAddress http2(String host) {
@ -55,7 +57,7 @@ public class HttpAddress implements PoolKey {
} }
public static HttpAddress http1(URL url) { public static HttpAddress http1(URL url) {
return new HttpAddress(url, HttpVersion.HTTP_1_1); return new HttpAddress(url, HTTP_1_1);
} }
public static HttpAddress http2(URL url) { public static HttpAddress http2(URL url) {
@ -63,7 +65,7 @@ public class HttpAddress implements PoolKey {
} }
public static HttpAddress of(URL url) { public static HttpAddress of(URL url) {
return new HttpAddress(url, HttpVersion.HTTP_1_1); return new HttpAddress(url, HTTP_1_1);
} }
public static HttpAddress of(URL url, HttpVersion httpVersion) { public static HttpAddress of(URL url, HttpVersion httpVersion) {
@ -95,7 +97,9 @@ public class HttpAddress implements PoolKey {
} }
public URL base() { public URL base() {
return isSecure() ? URL.https().host(host).port(port).build() : URL.http().host(host).port(port).build(); return isSecure() ?
URL.https().host(host).port(port).build() :
URL.http().host(host).port(port).build();
} }
public HttpVersion getVersion() { public HttpVersion getVersion() {

View file

@ -1,11 +1,14 @@
package org.xbib.netty.http.common; package org.xbib.netty.http.common;
import io.netty.handler.codec.http.HttpHeaderValues;
import org.xbib.net.PercentDecoder; import org.xbib.net.PercentDecoder;
import org.xbib.net.PercentEncoder; import org.xbib.net.PercentEncoder;
import org.xbib.net.PercentEncoders; import org.xbib.net.PercentEncoders;
import org.xbib.netty.http.common.util.LimitedSet; import org.xbib.netty.http.common.util.LimitedSet;
import org.xbib.netty.http.common.util.LimitedTreeMap; import org.xbib.netty.http.common.util.LimitedTreeMap;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.MalformedInputException; import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.charset.UnmappableCharacterException; import java.nio.charset.UnmappableCharacterException;
@ -34,8 +37,6 @@ public class HttpParameters implements Map<String, SortedSet<String>> {
private static final String AMPERSAND = "&"; private static final String AMPERSAND = "&";
private static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded";
private final int maxParam; private final int maxParam;
private final int sizeLimit; private final int sizeLimit;
@ -48,24 +49,30 @@ public class HttpParameters implements Map<String, SortedSet<String>> {
private final PercentDecoder percentDecoder; private final PercentDecoder percentDecoder;
private final String contentType; private final CharSequence contentType;
private final Charset charset;
public HttpParameters() { public HttpParameters() {
this(1024, 1024, 65536, APPLICATION_X_WWW_FORM_URLENCODED); this(1024, 1024, 65536,
HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED, StandardCharsets.UTF_8);
} }
public HttpParameters(String contentType) { public HttpParameters(String contentType) {
this(1024, 1024, 65536, contentType); this(1024, 1024, 65536,
contentType, StandardCharsets.UTF_8);
} }
public HttpParameters(int maxParam, int sizeLimit, int elementSizeLimit, String contentType) { public HttpParameters(int maxParam, int sizeLimit, int elementSizeLimit,
CharSequence contentType, Charset charset) {
this.maxParam = maxParam; this.maxParam = maxParam;
this.sizeLimit = sizeLimit; this.sizeLimit = sizeLimit;
this.elementSizeLimit = elementSizeLimit; this.elementSizeLimit = elementSizeLimit;
this.map = new LimitedTreeMap<>(maxParam); this.map = new LimitedTreeMap<>(maxParam);
this.percentEncoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8); this.percentEncoder = PercentEncoders.getQueryEncoder(charset);
this.percentDecoder = new PercentDecoder(); this.percentDecoder = new PercentDecoder();
this.contentType = contentType; this.contentType = contentType;
this.charset = charset;
} }
@Override @Override
@ -139,8 +146,7 @@ public class HttpParameters implements Map<String, SortedSet<String>> {
return map.entrySet(); return map.entrySet();
} }
public SortedSet<String> put(String key, SortedSet<String> values, boolean percentEncode) public SortedSet<String> put(String key, SortedSet<String> values, boolean percentEncode) {
throws MalformedInputException, UnmappableCharacterException {
if (percentEncode) { if (percentEncode) {
remove(key); remove(key);
for (String v : values) { for (String v : values) {
@ -158,11 +164,8 @@ public class HttpParameters implements Map<String, SortedSet<String>> {
* @param key the parameter name * @param key the parameter name
* @param value the parameter value * @param value the parameter value
* @return the value * @return the value
* @throws MalformedInputException if input is malformed
* @throws UnmappableCharacterException if characters are unmappable
*/ */
public String add(String key, String value) public String add(String key, String value) {
throws MalformedInputException, UnmappableCharacterException {
return add(key, value, false); return add(key, value, false);
} }
@ -175,22 +178,24 @@ public class HttpParameters implements Map<String, SortedSet<String>> {
* @param percentEncode whether key and value should be percent encoded before being * @param percentEncode whether key and value should be percent encoded before being
* inserted into the map * inserted into the map
* @return the value * @return the value
* @throws MalformedInputException if input is malformed
* @throws UnmappableCharacterException if characters are unmappable
*/ */
public String add(String key, String value, boolean percentEncode) public String add(String key, String value, boolean percentEncode) {
throws MalformedInputException, UnmappableCharacterException { String v = null;
try {
String k = percentEncode ? percentEncoder.encode(key) : key; String k = percentEncode ? percentEncoder.encode(key) : key;
SortedSet<String> values = map.get(k); SortedSet<String> values = map.get(k);
if (values == null) { if (values == null) {
values = new LimitedSet<>(sizeLimit, elementSizeLimit); values = new LimitedSet<>(sizeLimit, elementSizeLimit);
map.put(k, values); map.put(k, values);
} }
String v = null;
if (value != null) { if (value != null) {
v = percentEncode ? percentEncoder.encode(value) : value; v = percentEncode ? percentEncoder.encode(value) : value;
values.add(v); values.add(v);
} }
} catch (CharacterCodingException e) {
throw new IllegalArgumentException(e);
}
return v; return v;
} }
@ -201,11 +206,8 @@ public class HttpParameters implements Map<String, SortedSet<String>> {
* @param key the parameter name * @param key the parameter name
* @param nullString can be anything, but probably... null? * @param nullString can be anything, but probably... null?
* @return null * @return null
* @throws MalformedInputException if input is malformed
* @throws UnmappableCharacterException if characters are unmappable
*/ */
public String addNull(String key, String nullString) public String addNull(String key, String nullString) {
throws MalformedInputException, UnmappableCharacterException {
return add(key, nullString); return add(key, nullString);
} }
@ -220,8 +222,7 @@ public class HttpParameters implements Map<String, SortedSet<String>> {
} }
} }
public void addAll(String[] keyValuePairs, boolean percentEncode) public void addAll(String[] keyValuePairs, boolean percentEncode) {
throws MalformedInputException, UnmappableCharacterException {
for (int i = 0; i < keyValuePairs.length - 1; i += 2) { for (int i = 0; i < keyValuePairs.length - 1; i += 2) {
add(keyValuePairs[i], keyValuePairs[i + 1], percentEncode); add(keyValuePairs[i], keyValuePairs[i + 1], percentEncode);
} }
@ -274,7 +275,7 @@ public class HttpParameters implements Map<String, SortedSet<String>> {
return percentDecoder.decode(value); return percentDecoder.decode(value);
} }
public String getContentType() { public CharSequence getContentType() {
return contentType; return contentType;
} }
@ -340,7 +341,8 @@ public class HttpParameters implements Map<String, SortedSet<String>> {
} }
public HttpParameters getOAuthParameters() { public HttpParameters getOAuthParameters() {
HttpParameters oauthParams = new HttpParameters(maxParam, sizeLimit, elementSizeLimit, contentType); HttpParameters oauthParams =
new HttpParameters(maxParam, sizeLimit, elementSizeLimit, contentType, StandardCharsets.UTF_8);
entrySet().stream().filter(entry -> entry.getKey().startsWith("oauth_") || entry.getKey().startsWith("x_oauth_")) entrySet().stream().filter(entry -> entry.getKey().startsWith("oauth_") || entry.getKey().startsWith("x_oauth_"))
.forEach(entry -> oauthParams.put(entry.getKey(), entry.getValue())); .forEach(entry -> oauthParams.put(entry.getKey(), entry.getValue()));
return oauthParams; return oauthParams;

View file

@ -2,6 +2,7 @@ package org.xbib.netty.http.common;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import org.xbib.netty.http.common.cookie.CookieBox;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -13,6 +14,8 @@ public interface HttpResponse {
HttpHeaders getHeaders(); HttpHeaders getHeaders();
CookieBox getCookies();
ByteBuf getBody(); ByteBuf getBody();
InputStream getBodyAsStream(); InputStream getBodyAsStream();

View file

@ -21,16 +21,12 @@ public class DateTimeUtil {
private DateTimeUtil() { private DateTimeUtil() {
} }
public static String formatInstant(Instant instant) { public static String formatRfc1123(Instant instant) {
return DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.ofInstant(instant, ZoneOffset.UTC)); return DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.ofInstant(instant, ZoneOffset.UTC));
} }
public static String formatMillis(long millis) { public static String formatRfc1123(long millis) {
return formatInstant(Instant.ofEpochMilli(millis)); return formatRfc1123(Instant.ofEpochMilli(millis));
}
public static String formatSeconds(long seconds) {
return formatInstant(Instant.now().plusSeconds(seconds));
} }
// RFC 2616 allows RFC 1123, RFC 1036, ASCII time // RFC 2616 allows RFC 1123, RFC 1036, ASCII time

5
netty-http-rx/NOTICE.txt Normal file
View file

@ -0,0 +1,5 @@
This work is based on
https://github.com/ReactiveX/RxNetty
(branch 0.5.x as of 22-Sep-2019)

View file

@ -0,0 +1,7 @@
dependencies {
compile "io.netty:netty-codec-http:${project.property('netty.version')}"
compile "io.netty:netty-transport-native-epoll:${project.property('netty.version')}"
compile "io.reactivex:rxjava:${project.property('reactivex.version')}"
testCompile "org.hamcrest:hamcrest-all:${project.property('hamcrest.version')}"
testCompile "org.mockito:mockito-all:${project.property('mockito.version')}"
}

View file

@ -0,0 +1,46 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty;
/**
* A list of all handler names added by the framework. This is just to ensure consistency in naming.
*/
public enum HandlerNames {
SslHandler("ssl-handler"),
SslConnectionEmissionHandler("ssl-connection-emitter"),
WireLogging("wire-logging-handler"),
WriteTransformer("write-transformer"),
ClientReadTimeoutHandler("client-read-timeout-handler"),
ClientChannelActiveBufferingHandler("client-channel-active-buffer-handler"),
;
private final String name;
HandlerNames(String name) {
this.name = qualify(name);
}
public String getName() {
return name;
}
private static String qualify(String name) {
return "_rx_netty_" + name;
}
}

View file

@ -0,0 +1,97 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty;
import io.reactivex.netty.threads.RxEventLoopProvider;
import io.reactivex.netty.threads.SingleNioLoopProvider;
public final class RxNetty {
private static volatile RxEventLoopProvider rxEventLoopProvider = new SingleNioLoopProvider(Runtime.getRuntime().availableProcessors());
private static volatile boolean usingNativeTransport;
private static volatile boolean disableEventPublishing;
private RxNetty() {
}
/**
* An implementation of {@link RxEventLoopProvider} to be used by all clients and servers created after this call.
*
* @param provider New provider to use.
*
* @return Existing provider.
*/
public static RxEventLoopProvider useEventLoopProvider(RxEventLoopProvider provider) {
RxEventLoopProvider oldProvider = rxEventLoopProvider;
rxEventLoopProvider = provider;
return oldProvider;
}
public static RxEventLoopProvider getRxEventLoopProvider() {
return rxEventLoopProvider;
}
/**
* A global flag to start using netty's <a href="https://github.com/netty/netty/wiki/Native-transports">native protocol</a>
* if applicable for a client or server.
*
* <b>This does not evaluate whether the native transport is available for the OS or not.</b>
*
* So, this method should be called conditionally when the caller is sure that the OS supports the native protocol.
*
* Alternatively, this can be done selectively per client and server instance.
*/
public static void useNativeTransportIfApplicable() {
usingNativeTransport = true;
}
/**
* A global flag to disable the effects of calling {@link #useNativeTransportIfApplicable()}
*/
public static void disableNativeTransport() {
usingNativeTransport = false;
}
/**
* Enables publishing of events for RxNetty.
*/
public static void enableEventPublishing() {
disableEventPublishing = false;
}
/**
* Disables publishing of events for RxNetty.
*/
public static void disableEventPublishing() {
disableEventPublishing = true;
}
/**
* Returns {@code true} if event publishing is disabled.
*
* @return {@code true} if event publishing is disabled.
*/
public static boolean isEventPublishingDisabled() {
return disableEventPublishing;
}
public static boolean isUsingNativeTransport() {
return usingNativeTransport;
}
}

View file

@ -0,0 +1,392 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOption;
import io.netty.util.AttributeKey;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.internal.EmptyArrays;
import io.reactivex.netty.channel.events.ConnectionEventListener;
import io.reactivex.netty.events.EventPublisher;
import java.util.logging.Level;
import java.util.logging.Logger;
import rx.Producer;
import rx.Subscriber;
import rx.exceptions.MissingBackpressureException;
import java.nio.channels.ClosedChannelException;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
/**
* A bridge between a {@link Connection} instance and the associated {@link Channel}.
*
* All operations on {@link Connection} will pass through this bridge to an appropriate action on the {@link Channel}
*
* <h2>Lazy {@link Connection#getInput()} subscription</h2>
*
* Lazy subscriptions are allowed on {@link Connection#getInput()} if and only if the channel is configured to
* not read data automatically (i.e. {@link ChannelOption#AUTO_READ} is set to {@code false}). Otherwise,
* if {@link Connection#getInput()} is subscribed lazily, the subscriber always receives an error. The content
* in this case is disposed upon reading.
*
* @param <R> Type read from the connection held by this handler.
* @param <W> Type written to the connection held by this handler.
*/
public abstract class AbstractConnectionToChannelBridge<R, W> extends BackpressureManagingHandler {
private static final Logger logger = Logger.getLogger(AbstractConnectionToChannelBridge.class.getName());
@SuppressWarnings("ThrowableInstanceNeverThrown")
private static final IllegalStateException ONLY_ONE_CONN_SUB_ALLOWED =
new IllegalStateException("Only one subscriber allowed for connection observable.");
@SuppressWarnings("ThrowableInstanceNeverThrown")
private static final IllegalStateException ONLY_ONE_CONN_INPUT_SUB_ALLOWED =
new IllegalStateException("Only one subscriber allowed for connection input.");
@SuppressWarnings("ThrowableInstanceNeverThrown")
private static final IllegalStateException LAZY_CONN_INPUT_SUB =
new IllegalStateException("Channel is set to auto-read but the subscription was lazy.");
@SuppressWarnings("ThrowableInstanceNeverThrown")
private static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException();
static {
ONLY_ONE_CONN_INPUT_SUB_ALLOWED.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE);
ONLY_ONE_CONN_SUB_ALLOWED.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE);
LAZY_CONN_INPUT_SUB.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE);
CLOSED_CHANNEL_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE);
}
private final AttributeKey<ConnectionEventListener> eventListenerAttributeKey;
private final AttributeKey<EventPublisher> eventPublisherAttributeKey;
protected ConnectionEventListener eventListener;
protected EventPublisher eventPublisher;
private Subscriber<? super Channel> newChannelSub;
private ReadProducer<R> readProducer;
private boolean raiseErrorOnInputSubscription;
private boolean connectionEmitted;
protected AbstractConnectionToChannelBridge(String thisHandlerName, ConnectionEventListener eventListener,
EventPublisher eventPublisher) {
super(thisHandlerName);
if (null == eventListener) {
throw new IllegalArgumentException("Event listener can not be null.");
}
if (null == eventPublisher) {
throw new IllegalArgumentException("Event publisher can not be null.");
}
this.eventListener = eventListener;
this.eventPublisher = eventPublisher;
eventListenerAttributeKey = null;
eventPublisherAttributeKey = null;
}
protected AbstractConnectionToChannelBridge(String thisHandlerName,
AttributeKey<ConnectionEventListener> eventListenerAttributeKey,
AttributeKey<EventPublisher> eventPublisherAttributeKey) {
super(thisHandlerName);
this.eventListenerAttributeKey = eventListenerAttributeKey;
this.eventPublisherAttributeKey = eventPublisherAttributeKey;
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
if (null == eventListener && null == eventPublisher) {
eventListener = ctx.channel().attr(eventListenerAttributeKey).get();
eventPublisher = ctx.channel().attr(eventPublisherAttributeKey).get();
}
if (null == eventPublisher) {
logger.log(Level.SEVERE, "No Event publisher bound to the channel, closing channel.");
ctx.channel().close();
return;
}
if (eventPublisher.publishingEnabled() && null == eventListener) {
logger.log(Level.SEVERE, "No Event listener bound to the channel and publising is enabled, closing channel.");
ctx.channel().close();
return;
}
ctx.pipeline().addFirst(new BytesInspector(eventPublisher, eventListener));
super.handlerAdded(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (!connectionEmitted && isValidToEmit(newChannelSub)) {
emitNewConnection(ctx.channel());
connectionEmitted = true;
}
super.channelInactive(ctx);
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
if (isValidToEmitToReadSubscriber(readProducer)) {
/*If the subscriber is still active, then it expects data but the channel is closed.*/
readProducer.sendOnError(CLOSED_CHANNEL_EXCEPTION);
}
super.channelUnregistered(ctx);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof EmitConnectionEvent) {
if (!connectionEmitted) {
emitNewConnection(ctx.channel());
connectionEmitted = true;
}
} else if (evt instanceof ConnectionCreationFailedEvent) {
if (isValidToEmit(newChannelSub)) {
newChannelSub.onError(((ConnectionCreationFailedEvent)evt).getThrowable());
}
} else if (evt instanceof ChannelSubscriberEvent) {
@SuppressWarnings("unchecked")
final ChannelSubscriberEvent<R, W> channelSubscriberEvent = (ChannelSubscriberEvent<R, W>) evt;
newConnectionSubscriber(channelSubscriberEvent);
} else if (evt instanceof ConnectionInputSubscriberEvent) {
@SuppressWarnings("unchecked")
ConnectionInputSubscriberEvent<R, W> event = (ConnectionInputSubscriberEvent<R, W>) evt;
newConnectionInputSubscriber(ctx.channel(), event.getSubscriber(), false);
} else if (evt instanceof ConnectionInputSubscriberResetEvent) {
resetConnectionInputSubscriber();
} else if (evt instanceof ConnectionInputSubscriberReplaceEvent) {
@SuppressWarnings("unchecked")
ConnectionInputSubscriberReplaceEvent<R, W> event = (ConnectionInputSubscriberReplaceEvent<R, W>) evt;
replaceConnectionInputSubscriber(ctx.channel(), event);
}
super.userEventTriggered(ctx, evt);
}
@SuppressWarnings("unchecked")
@Override
public void newMessage(ChannelHandlerContext ctx, Object msg) {
if (isValidToEmitToReadSubscriber(readProducer)) {
try {
readProducer.sendOnNext((R) msg);
} catch (ClassCastException e) {
ReferenceCountUtil.release(msg); // Since, this was not sent to the subscriber, release the msg.
readProducer.sendOnError(e);
}
} else {
logger.log(Level.WARNING, "Data received on channel, but no subscriber registered. Discarding data. Message class: "
+ msg.getClass().getName() + ", channel: " + ctx.channel());
ReferenceCountUtil.release(msg); // No consumer of the message, so discard.
}
}
@Override
public boolean shouldReadMore(ChannelHandlerContext ctx) {
return null != readProducer && readProducer.shouldReadMore(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (!connectionEmitted && isValidToEmit(newChannelSub)) {
newChannelSub.onError(cause);
} else if (isValidToEmitToReadSubscriber(readProducer)) {
readProducer.sendOnError(cause);
} else {
logger.log(Level.INFO, "Exception in the pipeline and none of the subscribers are active.", cause);
}
}
protected static boolean isValidToEmit(Subscriber<?> subscriber) {
return null != subscriber && !subscriber.isUnsubscribed();
}
private static boolean isValidToEmitToReadSubscriber(ReadProducer<?> readProducer) {
return null != readProducer && !readProducer.subscriber.isUnsubscribed();
}
protected boolean connectionInputSubscriberExists(Channel channel) {
assert channel.eventLoop().inEventLoop();
return null != readProducer && null != readProducer.subscriber && !readProducer.subscriber.isUnsubscribed();
}
protected void onNewReadSubscriber(Subscriber<? super R> subscriber) {
// NOOP
}
protected final void checkEagerSubscriptionIfConfigured(Channel channel) {
if (channel.config().isAutoRead() && null == readProducer) {
// If the channel is set to auto-read and there is no eager subscription then, we should raise errors
// when a subscriber arrives.
raiseErrorOnInputSubscription = true;
final Subscriber<? super R> discardAll = ConnectionInputSubscriberEvent.discardAllInput()
.getSubscriber();
final ReadProducer<R> producer = new ReadProducer<>(discardAll, channel);
discardAll.setProducer(producer);
readProducer = producer;
}
}
protected final Subscriber<? super Channel> getNewChannelSub() {
return newChannelSub;
}
private void emitNewConnection(Channel channel) {
if (isValidToEmit(newChannelSub)) {
try {
newChannelSub.onNext(channel);
connectionEmitted = true;
checkEagerSubscriptionIfConfigured(channel);
newChannelSub.onCompleted();
} catch (Exception e) {
logger.log(Level.SEVERE, "Error emitting a new connection. Closing this channel.", e);
channel.close();
}
} else {
channel.close(); // Closing the connection if not sent to a subscriber.
}
}
private void resetConnectionInputSubscriber() {
final Subscriber<? super R> connInputSub = null == readProducer? null : readProducer.subscriber;
if (isValidToEmit(connInputSub)) {
connInputSub.onCompleted();
}
raiseErrorOnInputSubscription = false;
readProducer = null; // A subsequent event should set it to the desired subscriber.
}
private void newConnectionInputSubscriber(final Channel channel, final Subscriber<? super R> subscriber,
boolean replace) {
final Subscriber<? super R> connInputSub = null == readProducer ? null : readProducer.subscriber;
if (isValidToEmit(connInputSub)) {
if (!replace) {
/*Allow only once concurrent input subscriber but allow concatenated subscribers*/
subscriber.onError(ONLY_ONE_CONN_INPUT_SUB_ALLOWED);
} else {
setNewReadProducer(channel, subscriber);
connInputSub.onCompleted();
}
} else if (raiseErrorOnInputSubscription) {
subscriber.onError(LAZY_CONN_INPUT_SUB);
} else {
setNewReadProducer(channel, subscriber);
}
}
private void setNewReadProducer(Channel channel, Subscriber<? super R> subscriber) {
final ReadProducer<R> producer = new ReadProducer<>(subscriber, channel);
subscriber.setProducer(producer);
onNewReadSubscriber(subscriber);
readProducer = producer;
}
private void replaceConnectionInputSubscriber(Channel channel, ConnectionInputSubscriberReplaceEvent<R, W> event) {
ConnectionInputSubscriberEvent<R, W> newSubEvent = event.getNewSubEvent();
newConnectionInputSubscriber(channel, newSubEvent.getSubscriber(),
true);
}
private void newConnectionSubscriber(ChannelSubscriberEvent<R, W> event) {
if (null == newChannelSub) {
newChannelSub = event.getSubscriber();
} else {
event.getSubscriber().onError(ONLY_ONE_CONN_SUB_ALLOWED);
}
}
/*Visible for testing*/ static final class ReadProducer<T> extends RequestReadIfRequiredEvent implements Producer {
@SuppressWarnings("rawtypes")
private static final AtomicLongFieldUpdater<ReadProducer> REQUEST_UPDATER =
AtomicLongFieldUpdater.newUpdater(ReadProducer.class, "requested");/*Updater for requested*/
private volatile long requested; // Updated by REQUEST_UPDATER, required to be volatile.
private final Subscriber<? super T> subscriber;
private final Channel channel;
/*Visible for testing*/ ReadProducer(Subscriber<? super T> subscriber, Channel channel) {
this.subscriber = subscriber;
this.channel = channel;
}
@Override
public void request(long n) {
if (Long.MAX_VALUE != requested) {
if (Long.MAX_VALUE == n) {
// Now turning off backpressure
REQUEST_UPDATER.set(this, Long.MAX_VALUE);
} else {
// add n to field but check for overflow
while (true) {
final long current = requested;
long next = current + n;
// check for overflow
if (next < 0) {
next = Long.MAX_VALUE;
}
if (REQUEST_UPDATER.compareAndSet(this, current, next)) {
break;
}
}
}
}
if (!channel.config().isAutoRead()) {
channel.pipeline().fireUserEventTriggered(this);
}
}
public void sendOnError(Throwable throwable) {
subscriber.onError(throwable);
}
public void sendOnComplete() {
subscriber.onCompleted();
}
public void sendOnNext(T nextItem) {
if (requested > 0) {
if (REQUEST_UPDATER.get(this) != Long.MAX_VALUE) {
REQUEST_UPDATER.decrementAndGet(this);
}
subscriber.onNext(nextItem);
} else {
subscriber.onError(new MissingBackpressureException(
"Received more data on the channel than demanded by the subscriber."));
}
}
@Override
protected boolean shouldReadMore(ChannelHandlerContext ctx) {
return !subscriber.isUnsubscribed() && REQUEST_UPDATER.get(this) > 0;
}
/*Visible for testing*/long getRequested() {
return requested;
}
@Override
public String toString() {
return "ReadProducer{" + "requested=" + requested + '}';
}
}
}

View file

@ -0,0 +1,72 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.buffer.ByteBufAllocator;
import io.netty.util.internal.TypeParameterMatcher;
import rx.annotations.Beta;
import java.util.List;
/**
* A transformer to be used for modifying the type of objects written on a {@link Connection}.
*
* <h2>Why is this required?</h2>
*
* The type of an object can usually be transformed using {@code Observable.map()}, however, while writing on a
* {@link Connection}, typically one requires to allocate buffers. Although a {@code Connection} provides a way to
* retrieve the {@link ByteBufAllocator} via the {@code Channel}, allocating buffers from outside the eventloop will
* lead to buffer bloats as the allocators will typically use thread-local buffer pools. <p>
*
* This transformer is always invoked from within the eventloop and hence does not have buffer bloating issues, even
* when transformations happen outside the eventloop.
*
* @param <T> Source type.
* @param <TT> Target type.
*/
@Beta
public abstract class AllocatingTransformer<T, TT> {
private final TypeParameterMatcher matcher;
protected AllocatingTransformer() {
matcher = TypeParameterMatcher.find(this, AllocatingTransformer.class, "T");
}
/**
* Asserts whether the passed message can be transformed using this transformer.
*
* @param msg Message to transform.
*
* @return {@code true} if the message can be transformed.
*/
protected boolean acceptMessage(Object msg) {
return matcher.match(msg);
}
/**
* Transforms the passed message and adds the output to the returned list.
*
* @param toTransform Message to transform.
* @param allocator Allocating for allocating buffers, if required.
*
* @return Output of the transformation.
*/
public abstract List<TT> transform(T toTransform, ByteBufAllocator allocator);
}

View file

@ -0,0 +1,40 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
/**
* An event to register a custom transformer of data written on a channel.
*
* @param <T> Source type for the transformer.
* @param <TT> Target type for the transformer.
*/
public final class AppendTransformerEvent<T, TT> {
private final AllocatingTransformer<T, TT> transformer;
public AppendTransformerEvent(AllocatingTransformer<T, TT> transformer) {
if (null == transformer) {
throw new NullPointerException("Transformer can not be null.");
}
this.transformer = transformer;
}
public AllocatingTransformer<T, TT> getTransformer() {
return transformer;
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.util.ReferenceCountUtil;
import rx.Observable.Operator;
import rx.Subscriber;
class AutoReleaseOperator<T> implements Operator<T, T> {
@Override
public Subscriber<? super T> call(final Subscriber<? super T> subscriber) {
return new Subscriber<T>(subscriber) {
@Override
public void onCompleted() {
subscriber.onCompleted();
}
@Override
public void onError(Throwable e) {
subscriber.onError(e);
}
@Override
public void onNext(T t) {
try {
subscriber.onNext(t);
} finally {
ReferenceCountUtil.release(t);
}
}
};
}
}

View file

@ -0,0 +1,710 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.internal.RecyclableArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import rx.Observable;
import rx.Scheduler;
import rx.Subscriber;
import rx.functions.Action0;
import rx.schedulers.Schedulers;
import rx.subscriptions.Subscriptions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
public abstract class BackpressureManagingHandler extends ChannelDuplexHandler {
private static final Logger logger = Logger.getLogger(BackpressureManagingHandler.class.getName());
/*Visible for testing*/ enum State {
ReadRequested,
Reading,
Buffering,
DrainingBuffer,
Stopped,
}
private RecyclableArrayList buffer;
private int currentBufferIndex;
private State currentState = State.Buffering; /*Buffer unless explicitly asked to read*/
private boolean continueDraining;
private final BytesWriteInterceptor bytesWriteInterceptor;
protected BackpressureManagingHandler(String thisHandlerName) {
bytesWriteInterceptor = new BytesWriteInterceptor(thisHandlerName);
}
@SuppressWarnings("fallthrough")
@Override
public final void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (State.Stopped != currentState && !shouldReadMore(ctx)) {
currentState = State.Buffering;
}
switch (currentState) {
case ReadRequested:
currentState = State.Reading;
case Reading:
newMessage(ctx, msg);
break;
case Buffering:
case DrainingBuffer:
if (null == buffer) {
buffer = RecyclableArrayList.newInstance();
}
buffer.add(msg);
break;
case Stopped:
logger.log(Level.WARNING, "Message read after handler removed, discarding the same. Message class: "
+ msg.getClass().getName());
ReferenceCountUtil.release(msg);
break;
}
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
ctx.pipeline().addFirst(bytesWriteInterceptor);
currentState = State.Buffering;
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
/*On shut down, all the handlers are removed from the pipeline, so we don't need to explicitly remove the
additional handlers added in handlerAdded()*/
currentState = State.Stopped;
if (null != buffer) {
if (!buffer.isEmpty()) {
for (Object item : buffer) {
ReferenceCountUtil.release(item);
}
}
buffer.recycle();
buffer = null;
}
}
@Override
public final void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
switch (currentState) {
case ReadRequested:
/*Nothing read from the last request, forward to read() and let it take the decision on what to do.*/
break;
case Reading:
/*
* After read completion, move to Buffering, unless an explicit read is issued, which moves to an
* appropriate state.
*/
currentState = State.Buffering;
break;
case Buffering:
/*Keep buffering, unless the buffer drains and more items are requested*/
break;
case DrainingBuffer:
/*Keep draining, unless the buffer drains and more items are requested*/
break;
case Stopped:
break;
}
ctx.fireChannelReadComplete();
if (!ctx.channel().config().isAutoRead() && shouldReadMore(ctx)) {
read(ctx);
}
}
@Override
public final void read(ChannelHandlerContext ctx) throws Exception {
switch (currentState) {
case ReadRequested:
/*Nothing read since last request, but requested more, so push the demand upstream.*/
ctx.read();
break;
case Reading:
/*
* We are already reading data and the read has not completed as that would move the state to buffering.
* So, ignore this read, or otherwise, read is requested on the channel, unnecessarily.
*/
break;
case Buffering:
/*
* We were buffering and now a read was requested, so start draining the buffer.
*/
currentState = State.DrainingBuffer;
continueDraining = true;
/*
* Looping here to drain, instead of having it done via readComplete -> read -> readComplete loop to reduce
* call stack depth. Otherwise, the stackdepth is proportional to number of items in the buffer and hence
* for large buffers will overflow stack.
*/
while (continueDraining && null != buffer && currentBufferIndex < buffer.size()) {
Object nextItem = buffer.get(currentBufferIndex++);
newMessage(ctx, nextItem); /*Send the next message.*/
/*
* If there is more read demand then that should come as part of read complete or later as another
* read (this method) invocation. */
continueDraining = false;
channelReadComplete(ctx);
}
if (continueDraining) {
if (null != buffer) {
/*Outstanding read demand and buffer is empty, so recycle the buffer and pass the read upstream.*/
recycleBuffer();
}
/*
* Since, continueDraining is true and we have broken out of the drain loop, it means that there are no
* items in the buffer and there is more read demand. Switch to read requested and send the read demand
* downstream.
*/
currentState = State.ReadRequested;
ctx.read();
} else {
/*
* There is no more demand, so set the state to buffering and so another read invocation can start
* draining.
*/
currentState = State.Buffering;
/*If buffer is empty, then recycle.*/
if (null != buffer && currentBufferIndex >= buffer.size()) {
recycleBuffer();
}
}
break;
case DrainingBuffer:
/*Already draining buffer, so break the call stack and let the caller keep draining.*/
continueDraining = true;
break;
case Stopped:
/*Invalid, pass it downstream.*/
ctx.read();
break;
}
}
/**
* Intercepts a write on the channel. The following message types are handled:
*
* <ul>
<li>String: If the pipeline is not configured to write a String, this converts the string to a {@link io.netty.buffer.ByteBuf} and
then writes it on the channel.</li>
<li>byte[]: If the pipeline is not configured to write a byte[], this converts the byte[] to a {@link io.netty.buffer.ByteBuf} and
then writes it on the channel.</li>
<li>Observable: Subscribes to the {@link Observable} and writes all items, requesting the next item if and only if
the channel is writable as indicated by {@link Channel#isWritable()}</li>
</ul>
*
* @param ctx Channel handler context.
* @param msg Message to write.
* @param promise Promise for the completion of write.
*
* @throws Exception If there is an error handling this write.
*/
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof Observable) {
@SuppressWarnings("rawtypes")
Observable observable = (Observable) msg; /*One can write heterogeneous objects on a channel.*/
final WriteStreamSubscriber subscriber = bytesWriteInterceptor.newSubscriber(ctx, promise);
subscriber.subscribeTo(observable);
} else {
ctx.write(msg, promise);
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof RequestReadIfRequiredEvent) {
RequestReadIfRequiredEvent requestReadIfRequiredEvent = (RequestReadIfRequiredEvent) evt;
if (requestReadIfRequiredEvent.shouldReadMore(ctx)) {
read(ctx);
}
}
super.userEventTriggered(ctx, evt);
}
protected abstract void newMessage(ChannelHandlerContext ctx, Object msg);
protected abstract boolean shouldReadMore(ChannelHandlerContext ctx);
/*Visible for testing*/ RecyclableArrayList getBuffer() {
return buffer;
}
/*Visible for testing*/ int getCurrentBufferIndex() {
return currentBufferIndex;
}
/*Visible for testing*/ State getCurrentState() {
return currentState;
}
private void recycleBuffer() {
buffer.recycle();
currentBufferIndex = 0;
buffer = null;
}
protected static abstract class RequestReadIfRequiredEvent {
protected abstract boolean shouldReadMore(ChannelHandlerContext ctx);
}
/**
* This handler inspects write to see if a write made it to {@link BytesWriteInterceptor} inline with a write call.
* The reasons why a write would not make it to the channel, would be:
* <ul>
<li>If there is a handler in the pipeline that runs in a different group.</li>
<li>If there is a handler that collects many items to produce a single item.</li>
</ul>
*
* When a write did not reach the {@link BytesWriteInterceptor}, no request for more items will be generated and
* we could get into a deadlock where a handler is waiting for more items (collect case) but no more items arrive as
* no more request is generated. In order to avoid this deadlock, this handler will detect the situation and
* trigger more request in this case.
*
* Why a separate handler?
*
* This needs to be different than {@link BytesWriteInterceptor} as we need it immediately after
* {@link BackpressureManagingHandler} so that no other handler eats a write and {@link BytesWriteInterceptor} is
* always the first handler in the pipeline to be right before the channel and hence maintain proper demand.
*/
static final class WriteInspector extends ChannelDuplexHandler {
private final BytesWriteInterceptor bytesWriteInterceptor;
WriteInspector(BytesWriteInterceptor bytesWriteInterceptor) {
this.bytesWriteInterceptor = bytesWriteInterceptor;
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
/*Both these handlers always run in the same executor, so it's safe to access this variable.*/
bytesWriteInterceptor.messageReceived = false; /*reset flag for this write*/
ctx.write(msg, promise);
if (!bytesWriteInterceptor.messageReceived) {
bytesWriteInterceptor.requestMoreIfWritable(ctx.channel());
}
}
}
/**
* Regulates write->request more->write process on the channel.
*
* Why is this a separate handler?
* The sole purpose of this handler is to request more items from each of the Observable streams producing items to
* write. It is important to request more items only when the current item is written on the channel i.e. added to
* the ChannelOutboundBuffer. If we request more from outside the pipeline (from WriteStreamSubscriber.onNext())
* then it may so happen that the onNext is not from within this eventloop and hence instead of being written to
* the channel, is added to the task queue of the EventLoop. Requesting more items in such a case, would mean we
* keep adding the writes to the eventloop queue and not on the channel buffer. This would mean that the channel
* writability would not truly indicate the buffer.
*/
/*Visible for testing*/ static final class BytesWriteInterceptor extends ChannelDuplexHandler implements Runnable {
/*Visible for testing*/ static final String WRITE_INSPECTOR_HANDLER_NAME = "write-inspector";
/*Visible for testing*/ static final int MAX_PER_SUBSCRIBER_REQUEST = 64;
/*
* Since, unsubscribes can happen on a different thread, this has to be thread-safe.
*/
private final ConcurrentLinkedQueue<WriteStreamSubscriber> subscribers = new ConcurrentLinkedQueue<>();
private final String parentHandlerName;
/* This should always be access from the eventloop and can be used to manage state before and after a write to
* see if a write started from {@link WriteInspector} made it to this handler.
*/
private boolean messageReceived;
/**
* The intent here is to equally divide the request to all subscribers but do not put a hard-bound on whether
* the subscribers are actually adhering to the limit (by not throwing MissingBackpressureException). This keeps
* the request distribution simple and still give opprotunities for subscribers to optimize (increase the limit)
* if there is a signal that the consumption is slower than the producer.
*
* Worst case of this scheme is request-1 per subscriber which happens when there are as many subscribers as
* the max limit.
*/
private int perSubscriberMaxRequest = MAX_PER_SUBSCRIBER_REQUEST;
private Channel channel;
private boolean removeTaskScheduled; // Guarded by this
BytesWriteInterceptor(String parentHandlerName) {
this.parentHandlerName = parentHandlerName;
}
@Override
public void write(ChannelHandlerContext ctx, final Object msg, ChannelPromise promise) throws Exception {
ctx.write(msg, promise);
messageReceived = true;
requestMoreIfWritable(ctx.channel());
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
channel = ctx.channel();
WriteInspector writeInspector = new WriteInspector(this);
ChannelHandler parent = ctx.pipeline().get(parentHandlerName);
if (null != parent) {
ctx.pipeline().addBefore(parentHandlerName, WRITE_INSPECTOR_HANDLER_NAME, writeInspector);
}
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
if (ctx.channel().isWritable()) {
requestMoreIfWritable(ctx.channel());
}
super.channelWritabilityChanged(ctx);
}
public WriteStreamSubscriber newSubscriber(final ChannelHandlerContext ctx, ChannelPromise promise) {
int currentSubCount = subscribers.size();
recalculateMaxPerSubscriber(currentSubCount, currentSubCount + 1);
final WriteStreamSubscriber sub = new WriteStreamSubscriber(ctx, promise, perSubscriberMaxRequest);
sub.add(Subscriptions.create(new Action0() {
@Override
public void call() {
boolean _schedule;
/*Schedule the task once as the task runs through and removes all unsubscribed subscribers*/
synchronized (BytesWriteInterceptor.this) {
_schedule = !removeTaskScheduled;
removeTaskScheduled = true;
}
if (_schedule) {
ctx.channel().eventLoop().execute(BytesWriteInterceptor.this);
}
}
}));
subscribers.add(sub);
return sub;
}
/*Visible for testing*/List<WriteStreamSubscriber> getSubscribers() {
return Collections.unmodifiableList(new ArrayList<>(subscribers));
}
private void requestMoreIfWritable(Channel channel) {
assert channel.eventLoop().inEventLoop();
for (WriteStreamSubscriber subscriber: subscribers) {
if (!subscriber.isUnsubscribed() && channel.isWritable()) {
subscriber.requestMoreIfNeeded(perSubscriberMaxRequest);
}
}
}
@Override
public void run() {
synchronized (this) {
removeTaskScheduled = false;
}
int oldSubCount = subscribers.size();
for (Iterator<WriteStreamSubscriber> iterator = subscribers.iterator(); iterator.hasNext(); ) {
WriteStreamSubscriber subscriber = iterator.next();
if (subscriber.isUnsubscribed()) {
iterator.remove();
}
}
int newSubCount = subscribers.size();
recalculateMaxPerSubscriber(oldSubCount, newSubCount);
}
/**
* Called from within the eventloop, whenever the subscriber queue is modified. This modifies the per subscriber
* request limit by equally distributing the demand. Minimum demand to any subscriber is 1.
*/
private void recalculateMaxPerSubscriber(int oldSubCount, int newSubCount) {
assert channel.eventLoop().inEventLoop();
perSubscriberMaxRequest = newSubCount == 0 || oldSubCount == 0
? MAX_PER_SUBSCRIBER_REQUEST
: perSubscriberMaxRequest * oldSubCount / newSubCount;
perSubscriberMaxRequest = Math.max(1, perSubscriberMaxRequest);
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "Channel " + channel +
" modifying per subscriber max request. Old subscribers count " + oldSubCount +
" new subscribers count " + newSubCount +
" new Value {} " + perSubscriberMaxRequest);
}
}
}
/**
* Backpressure enabled subscriber to an Observable written on this channel. This connects the promise for writing
* the Observable to all the promises created per write (per onNext).
*/
/*Visible for testing*/static class WriteStreamSubscriber extends Subscriber<Object> {
private final ChannelHandlerContext ctx;
private final ChannelPromise overarchingWritePromise;
private final int initialRequest;
private long maxBufferSize;
private long pending; /*Guarded by guard*/
private long lowWaterMark;
private final Object guard = new Object();
private boolean isDone; /*Guarded by guard*/
private Scheduler.Worker writeWorker; /*Guarded by guard*/
private boolean atleastOneWriteEnqueued; /*Guarded by guard*/
private int enqueued; /*Guarded by guard*/
private boolean isPromiseCompletedOnWriteComplete; /*Guarded by guard. Only transition should be false->true*/
private int listeningTo;
/*Visible for testing*/ WriteStreamSubscriber(ChannelHandlerContext ctx, ChannelPromise promise,
int initialRequest) {
this.ctx = ctx;
overarchingWritePromise = promise;
this.initialRequest = initialRequest;
promise.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isCancelled()) {
unsubscribe(); /*Unsubscribe from source if the promise is cancelled.*/
}
}
});
}
@Override
public void onStart() {
requestMoreIfNeeded(initialRequest);
}
@Override
public void onCompleted() {
onTermination(null);
}
@Override
public void onError(Throwable e) {
onTermination(e);
}
@Override
public void onNext(Object nextItem) {
final boolean enqueue;
boolean inEL = ctx.channel().eventLoop().inEventLoop();
synchronized (guard) {
pending--;
if (null == writeWorker) {
if (!inEL) {
atleastOneWriteEnqueued = true;
}
if (atleastOneWriteEnqueued) {
writeWorker = Schedulers.computation().createWorker();
}
}
enqueue = null != writeWorker && (inEL || enqueued > 0);
if (enqueue) {
enqueued++;
}
}
final ChannelFuture channelFuture = enqueue ? enqueueWrite(nextItem) : ctx.write(nextItem);
synchronized (guard) {
listeningTo++;
}
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (overarchingWritePromise.isDone()) {
/*
* Overarching promise will be done if and only if there was an error or all futures have
* completed. In both cases, this callback is useless, hence return from here.
* IOW, if we are here, it can be two cases:
*
* - There has already been a write that has failed. So, the promise is done with failure.
* - There was a write that arrived after termination of the Observable.
*
* Two above isn't possible as per Rx contract.
* One above is possible but is not of any consequence w.r.t this listener as this listener does
* not give callbacks to specific writes
*/
return;
}
boolean _isPromiseCompletedOnWriteComplete;
/*
* The intent here is to NOT give listener callbacks via promise completion within the sync block.
* So, a co-ordination b/w the thread sending Observable terminal event and thread sending write
* completion event is required.
* The only work to be done in the Observable terminal event thread is to whether the
* overarchingWritePromise is to be completed or not.
* The errors are not buffered, so the overarchingWritePromise is completed in this callback w/o
* knowing whether any more writes will arive or not.
* This co-oridantion is done via the flag isPromiseCompletedOnWriteComplete
*/
synchronized (guard) {
listeningTo--;
if (0 == listeningTo && isDone) {
/*
* If the listening count is 0 and no more items will arrive, this thread wins the race of
* completing the overarchingWritePromise
*/
isPromiseCompletedOnWriteComplete = true;
}
_isPromiseCompletedOnWriteComplete = isPromiseCompletedOnWriteComplete;
}
/*
* Exceptions are not buffered but completion is only sent when there are no more items to be
* received for write.
*/
if (!future.isSuccess()) {
overarchingWritePromise.tryFailure(future.cause());
/*
* Unsubscribe this subscriber when write fails as we are completing the promise which is
* attached to the listener of the write results.
*/
unsubscribe();
} else if (_isPromiseCompletedOnWriteComplete) { /*Once set to true, never goes back to false.*/
/*Complete only when no more items will arrive and all writes are completed*/
overarchingWritePromise.trySuccess();
}
}
});
}
private ChannelFuture enqueueWrite(final Object nextItem) {
final ChannelPromise toReturn = ctx.channel().newPromise();
writeWorker.schedule(new Action0() {
@Override
public void call() {
ctx.write(nextItem, toReturn);
synchronized (guard) {
enqueued--;
}
}
});
return toReturn;
}
private void onTermination(Throwable throwableIfAny) {
int _listeningTo;
boolean _shouldCompletePromise;
final boolean enqueueFlush;
/*
* The intent here is to NOT give listener callbacks via promise completion within the sync block.
* So, a co-ordination b/w the thread sending Observable terminal event and thread sending write
* completion event is required.
* The only work to be done in the Observable terminal event thread is to whether the
* overarchingWritePromise is to be completed or not.
* The errors are not buffered, so the overarchingWritePromise is completed in this callback w/o
* knowing whether any more writes will arive or not.
* This co-oridantion is done via the flag isPromiseCompletedOnWriteComplete
*/
synchronized (guard) {
enqueueFlush = atleastOneWriteEnqueued;
isDone = true;
_listeningTo = listeningTo;
/*
* Flag to indicate whether the write complete thread won the race and will complete the
* overarchingWritePromise
*/
_shouldCompletePromise = 0 == _listeningTo && !isPromiseCompletedOnWriteComplete;
}
if (enqueueFlush) {
writeWorker.schedule(new Action0() {
@Override
public void call() {
ctx.flush();
}
});
}
if (null != throwableIfAny) {
overarchingWritePromise.tryFailure(throwableIfAny);
} else {
if (_shouldCompletePromise) {
overarchingWritePromise.trySuccess();
}
}
}
/**
* Signals this subscriber to request more data from upstream, optionally modifying the max buffer size or max
* requests upstream. This will request more either if the new buffer size is greater than existing or pending
* items from upstream are less than the low water mark (which is half the max size).
*
* @param newMaxBufferSize New max buffer size, ignored if it is the same as existing.
*/
/*Visible for testing*/void requestMoreIfNeeded(long newMaxBufferSize) {
long toRequest = 0;
synchronized (guard) {
if (newMaxBufferSize > maxBufferSize) {
// Applicable only when request up is not triggered by pending < lowWaterMark.
toRequest = newMaxBufferSize - maxBufferSize;
}
maxBufferSize = newMaxBufferSize;
lowWaterMark = maxBufferSize / 2;
if (pending < lowWaterMark) {
// Intentionally overwrites the existing toRequest as this includes all required changes.
toRequest = maxBufferSize - pending;
}
pending += toRequest;
}
if (toRequest > 0) {
request(toRequest);
}
}
@SuppressWarnings({"rawtypes", "unchecked"})
public void subscribeTo(Observable observable) {
observable.subscribe(this); /*Need safe subscription as this is the subscriber and not a sub passed in*/
}
}
}

View file

@ -0,0 +1,99 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.FileRegion;
import io.reactivex.netty.channel.events.ConnectionEventListener;
import io.reactivex.netty.events.EventPublisher;
import java.util.logging.Level;
import java.util.logging.Logger;
public class BytesInspector extends ChannelDuplexHandler {
private static final Logger logger = Logger.getLogger(BytesInspector.class.getName());
private final ConnectionEventListener eventListener;
private final EventPublisher eventPublisher;
public BytesInspector(EventPublisher eventPublisher, ConnectionEventListener eventListener) {
this.eventPublisher = eventPublisher;
this.eventListener = eventListener;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
if (ByteBuf.class.isAssignableFrom(msg.getClass())) {
publishBytesRead((ByteBuf) msg);
} else if (ByteBufHolder.class.isAssignableFrom(msg.getClass())) {
ByteBufHolder holder = (ByteBufHolder) msg;
publishBytesRead(holder.content());
}
} catch (Exception e) {
logger.log(Level.WARNING, "Failed to publish bytes read metrics event. This does *not* stop the pipeline processing.", e);
} finally {
super.channelRead(ctx, msg);
}
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
try {
if (ByteBuf.class.isAssignableFrom(msg.getClass())) {
publishBytesWritten(((ByteBuf) msg).readableBytes(), promise);
} else if (ByteBufHolder.class.isAssignableFrom(msg.getClass())) {
publishBytesWritten(((ByteBufHolder)msg).content().readableBytes(), promise);
} else if (FileRegion.class.isAssignableFrom(msg.getClass())) {
publishBytesWritten(((FileRegion) msg).count(), promise);
}
} catch (Exception e) {
logger.log(Level.WARNING, "Failed to publish bytes write metrics event. This does *not* stop the pipeline processing.", e);
} finally {
super.write(ctx, msg, promise);
}
}
@SuppressWarnings("unchecked")
protected void publishBytesWritten(final long bytesToWrite, ChannelPromise promise) {
if (bytesToWrite <= 0) {
return;
}
if (eventPublisher.publishingEnabled()) {
promise.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
eventListener.onByteWritten(bytesToWrite);
}
});
}
}
@SuppressWarnings("unchecked")
protected void publishBytesRead(ByteBuf byteBuf) {
if (null != byteBuf) {
eventListener.onByteRead(byteBuf.readableBytes());
}
}
}

View file

@ -0,0 +1,285 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.channel.Channel;
import io.netty.channel.FileRegion;
import io.netty.util.AttributeKey;
import rx.Observable;
import rx.functions.Func1;
/**
* A list of user initiated operations that can be done on a channel.
*
* @param <W> Type of data that can be written on the associated channel.
*/
public interface ChannelOperations<W> {
/**
* Flush selector that always returns true.
*/
Func1<String, Boolean> FLUSH_ON_EACH_STRING = new Func1<String, Boolean>() {
@Override
public Boolean call(String next) {
return true;
}
};
/**
* Flush selector that always returns true.
*/
Func1<byte[], Boolean> FLUSH_ON_EACH_BYTES = new Func1<byte[], Boolean>() {
@Override
public Boolean call(byte[] next) {
return true;
}
};
/**
* Flush selector that always returns true.
*/
Func1<FileRegion, Boolean> FLUSH_ON_EACH_FILE_REGION = new Func1<FileRegion, Boolean>() {
@Override
public Boolean call(FileRegion next) {
return true;
}
};
AttributeKey<Boolean> FLUSH_ONLY_ON_READ_COMPLETE =
AttributeKey.valueOf("_rxnetyy-flush-only-on-read-complete");
/**
* On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel.
*
* <h2>Flush.</h2>
*
* All writes will be flushed on completion of the passed {@code Observable}
*
* @param msgs Stream of messages to write.
*
* @return {@link Observable} representing the result of this write. Every subscription to this {@link Observable}
* will replay the write on the channel.
*/
Observable<Void> write(Observable<W> msgs);
/**
* On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel
* and flushes the channel, everytime, {@code flushSelector} returns {@code true} . Any writes issued before
* subscribing, will also be flushed. However, the returned {@link Observable} will not capture the result of those
* writes, i.e. if the other writes, fail and this write does not, the returned {@link Observable} will not fail.
*
* @param msgs Message stream to write.
* @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. Channel is
* flushed, iff this function returns, {@code true}.
*
* @return An {@link Observable} representing the result of this write. Every
* subscription to this {@link Observable} will write the passed messages and flush all pending writes, when the
* {@code flushSelector} returns {@code true}
*/
Observable<Void> write(Observable<W> msgs, Func1<W, Boolean> flushSelector);
/**
* On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel
* and flushes the channel, on every write. Any writes issued before subscribing, will also be flushed. However, the
* returned {@link Observable} will not capture the result of those writes, i.e. if the other writes, fail and this
* write does not, the returned {@link Observable} will not fail.
*
* @param msgs Message stream to write.
*
* @return An {@link Observable} representing the result of this write. Every
* subscription to this {@link Observable} will write the passed messages and flush all pending writes, on every
* write.
*/
Observable<Void> writeAndFlushOnEach(Observable<W> msgs);
/**
* On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel.
*
* <h2>Flush.</h2>
*
* All writes will be flushed on completion of the passed {@code Observable}
*
* @param msgs Stream of messages to write.
*
* @return {@link Observable} representing the result of this write. Every subscription to this {@link Observable}
* will replay the write on the channel.
*/
Observable<Void> writeString(Observable<String> msgs);
/**
* On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel
* and flushes the channel, everytime, {@code flushSelector} returns {@code true} . Any writes issued before
* subscribing, will also be flushed. However, the returned {@link Observable} will not capture the result of those
* writes, i.e. if the other writes, fail and this write does not, the returned {@link Observable} will not fail.
*
* @param msgs Message stream to write.
* @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. Channel is
* flushed, iff this function returns, {@code true}.
*
* @return An {@link Observable} representing the result of this write. Every
* subscription to this {@link Observable} will write the passed messages and flush all pending writes, when the
* {@code flushSelector} returns {@code true}
*/
Observable<Void> writeString(Observable<String> msgs, Func1<String, Boolean> flushSelector);
/**
* On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel
* and flushes the channel, on every write. Any writes issued before subscribing, will also be flushed. However, the
* returned {@link Observable} will not capture the result of those writes, i.e. if the other writes, fail and this
* write does not, the returned {@link Observable} will not fail.
*
* @param msgs Message stream to write.
*
* @return An {@link Observable} representing the result of this write. Every
* subscription to this {@link Observable} will write the passed messages and flush all pending writes, on every
* write.
*/
Observable<Void> writeStringAndFlushOnEach(Observable<String> msgs);
/**
* On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel.
*
* <h2>Flush.</h2>
*
* All writes will be flushed on completion of the passed {@code Observable}
*
* @param msgs Stream of messages to write.
*
* @return {@link Observable} representing the result of this write. Every subscription to this {@link Observable}
* will replay the write on the channel.
*/
Observable<Void> writeBytes(Observable<byte[]> msgs);
/**
* On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel
* and flushes the channel, everytime, {@code flushSelector} returns {@code true} . Any writes issued before
* subscribing, will also be flushed. However, the returned {@link Observable} will not capture the result of those
* writes, i.e. if the other writes, fail and this write does not, the returned {@link Observable} will not fail.
*
* @param msgs Message stream to write.
* @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. Channel is
* flushed, iff this function returns, {@code true}.
*
* @return An {@link Observable} representing the result of this write. Every
* subscription to this {@link Observable} will write the passed messages and flush all pending writes, when the
* {@code flushSelector} returns {@code true}
*/
Observable<Void> writeBytes(Observable<byte[]> msgs, Func1<byte[], Boolean> flushSelector);
/**
* On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel
* and flushes the channel, on every write. Any writes issued before subscribing, will also be flushed. However, the
* returned {@link Observable} will not capture the result of those writes, i.e. if the other writes, fail and this
* write does not, the returned {@link Observable} will not fail.
*
* @param msgs Message stream to write.
*
* @return An {@link Observable} representing the result of this write. Every
* subscription to this {@link Observable} will write the passed messages and flush all pending writes, on every
* write.
*/
Observable<Void> writeBytesAndFlushOnEach(Observable<byte[]> msgs);
/**
* On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel.
*
* <h2>Flush.</h2>
*
* All writes will be flushed on completion of the passed {@code Observable}
*
* @param msgs Stream of messages to write.
*
* @return {@link Observable} representing the result of this write. Every subscription to this {@link Observable}
* will replay the write on the channel.
*/
Observable<Void> writeFileRegion(Observable<FileRegion> msgs);
/**
* On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel
* and flushes the channel, everytime, {@code flushSelector} returns {@code true} . Any writes issued before
* subscribing, will also be flushed. However, the returned {@link Observable} will not capture the result of those
* writes, i.e. if the other writes, fail and this write does not, the returned {@link Observable} will not fail.
*
* @param msgs Message stream to write.
* @param flushSelector A {@link Func1} which is invoked for every item emitted from {@code msgs}. Channel is
* flushed, iff this function returns, {@code true}.
*
* @return An {@link Observable} representing the result of this write. Every
* subscription to this {@link Observable} will write the passed messages and flush all pending writes, when the
* {@code flushSelector} returns {@code true}
*/
Observable<Void> writeFileRegion(Observable<FileRegion> msgs, Func1<FileRegion, Boolean> flushSelector);
/**
* On subscription of the returned {@link Observable}, writes the passed message stream on the underneath channel
* and flushes the channel, on every write. Any writes issued before subscribing, will also be flushed. However, the
* returned {@link Observable} will not capture the result of those writes, i.e. if the other writes, fail and this
* write does not, the returned {@link Observable} will not fail.
*
* @param msgs Message stream to write.
*
* @return An {@link Observable} representing the result of this write. Every
* subscription to this {@link Observable} will write the passed messages and flush all pending writes, on every
* write.
*/
Observable<Void> writeFileRegionAndFlushOnEach(Observable<FileRegion> msgs);
/**
* Modifies the underneath channel to enable writing new type of objects that will be transformed using the passed
* {@code transformer}
*
* @param transformer Transformer to transform objects written to the channel.
*
* @param <WW> The target type of the transformer.
*
* @return A new instance of {@code ChannelOperations} that accepts the transformed type to write.
*/
<WW> ChannelOperations<WW> transformWrite(AllocatingTransformer<WW, W> transformer);
/**
* Flushes any pending writes on this connection by calling {@link Channel#flush()}. This can be used for
* implementing any custom flusing strategies that otherwise can not be implemented by methods like
* {@link #write(Observable, Func1)}.
*/
void flush();
/**
* Flushes any pending writes and closes the connection. Same as calling {@code close(true)}
*
* @return {@link Observable} representing the result of close.
*/
Observable<Void> close();
/**
* Closes this channel after flushing all pending writes.
*
* @return {@link Observable} representing the result of close and flush.
*/
Observable<Void> close(boolean flush);
/**
* Closes the connection immediately. Same as calling {@link #close()} and subscribing to the returned
* {@code Observable}
*/
void closeNow();
/**
* Returns an {@link Observable} that completes when this connection is closed.
*
* @return An {@link Observable} that completes when this connection is closed.
*/
Observable<Void> closeListener();
}

View file

@ -0,0 +1,45 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.channel.Channel;
import rx.Subscriber;
/**
* An event to communicate the subscriber of a new channel created by {@link AbstractConnectionToChannelBridge}.
*
* <h2>Connection reuse</h2>
*
* For cases, where the {@link Connection} is pooled, reuse should be indicated explicitly via
* {@link ConnectionInputSubscriberResetEvent}. There can be multiple {@link ConnectionInputSubscriberResetEvent}s
* sent to the same channel and hence the same instance of {@link AbstractConnectionToChannelBridge}.
*
* @param <R> Type read from the connection held by the event.
* @param <W> Type written to the connection held by the event.
*/
public class ChannelSubscriberEvent<R, W> {
private final Subscriber<? super Channel> subscriber;
public ChannelSubscriberEvent(Subscriber<? super Channel> subscriber) {
this.subscriber = subscriber;
}
public Subscriber<? super Channel> getSubscriber() {
return subscriber;
}
}

View file

@ -0,0 +1,316 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.util.AttributeKey;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.EventExecutorGroup;
import rx.Observable;
import rx.Observable.Transformer;
import rx.functions.Action1;
import rx.functions.Func1;
/**
* An abstraction over netty's channel providing Rx APIs.
*
* <h2>Reading data</h2>
*
* Unless, {@link ChannelOption#AUTO_READ} is set to {@code true} on the underneath channel, data will be read from the
* connection if and only if there is a subscription to the input stream returned by {@link #getInput()}.
* In case, the input data is not required to be consumed, one should call {@link #ignoreInput()}, otherwise, data will
* never be read from the channel.
*
* @param <R> Type of object that is read from this connection.
* @param <W> Type of object that is written to this connection.
*/
public abstract class Connection<R, W> implements ChannelOperations<W> {
public static final AttributeKey<Connection> CONNECTION_ATTRIBUTE_KEY = AttributeKey.valueOf("rx-netty-conn-attr");
private final Channel nettyChannel;
private final ContentSource<R> contentSource;
protected final MarkAwarePipeline markAwarePipeline;
protected Connection(final Channel nettyChannel) {
if (null == nettyChannel) {
throw new IllegalArgumentException("Channel can not be null");
}
this.nettyChannel = nettyChannel;
markAwarePipeline = new MarkAwarePipeline(nettyChannel.pipeline());
contentSource = new ContentSource<>(nettyChannel, ConnectionInputSubscriberEvent::new);
}
protected Connection(Connection<R, W> toCopy) {
nettyChannel = toCopy.nettyChannel;
markAwarePipeline = toCopy.markAwarePipeline;
contentSource = toCopy.contentSource;
}
protected Connection(Connection<?, ?> toCopy, ContentSource<R> contentSource) {
nettyChannel = toCopy.nettyChannel;
markAwarePipeline = toCopy.markAwarePipeline;
this.contentSource = contentSource;
}
/**
* Returns a stream of data that is read from the connection.
*
* Unless, {@link ChannelOption#AUTO_READ} is set to {@code true}, the content will only be read from the
* underneath channel, if there is a subscriber to the input.
* In case, input is not required to be read, call {@link #ignoreInput()}
*
* @return The stream of data that is read from the connection.
*/
public ContentSource<R> getInput() {
return contentSource;
}
/**
* Ignores all input on this connection.
*
* Unless, {@link ChannelOption#AUTO_READ} is set to {@code true}, the content will only be read from the
* underneath channel, if there is a subscriber to the input. So, upon recieving this connection, either one should
* call this method or eventually subscribe to the stream returned by {@link #getInput()}
*
* @return An {@link Observable}, subscription to which will discard the input. This {@code Observable} will
* error/complete when the input errors/completes and unsubscription from here will unsubscribe from the content.
*/
public Observable<Void> ignoreInput() {
return getInput().map(new Func1<R, Void>() {
@Override
public Void call(R r) {
ReferenceCountUtil.release(r);
return null;
}
}).ignoreElements();
}
/**
* Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified handler is added at
* the first position of the pipeline as specified by {@link ChannelPipeline#addFirst(String, ChannelHandler)}
*
* <em>For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be
* more convenient.</em>
*
* @param name Name of the handler.
* @param handler Handler instance to add.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> addChannelHandlerFirst(String name, ChannelHandler handler);
/**
* Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified handler is added at
* the first position of the pipeline as specified by
* {@link ChannelPipeline#addFirst(EventExecutorGroup, String, ChannelHandler)}
*
* <em>For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be
* more convenient.</em>
*
* @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} methods
* @param name the name of the handler to append
* @param handler Handler instance to add.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> addChannelHandlerFirst(EventExecutorGroup group, String name,
ChannelHandler handler);
/**
* Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified handler is added at
* the last position of the pipeline as specified by {@link ChannelPipeline#addLast(String, ChannelHandler)}
*
* <em>For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be
* more convenient.</em>
*
* @param name Name of the handler.
* @param handler Handler instance to add.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> addChannelHandlerLast(String name, ChannelHandler handler);
/**
* Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified handler is added at
* the last position of the pipeline as specified by
* {@link ChannelPipeline#addLast(EventExecutorGroup, String, ChannelHandler)}
*
* <em>For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be
* more convenient.</em>
*
* @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} methods
* @param name the name of the handler to append
* @param handler Handler instance to add.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> addChannelHandlerLast(EventExecutorGroup group, String name,
ChannelHandler handler);
/**
* Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified
* handler is added before an existing handler with the passed {@code baseName} in the pipeline as specified by
* {@link ChannelPipeline#addBefore(String, String, ChannelHandler)}
*
* <em>For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be
* more convenient.</em>
*
* @param baseName the name of the existing handler
* @param name Name of the handler.
* @param handler Handler instance to add.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> addChannelHandlerBefore(String baseName, String name,
ChannelHandler handler);
/**
* Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified
* handler is added before an existing handler with the passed {@code baseName} in the pipeline as specified by
* {@link ChannelPipeline#addBefore(EventExecutorGroup, String, String, ChannelHandler)}
*
* <em>For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be
* more convenient.</em>
*
* @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler}
* methods
* @param baseName the name of the existing handler
* @param name the name of the handler to append
* @param handler Handler instance to add.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> addChannelHandlerBefore(EventExecutorGroup group, String baseName,
String name, ChannelHandler handler);
/**
* Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified
* handler is added after an existing handler with the passed {@code baseName} in the pipeline as specified by
* {@link ChannelPipeline#addAfter(String, String, ChannelHandler)}
*
* <em>For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be
* more convenient.</em>
*
* @param baseName the name of the existing handler
* @param name Name of the handler.
* @param handler Handler instance to add.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> addChannelHandlerAfter(String baseName, String name,
ChannelHandler handler);
/**
* Adds a {@link ChannelHandler} to {@link ChannelPipeline} for this connection. The specified
* handler is added after an existing handler with the passed {@code baseName} in the pipeline as specified by
* {@link ChannelPipeline#addAfter(EventExecutorGroup, String, String, ChannelHandler)}
*
* <em>For better flexibility of pipeline modification, the method {@link #pipelineConfigurator(Action1)} will be
* more convenient.</em>
*
* @param group the {@link EventExecutorGroup} which will be used to execute the {@link ChannelHandler} methods
* @param baseName the name of the existing handler
* @param name the name of the handler to append
* @param handler Handler instance to add.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> addChannelHandlerAfter(EventExecutorGroup group, String baseName,
String name, ChannelHandler handler);
/**
* Configures the {@link ChannelPipeline} for this channel, using the passed {@code pipelineConfigurator}.
*
* @param pipelineConfigurator Action to configure {@link ChannelPipeline}.
*
* @return {@code this}.
*/
public abstract <RR, WW> Connection<RR, WW> pipelineConfigurator(Action1<ChannelPipeline> pipelineConfigurator);
/**
* Transforms this connection's input stream using the passed {@code transformer} to create a new
* {@code Connection} instance.
*
* @param transformer Transformer to transform the input stream.
*
* @param <RR> New type of the input stream.
*
* @return A new connection instance with the transformed read stream.
*/
public abstract <RR> Connection<RR, W> transformRead(Transformer<R, RR> transformer);
/**
* Transforms this connection to enable writing a different object type.
*
* @param transformer Transformer to transform objects written to the channel.
*
* @param <WW> New object types to be written to the connection.
*
* @return A new connection instance with the new write type.
*/
public abstract <WW> Connection<R, WW> transformWrite(AllocatingTransformer<WW, W> transformer);
/**
* Returns the {@link MarkAwarePipeline} for this connection, changes to which can be reverted at any point in time.
*/
public MarkAwarePipeline getResettableChannelPipeline() {
return markAwarePipeline;
}
/**
* Returns the {@link ChannelPipeline} for this connection.
*
* @return {@link ChannelPipeline} for this connection.
*/
public ChannelPipeline getChannelPipeline() {
return nettyChannel.pipeline();
}
/**
* Returns the underlying netty {@link Channel} for this connection.
*
* <h2>Why unsafe?</h2>
*
* It is advisable to use this connection abstraction for all interactions with the channel, however, advanced users
* may find directly using the netty channel useful in some cases.
*
* @return The underlying netty {@link Channel} for this connection.
*/
public Channel unsafeNettyChannel() {
return nettyChannel;
}
/*
* In order to make sure that the connection is correctly initialized, the listener needs to be added post
* constructor. Otherwise, there is a race-condition of the channel closed before the connection is completely
* created and the Connection.close() call on channel close can access the Connection object which isn't
* constructed completely. IOW, "this" escapes from the constructor if the listener is added in the constructor.
*/
protected void connectCloseToChannelClose() {
nettyChannel.closeFuture()
.addListener((ChannelFutureListener) future -> {
closeNow(); // Close this connection when the channel is closed.
});
nettyChannel.attr(CONNECTION_ATTRIBUTE_KEY).set(this);
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandler;
/**
* An event to indicate to {@link AbstractConnectionToChannelBridge} that the subscriber as published by
* {@link ChannelSubscriberEvent} should be informed of a connection creation failure, instead of a new connection.
*
* <h2>Why do we need this?</h2>
*
* Since, emitting a connection can include a handshake for protocols such as TLS/SSL, it is not so that a new
* {@link io.reactivex.netty.channel.Connection} should be emitted as soon as the channel is active (i.e. inside
* {@link ChannelInboundHandler#channelActive(ChannelHandlerContext)}).
* For this reason, this event leaves it to the pipeline or any other entity outside to decide, when is the rite time to
* determine that a connection for a channel has failed creation.
*/
public final class ConnectionCreationFailedEvent {
private final Throwable throwable;
public ConnectionCreationFailedEvent(Throwable throwable) {
this.throwable = throwable;
}
public Throwable getThrowable() {
return throwable;
}
}

View file

@ -0,0 +1,240 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.FileRegion;
import io.netty.util.concurrent.EventExecutorGroup;
import io.reactivex.netty.channel.events.ConnectionEventListener;
import io.reactivex.netty.events.EventAttributeKeys;
import io.reactivex.netty.events.EventPublisher;
import rx.Observable;
import rx.Observable.Transformer;
import rx.functions.Action1;
import rx.functions.Func1;
/**
* An implementation of {@link Connection} delegating all {@link ChannelOperations} methods to
* {@link DefaultChannelOperations}.
*/
public final class ConnectionImpl<R, W> extends Connection<R, W> {
private final ChannelOperations<W> delegate;
private ConnectionImpl(Channel nettyChannel, ConnectionEventListener eventListener, EventPublisher eventPublisher) {
super(nettyChannel);
delegate = new DefaultChannelOperations<>(nettyChannel, eventListener, eventPublisher);
}
private ConnectionImpl(Channel nettyChannel, ChannelOperations<W> delegate) {
super(nettyChannel);
this.delegate = delegate;
}
private ConnectionImpl(ConnectionImpl<?, ?> toCopy, ContentSource<R> contentSource, ChannelOperations<W> delegate) {
super(toCopy, contentSource);
this.delegate = delegate;
}
@Override
public Observable<Void> write(Observable<W> msgs) {
return delegate.write(msgs);
}
@Override
public Observable<Void> write(Observable<W> msgs, Func1<W, Boolean> flushSelector) {
return delegate.write(msgs, flushSelector);
}
@Override
public Observable<Void> writeAndFlushOnEach(Observable<W> msgs) {
return delegate.writeAndFlushOnEach(msgs);
}
@Override
public Observable<Void> writeString(Observable<String> msgs) {
return delegate.writeString(msgs);
}
@Override
public Observable<Void> writeString(Observable<String> msgs, Func1<String, Boolean> flushSelector) {
return delegate.writeString(msgs, flushSelector);
}
@Override
public Observable<Void> writeStringAndFlushOnEach(Observable<String> msgs) {
return delegate.writeStringAndFlushOnEach(msgs);
}
@Override
public Observable<Void> writeBytes(Observable<byte[]> msgs) {
return delegate.writeBytes(msgs);
}
@Override
public Observable<Void> writeBytes(Observable<byte[]> msgs,
Func1<byte[], Boolean> flushSelector) {
return delegate.writeBytes(msgs, flushSelector);
}
@Override
public Observable<Void> writeBytesAndFlushOnEach(Observable<byte[]> msgs) {
return delegate.writeBytesAndFlushOnEach(msgs);
}
@Override
public Observable<Void> writeFileRegion(Observable<FileRegion> msgs) {
return delegate.writeFileRegion(msgs);
}
@Override
public Observable<Void> writeFileRegion(Observable<FileRegion> msgs,
Func1<FileRegion, Boolean> flushSelector) {
return delegate.writeFileRegion(msgs, flushSelector);
}
@Override
public Observable<Void> writeFileRegionAndFlushOnEach(Observable<FileRegion> msgs) {
return delegate.writeFileRegionAndFlushOnEach(msgs);
}
@Override
public void flush() {
delegate.flush();
}
@Override
public Observable<Void> close() {
return delegate.close();
}
@Override
public Observable<Void> close(boolean flush) {
return delegate.close(flush);
}
@Override
public void closeNow() {
delegate.closeNow();
}
@Override
public Observable<Void> closeListener() {
return delegate.closeListener();
}
public static <R, W> ConnectionImpl<R, W> fromChannel(Channel nettyChannel) {
EventPublisher ep = nettyChannel.attr(EventAttributeKeys.EVENT_PUBLISHER).get();
if (null == ep) {
throw new IllegalArgumentException("No event publisher set in the channel.");
}
ConnectionEventListener l = null;
if (ep.publishingEnabled()) {
l = nettyChannel.attr(EventAttributeKeys.CONNECTION_EVENT_LISTENER).get();
if (null == l) {
throw new IllegalArgumentException("No event listener set in the channel.");
}
}
final ConnectionImpl<R, W> toReturn = new ConnectionImpl<>(nettyChannel, l, ep);
toReturn.connectCloseToChannelClose();
return toReturn;
}
/*Visible for testing*/static <R, W> ConnectionImpl<R, W> create(Channel nettyChannel,
ChannelOperations<W> delegate) {
final ConnectionImpl<R, W> toReturn = new ConnectionImpl<>(nettyChannel, delegate);
toReturn.connectCloseToChannelClose();
return toReturn;
}
@Override
public <RR, WW> Connection<RR, WW> addChannelHandlerFirst(String name, ChannelHandler handler) {
getResettableChannelPipeline().markIfNotYetMarked().addFirst(name, handler);
return cast();
}
@Override
public <RR, WW> Connection<RR, WW> addChannelHandlerFirst(EventExecutorGroup group, String name,
ChannelHandler handler) {
getResettableChannelPipeline().markIfNotYetMarked().addFirst(group, name, handler);
return cast();
}
@Override
public <RR, WW> Connection<RR, WW> addChannelHandlerLast(String name, ChannelHandler handler) {
getResettableChannelPipeline().markIfNotYetMarked().addLast(name, handler);
return cast();
}
@Override
public <RR, WW> Connection<RR, WW> addChannelHandlerLast(EventExecutorGroup group, String name,
ChannelHandler handler) {
getResettableChannelPipeline().markIfNotYetMarked().addLast(group, name, handler);
return cast();
}
@Override
public <RR, WW> Connection<RR, WW> addChannelHandlerBefore(String baseName, String name, ChannelHandler handler) {
getResettableChannelPipeline().markIfNotYetMarked().addBefore(baseName, name, handler);
return cast();
}
@Override
public <RR, WW> Connection<RR, WW> addChannelHandlerBefore(EventExecutorGroup group, String baseName, String name,
ChannelHandler handler) {
getResettableChannelPipeline().markIfNotYetMarked().addBefore(group, baseName, name, handler);
return cast();
}
@Override
public <RR, WW> Connection<RR, WW> addChannelHandlerAfter(String baseName, String name, ChannelHandler handler) {
getResettableChannelPipeline().markIfNotYetMarked().addAfter(baseName, name, handler);
return cast();
}
@Override
public <RR, WW> Connection<RR, WW> addChannelHandlerAfter(EventExecutorGroup group, String baseName, String name,
ChannelHandler handler) {
getResettableChannelPipeline().markIfNotYetMarked().addAfter(group, baseName, name, handler);
return cast();
}
@Override
public <RR, WW> Connection<RR, WW> pipelineConfigurator(Action1<ChannelPipeline> pipelineConfigurator) {
pipelineConfigurator.call(getResettableChannelPipeline().markIfNotYetMarked());
return cast();
}
@Override
public <RR> Connection<RR, W> transformRead(Transformer<R, RR> transformer) {
return new ConnectionImpl<>(this, getInput().transform(transformer), delegate);
}
@Override
public <WW> Connection<R, WW> transformWrite(AllocatingTransformer<WW, W> transformer) {
return new ConnectionImpl<>(this, getInput(), delegate.transformWrite(transformer));
}
@SuppressWarnings("unchecked")
protected <RR, WW> Connection<RR, WW> cast() {
return (Connection<RR, WW>) this;
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.util.ReferenceCountUtil;
import rx.Subscriber;
import rx.functions.Action1;
import rx.observers.Subscribers;
/**
* An event to communicate the subscriber of the associated connection input stream created by
* {@link AbstractConnectionToChannelBridge}.
*
* <h2>Multiple events on the same channel</h2>
*
* Multiple instance of this event can be sent on the same channel, provided that there is a
* {@link ConnectionInputSubscriberResetEvent} between two consecutive {@link ConnectionInputSubscriberEvent}s
*
* @param <R> Type read from the connection held by the event.
* @param <W> Type written to the connection held by the event.
*/
public final class ConnectionInputSubscriberEvent<R, W> {
private final Subscriber<? super R> subscriber;
public ConnectionInputSubscriberEvent(Subscriber<? super R> subscriber) {
if (null == subscriber) {
throw new NullPointerException("Subscriber can not be null");
}
this.subscriber = subscriber;
}
public Subscriber<? super R> getSubscriber() {
return subscriber;
}
public static <II, OO> ConnectionInputSubscriberEvent<II, OO> discardAllInput() {
return new ConnectionInputSubscriberEvent<>(Subscribers.create(new Action1<II>() {
@Override
public void call(II msg) {
ReferenceCountUtil.release(msg);
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
// Empty as we are discarding input anyways.
}
}));
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
/**
* This event is an indication to atomically replace existing connection input subscriber, if any, with another.
*/
public class ConnectionInputSubscriberReplaceEvent<R, W> {
private final ConnectionInputSubscriberEvent<R, W> newSubEvent;
public ConnectionInputSubscriberReplaceEvent(ConnectionInputSubscriberEvent<R, W> newSubEvent) {
this.newSubEvent = newSubEvent;
}
public ConnectionInputSubscriberEvent<R, W> getNewSubEvent() {
return newSubEvent;
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
/**
* This event is an indication that there will be multiple subscribers to the connection input stream. This event
* must be sent as many times as the subscribers to the input. This typically will be the case for client-side
* connections when a channel is pooled and reused.
*/
public interface ConnectionInputSubscriberResetEvent {
}

View file

@ -0,0 +1,44 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import rx.Subscriber;
/**
* An event to communicate the subscriber of a new connection created by {@link AbstractConnectionToChannelBridge}.
*
* <h2>Connection reuse</h2>
*
* For cases, where the {@link Connection} is pooled, reuse should be indicated explicitly via
* {@link ConnectionInputSubscriberResetEvent}. There can be multiple {@link ConnectionInputSubscriberResetEvent}s
* sent to the same channel and hence the same instance of {@link AbstractConnectionToChannelBridge}.
*
* @param <R> Type read from the connection held by the event.
* @param <W> Type written to the connection held by the event.
*/
public class ConnectionSubscriberEvent<R, W> {
private final Subscriber<? super Connection<R, W>> subscriber;
public ConnectionSubscriberEvent(Subscriber<? super Connection<R, W>> subscriber) {
this.subscriber = subscriber;
}
public Subscriber<? super Connection<R, W>> getSubscriber() {
return subscriber;
}
}

View file

@ -0,0 +1,96 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.channel.Channel;
import rx.Observable;
import rx.Subscriber;
import rx.functions.Func1;
/**
* A source for any content/data read from a channel.
*
* <h2>Managing {@link ByteBuf} lifecycle.</h2>
*
* If this source emits {@link ByteBuf} or a {@link ByteBufHolder}, using {@link #autoRelease()} will release the buffer
* after emitting it from this source.
*
* <h2>Replaying content</h2>
*
* Since, the content read from a channel is not re-readable, this also provides a {@link #replayable()} function that
* produces a source which can be subscribed multiple times to replay the same data. This is specially useful if the
* content read from one channel is written on to another with an option to retry.
*
* @param <T>
*/
public final class ContentSource<T> extends Observable<T> {
private ContentSource(final Observable<T> source) {
super(new OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
source.unsafeSubscribe(subscriber);
}
});
}
public ContentSource(final Channel channel, final Func1<Subscriber<? super T>, Object> subscriptionEventFactory) {
super(new OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
channel.pipeline()
.fireUserEventTriggered(subscriptionEventFactory.call(subscriber));
}
});
}
public ContentSource(final Throwable error) {
super(new OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
subscriber.onError(error);
}
});
}
/**
* If this source emits {@link ByteBuf} or {@link ByteBufHolder} then using this operator will release the buffer
* after it is emitted from this source.
*
* @return A new instance of the stream with auto-release enabled.
*/
public Observable<T> autoRelease() {
return this.lift(new AutoReleaseOperator<T>());
}
/**
* This provides a replayable content source that only subscribes once to the actual content and then caches it,
* till {@link DisposableContentSource#dispose()} is called.
*
* @return A new replayable content source.
*/
public DisposableContentSource<T> replayable() {
return DisposableContentSource.createNew(this);
}
public <R> ContentSource<R> transform(Transformer<T, R> transformer) {
return new ContentSource<>(transformer.call(this));
}
}

View file

@ -0,0 +1,368 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.FileRegion;
import io.reactivex.netty.channel.events.ConnectionEventListener;
import io.reactivex.netty.events.Clock;
import io.reactivex.netty.events.EventPublisher;
import java.util.logging.Level;
import java.util.logging.Logger;
import rx.Observable;
import rx.Observable.OnSubscribe;
import rx.Subscriber;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Actions;
import rx.functions.Func1;
import rx.subscriptions.Subscriptions;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import static java.util.concurrent.TimeUnit.*;
/**
* Default implementation for {@link ChannelOperations}.
*
* @param <W> Type of data that can be written on the associated channel.
*/
public class DefaultChannelOperations<W> implements ChannelOperations<W> {
private static final Logger logger = Logger.getLogger(DefaultChannelOperations.class.getName());
/** Field updater for closeIssued. */
@SuppressWarnings("rawtypes")
private static final AtomicIntegerFieldUpdater<DefaultChannelOperations> CLOSE_ISSUED_UPDATER
= AtomicIntegerFieldUpdater.newUpdater(DefaultChannelOperations.class, "closeIssued");
@SuppressWarnings("unused")
private volatile int closeIssued; // updated by the atomic updater, so required to be volatile.
private final Channel nettyChannel;
private final ConnectionEventListener eventListener;
private final EventPublisher eventPublisher;
private final Observable<Void> closeObservable;
private final Observable<Void> flushAndCloseObservable;
private final Func1<W, Boolean> flushOnEachSelector = new Func1<W, Boolean>() {
@Override
public Boolean call(W w) {
return true;
}
};
public DefaultChannelOperations(final Channel nettyChannel, ConnectionEventListener eventListener,
EventPublisher eventPublisher) {
this.nettyChannel = nettyChannel;
this.eventListener = eventListener;
this.eventPublisher = eventPublisher;
closeObservable = Observable.create(new OnSubscribeForClose(nettyChannel));
flushAndCloseObservable = closeObservable.doOnSubscribe(new Action0() {
@Override
public void call() {
flush();
}
});
}
@Override
public Observable<Void> write(final Observable<W> msgs) {
return _write(msgs);
}
@Override
public Observable<Void> write(Observable<W> msgs, final Func1<W, Boolean> flushSelector) {
return _write(msgs, flushSelector);
}
@Override
public Observable<Void> writeAndFlushOnEach(Observable<W> msgs) {
return _write(msgs, flushOnEachSelector);
}
@Override
public Observable<Void> writeString(Observable<String> msgs) {
return _write(msgs);
}
@Override
public Observable<Void> writeString(Observable<String> msgs, Func1<String, Boolean> flushSelector) {
return _write(msgs, flushSelector);
}
@Override
public Observable<Void> writeStringAndFlushOnEach(Observable<String> msgs) {
return writeString(msgs, FLUSH_ON_EACH_STRING);
}
@Override
public Observable<Void> writeBytes(Observable<byte[]> msgs) {
return _write(msgs);
}
@Override
public Observable<Void> writeBytes(Observable<byte[]> msgs, Func1<byte[], Boolean> flushSelector) {
return _write(msgs, flushSelector);
}
@Override
public Observable<Void> writeBytesAndFlushOnEach(Observable<byte[]> msgs) {
return _write(msgs, FLUSH_ON_EACH_BYTES);
}
@Override
public Observable<Void> writeFileRegion(Observable<FileRegion> msgs) {
return _write(msgs);
}
@Override
public Observable<Void> writeFileRegion(Observable<FileRegion> msgs, Func1<FileRegion, Boolean> flushSelector) {
return _write(msgs, flushSelector);
}
@Override
public Observable<Void> writeFileRegionAndFlushOnEach(Observable<FileRegion> msgs) {
return writeFileRegion(msgs, FLUSH_ON_EACH_FILE_REGION);
}
@Override
public <WW> ChannelOperations<WW> transformWrite(AllocatingTransformer<WW, W> transformer) {
nettyChannel.pipeline().fireUserEventTriggered(new AppendTransformerEvent<>(transformer));
return new DefaultChannelOperations<>(nettyChannel, eventListener, eventPublisher);
}
@Override
public void flush() {
if (eventPublisher.publishingEnabled()) {
final long startTimeNanos = Clock.newStartTimeNanos();
eventListener.onFlushStart();
if (nettyChannel.eventLoop().inEventLoop()) {
_flushInEventloop(startTimeNanos);
} else {
nettyChannel.eventLoop()
.execute(new Runnable() {
@Override
public void run() {
_flushInEventloop(startTimeNanos);
}
});
}
} else {
nettyChannel.flush();
}
}
@Override
public Observable<Void> close() {
return close(true);
}
@Override
public Observable<Void> close(boolean flush) {
return flush ? flushAndCloseObservable : closeObservable;
}
@Override
public void closeNow() {
close().subscribe(Actions.empty(), new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
logger.log(Level.SEVERE, "Error closing connection.", throwable);
}
});
}
@Override
public Observable<Void> closeListener() {
return Observable.create(new OnSubscribe<Void>() {
@Override
public void call(final Subscriber<? super Void> subscriber) {
final SubscriberToChannelFutureBridge l = new SubscriberToChannelFutureBridge() {
@Override
protected void doOnSuccess(ChannelFuture future) {
subscriber.onCompleted();
}
@Override
protected void doOnFailure(ChannelFuture future, Throwable cause) {
subscriber.onCompleted();
}
};
l.bridge(nettyChannel.closeFuture(), subscriber);
}
});
}
private <X> Observable<Void> _write(final Observable<X> msgs, Func1<X, Boolean> flushSelector) {
return _write(msgs.lift(new FlushSelectorOperator<>(flushSelector, this)));
}
private void _flushInEventloop(long startTimeNanos) {
assert nettyChannel.eventLoop().inEventLoop();
nettyChannel.flush(); // Flush is sync when from eventloop.
eventListener.onFlushComplete(Clock.onEndNanos(startTimeNanos), NANOSECONDS);
}
private Observable<Void> _write(final Observable<?> msgs) {
return Observable.create(new OnSubscribe<Void>() {
@Override
public void call(final Subscriber<? super Void> subscriber) {
final long startTimeNanos = Clock.newStartTimeNanos();
if (eventPublisher.publishingEnabled()) {
eventListener.onWriteStart();
}
/*
* If a write happens from outside the eventloop, it does not wakeup the selector, till a flush happens.
* In absence of a selector wakeup, this write will be delayed by the selector sleep interval.
* The code below makes sure that the selector is woken up on a write (by executing a task that does
* the write)
*/
if (nettyChannel.eventLoop().inEventLoop()) {
_writeStreamToChannel(subscriber, startTimeNanos);
} else {
nettyChannel.eventLoop()
.execute(new Runnable() {
@Override
public void run() {
_writeStreamToChannel(subscriber, startTimeNanos);
}
});
}
}
private void _writeStreamToChannel(final Subscriber<? super Void> subscriber, final long startTimeNanos) {
final ChannelFuture writeFuture = nettyChannel.write(msgs.doOnCompleted(new Action0() {
@Override
public void call() {
Boolean shdNotFlush = nettyChannel.attr(FLUSH_ONLY_ON_READ_COMPLETE).get();
if (null == shdNotFlush || !shdNotFlush) {
flush();
}
}
}));
subscriber.add(Subscriptions.create(new Action0() {
@Override
public void call() {
writeFuture.cancel(false); // cancel write on unsubscribe.
}
}));
writeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (subscriber.isUnsubscribed()) {
/*short-circuit if subscriber is unsubscribed*/
return;
}
if (future.isSuccess()) {
if (eventPublisher.publishingEnabled()) {
eventListener.onWriteSuccess(Clock.onEndNanos(startTimeNanos), NANOSECONDS);
}
subscriber.onCompleted();
} else {
if (eventPublisher.publishingEnabled()) {
eventListener.onWriteFailed(Clock.onEndNanos(startTimeNanos), NANOSECONDS,
future.cause());
}
subscriber.onError(future.cause());
}
}
});
}
});
}
private class OnSubscribeForClose implements OnSubscribe<Void> {
private final Channel nettyChannel;
public OnSubscribeForClose(Channel nettyChannel) {
this.nettyChannel = nettyChannel;
}
@Override
@SuppressWarnings("unchecked")
public void call(final Subscriber<? super Void> subscriber) {
final long closeStartTimeNanos = Clock.newStartTimeNanos();
final ChannelCloseListener closeListener;
if (CLOSE_ISSUED_UPDATER.compareAndSet(DefaultChannelOperations.this, 0, 1)) {
if (eventPublisher.publishingEnabled()) {
eventListener.onConnectionCloseStart();
}
nettyChannel.close(); // close only once.
closeListener = new ChannelCloseListener(eventListener, eventPublisher, closeStartTimeNanos,
subscriber);
} else {
closeListener = new ChannelCloseListener(subscriber);
}
closeListener.bridge(nettyChannel.closeFuture(), subscriber);
}
private class ChannelCloseListener extends SubscriberToChannelFutureBridge {
private final long closeStartTimeNanos;
private final Subscriber<? super Void> subscriber;
private final ConnectionEventListener eventListener;
private final EventPublisher eventPublisher;
public ChannelCloseListener(ConnectionEventListener eventListener, EventPublisher eventPublisher,
long closeStartTimeNanos, Subscriber<? super Void> subscriber) {
this.eventListener = eventListener;
this.eventPublisher = eventPublisher;
this.closeStartTimeNanos = closeStartTimeNanos;
this.subscriber = subscriber;
}
public ChannelCloseListener(Subscriber<? super Void> subscriber) {
this(null, null, -1, subscriber);
}
@Override
protected void doOnSuccess(ChannelFuture future) {
if (null != eventListener && eventPublisher.publishingEnabled()) {
eventListener.onConnectionCloseSuccess(Clock.onEndNanos(closeStartTimeNanos), NANOSECONDS);
}
if (!subscriber.isUnsubscribed()) {
subscriber.onCompleted();
}
}
@Override
protected void doOnFailure(ChannelFuture future, Throwable cause) {
if (null != eventListener && eventPublisher.publishingEnabled()) {
eventListener.onConnectionCloseFailed(Clock.onEndNanos(closeStartTimeNanos), NANOSECONDS,
future.cause());
}
if (!subscriber.isUnsubscribed()) {
subscriber.onError(future.cause());
}
}
}
}
}

View file

@ -0,0 +1,387 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.util.concurrent.EventExecutorGroup;
import java.util.logging.Level;
import java.util.logging.Logger;
import rx.functions.Action1;
import rx.functions.Func0;
import java.util.LinkedList;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
/**
* An implementation of {@link ChannelPipeline} which is detached from a channel and provides a
* {@link #addToChannel(Channel)} method to be invoked when this pipeline handlers are to be added to an actual channel
* pipeline.
*
* This must NOT be used on an actual channel, it does not support any channel operations. It only supports pipeline
* modification operations.
*/
public class DetachedChannelPipeline {
private static final Logger logger = Logger.getLogger(DetachedChannelPipeline.class.getName());
private final LinkedList<HandlerHolder> holdersInOrder;
private final Action1<ChannelPipeline> nullableTail;
public DetachedChannelPipeline() {
this(null);
}
public DetachedChannelPipeline(final Action1<ChannelPipeline> nullableTail) {
this.nullableTail = nullableTail;
holdersInOrder = new LinkedList<>();
}
private DetachedChannelPipeline(final DetachedChannelPipeline copyFrom,
final Action1<ChannelPipeline> nullableTail) {
this.nullableTail = nullableTail;
holdersInOrder = new LinkedList<>();
synchronized (copyFrom.holdersInOrder) {
for (HandlerHolder handlerHolder : copyFrom.holdersInOrder) {
holdersInOrder.addLast(handlerHolder);
}
}
}
public ChannelInitializer<Channel> getChannelInitializer() {
return new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
synchronized (holdersInOrder) {
unguardedCopyToPipeline(pipeline);
}
}
};
}
public void addToChannel(Channel channel) {
final ChannelPipeline pipeline = channel.pipeline();
synchronized (holdersInOrder) {
unguardedCopyToPipeline(pipeline);
}
}
public DetachedChannelPipeline copy() {
return copy(null);
}
public DetachedChannelPipeline copy(Action1<ChannelPipeline> newTail) {
return new DetachedChannelPipeline(this, newTail);
}
public DetachedChannelPipeline addFirst(String name, Func0<ChannelHandler> handlerFactory) {
return _guardedAddFirst(new HandlerHolder(name, handlerFactory));
}
public DetachedChannelPipeline addFirst(EventExecutorGroup group,
String name, Func0<ChannelHandler> handlerFactory) {
return _guardedAddFirst(new HandlerHolder(name, handlerFactory, group));
}
public DetachedChannelPipeline addLast(String name, Func0<ChannelHandler> handlerFactory) {
return _guardedAddLast(new HandlerHolder(name, handlerFactory));
}
public DetachedChannelPipeline addLast(EventExecutorGroup group, String name, Func0<ChannelHandler> handlerFactory) {
return _guardedAddLast(new HandlerHolder(name, handlerFactory, group));
}
public DetachedChannelPipeline addBefore(String baseName, String name, Func0<ChannelHandler> handlerFactory) {
return _guardedAddBefore(baseName, new HandlerHolder(name, handlerFactory));
}
public DetachedChannelPipeline addBefore(EventExecutorGroup group, String baseName, String name, Func0<ChannelHandler> handlerFactory) {
return _guardedAddBefore(baseName, new HandlerHolder(name, handlerFactory, group));
}
public DetachedChannelPipeline addAfter(String baseName, String name, Func0<ChannelHandler> handlerFactory) {
return _guardedAddAfter(baseName, new HandlerHolder(name, handlerFactory));
}
public DetachedChannelPipeline addAfter(EventExecutorGroup group, String baseName, String name, Func0<ChannelHandler> handlerFactory) {
return _guardedAddAfter(baseName, new HandlerHolder(name, handlerFactory, group));
}
@SafeVarargs
public final DetachedChannelPipeline addFirst(Func0<ChannelHandler>... handlerFactories) {
synchronized (holdersInOrder) {
for (int i = handlerFactories.length - 1; i >= 0; i--) {
Func0<ChannelHandler> handlerFactory = handlerFactories[i];
holdersInOrder.addFirst(new HandlerHolder(handlerFactory));
}
}
return this;
}
@SafeVarargs
public final DetachedChannelPipeline addFirst(EventExecutorGroup group, Func0<ChannelHandler>... handlerFactories) {
synchronized (holdersInOrder) {
for (int i = handlerFactories.length - 1; i >= 0; i--) {
Func0<ChannelHandler> handlerFactory = handlerFactories[i];
holdersInOrder.addFirst(new HandlerHolder(null, handlerFactory, group));
}
}
return this;
}
@SafeVarargs
public final DetachedChannelPipeline addLast(Func0<ChannelHandler>... handlerFactories) {
for (Func0<ChannelHandler> handlerFactory : handlerFactories) {
_guardedAddLast(new HandlerHolder(handlerFactory));
}
return this;
}
@SafeVarargs
public final DetachedChannelPipeline addLast(EventExecutorGroup group, Func0<ChannelHandler>... handlerFactories) {
for (Func0<ChannelHandler> handlerFactory : handlerFactories) {
_guardedAddLast(new HandlerHolder(null, handlerFactory, group));
}
return this;
}
public DetachedChannelPipeline configure(Action1<ChannelPipeline> configurator) {
_guardedAddLast(new HandlerHolder(configurator));
return this;
}
public void copyTo(ChannelPipeline pipeline) {
synchronized (holdersInOrder) {
unguardedCopyToPipeline(pipeline);
}
}
/*Visible for testing*/ LinkedList<HandlerHolder> getHoldersInOrder() {
return holdersInOrder;
}
private void unguardedCopyToPipeline(ChannelPipeline pipeline) { /*To be guarded by lock on holders*/
for (HandlerHolder holder : holdersInOrder) {
if (holder.hasPipelineConfigurator()) {
holder.getPipelineConfigurator().call(pipeline);
continue;
}
if (holder.hasGroup()) {
if (holder.hasName()) {
pipeline.addLast(holder.getGroupIfConfigured(), holder.getNameIfConfigured(),
holder.getHandlerFactoryIfConfigured().call());
} else {
pipeline.addLast(holder.getGroupIfConfigured(), holder.getHandlerFactoryIfConfigured().call());
}
} else if (holder.hasName()) {
pipeline.addLast(holder.getNameIfConfigured(), holder.getHandlerFactoryIfConfigured().call());
} else {
pipeline.addLast(holder.getHandlerFactoryIfConfigured().call());
}
}
if (null != nullableTail) {
nullableTail.call(pipeline); // This is the last handler to be added to the pipeline always.
}
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "Channel pipeline in initializer: " + pipelineToString(pipeline));
}
}
private HandlerHolder unguardedFindHandlerByName(String baseName, boolean leniant) {
for (HandlerHolder handlerHolder : holdersInOrder) {
if (handlerHolder.hasName() && handlerHolder.getNameIfConfigured().equals(baseName)) {
return handlerHolder;
}
}
if (leniant) {
return null;
} else {
throw new NoSuchElementException("No handler with name: " + baseName + " configured in the pipeline.");
}
}
private DetachedChannelPipeline _guardedAddFirst(HandlerHolder toAdd) {
synchronized (holdersInOrder) {
holdersInOrder.addFirst(toAdd);
}
return this;
}
private DetachedChannelPipeline _guardedAddLast(HandlerHolder toAdd) {
synchronized (holdersInOrder) {
holdersInOrder.addLast(toAdd);
}
return this;
}
private DetachedChannelPipeline _guardedAddBefore(String baseName, HandlerHolder toAdd) {
synchronized (holdersInOrder) {
HandlerHolder before = unguardedFindHandlerByName(baseName, false);
final int indexOfBefore = holdersInOrder.indexOf(before);
holdersInOrder.add(indexOfBefore, toAdd);
}
return this;
}
private DetachedChannelPipeline _guardedAddAfter(String baseName, HandlerHolder toAdd) {
synchronized (holdersInOrder) {
HandlerHolder after = unguardedFindHandlerByName(baseName, false);
final int indexOfAfter = holdersInOrder.indexOf(after);
holdersInOrder.add(indexOfAfter + 1, toAdd);
}
return this;
}
private static String pipelineToString(ChannelPipeline pipeline) {
StringBuilder builder = new StringBuilder();
for (Entry<String, ChannelHandler> handlerEntry : pipeline) {
if (builder.length() == 0) {
builder.append("[\n");
} else {
builder.append(" ==> ");
}
builder.append("{ name =>")
.append(handlerEntry.getKey())
.append(", handler => ")
.append(handlerEntry.getValue())
.append("}\n")
;
}
if (builder.length() > 0) {
builder.append("}\n");
}
return builder.toString();
}
/**
* A holder class for holding handler information, required to add handlers to the actual pipeline.
*/
/*Visible for testing*/ static class HandlerHolder {
private final String nameIfConfigured;
private final Func0<ChannelHandler> handlerFactoryIfConfigured;
private final Action1<ChannelPipeline> pipelineConfigurator;
private final EventExecutorGroup groupIfConfigured;
HandlerHolder(Action1<ChannelPipeline> pipelineConfigurator) {
this.pipelineConfigurator = pipelineConfigurator;
nameIfConfigured = null;
handlerFactoryIfConfigured = null;
groupIfConfigured = null;
}
HandlerHolder(Func0<ChannelHandler> handlerFactory) {
this(null, handlerFactory);
}
HandlerHolder(String name, Func0<ChannelHandler> handlerFactory) {
this(name, handlerFactory, null);
}
HandlerHolder(String name, Func0<ChannelHandler> handlerFactory, EventExecutorGroup group) {
nameIfConfigured = name;
handlerFactoryIfConfigured = handlerFactory;
groupIfConfigured = group;
pipelineConfigurator = null;
}
public String getNameIfConfigured() {
return nameIfConfigured;
}
public boolean hasName() {
return null != nameIfConfigured;
}
public Func0<ChannelHandler> getHandlerFactoryIfConfigured() {
return handlerFactoryIfConfigured;
}
public EventExecutorGroup getGroupIfConfigured() {
return groupIfConfigured;
}
public boolean hasGroup() {
return null != groupIfConfigured;
}
public Action1<ChannelPipeline> getPipelineConfigurator() {
return pipelineConfigurator;
}
public boolean hasPipelineConfigurator() {
return null != pipelineConfigurator;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof HandlerHolder)) {
return false;
}
HandlerHolder that = (HandlerHolder) o;
if (groupIfConfigured != null? !groupIfConfigured.equals(that.groupIfConfigured) :
that.groupIfConfigured != null) {
return false;
}
if (handlerFactoryIfConfigured != null?
!handlerFactoryIfConfigured.equals(that.handlerFactoryIfConfigured) :
that.handlerFactoryIfConfigured != null) {
return false;
}
if (nameIfConfigured != null? !nameIfConfigured.equals(that.nameIfConfigured) :
that.nameIfConfigured != null) {
return false;
}
if (pipelineConfigurator != null? !pipelineConfigurator.equals(that.pipelineConfigurator) :
that.pipelineConfigurator != null) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = nameIfConfigured != null? nameIfConfigured.hashCode() : 0;
result = 31 * result + (handlerFactoryIfConfigured != null? handlerFactoryIfConfigured.hashCode() : 0);
result = 31 * result + (pipelineConfigurator != null? pipelineConfigurator.hashCode() : 0);
result = 31 * result + (groupIfConfigured != null? groupIfConfigured.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "HandlerHolder{" + "nameIfConfigured='" + nameIfConfigured + '\'' + ", handlerFactoryIfConfigured=" +
handlerFactoryIfConfigured + ", pipelineConfigurator=" + pipelineConfigurator
+ ", groupIfConfigured=" + groupIfConfigured + '}';
}
}
}

View file

@ -0,0 +1,134 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.util.ReferenceCountUtil;
import rx.Observable;
import rx.Subscriber;
import rx.functions.Action1;
import rx.observables.ConnectableObservable;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Similar to {@link ContentSource} but supports multicast to multiple subscriptions. This source, subscribes upstream
* once and then caches the content, till the time {@link #dispose()} is called.
*
* <h2>Managing {@link ByteBuf} lifecycle.</h2>
*
* If this source emits {@link ByteBuf} or a {@link ByteBufHolder}, using {@link #autoRelease()} will release the buffer
* after emitting it from this source.
*
* Every subscriber to this source must manage it's own lifecycle of the items it receives i.e. the buffers must be
* released by every subscriber post processing.
*
* <h2>Disposing the source</h2>
*
* It is mandatory to call {@link #dispose()} on this source when no more subscriptions are required. Failure to do so,
* will cause a buffer leak as this source, caches the contents till disposed.
*
* Typically, {@link #dispose()} can be called as an {@link Subscriber#unsubscribe()} action.
*
* @param <T> Type of objects emitted by this source.
*/
public final class DisposableContentSource<T> extends Observable<T> {
private final OnSubscribeImpl<T> onSubscribe;
private DisposableContentSource(final OnSubscribeImpl<T> onSubscribe) {
super(onSubscribe);
this.onSubscribe = onSubscribe;
}
/**
* If this source emits {@link ByteBuf} or {@link ByteBufHolder} then using this operator will release the buffer
* after it is emitted from this source.
*
* @return A new instance of the stream with auto-release enabled.
*/
public Observable<T> autoRelease() {
return this.lift(new AutoReleaseOperator<T>());
}
/**
* Disposes this source.
*/
public void dispose() {
if (onSubscribe.disposed.compareAndSet(false, true)) {
for (Object chunk : onSubscribe.chunks) {
ReferenceCountUtil.release(chunk);
}
onSubscribe.chunks.clear();
}
}
static <X> DisposableContentSource<X> createNew(Observable<X> source) {
final ArrayList<X> chunks = new ArrayList<>();
ConnectableObservable<X> replay = source.doOnNext(new Action1<X>() {
@Override
public void call(X x) {
chunks.add(x);
}
}).replay();
return new DisposableContentSource<>(new OnSubscribeImpl<X>(replay, chunks));
}
private static class OnSubscribeImpl<T> implements OnSubscribe<T> {
private final ConnectableObservable<T> source;
private final ArrayList<T> chunks;
private boolean subscribed;
private final AtomicBoolean disposed = new AtomicBoolean();
public OnSubscribeImpl(ConnectableObservable<T> source, ArrayList<T> chunks) {
this.source = source;
this.chunks = chunks;
}
@Override
public void call(Subscriber<? super T> subscriber) {
if (disposed.get()) {
subscriber.onError(new IllegalStateException("Content source is already disposed."));
}
boolean connectNow = false;
synchronized (this) {
if (!subscribed) {
connectNow = true;
subscribed = true;
}
}
source.doOnNext(new Action1<T>() {
@Override
public void call(T msg) {
ReferenceCountUtil.retain(msg);
}
}).unsafeSubscribe(subscriber);
if (connectNow) {
source.connect();
}
}
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandler;
/**
* An event to indicate to {@link AbstractConnectionToChannelBridge} that the channel is ready to emit a new
* {@link io.reactivex.netty.channel.Connection} to the subscriber as published by {@link ChannelSubscriberEvent}
*
* <h2>Why do we need this?</h2>
*
* Since, emitting a connection can include a handshake for protocols such as TLS/SSL, it is not so that a new
* {@link io.reactivex.netty.channel.Connection} should be emitted as soon as the channel is active (i.e. inside
* {@link ChannelInboundHandler#channelActive(ChannelHandlerContext)}).
* For this reason, this event leaves it to the pipeline or any other entity outside to decide, when is the rite time to
* emit a connection.
*/
public final class EmitConnectionEvent {
public static final EmitConnectionEvent INSTANCE = new EmitConnectionEvent();
private EmitConnectionEvent() {
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import rx.Observable.Operator;
import rx.Subscriber;
import rx.functions.Func1;
public class FlushSelectorOperator<T> implements Operator<T, T> {
private final Func1<T, Boolean> flushSelector;
private final ChannelOperations<?> channelOps;
public FlushSelectorOperator(Func1<T, Boolean> flushSelector, ChannelOperations<?> channelOps) {
this.flushSelector = flushSelector;
this.channelOps = channelOps;
}
@Override
public Subscriber<? super T> call(final Subscriber<? super T> subscriber) {
return new Subscriber<T>(subscriber) {
@Override
public void onCompleted() {
subscriber.onCompleted();
}
@Override
public void onError(Throwable e) {
subscriber.onError(e);
}
@Override
public void onNext(T next) {
subscriber.onNext(next);
/*Call the selector _after_ writing an element*/
if (flushSelector.call(next)) {
channelOps.flush();
}
}
};
}
}

View file

@ -0,0 +1,471 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundInvoker;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelProgressivePromise;
import io.netty.channel.ChannelPromise;
import io.netty.util.concurrent.EventExecutorGroup;
import java.net.SocketAddress;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* An implementation of {@link ChannelPipeline} that allows a mark-reset scheme for {@link ChannelHandler}s. This allows
* temporary modifications to the underlying {@link ChannelPipeline} instance for usecases like pooled connections,
* server response upgrades, etc.
*
* <b>This only supports a single mark at a time, although mark-reset-mark cycles can be repeated any number of times.</b>
*
* <h2>Usage:</h2>
*
* To start recording resetable changes, call {@link #mark()} and to reset back to the state before {@link #mark()} was
* called, call {@link #reset()}
*
* <h2>Thread safety</h2>
*
* All operations of {@link ChannelPipeline} are delegated to the passed {@link ChannelPipeline} instance while
* creation. {@link #mark()} and {@link #reset()} uses the same mutex as {@link ChannelPipeline} for synchronization
* across different method calls.
*/
public final class MarkAwarePipeline implements ChannelPipeline {
private boolean marked; // Guarded by this
private final ChannelPipeline delegate;
public MarkAwarePipeline(ChannelPipeline delegate) {
this.delegate = delegate;
}
/**
* Marks this pipeline and record further changes which can be reverted by calling {@link #reset()}
*
* @throws IllegalStateException If this method is called more than once without calling {@link #reset()} in
* between.
*/
public synchronized MarkAwarePipeline mark() {
if (marked) {
throw new IllegalStateException("Pipeline does not support nested marks.");
}
return this;
}
/**
* Marks this pipeline and record further changes which can be reverted by calling {@link #reset()}
*
* @throws IllegalStateException If this method is called more than once without calling {@link #reset()} in
* between.
*/
public synchronized MarkAwarePipeline markIfNotYetMarked() {
if (!marked) {
return mark();
}
return this;
}
/**
* If {@link #mark()} was called before, resets the pipeline to the state it was before calling {@link #mark()}.
* Otherwise, ignores the reset.
*/
public synchronized MarkAwarePipeline reset() {
if (!marked) {
return this; /*If there is no mark, there is nothing to reset.*/
}
marked = false;
return this;
}
public synchronized boolean isMarked() {
return marked;
}
@Override
public ChannelPipeline addFirst(String name, ChannelHandler handler) {
delegate.addFirst(name, handler);
return this;
}
@Override
public ChannelPipeline addFirst(EventExecutorGroup group,
String name, ChannelHandler handler) {
delegate.addFirst(group, name, handler);
return this;
}
@Override
public ChannelPipeline addLast(String name, ChannelHandler handler) {
delegate.addLast(name, handler);
return this;
}
@Override
public ChannelPipeline addLast(EventExecutorGroup group,
String name, ChannelHandler handler) {
delegate.addLast(group, name, handler);
return this;
}
@Override
public ChannelPipeline addBefore(String baseName, String name,
ChannelHandler handler) {
return delegate.addBefore(baseName, name, handler);
}
@Override
public ChannelPipeline addBefore(EventExecutorGroup group,
String baseName, String name,
ChannelHandler handler) {
delegate.addBefore(group, baseName, name, handler);
return this;
}
@Override
public ChannelPipeline addAfter(String baseName, String name,
ChannelHandler handler) {
delegate.addAfter(baseName, name, handler);
return this;
}
@Override
public ChannelPipeline addAfter(EventExecutorGroup group,
String baseName, String name,
ChannelHandler handler) {
delegate.addAfter(group, baseName, name, handler);
return this;
}
@Override
public ChannelPipeline addFirst(ChannelHandler... handlers) {
delegate.addFirst(handlers);
return this;
}
@Override
public ChannelPipeline addFirst(EventExecutorGroup group,
ChannelHandler... handlers) {
delegate.addFirst(group, handlers);
return this;
}
@Override
public ChannelPipeline addLast(ChannelHandler... handlers) {
delegate.addLast(handlers);
return this;
}
@Override
public ChannelPipeline addLast(EventExecutorGroup group,
ChannelHandler... handlers) {
delegate.addLast(group, handlers);
return this;
}
@Override
public ChannelPipeline remove(ChannelHandler handler) {
delegate.remove(handler);
return this;
}
@Override
public ChannelHandler remove(String name) {
return delegate.remove(name);
}
@Override
public <T extends ChannelHandler> T remove(Class<T> handlerType) {
return delegate.remove(handlerType);
}
@Override
public ChannelHandler removeFirst() {
return delegate.removeFirst();
}
@Override
public ChannelHandler removeLast() {
return delegate.removeLast();
}
@Override
public ChannelPipeline replace(ChannelHandler oldHandler,
String newName, ChannelHandler newHandler) {
delegate.replace(oldHandler, newName, newHandler);
return this;
}
@Override
public ChannelHandler replace(String oldName, String newName,
ChannelHandler newHandler) {
return delegate.replace(oldName, newName, newHandler);
}
@Override
public <T extends ChannelHandler> T replace(Class<T> oldHandlerType, String newName,
ChannelHandler newHandler) {
return delegate.replace(oldHandlerType, newName, newHandler);
}
@Override
public ChannelHandler first() {
return delegate.first();
}
@Override
public ChannelHandlerContext firstContext() {
return delegate.firstContext();
}
@Override
public ChannelHandler last() {
return delegate.last();
}
@Override
public ChannelHandlerContext lastContext() {
return delegate.lastContext();
}
@Override
public ChannelHandler get(String name) {
return delegate.get(name);
}
@Override
public <T extends ChannelHandler> T get(Class<T> handlerType) {
return delegate.get(handlerType);
}
@Override
public ChannelHandlerContext context(ChannelHandler handler) {
return delegate.context(handler);
}
@Override
public ChannelHandlerContext context(String name) {
return delegate.context(name);
}
@Override
public ChannelHandlerContext context(Class<? extends ChannelHandler> handlerType) {
return delegate.context(handlerType);
}
@Override
public Channel channel() {
return delegate.channel();
}
@Override
public List<String> names() {
return delegate.names();
}
@Override
public Map<String, ChannelHandler> toMap() {
return delegate.toMap();
}
@Override
public ChannelPipeline fireChannelRegistered() {
delegate.fireChannelRegistered();
return this;
}
@Override
public ChannelPipeline fireChannelUnregistered() {
delegate.fireChannelUnregistered();
return this;
}
@Override
public ChannelPipeline fireChannelActive() {
delegate.fireChannelActive();
return this;
}
@Override
public ChannelPipeline fireChannelInactive() {
delegate.fireChannelInactive();
return this;
}
@Override
public ChannelPipeline fireExceptionCaught(Throwable cause) {
delegate.fireExceptionCaught(cause);
return this;
}
@Override
public ChannelPipeline fireUserEventTriggered(Object event) {
delegate.fireUserEventTriggered(event);
return this;
}
@Override
public ChannelPipeline fireChannelRead(Object msg) {
delegate.fireChannelRead(msg);
return this;
}
@Override
public ChannelPipeline fireChannelReadComplete() {
delegate.fireChannelReadComplete();
return this;
}
@Override
public ChannelPipeline fireChannelWritabilityChanged() {
delegate.fireChannelWritabilityChanged();
return this;
}
@Override
public ChannelFuture bind(SocketAddress localAddress) {
return delegate.bind(localAddress);
}
@Override
public ChannelFuture connect(SocketAddress remoteAddress) {
return delegate.connect(remoteAddress);
}
@Override
public ChannelFuture connect(SocketAddress remoteAddress,
SocketAddress localAddress) {
return delegate.connect(remoteAddress, localAddress);
}
@Override
public ChannelFuture disconnect() {
return delegate.disconnect();
}
@Override
public ChannelFuture close() {
return delegate.close();
}
@Override
public ChannelFuture deregister() {
return delegate.deregister();
}
@Override
public ChannelFuture bind(SocketAddress localAddress,
ChannelPromise promise) {
return delegate.bind(localAddress, promise);
}
@Override
public ChannelFuture connect(SocketAddress remoteAddress,
ChannelPromise promise) {
return delegate.connect(remoteAddress, promise);
}
@Override
public ChannelFuture connect(SocketAddress remoteAddress,
SocketAddress localAddress,
ChannelPromise promise) {
return delegate.connect(remoteAddress, localAddress, promise);
}
@Override
public ChannelFuture disconnect(ChannelPromise promise) {
return delegate.disconnect(promise);
}
@Override
public ChannelFuture close(ChannelPromise promise) {
return delegate.close(promise);
}
@Override
public ChannelFuture deregister(ChannelPromise promise) {
return delegate.deregister(promise);
}
@Override
public ChannelOutboundInvoker read() {
return delegate.read();
}
@Override
public ChannelFuture write(Object msg) {
return delegate.write(msg);
}
@Override
public ChannelFuture write(Object msg, ChannelPromise promise) {
return delegate.write(msg, promise);
}
@Override
public ChannelPipeline flush() {
return delegate.flush();
}
@Override
public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
return delegate.writeAndFlush(msg, promise);
}
@Override
public ChannelFuture writeAndFlush(Object msg) {
return delegate.writeAndFlush(msg);
}
@Override
public ChannelPromise newPromise() {
return delegate.newPromise();
}
@Override
public ChannelProgressivePromise newProgressivePromise() {
return delegate.newProgressivePromise();
}
@Override
public ChannelFuture newSucceededFuture() {
return delegate.newSucceededFuture();
}
@Override
public ChannelFuture newFailedFuture(Throwable cause) {
return delegate.newFailedFuture(cause);
}
@Override
public ChannelPromise voidPromise() {
return delegate.voidPromise();
}
@Override
public Iterator<Entry<String, ChannelHandler>> iterator() {
return delegate.iterator();
}
}

View file

@ -0,0 +1,70 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import rx.Subscriber;
import rx.functions.Action0;
import rx.subscriptions.Subscriptions;
/**
* A bridge to connect a {@link Subscriber} to a {@link ChannelFuture} so that when the {@code subscriber} is
* unsubscribed, the listener will get removed from the {@code future}. Failure to do so for futures that are long
* living, eg: {@link Channel#closeFuture()} will lead to a memory leak where the attached listener will be in the
* listener queue of the future till the channel closes.
*
* In order to bridge the future and subscriber, {@link #bridge(ChannelFuture, Subscriber)} must be called.
*/
public abstract class SubscriberToChannelFutureBridge implements ChannelFutureListener {
@Override
public final void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
doOnSuccess(future);
} else {
doOnFailure(future, future.cause());
}
}
protected abstract void doOnSuccess(ChannelFuture future);
protected abstract void doOnFailure(ChannelFuture future, Throwable cause);
/**
* Bridges the passed subscriber and future, which means the following:
*
* <ul>
<li>Add this listener to the passed future.</li>
<li>Add a callback to the subscriber, such that on unsubscribe this listener is removed from the future.</li>
</ul>
*
* @param future Future to bridge.
* @param subscriber Subscriber to connect to the future.
*/
public void bridge(final ChannelFuture future, Subscriber<?> subscriber) {
future.addListener(this);
subscriber.add(Subscriptions.create(new Action0() {
@Override
public void call() {
future.removeListener(SubscriberToChannelFutureBridge.this);
}
}));
}
}

View file

@ -0,0 +1,98 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.buffer.ByteBufAllocator;
import java.util.LinkedList;
import java.util.List;
/**
* A holder for all transformations that are applied on a channel. Out of the box, it comes with a {@code String} and
* {@code byte[]} transformer to {@code ByteBuf}. Additional transformations can be applied using
* {@link #appendTransformer(AllocatingTransformer)}.
*/
public class WriteTransformations {
private TransformerChain transformers;
public boolean transform(Object msg, ByteBufAllocator allocator, List<Object> out) {
boolean transformed = false;
if (msg instanceof String) {
out.add(allocator.buffer().writeBytes(((String) msg).getBytes()));
transformed = true;
} else if (msg instanceof byte[]) {
out.add(allocator.buffer().writeBytes((byte[]) msg));
transformed = true;
} else if (null != transformers && transformers.acceptMessage(msg)) {
out.addAll(transformers.transform(msg, allocator));
transformed = true;
}
return transformed;
}
public <T, TT> void appendTransformer(AllocatingTransformer<T, TT> transformer) {
transformers = new TransformerChain(transformer, transformers);
}
public void resetTransformations() {
transformers = null;
}
public boolean acceptMessage(Object msg) {
return msg instanceof String || msg instanceof byte[] || null != transformers && transformers.acceptMessage(msg);
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static class TransformerChain extends AllocatingTransformer {
private final AllocatingTransformer start;
private final AllocatingTransformer next;
public TransformerChain(AllocatingTransformer start, AllocatingTransformer next) {
this.start = start;
this.next = next;
}
@Override
public List transform(Object toTransform, ByteBufAllocator allocator) {
if (null == next) {
return start.transform(toTransform, allocator);
}
List transformed = start.transform(toTransform, allocator);
if (transformed.size() == 1) {
return next.transform(transformed.get(0), allocator);
} else {
final LinkedList toReturn = new LinkedList();
for (Object nextItem : transformed) {
toReturn.addAll(next.transform(nextItem, allocator));
}
return toReturn;
}
}
@Override
protected boolean acceptMessage(Object msg) {
return start.acceptMessage(msg);
}
}
}

View file

@ -0,0 +1,78 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import io.netty.util.ReferenceCountUtil;
import io.reactivex.netty.client.ClientConnectionToChannelBridge.ConnectionReuseEvent;
import java.util.List;
/**
* A {@link ChannelHandler} that transforms objects written to this channel. <p>
*
* Any {@code String} or {@code byte[]} written to the channel are converted to {@code ByteBuf} if no other
* {@link AllocatingTransformer} is added that accepts these types.
*
* If the last added {@link AllocatingTransformer} accepts the written message, then invoke all added transformers and
* skip the primitive conversions.
*/
public class WriteTransformer extends MessageToMessageCodec<Object, Object> {
private final WriteTransformations transformations = new WriteTransformations();
@Override
public boolean acceptInboundMessage(Object msg) throws Exception {
return false;
}
@Override
public boolean acceptOutboundMessage(Object msg) throws Exception {
return true;// Always return true and let the encode do the checking as opposed to be done at both places.
}
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
if (!transformations.transform(msg, ctx.alloc(), out)) {
/*
* M2MCodec will release the passed message after encode but we are adding the same object to out.
* So, the message needs to be retained and subsequently released by the next consumer in the pipeline.
*/
out.add(ReferenceCountUtil.retain(msg));
}
}
@Override
protected void decode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
// Never decode (acceptInbound) always returns false.
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof AppendTransformerEvent) {
@SuppressWarnings("rawtypes")
AppendTransformerEvent ate = (AppendTransformerEvent) evt;
transformations.appendTransformer(ate.getTransformer());
} else if(evt instanceof ConnectionReuseEvent) {
transformations.resetTransformations();
}
super.userEventTriggered(ctx, evt);
}
}

View file

@ -0,0 +1,124 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel.events;
import io.reactivex.netty.events.EventListener;
import java.util.concurrent.TimeUnit;
/**
* An event listener for all events releated to a {@link io.reactivex.netty.channel.Connection}
*/
public abstract class ConnectionEventListener implements EventListener {
/**
* Event whenever any bytes are read on any open connection.
*
* @param bytesRead Number of bytes read.
*/
@SuppressWarnings("unused")
public void onByteRead(long bytesRead) { }
/**
* Event whenever any bytes are successfully written on any open connection.
*
* @param bytesWritten Number of bytes written.
*/
@SuppressWarnings("unused")
public void onByteWritten(long bytesWritten) { }
/**
* Event whenever a flush is issued on a connection.
*/
public void onFlushStart() {}
/**
* Event whenever flush completes.
*
* @param duration Duration between flush start and completion.
* @param timeUnit Timeunit for the duration.
*/
@SuppressWarnings("unused")
public void onFlushComplete(long duration, TimeUnit timeUnit) {}
/**
* Event whenever a write is issued on a connection.
*/
public void onWriteStart() {}
/**
* Event whenever data is written successfully on a connection. Use {@link #onByteWritten(long)} to capture number
* of bytes written.
*
* @param duration Duration between write start and completion.
* @param timeUnit Timeunit for the duration.
*/
@SuppressWarnings("unused")
public void onWriteSuccess(long duration, TimeUnit timeUnit) {}
/**
* Event whenever a write failed on a connection.
*
* @param duration Duration between write start and failure.
* @param timeUnit Timeunit for the duration.
* @param throwable Error that caused the failure..
*/
@SuppressWarnings("unused")
public void onWriteFailed(long duration, TimeUnit timeUnit, Throwable throwable) {}
/**
* Event whenever a close of any connection is issued. This event will only be fired when the physical connection
* is closed and not when a pooled connection is closed and put back in the pool.
*/
@SuppressWarnings("unused")
public void onConnectionCloseStart() {}
/**
* Event whenever a close of any connection is successful.
*
* @param duration Duration between close start and completion.
* @param timeUnit Timeunit for the duration.
*/
@SuppressWarnings("unused")
public void onConnectionCloseSuccess(long duration, TimeUnit timeUnit) {}
/**
* Event whenever a connection close failed.
*
* @param duration Duration between close start and failure.
* @param timeUnit Timeunit for the duration.
* @param throwable Error that caused the failure.
*/
@SuppressWarnings("unused")
public void onConnectionCloseFailed(long duration, TimeUnit timeUnit, Throwable throwable) {}
@Override
public void onCustomEvent(Object event) { }
@Override
public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) { }
@Override
public void onCustomEvent(Object event, Throwable throwable) { }
@Override
public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) { }
@Override
public void onCompleted() { }
}

View file

@ -0,0 +1,238 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.channel.events;
import io.reactivex.netty.events.EventListener;
import io.reactivex.netty.events.EventPublisher;
import io.reactivex.netty.events.EventSource;
import io.reactivex.netty.events.ListenersHolder;
import rx.Subscription;
import rx.functions.Action1;
import rx.functions.Action2;
import rx.functions.Action3;
import rx.functions.Action4;
import rx.functions.Action5;
import java.util.concurrent.TimeUnit;
/**
* A publisher which is both {@link EventSource} and {@link EventListener} for connection events.
*
* @param <T> Type of listener to expect.
*/
public final class ConnectionEventPublisher<T extends ConnectionEventListener> extends ConnectionEventListener
implements EventSource<T>, EventPublisher {
private final Action2<T, Long> bytesReadAction = new Action2<T, Long>() {
@Override
public void call(T l, Long bytesRead) {
l.onByteRead(bytesRead);
}
};
private final Action2<T, Long> bytesWrittenAction = new Action2<T, Long>() {
@Override
public void call(T l, Long bytesWritten) {
l.onByteWritten(bytesWritten);
}
};
private final Action1<T> flushStartAction = new Action1<T>() {
@Override
public void call(T l) {
l.onFlushStart();
}
};
private final Action3<T, Long, TimeUnit> flushCompleteAction = new Action3<T, Long, TimeUnit>() {
@Override
public void call(T l, Long duration, TimeUnit timeUnit) {
l.onFlushComplete(duration, timeUnit);
}
};
private final Action1<T> writeStartAction = new Action1<T>() {
@Override
public void call(T l) {
l.onWriteStart();
}
};
private final Action3<T, Long, TimeUnit> writeSuccessAction = new Action3<T, Long, TimeUnit>() {
@Override
public void call(T l, Long duration, TimeUnit timeUnit) {
l.onWriteSuccess(duration, timeUnit);
}
};
private final Action4<T, Long, TimeUnit, Throwable> writeFailedAction =
new Action4<T, Long, TimeUnit, Throwable>() {
@Override
public void call(T l, Long duration, TimeUnit timeUnit, Throwable t) {
l.onWriteFailed(duration, timeUnit, t);
}
};
private final Action1<T> closeStartAction = new Action1<T>() {
@Override
public void call(T l) {
l.onConnectionCloseStart();
}
};
private final Action3<T, Long, TimeUnit> closeSuccessAction = new Action3<T, Long, TimeUnit>() {
@Override
public void call(T l, Long duration, TimeUnit timeUnit) {
l.onConnectionCloseSuccess(duration, timeUnit);
}
};
private final Action4<T, Long, TimeUnit, Throwable> closeFailedAction =
new Action4<T, Long, TimeUnit, Throwable>() {
@Override
public void call(T l, Long duration, TimeUnit timeUnit, Throwable t) {
l.onConnectionCloseFailed(duration, timeUnit, t);
}
};
private final Action2<T, Object> customEventAction = new Action2<T, Object>() {
@Override
public void call(T l, Object event) {
l.onCustomEvent(event);
}
};
private final Action3<T, Throwable, Object> customEventErrorAction = new Action3<T, Throwable, Object>() {
@Override
public void call(T l, Throwable throwable, Object event) {
l.onCustomEvent(event, throwable);
}
};
private final Action4<T, Long, TimeUnit, Object> customEventDurationAction = new Action4<T, Long, TimeUnit, Object>() {
@Override
public void call(T l, Long duration, TimeUnit timeUnit, Object event) {
l.onCustomEvent(event, duration, timeUnit);
}
};
private final Action5<T, Long, TimeUnit, Throwable, Object> customEventDurationErrAction =
new Action5<T, Long, TimeUnit, Throwable, Object>() {
@Override
public void call(T l, Long duration, TimeUnit timeUnit, Throwable throwable, Object event) {
l.onCustomEvent(event, duration, timeUnit, throwable);
}
};
private final ListenersHolder<T> listeners;
public ConnectionEventPublisher() {
listeners = new ListenersHolder<>();
}
public ConnectionEventPublisher(ConnectionEventPublisher<T> toCopy) {
listeners = toCopy.listeners.copy();
}
@Override
public void onByteRead(final long bytesRead) {
listeners.invokeListeners(bytesReadAction, bytesRead);
}
@Override
public void onByteWritten(long bytesWritten) {
listeners.invokeListeners(bytesWrittenAction, bytesWritten);
}
@Override
public void onFlushStart() {
listeners.invokeListeners(flushStartAction);
}
@Override
public void onFlushComplete(final long duration, final TimeUnit timeUnit) {
listeners.invokeListeners(flushCompleteAction, duration, timeUnit);
}
@Override
public void onWriteStart() {
listeners.invokeListeners(writeStartAction);
}
@Override
public void onWriteSuccess(final long duration, final TimeUnit timeUnit) {
listeners.invokeListeners(writeSuccessAction, duration, timeUnit);
}
@Override
public void onWriteFailed(final long duration, final TimeUnit timeUnit, final Throwable throwable) {
listeners.invokeListeners(writeFailedAction, duration, timeUnit, throwable);
}
@Override
public void onConnectionCloseStart() {
listeners.invokeListeners(closeStartAction);
}
@Override
public void onConnectionCloseSuccess(final long duration, final TimeUnit timeUnit) {
listeners.invokeListeners(closeSuccessAction, duration, timeUnit);
}
@Override
public void onConnectionCloseFailed(final long duration, final TimeUnit timeUnit, final Throwable throwable) {
listeners.invokeListeners(closeFailedAction, duration, timeUnit, throwable);
}
@Override
public void onCustomEvent(Object event) {
listeners.invokeListeners(customEventAction, event);
}
@Override
public void onCustomEvent(Object event, long duration, TimeUnit timeUnit) {
listeners.invokeListeners(customEventDurationAction, duration, timeUnit, event);
}
@Override
public void onCustomEvent(Object event, long duration, TimeUnit timeUnit, Throwable throwable) {
listeners.invokeListeners(customEventDurationErrAction, duration, timeUnit, throwable, event);
}
@Override
public void onCustomEvent(Object event, Throwable throwable) {
listeners.invokeListeners(customEventErrorAction, throwable, event);
}
@Override
public Subscription subscribe(T listener) {
return listeners.subscribe(listener);
}
@Override
public boolean publishingEnabled() {
return listeners.publishingEnabled();
}
public ConnectionEventPublisher<T> copy() {
return new ConnectionEventPublisher<>(this);
}
/*Visible for testing*/ ListenersHolder<T> getListeners() {
return listeners;
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.client;
import io.netty.channel.Channel;
import rx.Observable;
public interface ChannelProvider {
Observable<Channel> newChannel(Observable<Channel> input);
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.client;
import io.reactivex.netty.client.events.ClientEventListener;
import io.reactivex.netty.events.EventPublisher;
import io.reactivex.netty.events.EventSource;
public interface ChannelProviderFactory {
ChannelProvider newProvider(Host host, EventSource<? super ClientEventListener> eventSource,
EventPublisher publisher, ClientEventListener clientPublisher);
}

View file

@ -0,0 +1,202 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.client;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.util.AttributeKey;
import io.reactivex.netty.channel.AbstractConnectionToChannelBridge;
import io.reactivex.netty.channel.ChannelSubscriberEvent;
import io.reactivex.netty.channel.Connection;
import io.reactivex.netty.channel.ConnectionInputSubscriberResetEvent;
import io.reactivex.netty.channel.EmitConnectionEvent;
import io.reactivex.netty.client.events.ClientEventListener;
import io.reactivex.netty.client.pool.PooledConnection;
import io.reactivex.netty.events.EventAttributeKeys;
import io.reactivex.netty.events.EventPublisher;
import io.reactivex.netty.internal.ExecuteInEventloopAction;
import java.util.logging.Level;
import java.util.logging.Logger;
import rx.Subscriber;
import rx.functions.Action1;
import rx.functions.Actions;
import rx.observers.SafeSubscriber;
import rx.subscriptions.Subscriptions;
/**
* An implementation of {@link AbstractConnectionToChannelBridge} for clients.
*
* <h2>Reuse</h2>
*
* A channel can be reused for multiple operations, provided the reuses is signalled by {@link ConnectionReuseEvent}.
* Failure to do so, will result in errors on the {@link Subscriber} trying to reuse the channel.
* A typical reuse should have the following events:
*
<PRE>
ChannelSubscriberEvent =&gt; ConnectionInputSubscriberEvent =&gt; ConnectionReuseEvent =&gt;
ConnectionInputSubscriberEvent =&gt; ConnectionReuseEvent =&gt; ConnectionInputSubscriberEvent
</PRE>
*
* @param <R> Type read from the connection held by this handler.
* @param <W> Type written to the connection held by this handler.
*/
public class ClientConnectionToChannelBridge<R, W> extends AbstractConnectionToChannelBridge<R, W> {
public static final AttributeKey<Boolean> DISCARD_CONNECTION = AttributeKey.valueOf("rxnetty_discard_connection");
private static final Logger logger = Logger.getLogger(ClientConnectionToChannelBridge.class.getName());
private static final String HANDLER_NAME = "client-conn-channel-bridge";
private EventPublisher eventPublisher;
private ClientEventListener eventListener;
private final boolean isSecure;
private Channel channel;
private ClientConnectionToChannelBridge(boolean isSecure) {
super(HANDLER_NAME, EventAttributeKeys.CONNECTION_EVENT_LISTENER, EventAttributeKeys.EVENT_PUBLISHER);
this.isSecure = isSecure;
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
channel = ctx.channel();
eventPublisher = channel.attr(EventAttributeKeys.EVENT_PUBLISHER).get();
eventListener = ctx.channel().attr(EventAttributeKeys.CLIENT_EVENT_LISTENER).get();
if (null == eventPublisher) {
logger.log(Level.SEVERE, "No Event publisher bound to the channel, closing channel.");
ctx.channel().close();
return;
}
if (eventPublisher.publishingEnabled() && null == eventListener) {
logger.log(Level.SEVERE, "No Event listener bound to the channel and event publishing is enabled., closing channel.");
ctx.channel().close();
return;
}
super.handlerAdded(ctx);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
if (!isSecure) {/*When secure, the event is triggered post SSL handshake via the SslCodec*/
userEventTriggered(ctx, EmitConnectionEvent.INSTANCE);
}
super.channelActive(ctx);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt); // Super handles ConnectionInputSubscriberResetEvent to reset the subscriber.
if (evt instanceof ConnectionReuseEvent) {
@SuppressWarnings("unchecked")
ConnectionReuseEvent<R, W> event = (ConnectionReuseEvent<R, W>) evt;
newConnectionReuseEvent(ctx.channel(), event);
}
}
@Override
protected void onNewReadSubscriber(Subscriber<? super R> subscriber) {
// Unsubscribe from the input closes the connection as there can only be one subscriber to the
// input and, if nothing is read, it means, nobody is using the connection.
// For fire-and-forget usecases, one should explicitly ignore content on the connection which
// adds a discard all subscriber that never unsubscribes. For this case, then, the close becomes
// explicit.
subscriber.add(Subscriptions.create(new ExecuteInEventloopAction(channel) {
@Override
public void run() {
if (!connectionInputSubscriberExists(channel)) {
Connection<?, ?> connection = channel.attr(Connection.CONNECTION_ATTRIBUTE_KEY).get();
if (null != connection) {
connection.closeNow();
}
}
}
}));
}
private void newConnectionReuseEvent(Channel channel, final ConnectionReuseEvent<R, W> event) {
Subscriber<? super PooledConnection<R, W>> subscriber = event.getSubscriber();
if (isValidToEmit(subscriber)) {
subscriber.onNext(event.getPooledConnection());
checkEagerSubscriptionIfConfigured(channel);
} else {
// If pooled connection not sent to the subscriber, release to the pool.
event.getPooledConnection().close(false).subscribe(Actions.empty(), new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
logger.log(Level.SEVERE, "Error closing connection.", throwable);
}
});
}
}
public static <R, W> ClientConnectionToChannelBridge<R, W> addToPipeline(ChannelPipeline pipeline,
boolean isSecure) {
ClientConnectionToChannelBridge<R, W> toAdd = new ClientConnectionToChannelBridge<>(isSecure);
pipeline.addLast(HANDLER_NAME, toAdd);
return toAdd;
}
/**
* An event to indicate channel/{@link Connection} reuse. This event should be used for clients that pool
* connections. For every reuse of a connection (connection creation still uses {@link ChannelSubscriberEvent})
* the corresponding subscriber must be sent via this event.
*
* Every instance of this event resets the older subscriber attached to the connection and connection input. This
* means sending an {@link Subscriber#onCompleted()} to both of those subscribers. It is assumed that the actual
* {@link Subscriber} is similar to {@link SafeSubscriber} which can handle duplicate terminal events.
*
* @param <I> Type read from the connection held by the event.
* @param <O> Type written to the connection held by the event.
*/
public static final class ConnectionReuseEvent<I, O> implements ConnectionInputSubscriberResetEvent {
private final Subscriber<? super PooledConnection<I, O>> subscriber;
private final PooledConnection<I, O> pooledConnection;
public ConnectionReuseEvent(Subscriber<? super PooledConnection<I, O>> subscriber,
PooledConnection<I, O> pooledConnection) {
this.subscriber = subscriber;
this.pooledConnection = pooledConnection;
}
public Subscriber<? super PooledConnection<I, O>> getSubscriber() {
return subscriber;
}
public PooledConnection<I, O> getPooledConnection() {
return pooledConnection;
}
}
/**
* An event to indicate release of a {@link PooledConnection}.
*/
public static final class PooledConnectionReleaseEvent {
public static final PooledConnectionReleaseEvent INSTANCE = new PooledConnectionReleaseEvent();
private PooledConnectionReleaseEvent() {
}
}
}

View file

@ -0,0 +1,505 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.util.concurrent.EventExecutorGroup;
import io.reactivex.netty.RxNetty;
import io.reactivex.netty.channel.ChannelSubscriberEvent;
import io.reactivex.netty.channel.ConnectionCreationFailedEvent;
import io.reactivex.netty.channel.DetachedChannelPipeline;
import io.reactivex.netty.channel.WriteTransformer;
import io.reactivex.netty.client.events.ClientEventListener;
import io.reactivex.netty.events.Clock;
import io.reactivex.netty.events.EventPublisher;
import io.reactivex.netty.events.EventSource;
import io.reactivex.netty.ssl.DefaultSslCodec;
import io.reactivex.netty.ssl.SslCodec;
import io.reactivex.netty.util.LoggingHandlerFactory;
import rx.Observable;
import rx.exceptions.Exceptions;
import rx.functions.Action1;
import rx.functions.Func0;
import rx.functions.Func1;
import javax.net.ssl.SSLEngine;
import java.net.SocketAddress;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import static io.reactivex.netty.HandlerNames.*;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
/**
* A collection of state that a client holds. This supports the copy-on-write semantics of clients.
*
* @param <W> The type of objects written to the client owning this state.
* @param <R> The type of objects read from the client owning this state.
*/
public class ClientState<W, R> {
private final Observable<Host> hostStream;
private final ConnectionProviderFactory<W, R> factory;
private final DetachedChannelPipeline detachedPipeline;
private final Map<ChannelOption<?>, Object> options;
private final boolean isSecure;
private final EventLoopGroup eventLoopGroup;
private final Class<? extends Channel> channelClass;
private final ChannelProviderFactory channelProviderFactory;
protected ClientState(Observable<Host> hostStream, ConnectionProviderFactory<W, R> factory,
DetachedChannelPipeline detachedPipeline, EventLoopGroup eventLoopGroup,
Class<? extends Channel> channelClass) {
this.eventLoopGroup = eventLoopGroup;
this.channelClass = channelClass;
options = new LinkedHashMap<>(); /// Same as netty bootstrap, order matters.
this.hostStream = hostStream;
this.factory = factory;
this.detachedPipeline = detachedPipeline;
isSecure = false;
channelProviderFactory = new ChannelProviderFactory() {
@Override
public ChannelProvider newProvider(Host host, EventSource<? super ClientEventListener> eventSource,
EventPublisher publisher, ClientEventListener clientPublisher) {
return new ChannelProvider() {
@Override
public Observable<Channel> newChannel(Observable<Channel> input) {
return input;
}
};
}
};
}
protected ClientState(ClientState<W, R> toCopy, ChannelOption<?> option, Object value) {
options = new LinkedHashMap<>(toCopy.options); // Since, we are adding an option, copy it.
options.put(option, value);
detachedPipeline = toCopy.detachedPipeline;
hostStream = toCopy.hostStream;
factory = toCopy.factory;
eventLoopGroup = toCopy.eventLoopGroup;
channelClass = toCopy.channelClass;
isSecure = toCopy.isSecure;
channelProviderFactory = toCopy.channelProviderFactory;
}
protected ClientState(ClientState<?, ?> toCopy, DetachedChannelPipeline newPipeline, boolean secure) {
final ClientState<W, R> toCopyCast = toCopy.cast();
options = toCopy.options;
hostStream = toCopy.hostStream;
factory = toCopyCast.factory;
eventLoopGroup = toCopy.eventLoopGroup;
channelClass = toCopy.channelClass;
detachedPipeline = newPipeline;
isSecure = secure;
channelProviderFactory = toCopyCast.channelProviderFactory;
}
protected ClientState(ClientState<?, ?> toCopy, ChannelProviderFactory newFactory) {
final ClientState<W, R> toCopyCast = toCopy.cast();
options = toCopy.options;
hostStream = toCopy.hostStream;
factory = toCopyCast.factory;
eventLoopGroup = toCopy.eventLoopGroup;
channelClass = toCopy.channelClass;
detachedPipeline = toCopy.detachedPipeline;
channelProviderFactory = newFactory;
isSecure = toCopy.isSecure;
}
protected ClientState(ClientState<?, ?> toCopy, SslCodec sslCodec) {
this(toCopy, toCopy.detachedPipeline.copy(new TailHandlerFactory(true)).configure(sslCodec), true);
}
public <T> ClientState<W, R> channelOption(ChannelOption<T> option, T value) {
return new ClientState<>(this, option, value);
}
public <WW, RR> ClientState<WW, RR> addChannelHandlerFirst(String name, Func0<ChannelHandler> handlerFactory) {
ClientState<WW, RR> copy = copy();
copy.detachedPipeline.addFirst(name, handlerFactory);
return copy;
}
public <WW, RR> ClientState<WW, RR> addChannelHandlerFirst(EventExecutorGroup group, String name,
Func0<ChannelHandler> handlerFactory) {
ClientState<WW, RR> copy = copy();
copy.detachedPipeline.addFirst(group, name, handlerFactory);
return copy;
}
public <WW, RR> ClientState<WW, RR> addChannelHandlerLast(String name, Func0<ChannelHandler> handlerFactory) {
ClientState<WW, RR> copy = copy();
copy.detachedPipeline.addLast(name, handlerFactory);
return copy;
}
public <WW, RR> ClientState<WW, RR> addChannelHandlerLast(EventExecutorGroup group, String name,
Func0<ChannelHandler> handlerFactory) {
ClientState<WW, RR> copy = copy();
copy.detachedPipeline.addLast(group, name, handlerFactory);
return copy;
}
public <WW, RR> ClientState<WW, RR> addChannelHandlerBefore(String baseName, String name,
Func0<ChannelHandler> handlerFactory) {
ClientState<WW, RR> copy = copy();
copy.detachedPipeline.addBefore(baseName, name, handlerFactory);
return copy;
}
public <WW, RR> ClientState<WW, RR> addChannelHandlerBefore(EventExecutorGroup group, String baseName,
String name, Func0<ChannelHandler> handlerFactory) {
ClientState<WW, RR> copy = copy();
copy.detachedPipeline.addBefore(group, baseName, name, handlerFactory);
return copy;
}
public <WW, RR> ClientState<WW, RR> addChannelHandlerAfter(String baseName, String name,
Func0<ChannelHandler> handlerFactory) {
ClientState<WW, RR> copy = copy();
copy.detachedPipeline.addAfter(baseName, name, handlerFactory);
return copy;
}
public <WW, RR> ClientState<WW, RR> addChannelHandlerAfter(EventExecutorGroup group, String baseName,
String name, Func0<ChannelHandler> handlerFactory) {
ClientState<WW, RR> copy = copy();
copy.detachedPipeline.addAfter(group, baseName, name, handlerFactory);
return copy;
}
public <WW, RR> ClientState<WW, RR> pipelineConfigurator(Action1<ChannelPipeline> pipelineConfigurator) {
ClientState<WW, RR> copy = copy();
copy.detachedPipeline.configure(pipelineConfigurator);
return copy;
}
public ClientState<W, R> enableWireLogging(final LogLevel wireLoggingLevel) {
return enableWireLogging(LoggingHandler.class.getName(), wireLoggingLevel);
}
public ClientState<W, R> enableWireLogging(String name, final LogLevel wireLoggingLevel) {
return addChannelHandlerFirst(WireLogging.getName(),
LoggingHandlerFactory.getFactory(name, wireLoggingLevel));
}
public static <WW, RR> ClientState<WW, RR> create(ConnectionProviderFactory<WW, RR> factory,
Observable<Host> hostStream) {
return create(newChannelPipeline(new TailHandlerFactory(false)), factory, hostStream);
}
public static <WW, RR> ClientState<WW, RR> create(ConnectionProviderFactory<WW, RR> factory,
Observable<Host> hostStream,
EventLoopGroup eventLoopGroup,
Class<? extends Channel> channelClass) {
return new ClientState<>(hostStream, factory, newChannelPipeline(new TailHandlerFactory(false)), eventLoopGroup,
channelClass);
}
public static <WW, RR> ClientState<WW, RR> create(DetachedChannelPipeline detachedPipeline,
ConnectionProviderFactory<WW, RR> factory,
Observable<Host> hostStream) {
return create(detachedPipeline, factory, hostStream, defaultEventloopGroup(), defaultSocketChannelClass());
}
public static <WW, RR> ClientState<WW, RR> create(DetachedChannelPipeline detachedPipeline,
ConnectionProviderFactory<WW, RR> factory,
Observable<Host> hostStream,
EventLoopGroup eventLoopGroup,
Class<? extends Channel> channelClass) {
return new ClientState<>(hostStream, factory, detachedPipeline, eventLoopGroup, channelClass);
}
private static DetachedChannelPipeline newChannelPipeline(TailHandlerFactory thf) {
return new DetachedChannelPipeline(thf)
.addLast(WriteTransformer.getName(), new Func0<ChannelHandler>() {
@Override
public ChannelHandler call() {
return new WriteTransformer();
}
});
}
public Bootstrap newBootstrap(final EventPublisher eventPublisher, final ClientEventListener eventListener) {
final Bootstrap nettyBootstrap = new Bootstrap().group(eventLoopGroup)
.channel(channelClass)
.option(ChannelOption.AUTO_READ, false);// by default do not read content unless asked.
for (Entry<ChannelOption<?>, Object> optionEntry : options.entrySet()) {
// Type is just for safety for user of ClientState, internally in Bootstrap, types are thrown on the floor.
@SuppressWarnings("unchecked")
ChannelOption<Object> key = (ChannelOption<Object>) optionEntry.getKey();
nettyBootstrap.option(key, optionEntry.getValue());
}
nettyBootstrap.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(ClientChannelActiveBufferingHandler.getName(),
new ChannelActivityBufferingHandler(eventPublisher, eventListener));
}
});
return nettyBootstrap;
}
public DetachedChannelPipeline unsafeDetachedPipeline() {
return detachedPipeline;
}
public Map<ChannelOption<?>, Object> unsafeChannelOptions() {
return options;
}
public ClientState<W, R> channelProviderFactory(ChannelProviderFactory factory) {
return new ClientState<>(this, factory);
}
public ClientState<W, R> secure(Func1<ByteBufAllocator, SSLEngine> sslEngineFactory) {
return secure(new DefaultSslCodec(sslEngineFactory));
}
public ClientState<W, R> secure(SSLEngine sslEngine) {
return secure(new DefaultSslCodec(sslEngine));
}
public ClientState<W, R> secure(SslCodec sslCodec) {
return new ClientState<>(this, sslCodec);
}
public ClientState<W, R> unsafeSecure() {
return secure(new DefaultSslCodec(new Func1<ByteBufAllocator, SSLEngine>() {
@Override
public SSLEngine call(ByteBufAllocator allocator) {
try {
return SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build()
.newEngine(allocator);
} catch (Exception e) {
throw Exceptions.propagate(e);
}
}
}));
}
private <WW, RR> ClientState<WW, RR> copy() {
TailHandlerFactory newTail = new TailHandlerFactory(isSecure);
return new ClientState<>(this, detachedPipeline.copy(newTail), isSecure);
}
public ConnectionProviderFactory<W, R> getFactory() {
return factory;
}
public Observable<Host> getHostStream() {
return hostStream;
}
public ChannelProviderFactory getChannelProviderFactory() {
return channelProviderFactory;
}
@SuppressWarnings("unchecked")
private <WW, RR> ClientState<WW, RR> cast() {
return (ClientState<WW, RR>) this;
}
protected static class TailHandlerFactory implements Action1<ChannelPipeline> {
private final boolean isSecure;
public TailHandlerFactory(boolean isSecure) {
this.isSecure = isSecure;
}
@Override
public void call(ChannelPipeline pipeline) {
ClientConnectionToChannelBridge.addToPipeline(pipeline, isSecure);
}
}
public static EventLoopGroup defaultEventloopGroup() {
return RxNetty.getRxEventLoopProvider().globalClientEventLoop(true);
}
public static Class<? extends Channel> defaultSocketChannelClass() {
return RxNetty.isUsingNativeTransport() ? EpollSocketChannel.class : NioSocketChannel.class;
}
/**
* Clients construct the pipeline, outside of the {@link ChannelInitializer} through {@link ChannelProvider}.
* Thus channel registration and activation events may be lost due to a race condition when the channel is active
* before the pipeline is configured.
* This handler buffers, the channel events till the time, a subscriber appears for channel establishment.
*/
private static class ChannelActivityBufferingHandler extends ChannelDuplexHandler {
private enum State {
Initialized,
Registered,
Active,
Inactive,
ChannelSubscribed
}
private State state = State.Initialized;
/**
* Unregistered state will hide the active/inactive state, hence this is a different flag.
*/
private boolean unregistered;
private long connectStartTimeNanos;
private final EventPublisher eventPublisher;
private final ClientEventListener eventListener;
private ChannelActivityBufferingHandler(EventPublisher eventPublisher, ClientEventListener eventListener) {
this.eventPublisher = eventPublisher;
this.eventListener = eventListener;
}
@SuppressWarnings("unchecked")
@Override
public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
connectStartTimeNanos = Clock.newStartTimeNanos();
if (eventPublisher.publishingEnabled()) {
eventListener.onConnectStart();
promise.addListener(new ChannelFutureListener() {
@SuppressWarnings("unchecked")
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (eventPublisher.publishingEnabled()) {
long endTimeNanos = Clock.onEndNanos(connectStartTimeNanos);
if (!future.isSuccess()) {
eventListener.onConnectFailed(endTimeNanos, NANOSECONDS, future.cause());
} else {
eventListener.onConnectSuccess(endTimeNanos, NANOSECONDS);
}
}
}
});
}
super.connect(ctx, remoteAddress, localAddress, promise);
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
if (State.ChannelSubscribed == state) {
super.channelRegistered(ctx);
} else {
state = State.Registered;
}
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
if (State.ChannelSubscribed == state) {
super.channelUnregistered(ctx);
} else {
unregistered = true;
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
if (State.ChannelSubscribed == state) {
super.channelActive(ctx);
} else {
state = State.Active;
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (State.ChannelSubscribed == state) {
super.channelInactive(ctx);
} else {
state = State.Inactive;
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof ChannelSubscriberEvent) {
final State existingState = state;
state = State.ChannelSubscribed;
super.userEventTriggered(ctx, evt);
final ChannelPipeline pipeline = ctx.channel().pipeline();
switch (existingState) {
case Initialized:
break;
case Registered:
pipeline.fireChannelRegistered();
break;
case Active:
pipeline.fireChannelRegistered();
pipeline.fireChannelActive();
break;
case Inactive:
pipeline.fireChannelRegistered();
pipeline.fireChannelActive();
pipeline.fireChannelInactive();
break;
case ChannelSubscribed:
// Duplicate event, ignore.
break;
}
if (unregistered) {
pipeline.fireChannelUnregistered();
}
} else if (evt instanceof ConnectionCreationFailedEvent) {
ConnectionCreationFailedEvent failedEvent = (ConnectionCreationFailedEvent) evt;
onConnectFailedEvent(failedEvent);
super.userEventTriggered(ctx, evt);
} else {
super.userEventTriggered(ctx, evt);
}
}
@SuppressWarnings("unchecked")
private void onConnectFailedEvent(ConnectionCreationFailedEvent event) {
if (eventPublisher.publishingEnabled()) {
eventListener.onConnectFailed(connectStartTimeNanos, NANOSECONDS, event.getThrowable());
}
}
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.client;
import io.reactivex.netty.channel.Connection;
import rx.Observable;
/**
* A contract to control how connections are established from a client.
*
* @param <W> The type of objects written on the connections created by this provider.
* @param <R> The type of objects read from the connections created by this provider.
*/
public interface ConnectionProvider<W, R> {
/**
* Returns an {@code Observable} that emits a single connection every time it is subscribed.
*
* @return An {@code Observable} that emits a single connection every time it is subscribed.
*/
Observable<Connection<R, W>> newConnectionRequest();
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.client;
import rx.Observable;
public interface ConnectionProviderFactory<W, R> {
ConnectionProvider<W, R> newProvider(Observable<HostConnector<W, R>> hosts);
}

View file

@ -0,0 +1,42 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.client;
import io.reactivex.netty.channel.Connection;
import rx.Observable;
/**
* A connection request that is used to create connections for different protocols.
*
* <h2>Mutations</h2>
*
* All mutations to this request that creates a brand new instance.
*
* <h2> Inititating connections</h2>
*
* A new connection is initiated every time {@link ConnectionRequest#subscribe()} is called and is the only way of
* creating connections.
*
* @param <W> The type of the objects that are written to the connection created by this request.
* @param <R> The type of objects that are read from the connection created by this request.
*/
public abstract class ConnectionRequest<W, R> extends Observable<Connection<R, W>> {
protected ConnectionRequest(OnSubscribe<Connection<R, W>> f) {
super(f);
}
}

View file

@ -0,0 +1,70 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.client;
import rx.Observable;
import java.net.SocketAddress;
public final class Host {
private final SocketAddress host;
private final Observable<Void> closeNotifier;
public Host(SocketAddress host) {
this(host, Observable.<Void>never());
}
public Host(SocketAddress host, Observable<Void> closeNotifier) {
this.host = host;
this.closeNotifier = closeNotifier;
}
public SocketAddress getHost() {
return host;
}
public Observable<Void> getCloseNotifier() {
return closeNotifier;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Host)) {
return false;
}
Host host1 = (Host) o;
if (host != null? !host.equals(host1.host) : host1.host != null) {
return false;
}
return closeNotifier != null? closeNotifier.equals(host1.closeNotifier) : host1.closeNotifier == null;
}
@Override
public int hashCode() {
int result = host != null? host.hashCode() : 0;
result = 31 * result + (closeNotifier != null? closeNotifier.hashCode() : 0);
return result;
}
}

View file

@ -0,0 +1,110 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.client;
import io.reactivex.netty.client.events.ClientEventListener;
import io.reactivex.netty.events.EventPublisher;
import io.reactivex.netty.events.EventSource;
import rx.Subscription;
public class HostConnector<W, R> implements EventSource<ClientEventListener> {
private final Host host;
private final ConnectionProvider<W, R> connectionProvider;
@SuppressWarnings("rawtypes")
private final EventSource eventSource;
private final EventPublisher publisher;
private final ClientEventListener clientPublisher;
public HostConnector(Host host, ConnectionProvider<W, R> connectionProvider,
EventSource<? extends ClientEventListener> eventSource, EventPublisher publisher,
ClientEventListener clientPublisher) {
this.host = host;
this.connectionProvider = connectionProvider;
this.eventSource = eventSource;
this.publisher = publisher;
this.clientPublisher = clientPublisher;
}
public HostConnector(HostConnector<W, R> source, ConnectionProvider<W, R> connectionProvider) {
this.connectionProvider = connectionProvider;
host = source.host;
eventSource = source.eventSource;
clientPublisher = source.clientPublisher;
publisher = source.publisher;
}
public Host getHost() {
return host;
}
public ConnectionProvider<W, R> getConnectionProvider() {
return connectionProvider;
}
public ClientEventListener getClientPublisher() {
return clientPublisher;
}
public EventPublisher getEventPublisher() {
return publisher;
}
@Override
@SuppressWarnings("unchecked")
public Subscription subscribe(ClientEventListener listener) {
return eventSource.subscribe(listener);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof HostConnector)) {
return false;
}
HostConnector<?, ?> that = (HostConnector<?, ?>) o;
if (host != null? !host.equals(that.host) : that.host != null) {
return false;
}
if (connectionProvider != null? !connectionProvider.equals(that.connectionProvider) :
that.connectionProvider != null) {
return false;
}
if (eventSource != null? !eventSource.equals(that.eventSource) : that.eventSource != null) {
return false;
}
if (publisher != null? !publisher.equals(that.publisher) : that.publisher != null) {
return false;
}
return clientPublisher != null? clientPublisher.equals(that.clientPublisher) : that.clientPublisher == null;
}
@Override
public int hashCode() {
int result = host != null? host.hashCode() : 0;
result = 31 * result + (connectionProvider != null? connectionProvider.hashCode() : 0);
result = 31 * result + (eventSource != null? eventSource.hashCode() : 0);
result = 31 * result + (publisher != null? publisher.hashCode() : 0);
result = 31 * result + (clientPublisher != null? clientPublisher.hashCode() : 0);
return result;
}
}

View file

@ -0,0 +1,111 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.client.events;
import io.reactivex.netty.channel.events.ConnectionEventListener;
import java.util.concurrent.TimeUnit;
public class ClientEventListener extends ConnectionEventListener {
/**
* Event whenever a new connection attempt is made.
*/
@SuppressWarnings("unused")
public void onConnectStart() {}
/**
* Event whenever a new connection is successfully established.
*
* @param duration Duration between connect start and completion.
* @param timeUnit Timeunit for the duration.
*/
@SuppressWarnings("unused")
public void onConnectSuccess(long duration, TimeUnit timeUnit) {}
/**
* Event whenever a connect attempt failed.
*
* @param duration Duration between connect start and failure.
* @param timeUnit Timeunit for the duration.
* @param throwable Error that caused the failure.
*/
@SuppressWarnings("unused")
public void onConnectFailed(long duration, TimeUnit timeUnit, Throwable throwable) {}
/**
* Event whenever a connection release to the pool is initiated (by closing the connection)
*/
@SuppressWarnings("unused")
public void onPoolReleaseStart() {}
/**
* Event whenever a connection is successfully released to the pool.
*
* @param duration Duration between release start and completion.
* @param timeUnit Timeunit for the duration.
*/
@SuppressWarnings("unused")
public void onPoolReleaseSuccess(long duration, TimeUnit timeUnit) {}
/**
* Event whenever a connection release to pool fails.
*
* @param duration Duration between release start and failure.
* @param timeUnit Timeunit for the duration.
* @param throwable Error that caused the failure.
*/
@SuppressWarnings("unused")
public void onPoolReleaseFailed(long duration, TimeUnit timeUnit, Throwable throwable) {}
/**
* Event whenever an idle connection is removed/evicted from the pool.
*/
@SuppressWarnings("unused")
public void onPooledConnectionEviction() {}
/**
* Event whenever a connection is reused from the pool.
*/
@SuppressWarnings("unused")
public void onPooledConnectionReuse() {}
/**
* Event whenever an acquire from the pool is initiated.
*/
@SuppressWarnings("unused")
public void onPoolAcquireStart() {}
/**
* Event whenever an acquire from the pool is successful.
*
* @param duration Duration between acquire start and completion.
* @param timeUnit Timeunit for the duration.
*/
@SuppressWarnings("unused")
public void onPoolAcquireSuccess(long duration, TimeUnit timeUnit) {}
/**
* Event whenever an acquire from the pool failed.
*
* @param duration Duration between acquire start and failure.
* @param timeUnit Timeunit for the duration.
* @param throwable Error that caused the failure.
*/
@SuppressWarnings("unused")
public void onPoolAcquireFailed(long duration, TimeUnit timeUnit, Throwable throwable) {}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.client.internal;
import io.reactivex.netty.channel.Connection;
import io.reactivex.netty.client.ConnectionProvider;
import io.reactivex.netty.client.HostConnector;
import java.util.logging.Level;
import java.util.logging.Logger;
import rx.Observable;
import rx.functions.Action1;
/**
* A connection provider that only ever fetches a single host from the host stream provided to it.
*
* @param <W> The type of objects written on the connections created by this provider.
* @param <R> The type of objects read from the connections created by this provider.
*/
public class SingleHostConnectionProvider<W, R> implements ConnectionProvider<W, R> {
private static final Logger logger = Logger.getLogger(SingleHostConnectionProvider.class.getName());
private volatile ConnectionProvider<W, R> provider;
public SingleHostConnectionProvider(Observable<HostConnector<W, R>> connectors) {
connectors.toSingle()
.subscribe(connector -> provider = connector.getConnectionProvider(),
t -> logger.log(Level.SEVERE, "Failed while fetching a host connector from a scalar host source", t));
}
@Override
public Observable<Connection<R, W>> newConnectionRequest() {
return null != provider ? provider.newConnectionRequest()
: Observable.<Connection<R, W>>error(new IllegalStateException("No hosts available."));
}
}

View file

@ -0,0 +1,113 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.client.loadbalancer;
import io.reactivex.netty.channel.Connection;
import io.reactivex.netty.client.ConnectionProvider;
import io.reactivex.netty.client.Host;
import io.reactivex.netty.client.HostConnector;
import io.reactivex.netty.client.events.ClientEventListener;
import rx.Observable;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public abstract class AbstractP2CStrategy<W, R, L extends ClientEventListener> implements LoadBalancingStrategy<W, R> {
@Override
public ConnectionProvider<W, R> newStrategy(final List<HostHolder<W, R>> hosts) {
newHostsList(hosts.size());
return new ConnectionProvider<W, R>() {
@Override
public Observable<Connection<R, W>> newConnectionRequest() {
HostHolder<W, R> selected = null;
if (hosts.isEmpty()) {
noUsableHostsFound();
return Observable.error(NoHostsAvailableException.EMPTY_INSTANCE);
} else if (hosts.size() == 1) {
HostHolder<W, R> holder = hosts.get(0);
@SuppressWarnings("unchecked")
L eventListener = (L) holder.getEventListener();
double weight = getWeight(eventListener);
if (isUnusable(weight)) {
noUsableHostsFound();
return Observable.error(new NoHostsAvailableException("No usable hosts found."));
}
selected = holder;
} else {
ThreadLocalRandom rand = ThreadLocalRandom.current();
for (int i = 0; i < 5; i++) {
int pos = rand.nextInt(hosts.size());
HostHolder<W, R> first = hosts.get(pos);
int pos2 = (rand.nextInt(hosts.size() - 1) + pos + 1) % hosts.size();
HostHolder<W, R> second = hosts.get(pos2);
@SuppressWarnings("unchecked")
double w1 = getWeight((L) first.getEventListener());
@SuppressWarnings("unchecked")
double w2 = getWeight((L) second.getEventListener());
if (w1 > w2) {
selected = first;
break;
} else if (w1 < w2) {
selected = second;
break;
} else if (!isUnusable(w1)) {
selected = first;
break;
}
foundTwoUnusableHosts();
}
if (null == selected) {
noUsableHostsFound();
return Observable.error(new NoHostsAvailableException("No usable hosts found after 5 tries."));
}
}
return selected.getConnector().getConnectionProvider().newConnectionRequest();
}
};
}
protected boolean isUnusable(double weight) {
return weight < 0.0;
}
@Override
public HostHolder<W, R> toHolder(HostConnector<W, R> connector) {
return new HostHolder<>(connector, newListener(connector.getHost()));
}
protected abstract L newListener(Host host);
protected abstract double getWeight(L listener);
protected void noUsableHostsFound() {
// No Op by default
}
protected void foundTwoUnusableHosts() {
// No Op by default
}
protected void newHostsList(int size) {
// No Op by default
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.reactivex.netty.client.loadbalancer;
import rx.Single;
import rx.functions.Func1;
import java.util.List;
public interface HostCollector {
<W, R> Func1<HostUpdate<W, R>, Single<List<HostHolder<W, R>>>> newCollector();
final class HostUpdate<W, R> {
public enum Action{ Add, Remove }
private final Action action;
private final HostHolder<W, R> hostHolder;
public HostUpdate(Action action, HostHolder<W, R> hostHolder) {
this.action = action;
this.hostHolder = hostHolder;
}
public Action getAction() {
return action;
}
public HostHolder<W, R> getHostHolder() {
return hostHolder;
}
}
}

Some files were not shown because too many files have changed in this diff Show more