
com.eatthepath.pushy.apns.ApnsChannelPool Maven / Gradle / Ivy
/*
* Copyright (c) 2020 Jon Chambers
*
* 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.eatthepath.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;
/**
* 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.
*
* 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.
*
* 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.
*
* @since 0.11
*/
class ApnsChannelPool {
private final PooledObjectFactory channelFactory;
private final OrderedEventExecutor executor;
private final int capacity;
private final ApnsChannelPoolMetricsListener metricsListener;
private final ChannelGroup allChannels;
private final Queue idleChannels = new ArrayDeque<>();
private final Set> pendingCreateChannelFutures = new HashSet<>();
private final Queue> 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 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);
}
/**
* 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.
*
* When callers are done with a channel, they must release the channel back to the pool via the
* {@link ApnsChannelPool#release(Channel)} method.
*
* @return a {@code Future} that will be notified when a channel is available
*
* @see ApnsChannelPool#release(Channel)
*/
Future acquire() {
final Promise acquirePromise = new DefaultPromise<>(this.executor);
if (this.executor.inEventLoop()) {
this.acquireWithinEventExecutor(acquirePromise);
} else {
this.executor.submit(() ->
ApnsChannelPool.this.acquireWithinEventExecutor(acquirePromise)).addListener(future -> {
if (!future.isSuccess()) {
acquirePromise.tryFailure(future.cause());
}
});
}
return acquirePromise;
}
private void acquireWithinEventExecutor(final Promise 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 createChannelFuture = this.channelFactory.create(executor.newPromise());
this.pendingCreateChannelFutures.add(createChannelFuture);
createChannelFuture.addListener((GenericFutureListener>) future -> {
ApnsChannelPool.this.pendingCreateChannelFutures.remove(createChannelFuture);
if (future.isSuccess()) {
final Channel channel = future.getNow();
ApnsChannelPool.this.allChannels.add(channel);
ApnsChannelPool.this.metricsListener.handleConnectionAdded();
log.debug("Created channel {}", channel);
acquirePromise.trySuccess(channel);
} else {
ApnsChannelPool.this.metricsListener.handleConnectionCreationFailed();
log.warn("Failed to create channel", future.cause());
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(() -> 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);
log.debug("Discarded channel {}", channel);
this.metricsListener.handleConnectionRemoved();
this.channelFactory.destroy(channel, this.executor.newPromise()).addListener(destroyFuture -> {
if (!destroyFuture.isSuccess()) {
log.warn("Failed to destroy channel {}", 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 close() {
final Promise closePromise = new DefaultPromise<>(this.executor);
this.allChannels.close().addListener(allCloseFuture -> {
ApnsChannelPool.this.isClosed = true;
final Promise waitForPendingCreateChannelFuturesPromise = new DefaultPromise<>(ApnsChannelPool.this.executor);
final PromiseCombiner combiner = new PromiseCombiner(ApnsChannelPool.this.executor);
for (final Future f : pendingCreateChannelFutures) {
combiner.add(f);
}
combiner.finish(waitForPendingCreateChannelFuturesPromise);
waitForPendingCreateChannelFuturesPromise.addListener(pendingChannelsFuture -> {
if (ApnsChannelPool.this.channelFactory instanceof Closeable) {
((Closeable) ApnsChannelPool.this.channelFactory).close();
}
for (final Promise acquisitionPromise : ApnsChannelPool.this.pendingAcquisitionPromises) {
acquisitionPromise.tryFailure(POOL_CLOSED_EXCEPTION);
}
closePromise.setSuccess(null);
});
});
return closePromise;
}
}