com.turo.pushy.apns.ApnsChannelPool.java Source code

Java tutorial

Introduction

Here is the source code for com.turo.pushy.apns.ApnsChannelPool.java

Source

/*
 * Copyright (c) 2013-2017 Turo
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.turo.pushy.apns;

import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.Queue;
import java.util.Set;

/**
 * <p>A pool of channels connected to an APNs server. Channel pools use a {@link ApnsChannelFactory} to create
 * connections (up to a given maximum capacity) on demand.</p>
 *
 * <p>Callers acquire channels from the pool via the {@link ApnsChannelPool#acquire()} method, and must return them to
 * the pool with the {@link ApnsChannelPool#release(Channel)} method. When channels are acquired, they are unavailable
 * to other callers until they are released back into the pool.</p>
 *
 * <p>Channel pools are intended to be long-lived, persistent resources. When an application no longer needs a channel
 * pool (presumably because it is shutting down), it must shut down the channel pool via the
 * {@link ApnsChannelPool#close()} method.</p>
 *
 * @since 0.11
 */
class ApnsChannelPool {

    private final PooledObjectFactory<Channel> channelFactory;
    private final OrderedEventExecutor executor;
    private final int capacity;

    private final ApnsChannelPoolMetricsListener metricsListener;

    private final ChannelGroup allChannels;
    private final Queue<Channel> idleChannels = new ArrayDeque<>();

    private final Set<Future<Channel>> pendingCreateChannelFutures = new HashSet<>();
    private final Queue<Promise<Channel>> pendingAcquisitionPromises = new ArrayDeque<>();

    private boolean isClosed = false;

    private static final Exception POOL_CLOSED_EXCEPTION = new IllegalStateException(
            "Channel pool has closed and no more channels may be acquired.");

    private static final Logger log = LoggerFactory.getLogger(ApnsChannelPool.class);

    private static class NoopChannelPoolMetricsListener implements ApnsChannelPoolMetricsListener {

        @Override
        public void handleConnectionAdded() {
        }

        @Override
        public void handleConnectionRemoved() {
        }

        @Override
        public void handleConnectionCreationFailed() {
        }
    }

    /**
     * Constructs a new channel pool that will create new channels with the given {@code channelFactory} and has the
     * given maximum channel {@code capacity}.
     *
     * @param channelFactory the factory to be used to create new channels
     * @param capacity the maximum number of channels that may be held in this pool
     * @param executor the executor on which listeners for acquisition/release promises will be called
     * @param metricsListener an optional listener for metrics describing the performance and behavior of the pool
     */
    ApnsChannelPool(final PooledObjectFactory<Channel> channelFactory, final int capacity,
            final OrderedEventExecutor executor, final ApnsChannelPoolMetricsListener metricsListener) {
        this.channelFactory = channelFactory;
        this.capacity = capacity;
        this.executor = executor;

        this.metricsListener = metricsListener != null ? metricsListener : new NoopChannelPoolMetricsListener();

        this.allChannels = new DefaultChannelGroup(this.executor, true);
    }

    /**
     * <p>Asynchronously acquires a channel from this channel pool. The acquired channel may be a pre-existing channel
     * stored in the pool or may be a new channel created on demand. If no channels are available and the pool is at
     * capacity, acquisition may be delayed until another caller releases a channel to the pool.</p>
     *
     * <p>When callers are done with a channel, they <em>must</em> release the channel back to the pool via the
     * {@link ApnsChannelPool#release(Channel)} method.</p>
     *
     * @return a {@code Future} that will be notified when a channel is available
     *
     * @see ApnsChannelPool#release(Channel)
     */
    Future<Channel> acquire() {
        final Promise<Channel> acquirePromise = new DefaultPromise<>(this.executor);

        if (this.executor.inEventLoop()) {
            this.acquireWithinEventExecutor(acquirePromise);
        } else {
            this.executor.submit(new Runnable() {
                @Override
                public void run() {
                    ApnsChannelPool.this.acquireWithinEventExecutor(acquirePromise);
                }
            }).addListener(new GenericFutureListener() {
                @Override
                public void operationComplete(final Future future) throws Exception {
                    if (!future.isSuccess()) {
                        acquirePromise.tryFailure(future.cause());
                    }
                }
            });
        }

        return acquirePromise;
    }

