![JAR search and dependency download from the Maven repository](/logo.png)
com.microsoft.rest.v2.http.SharedChannelPool Maven / Gradle / Ivy
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for
* license information.
*/
package com.microsoft.rest.v2.http;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.pool.ChannelPool;
import io.netty.channel.pool.ChannelPoolHandler;
import io.netty.handler.proxy.HttpProxyHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.FailedFuture;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.Promise;
import io.netty.util.concurrent.SucceededFuture;
import io.reactivex.annotations.Nullable;
import io.reactivex.exceptions.Exceptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A Netty channel pool implementation shared between multiple requests.
*
* Requests with the same host, port, and scheme share the same internal
* pool. All the internal pools for all the requests have a fixed size limit.
* This channel pool should be shared between multiple Netty adapters.
*/
class SharedChannelPool implements ChannelPool {
private static final AttributeKey CHANNEL_URI = AttributeKey.newInstance("channel-uri");
private static final AttributeKey CHANNEL_AVAILABLE_SINCE = AttributeKey.newInstance("channel-available-since");
private static final AttributeKey CHANNEL_LEASED_SINCE = AttributeKey.newInstance("channel-leased-since");
private static final AttributeKey CHANNEL_CREATED_SINCE = AttributeKey.newInstance("channel-created-since");
private static final AttributeKey CHANNEL_CLOSED_SINCE = AttributeKey.newInstance("channel-closed-since");
private final Bootstrap bootstrap;
private final EventLoopGroup eventLoopGroup;
private final ChannelPoolHandler handler;
private final int poolSize;
private final AtomicInteger channelCount = new AtomicInteger(0);
private final SharedChannelPoolOptions poolOptions;
private final ConcurrentMultiDequeMap requests;
private final ConcurrentMultiDequeMap available;
private final ConcurrentMultiDequeMap leased;
private final Object sync = new Object();
private final SslContext sslContext;
private volatile boolean closed = false;
private final Logger logger = LoggerFactory.getLogger(SharedChannelPool.class);
AtomicInteger wip = new AtomicInteger(0);
private boolean isChannelHealthy(Channel channel) {
try {
if (!channel.isActive()) {
return false;
} else if (channel.pipeline().get("HttpResponseDecoder") == null && channel.pipeline().get("HttpClientCodec") == null) {
return false;
} else {
ZonedDateTime channelAvailableSince = channel.attr(CHANNEL_AVAILABLE_SINCE).get();
if (channelAvailableSince == null) {
channelAvailableSince = channel.attr(CHANNEL_LEASED_SINCE).get();
}
final long channelIdleDurationInSec = ChronoUnit.SECONDS.between(channelAvailableSince, ZonedDateTime.now(ZoneOffset.UTC));
return channelIdleDurationInSec < this.poolOptions.idleChannelKeepAliveDurationInSec();
}
} catch (Throwable t) {
return false;
}
}
/**
* Creates an instance of the shared channel pool.
* @param bootstrap the bootstrap to create channels
* @param handler the handler to apply to the channels on creation, acquisition and release
* @param options optional settings for the pool
* @param sslContext the SSL Context for the connections
*/
SharedChannelPool(final Bootstrap bootstrap, final EventLoopGroup eventLoopGroup, final ChannelPoolHandler handler, SharedChannelPoolOptions options, SslContext sslContext) {
this.poolOptions = options.clone();
this.bootstrap = bootstrap.clone();
this.eventLoopGroup = eventLoopGroup;
this.handler = handler;
this.poolSize = options.poolSize();
this.requests = new ConcurrentMultiDequeMap<>();
this.available = new ConcurrentMultiDequeMap<>();
this.leased = new ConcurrentMultiDequeMap<>();
try {
if (sslContext == null) {
this.sslContext = SslContextBuilder.forClient().build();
} else {
this.sslContext = sslContext;
}
} catch (SSLException e) {
throw new RuntimeException(e);
}
}
private void drain(URI preferredUri) {
if (!wip.compareAndSet(0, 1)) {
return;
}
while (!closed && wip.updateAndGet(x -> requests.size()) != 0) {
if (channelCount.get() >= poolSize && available.size() == 0) {
wip.set(0);
break;
}
// requests must be non-empty based on the above condition
ChannelRequest request;
if (preferredUri != null && requests.containsKey(preferredUri)) {
request = requests.poll(preferredUri);
} else {
request = requests.poll();
}
boolean foundHealthyChannelInPool = false;
// Try to retrieve a healthy channel from pool
if (available.containsKey(request.channelURI)) {
Channel channel = available.pop(request.channelURI); // try most recently used
if (isChannelHealthy(channel)) {
logger.debug("Channel picked up from pool: {}", channel.id());
leased.put(request.channelURI, channel);
foundHealthyChannelInPool = true;
channel.attr(CHANNEL_LEASED_SINCE).set(ZonedDateTime.now(ZoneOffset.UTC));
request.promise.setSuccess(channel);
try {
handler.channelAcquired(channel);
} catch (Exception e) {
throw Exceptions.propagate(e);
}
} else {
logger.debug("Channel disposed from pool due to timeout or half closure: {}", channel.id());
closeChannel(channel);
channelCount.decrementAndGet();
// Delete all channels created before this
while (available.containsKey(request.channelURI)) {
Channel broken = available.pop(request.channelURI);
logger.debug("Channel disposed from pool due to timeout or half closure: {}", broken.id());
closeChannel(broken);
channelCount.decrementAndGet();
}
}
}
if (!foundHealthyChannelInPool) {
// Not found a healthy channel in pool. Create a new channel - remove an available one if size overflows
if (channelCount.get() >= poolSize) {
Channel nextAvailable = available.poll(); // Dispose least recently used
logger.debug("Channel disposed due to overflow: {}", nextAvailable.id());
closeChannel(nextAvailable);
channelCount.decrementAndGet();
}
int port;
if (request.destinationURI.getPort() < 0) {
port = "https".equals(request.destinationURI.getScheme()) ? 443 : 80;
} else {
port = request.destinationURI.getPort();
}
channelCount.incrementAndGet();
SharedChannelPool.this.bootstrap.clone().handler(new ChannelInitializer() {
@Override
protected void initChannel(Channel ch) throws Exception {
assert ch.eventLoop().inEventLoop();
if (request.proxy != null) {
ch.pipeline().addFirst("HttpProxyHandler", new HttpProxyHandler(request.proxy.address()));
}
handler.channelCreated(ch);
}
}).connect(request.destinationURI.getHost(), port).addListener((ChannelFuture f) -> {
if (f.isSuccess()) {
Channel channel = f.channel();
channel.attr(CHANNEL_URI).set(request.channelURI);
// Apply SSL handler for https connections
if ("https".equalsIgnoreCase(request.destinationURI.getScheme())) {
channel.pipeline().addBefore("HttpClientCodec", "SslHandler", this.sslContext.newHandler(channel.alloc(), request.destinationURI.getHost(), port));
}
leased.put(request.channelURI, channel);
channel.attr(CHANNEL_CREATED_SINCE).set(ZonedDateTime.now(ZoneOffset.UTC));
channel.attr(CHANNEL_LEASED_SINCE).set(ZonedDateTime.now(ZoneOffset.UTC));
logger.debug("Channel created: {}", channel.id());
handler.channelAcquired(channel);
request.promise.setSuccess(channel);
} else {
request.promise.setFailure(f.cause());
channelCount.decrementAndGet();
}
});
}
}
}
/**
* Acquire a channel for a URI.
* @param uri the URI the channel acquired should be connected to
* @return the future to a connected channel
*/
public Future acquire(URI uri, @Nullable Proxy proxy) {
return this.acquire(uri, proxy, this.bootstrap.config().group().next().newPromise());
}
/**
* Acquire a channel for a URI.
* @param uri the URI the channel acquired should be connected to
* @param promise the writable future to a connected channel
* @return the future to a connected channel
*/
public Future acquire(URI uri, @Nullable Proxy proxy, final Promise promise) {
if (closed) {
throw new RejectedExecutionException("SharedChannelPool is closed");
}
ChannelRequest channelRequest = new ChannelRequest();
channelRequest.promise = promise;
channelRequest.proxy = proxy;
int port;
if (uri.getPort() < 0) {
port = "https".equals(uri.getScheme()) ? 443 : 80;
} else {
port = uri.getPort();
}
try {
channelRequest.destinationURI = new URI(String.format("%s://%s:%d", uri.getScheme(), uri.getHost(), port));
if (proxy == null) {
channelRequest.channelURI = channelRequest.destinationURI;
} else {
InetSocketAddress address = (InetSocketAddress) proxy.address();
channelRequest.channelURI = new URI(String.format("%s://%s:%d", uri.getScheme(), address.getHostString(), address.getPort()));
}
requests.put(channelRequest.channelURI, channelRequest);
drain(null);
} catch (URISyntaxException e) {
promise.setFailure(e);
}
return channelRequest.promise;
}
@Override
public Future acquire() {
throw new UnsupportedOperationException("Please pass host & port to shared channel pool.");
}
@Override
public Future acquire(Promise promise) {
throw new UnsupportedOperationException("Please pass host & port to shared channel pool.");
}
private Future closeChannel(final Channel channel) {
if (!channel.isOpen()) {
return new SucceededFuture<>(eventLoopGroup.next(), null);
}
channel.attr(CHANNEL_CLOSED_SINCE).set(ZonedDateTime.now(ZoneOffset.UTC));
logger.debug("Channel initiated to close: " + channel.id());
// Closing a channel doesn't change the channel count
try {
return channel.close().addListener(f -> {
if (!f.isSuccess()) {
logger.warn("Possible channel leak: failed to close " + channel.id(), f.cause());
}
});
} catch (Exception e) {
logger.warn("Possible channel leak: failed to close " + channel.id(), e);
return new FailedFuture<>(eventLoopGroup.next(), e);
}
}
/**
* Closes the channel and releases it back to the pool.
* @param channel the channel to close and release.
* @return a Future representing the operation.
*/
public Future closeAndRelease(final Channel channel) {
try {
Future closeFuture = closeChannel(channel).addListener(future -> {
URI channelUri = channel.attr(CHANNEL_URI).get();
if (leased.remove(channelUri, channel) || available.remove(channelUri, channel)) {
channelCount.decrementAndGet();
logger.debug("Channel closed and released out of pool: " + channel.id());
}
drain(channelUri);
});
return closeFuture;
} catch (Exception e) {
return bootstrap.config().group().next().newFailedFuture(e);
}
}
@Override
public Future release(final Channel channel) {
try {
handler.channelReleased(channel);
URI channelUri = channel.attr(CHANNEL_URI).get();
leased.remove(channelUri, channel);
if (isChannelHealthy(channel)) {
available.put(channelUri, channel);
channel.attr(CHANNEL_AVAILABLE_SINCE).set(ZonedDateTime.now(ZoneOffset.UTC));
logger.debug("Channel released to pool: " + channel.id());
} else {
channelCount.decrementAndGet();
logger.debug("Channel broken on release, dispose: " + channel.id());
}
drain(channelUri);
} catch (Exception e) {
return bootstrap.config().group().next().newFailedFuture(e);
}
return bootstrap.config().group().next().newSucceededFuture(null);
}
@Override
public Future release(final Channel channel, final Promise promise) {
return release(channel).addListener(f -> {
if (f.isSuccess()) {
promise.setSuccess(null);
} else {
promise.setFailure(f.cause());
}
});
}
@Override
public void close() {
closed = true;
while (requests.size() != 0) {
requests.poll().promise.setFailure(new CancellationException("Channel pool was closed"));
}
}
private static class ChannelRequest {
private URI destinationURI;
private URI channelURI;
private Proxy proxy;
private Promise promise;
}
/**
* Used to print a current overview of the channels in this pool.
*/
public void dump() {
logger.info(String.format("---- %s: size %d, keep alive (sec) %d ----", toString(), poolSize, poolOptions.idleChannelKeepAliveDurationInSec()));
logger.info("Channel\tState\tFor\tAge\tURL");
List closed = new ArrayList<>();
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
for (Channel channel : leased.values()) {
if (channel.hasAttr(CHANNEL_CLOSED_SINCE)) {
closed.add(channel);
continue;
}
long stateFor = ChronoUnit.SECONDS.between(channel.attr(CHANNEL_LEASED_SINCE).get(), now);
long age = ChronoUnit.SECONDS.between(channel.attr(CHANNEL_CREATED_SINCE).get(), now);
logger.info(String.format("%s\tLEASE\t%ds\t%ds\t%s", channel.id(), stateFor, age, channel.attr(CHANNEL_URI).get()));
}
for (Channel channel : available.values()) {
if (channel.hasAttr(CHANNEL_CLOSED_SINCE)) {
closed.add(channel);
continue;
}
long stateFor = ChronoUnit.SECONDS.between(channel.attr(CHANNEL_AVAILABLE_SINCE).get(), now);
long age = ChronoUnit.SECONDS.between(channel.attr(CHANNEL_CREATED_SINCE).get(), now);
logger.info(String.format("%s\tAVAIL\t%ds\t%ds\t%s", channel.id(), stateFor, age, channel.attr(CHANNEL_URI).get()));
}
for (Channel channel : closed) {
long stateFor = ChronoUnit.SECONDS.between(channel.attr(CHANNEL_CLOSED_SINCE).get(), now);
long age = ChronoUnit.SECONDS.between(channel.attr(CHANNEL_CREATED_SINCE).get(), now);
logger.info(String.format("%s\tCLOSE\t%ds\t%ds\t%s", channel.id(), stateFor, age, channel.attr(CHANNEL_URI).get()));
}
logger.info("Active channels: " + channelCount.get() + " Leaked or being initialized channels: " + (channelCount.get() - leased.size() - available.size()));
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy