/* * Copyright 2015 LINE Corporation * * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.pool; import static java.util.Objects.requireNonNull; import java.util.Deque; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.function.Function; import com.linecorp.armeria.common.util.Exceptions; import io.netty.channel.Channel; import io.netty.channel.EventLoop; import io.netty.channel.pool.ChannelHealthChecker; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.FutureListener; import io.netty.util.concurrent.Promise; /** * Default {@link KeyedChannelPool} implementation. * * @param <K> the key type */ public class DefaultKeyedChannelPool<K> implements KeyedChannelPool<K> { private static final IllegalStateException FULL_EXCEPTION = Exceptions.clearTrace(new IllegalStateException("ChannelPool full")); private static final IllegalStateException UNHEALTHY_NON_OFFERED_TO_POOL = Exceptions.clearTrace(new IllegalStateException( "Channel is unhealthy; not offering it back to pool")); private final EventLoop eventLoop; private final Function<K, Future<Channel>> channelFactory; private final ChannelHealthChecker healthCheck; private final KeyedChannelPoolHandler<K> channelPoolHandler; private final boolean releaseHealthCheck; private final Map<K, Deque<Channel>> pool; /** * Creates a new instance. */ public DefaultKeyedChannelPool(EventLoop eventLoop, Function<K, Future<Channel>> channelFactory, KeyedChannelPoolHandler<K> channelPoolHandler) { this(eventLoop, channelFactory, ChannelHealthChecker.ACTIVE, channelPoolHandler, true); } /** * Creates a new instance. */ public DefaultKeyedChannelPool(EventLoop eventLoop, Function<K, Future<Channel>> channelFactory, ChannelHealthChecker healthCheck, KeyedChannelPoolHandler<K> channelPoolHandler) { this(eventLoop, channelFactory, healthCheck, channelPoolHandler, true); } /** * Creates a new instance. */ public DefaultKeyedChannelPool(EventLoop eventLoop, Function<K, Future<Channel>> channelFactory, ChannelHealthChecker healthCheck, KeyedChannelPoolHandler<K> channelPoolHandler, boolean releaseHealthCheck) { this.eventLoop = requireNonNull(eventLoop, "eventLoop"); this.channelFactory = requireNonNull(channelFactory, "channelFactory"); this.healthCheck = requireNonNull(healthCheck, "healthCheck"); this.channelPoolHandler = new SafeKeyedChannelPoolHandler<>(requireNonNull(channelPoolHandler, "channelPoolHandler")); this.releaseHealthCheck = releaseHealthCheck; pool = new ConcurrentHashMap<>(); } @Override public Future<Channel> acquire(K key) { return acquire(key, eventLoop.newPromise()); } @Override public Future<Channel> acquire(final K key, final Promise<Channel> promise) { requireNonNull(key, "key"); requireNonNull(promise, "promise"); if (eventLoop.inEventLoop()) { acquireHealthyFromPoolOrNew(key, promise); } else { eventLoop.execute(() -> acquireHealthyFromPoolOrNew(key, promise)); } return promise; } private Future<Channel> acquireHealthyFromPoolOrNew(final K key, final Promise<Channel> promise) { final Deque<Channel> queue = pool.get(key); final Channel ch = queue == null ? null : queue.poll(); if (ch == null) { Future<Channel> f = channelFactory.apply(key); if (f.isDone()) { notifyConnect(key, f, promise); } else { f.addListener((Future<Channel> future) -> notifyConnect(key, future, promise)); } return promise; } EventLoop loop = ch.eventLoop(); if (loop.inEventLoop()) { doHealthCheck(key, ch, promise); } else { loop.execute(() -> doHealthCheck(key, ch, promise)); } return promise; } private void notifyConnect(K key, Future<Channel> future, Promise<Channel> promise) { assert future.isDone(); try { if (future.isSuccess()) { Channel channel = future.getNow(); channel.attr(KeyedChannelPoolUtil.POOL).set(this); channelPoolHandler.channelCreated(key, channel); channel.closeFuture().addListener(f -> channelPoolHandler.channelClosed(key, channel)); promise.setSuccess(channel); } else { promise.setFailure(future.cause()); } } catch (Exception e) { promise.setFailure(e); } } private void doHealthCheck(final K key, final Channel ch, final Promise<Channel> promise) { assert ch.eventLoop().inEventLoop(); Future<Boolean> f = healthCheck.isHealthy(ch); if (f.isDone()) { notifyHealthCheck(key, f, ch, promise); } else { f.addListener((FutureListener<Boolean>) future -> notifyHealthCheck(key, future, ch, promise)); } } private void notifyHealthCheck(final K key, Future<Boolean> future, Channel ch, Promise<Channel> promise) { assert ch.eventLoop().inEventLoop(); if (future.isSuccess()) { if (future.getNow() == Boolean.TRUE) { try { ch.attr(KeyedChannelPoolUtil.POOL).set(this); channelPoolHandler.channelAcquired(key, ch); promise.setSuccess(ch); } catch (Throwable cause) { closeAndFail(ch, cause, promise); } } else { closeChannel(ch); acquireHealthyFromPoolOrNew(key, promise); } } else { closeChannel(ch); acquireHealthyFromPoolOrNew(key, promise); } } private static void closeChannel(Channel channel) { channel.attr(KeyedChannelPoolUtil.POOL).set(null); channel.close(); } private static void closeAndFail(Channel channel, Throwable cause, Promise<?> promise) { closeChannel(channel); promise.setFailure(cause); } @Override public Future<Void> release(K key, Channel channel) { return release(key, channel, eventLoop.newPromise()); } @Override public Future<Void> release(final K key, final Channel channel, final Promise<Void> promise) { requireNonNull(key, "key"); requireNonNull(channel, "channel"); requireNonNull(promise, "promise"); try { EventLoop loop = channel.eventLoop(); if (loop.inEventLoop()) { doReleaseChannel(key, channel, promise); } else { loop.execute(() -> doReleaseChannel(key, channel, promise)); } } catch (Throwable cause) { closeAndFail(channel, cause, promise); } return promise; } private void doReleaseChannel(K key, Channel channel, Promise<Void> promise) { assert channel.eventLoop().inEventLoop(); if (channel.attr(KeyedChannelPoolUtil.POOL).getAndSet(null) != this) { // Better include a stracktrace here as this is an user error. closeAndFail(channel, new IllegalArgumentException( "Channel " + channel + " was not acquired from this ChannelPool"), promise); } else { try { if (releaseHealthCheck) { doHealthCheckOnRelease(key, channel, promise); } else { releaseAndOffer(key, channel, promise); } } catch (Throwable cause) { closeAndFail(channel, cause, promise); } } } private void doHealthCheckOnRelease(K key, final Channel channel, final Promise<Void> promise) throws Exception { final Future<Boolean> f = healthCheck.isHealthy(channel); if (f.isDone()) { releaseAndOfferIfHealthy(key, channel, promise, f); } else { f.addListener(future -> releaseAndOfferIfHealthy(key, channel, promise, f)); } } private void releaseAndOfferIfHealthy(K key, Channel channel, Promise<Void> promise, Future<Boolean> future) throws Exception { if (future.getNow()) { //channel turns out to be healthy, offering and releasing it. releaseAndOffer(key, channel, promise); } else { //channel ont healthy, just releasing it. channelPoolHandler.channelReleased(key, channel); closeAndFail(channel, UNHEALTHY_NON_OFFERED_TO_POOL, promise); } } private void releaseAndOffer(K key, Channel channel, Promise<Void> promise) throws Exception { if (offerChannel(key, channel)) { channelPoolHandler.channelReleased(key, channel); promise.setSuccess(null); } else { closeAndFail(channel, FULL_EXCEPTION, promise); } } /** * Removes a {@link Channel} that matches the specified {@code key} from this pool. * * @return the removed {@link Channel}. {@code null} if there's no matching {@link Channel}. */ protected Channel pollChannel(K key) { final Deque<Channel> queue = pool.get(key); final Channel ch; if (queue == null) { ch = null; } else { ch = queue.poll(); if (queue.isEmpty()) { pool.remove(key); } } return ch; } /** * Adds a {@link Channel} to this pool. * * @return whether adding the {@link Channel} has succeeded or not */ protected boolean offerChannel(K key, Channel channel) { return pool.computeIfAbsent(key, k -> new ConcurrentLinkedDeque<>()).offer(channel); } @Override public void close() { pool.forEach((k, v) -> { for (;;) { Channel channel = pollChannel(k); if (channel == null) { break; } if (channel.isOpen()) { channel.close(); } } }); } }