    private void acquireWithinEventExecutor(final Promise<Channel> acquirePromise) {
        assert this.executor.inEventLoop();

        if (!this.isClosed) {
            // We always want to open new channels if we have spare capacity. Once the pool is full, we'll start looking
            // for idle, pre-existing channels.
            if (this.allChannels.size() + this.pendingCreateChannelFutures.size() < this.capacity) {
                final Future<Channel> createChannelFuture = this.channelFactory
                        .create(executor.<Channel>newPromise());
                this.pendingCreateChannelFutures.add(createChannelFuture);

                createChannelFuture.addListener(new GenericFutureListener<Future<Channel>>() {

                    @Override
                    public void operationComplete(final Future<Channel> future) {
                        ApnsChannelPool.this.pendingCreateChannelFutures.remove(createChannelFuture);

                        if (future.isSuccess()) {
                            final Channel channel = future.getNow();

                            ApnsChannelPool.this.allChannels.add(channel);
                            ApnsChannelPool.this.metricsListener.handleConnectionAdded();

                            acquirePromise.trySuccess(channel);
                        } else {
                            ApnsChannelPool.this.metricsListener.handleConnectionCreationFailed();

                            acquirePromise.tryFailure(future.cause());

                            // If we failed to open a connection, this is the end of the line for this acquisition
                            // attempt, and callers won't be able to release the channel (since they didn't get one
                            // in the first place). Move on to the next acquisition attempt if one is present.
                            ApnsChannelPool.this.handleNextAcquisition();
                        }
                    }
                });
            } else {
                final Channel channelFromIdlePool = ApnsChannelPool.this.idleChannels.poll();

                if (channelFromIdlePool != null) {
                    if (channelFromIdlePool.isActive()) {
                        acquirePromise.trySuccess(channelFromIdlePool);
                    } else {
                        // The channel from the idle pool isn't usable; discard it and create a new one instead
                        this.discardChannel(channelFromIdlePool);
                        this.acquireWithinEventExecutor(acquirePromise);
                    }
                } else {
                    // We don't have any connections ready to go, and don't have any more capacity to create new
                    // channels. Add this acquisition to the queue waiting for channels to become available.
                    pendingAcquisitionPromises.add(acquirePromise);
                }
            }
        } else {
            acquirePromise.tryFailure(POOL_CLOSED_EXCEPTION);
        }
    }

    /**
     * Returns a previously-acquired channel to the pool.
     *
     * @param channel the channel to return to the pool
     */
    void release(final Channel channel) {
        if (this.executor.inEventLoop()) {
            this.releaseWithinEventExecutor(channel);
        } else {
            this.executor.submit(new Runnable() {
                @Override
                public void run() {
                    ApnsChannelPool.this.releaseWithinEventExecutor(channel);
                }
            });
        }
    }

    private void releaseWithinEventExecutor(final Channel channel) {
        assert this.executor.inEventLoop();

        this.idleChannels.add(channel);
        this.handleNextAcquisition();
    }

    private void handleNextAcquisition() {
        assert this.executor.inEventLoop();

        if (!this.pendingAcquisitionPromises.isEmpty()) {
            this.acquireWithinEventExecutor(this.pendingAcquisitionPromises.poll());
        }
    }

    private void discardChannel(final Channel channel) {
        assert this.executor.inEventLoop();

        this.idleChannels.remove(channel);
        this.allChannels.remove(channel);

        this.metricsListener.handleConnectionRemoved();

        this.channelFactory.destroy(channel, this.executor.<Void>newPromise())
                .addListener(new GenericFutureListener<Future<Void>>() {
                    @Override
                    public void operationComplete(final Future<Void> destroyFuture) throws Exception {
                        if (!destroyFuture.isSuccess()) {
                            log.warn("Failed to destroy channel.", destroyFuture.cause());
                        }
                    }
                });
    }

    /**
     * Shuts down this channel pool and releases all retained resources.
     *
     * @return a {@code Future} that will be completed when all resources held by this pool have been released
     */
    public Future<Void> close() {
        return this.allChannels.close().addListener(new GenericFutureListener<Future<Void>>() {
            @Override
            public void operationComplete(final Future<Void> future) throws Exception {
                ApnsChannelPool.this.isClosed = true;

                if (ApnsChannelPool.this.channelFactory instanceof Closeable) {
                    ((Closeable) ApnsChannelPool.this.channelFactory).close();
                }

                for (final Promise<Channel> acquisitionPromise : ApnsChannelPool.this.pendingAcquisitionPromises) {
                    acquisitionPromise.tryFailure(POOL_CLOSED_EXCEPTION);
                }
            }
        });
    }
}