/*
* Copyright 2016 the original author or authors.
*
* 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 ratpack.http.client.internal;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.pool.*;
import ratpack.exec.Execution;
import ratpack.exec.Promise;
import ratpack.exec.internal.ExecControllerInternal;
import ratpack.func.Action;
import ratpack.http.client.*;
import ratpack.server.ServerConfig;
import ratpack.util.internal.ChannelImplDetector;
import java.net.URI;
import java.time.Duration;
public class DefaultHttpClient implements HttpClientInternal {
private static final ChannelHealthChecker ALWAYS_UNHEALTHY = channel ->
channel.eventLoop().newSucceededFuture(Boolean.FALSE);
private static final ChannelPoolHandler NOOP_HANDLER = new AbstractChannelPoolHandler() {
@Override
public void channelCreated(Channel ch) throws Exception {}
@Override
public void channelReleased(Channel ch) throws Exception {
}
};
private static final ChannelPoolHandler POOLING_HANDLER = new AbstractChannelPoolHandler() {
@Override
public void channelCreated(Channel ch) throws Exception {
}
@Override
public void channelReleased(Channel ch) throws Exception {
if (ch.isOpen()) {
ch.config().setAutoRead(true);
ch.pipeline().addLast(IdlingConnectionHandler.INSTANCE);
}
}
@Override
public void channelAcquired(Channel ch) throws Exception {
ch.pipeline().remove(IdlingConnectionHandler.INSTANCE);
}
};
private final HttpChannelPoolMap channelPoolMap = new HttpChannelPoolMap() {
@Override
protected ChannelPool newPool(HttpChannelKey key) {
Bootstrap bootstrap = new Bootstrap()
.remoteAddress(key.host, key.port)
.group(key.execution.getEventLoop())
.channel(ChannelImplDetector.getSocketChannelImpl())
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) key.connectTimeout.toMillis())
.option(ChannelOption.ALLOCATOR, byteBufAllocator)
.option(ChannelOption.AUTO_READ, false)
.option(ChannelOption.SO_KEEPALIVE, isPooling());
if (isPooling()) {
ChannelPool channelPool = new FixedChannelPool(bootstrap, POOLING_HANDLER, getPoolSize());
((ExecControllerInternal) key.execution.getController()).onClose(() -> {
remove(key);
channelPool.close();
});
return channelPool;
} else {
return new SimpleChannelPool(bootstrap, NOOP_HANDLER, ALWAYS_UNHEALTHY);
}
}
};
private final ByteBufAllocator byteBufAllocator;
private final int maxContentLength;
private final int maxResponseChunkSize;
private final int poolSize;
private final Duration readTimeout;
private DefaultHttpClient(ByteBufAllocator byteBufAllocator, int maxContentLength, int maxResponseChunkSize, int poolSize, Duration readTimeout) {
this.byteBufAllocator = byteBufAllocator;
this.maxContentLength = maxContentLength;
this.maxResponseChunkSize = maxResponseChunkSize;
this.poolSize = poolSize;
this.readTimeout = readTimeout;
}
@Override
public int getPoolSize() {
return poolSize;
}
private boolean isPooling() {
return getPoolSize() > 0;
}
@Override
public HttpChannelPoolMap getChannelPoolMap() {
return channelPoolMap;
}
public ByteBufAllocator getByteBufAllocator() {
return byteBufAllocator;
}
public int getMaxContentLength() {
return maxContentLength;
}
@Override
public int getMaxResponseChunkSize() {
return maxResponseChunkSize;
}
public Duration getReadTimeout() {
return readTimeout;
}
@Override
public void close() {
channelPoolMap.close();
}
public static HttpClient of(Action<? super HttpClientSpec> action) throws Exception {
DefaultHttpClient.Spec spec = new DefaultHttpClient.Spec();
action.execute(spec);
return new DefaultHttpClient(
spec.byteBufAllocator,
spec.maxContentLength,
spec.responseMaxChunkSize,
spec.poolSize,
spec.readTimeout
);
}
private static class Spec implements HttpClientSpec {
private ByteBufAllocator byteBufAllocator = PooledByteBufAllocator.DEFAULT;
private int poolSize;
private int maxContentLength = ServerConfig.DEFAULT_MAX_CONTENT_LENGTH;
private int responseMaxChunkSize = 8192;
private Duration readTimeout = Duration.ofSeconds(30);
private Spec() {
}
@Override
public HttpClientSpec poolSize(int poolSize) {
this.poolSize = poolSize;
return this;
}
@Override
public HttpClientSpec byteBufAllocator(ByteBufAllocator byteBufAllocator) {
this.byteBufAllocator = byteBufAllocator;
return this;
}
@Override
public HttpClientSpec maxContentLength(int maxContentLength) {
this.maxContentLength = maxContentLength;
return this;
}
@Override
public HttpClientSpec responseMaxChunkSize(int numBytes) {
this.responseMaxChunkSize = numBytes;
return this;
}
@Override
public HttpClientSpec readTimeout(Duration readTimeout) {
this.readTimeout = readTimeout;
return this;
}
}
@Override
public Promise<ReceivedResponse> get(URI uri, Action<? super RequestSpec> action) {
return request(uri, action);
}
@Override
public Promise<ReceivedResponse> post(URI uri, Action<? super RequestSpec> action) {
return request(uri, action.prepend(RequestSpec::post));
}
@Override
public Promise<ReceivedResponse> request(URI uri, final Action<? super RequestSpec> requestConfigurer) {
return Promise.async(downstream -> new ContentAggregatingRequestAction(uri, this, 0, Execution.current(), requestConfigurer).connect(downstream));
}
@Override
public Promise<StreamedResponse> requestStream(URI uri, Action<? super RequestSpec> requestConfigurer) {
return Promise.async(downstream -> new ContentStreamingRequestAction(uri, this, 0, Execution.current(), requestConfigurer).connect(downstream));
}
}