pluggable HTTP protocols, add netty-http-rx, adapted from https://github.com/ReactiveX/RxNetty
This commit is contained in:
parent
833e502a7c
commit
59ac22d492
378 changed files with 44782 additions and 1046 deletions
|
@ -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
|
||||||
|
|
3
netty-http-client-api/build.gradle
Normal file
3
netty-http-client-api/build.gradle
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
dependencies {
|
||||||
|
compile project(":netty-http-common")
|
||||||
|
}
|
|
@ -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.
|
|
@ -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 {
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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) {
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* Listeners for Netty HTTP client.
|
||||||
|
*/
|
||||||
|
package org.xbib.netty.http.client.api;
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')}"
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
/**
|
|
||||||
* Listeners for Netty HTTP client.
|
|
||||||
*/
|
|
||||||
package org.xbib.netty.http.client.listener;
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
org.xbib.netty.http.client.Http1Provider
|
||||||
|
org.xbib.netty.http.client.Http2Provider
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
|
@ -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();
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 {
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
5
netty-http-rx/NOTICE.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
This work is based on
|
||||||
|
|
||||||
|
https://github.com/ReactiveX/RxNetty
|
||||||
|
|
||||||
|
(branch 0.5.x as of 22-Sep-2019)
|
7
netty-http-rx/build.gradle
Normal file
7
netty-http-rx/build.gradle
Normal 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')}"
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
97
netty-http-rx/src/main/java/io/reactivex/netty/RxNetty.java
Normal file
97
netty-http-rx/src/main/java/io/reactivex/netty/RxNetty.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 + '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 + '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() { }
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -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 => ConnectionInputSubscriberEvent => ConnectionReuseEvent =>
|
||||||
|
ConnectionInputSubscriberEvent => ConnectionReuseEvent => 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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {}
|
||||||
|
}
|
|
@ -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."));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
Loading…
Reference in a new issue