diff --git a/benchmark/src/main/java/com/eatthepath/pushy/apns/ApnsClientBenchmark.java b/benchmark/src/main/java/com/eatthepath/pushy/apns/ApnsClientBenchmark.java
index 438d864b1..6ba40b4f7 100644
--- a/benchmark/src/main/java/com/eatthepath/pushy/apns/ApnsClientBenchmark.java
+++ b/benchmark/src/main/java/com/eatthepath/pushy/apns/ApnsClientBenchmark.java
@@ -44,7 +44,7 @@
@State(Scope.Thread)
public class ApnsClientBenchmark {
- private NioEventLoopGroup clientEventLoopGroup;
+ private ApnsClientResources clientResources;
private NioEventLoopGroup serverEventLoopGroup;
private ApnsClient client;
@@ -75,7 +75,7 @@ public class ApnsClientBenchmark {
@Setup
public void setUp() throws Exception {
- this.clientEventLoopGroup = new NioEventLoopGroup(this.concurrentConnections);
+ this.clientResources = new ApnsClientResources(new NioEventLoopGroup(this.concurrentConnections));
this.serverEventLoopGroup = new NioEventLoopGroup(this.concurrentConnections);
final ApnsSigningKey signingKey;
@@ -91,7 +91,7 @@ public void setUp() throws Exception {
.setConcurrentConnections(this.concurrentConnections)
.setSigningKey(signingKey)
.setTrustedServerCertificateChain(ApnsClientBenchmark.class.getResourceAsStream(CA_CERTIFICATE_FILENAME))
- .setEventLoopGroup(this.clientEventLoopGroup)
+ .setApnsClientResources(this.clientResources)
.build();
this.server = new BenchmarkApnsServerBuilder()
@@ -136,7 +136,7 @@ public void tearDown() throws Exception {
this.client.close().get();
this.server.shutdown().get();
- final Future> clientShutdownFuture = this.clientEventLoopGroup.shutdownGracefully();
+ final Future> clientShutdownFuture = this.clientResources.shutdownGracefully();
final Future> serverShutdownFuture = this.serverEventLoopGroup.shutdownGracefully();
clientShutdownFuture.await();
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsChannelFactory.java b/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsChannelFactory.java
index c41b4575b..acd0fd4fc 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsChannelFactory.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsChannelFactory.java
@@ -31,8 +31,6 @@
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.resolver.AddressResolverGroup;
import io.netty.resolver.NoopAddressResolverGroup;
-import io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider;
-import io.netty.resolver.dns.RoundRobinDnsAddressResolverGroup;
import io.netty.util.AttributeKey;
import io.netty.util.ReferenceCounted;
import io.netty.util.concurrent.Future;
@@ -69,7 +67,7 @@ class ApnsChannelFactory implements PooledObjectFactory
Clients are constructed using an {@link ApnsClientBuilder}. Callers may - * optionally specify an {@link EventLoopGroup} when constructing a new client. If no event loop group is specified, - * clients will create and manage their own single-thread event loop group. If many clients are operating in parallel, - * specifying a shared event loop group serves as a mechanism to keep the total number of threads in check. Callers may - * also want to provide a specific event loop group to take advantage of platform-specific features (i.e. - * {@code epoll} or {@code KQueue}).
+ *Clients are constructed using an {@link ApnsClientBuilder}. Callers may optionally specify a set of + * {@link ApnsClientResources} when constructing a new client. If no client resources are specified, + * clients will create and manage their own resources with a single-thread event loop group. If many clients are + * operating in parallel, specifying a shared ser of resources serves as a mechanism to keep the total number of threads + * in check. Callers may also want to provide a specific event loop group to take advantage of platform-specific + * features (i.e. {@code epoll} or {@code KQueue}).
* *Callers must either provide an SSL context with the client's certificate or a signing key at client construction * time. If a signing key is provided, the client will use token authentication when sending notifications; otherwise, @@ -67,17 +66,17 @@ * *
APNs clients are intended to be long-lived, persistent resources. They are also inherently thread-safe and can be * shared across many threads in a complex application. Callers must shut them down via the {@link ApnsClient#close()} - * method when they are no longer needed (i.e. when shutting down the entire application). If an event loop group was - * specified at construction time, callers should shut down that event loop group when all clients using that group have - * been disconnected.
+ * method when they are no longer needed (i.e. when shutting down the entire application). If a set of client resources + * was provided at construction time, callers should shut down that resource set when all clients using that group have + * been disconnected (see {@link ApnsClientResources#shutdownGracefully()}). * * @author Jon Chambers * * @since 0.5 */ public class ApnsClient { - private final EventLoopGroup eventLoopGroup; - private final boolean shouldShutDownEventLoopGroup; + private final ApnsClientResources clientResources; + private final boolean shouldShutDownClientResources; private final ApnsChannelPool channelPool; @@ -122,21 +121,20 @@ public void handleConnectionCreationFailed(final ApnsClient apnsClient) { } } - protected ApnsClient(final ApnsClientConfiguration clientConfiguration, final EventLoopGroup eventLoopGroup) { + ApnsClient(final ApnsClientConfiguration clientConfiguration, final ApnsClientResources clientResources) { - if (eventLoopGroup != null) { - this.eventLoopGroup = eventLoopGroup; - this.shouldShutDownEventLoopGroup = false; + if (clientResources != null) { + this.clientResources = clientResources; + this.shouldShutDownClientResources = false; } else { - this.eventLoopGroup = new NioEventLoopGroup(1); - this.shouldShutDownEventLoopGroup = true; + this.clientResources = new ApnsClientResources(new NioEventLoopGroup(1)); + this.shouldShutDownClientResources = true; } this.metricsListener = clientConfiguration.getMetricsListener() .orElseGet(NoopApnsClientMetricsListener::new); - final ApnsChannelFactory channelFactory = - new ApnsChannelFactory(clientConfiguration, this.eventLoopGroup); + final ApnsChannelFactory channelFactory = new ApnsChannelFactory(clientConfiguration, this.clientResources); final ApnsChannelPoolMetricsListener channelPoolMetricsListener = new ApnsChannelPoolMetricsListener() { @@ -156,7 +154,10 @@ public void handleConnectionCreationFailed() { } }; - this.channelPool = new ApnsChannelPool(channelFactory, clientConfiguration.getConcurrentConnections(), this.eventLoopGroup.next(), channelPoolMetricsListener); + this.channelPool = new ApnsChannelPool(channelFactory, + clientConfiguration.getConcurrentConnections(), + this.clientResources.getEventLoopGroup().next(), + channelPoolMetricsListener); } /** @@ -230,7 +231,7 @@ publicThe returned {@code Future} will be marked as complete when all connections in this client's pool have closed - * completely and (if no {@code EventLoopGroup} was provided at construction time) the client's event loop group has + * completely and (if no {@code ApnsClientResources} were provided at construction time) the client's resources have * shut down. If the client has already shut down, the returned {@code Future} will be marked as complete * immediately.
* @@ -249,8 +250,8 @@ public CompletableFutureSets the event loop group to be used by the client under construction. If not set (or if {@code null}), the - * client will create and manage its own event loop group.
+ *Sets the client resources to be used by the client under construction. If not set (or if {@code null}), the + * client will manage its own resources.
* - *Generally speaking, callers don't need to set event loop groups for clients, but it may be useful to specify - * an event loop group under certain circumstances. In particular, specifying an event loop group that is shared - * among multiple {@code ApnsClient} instances can keep thread counts manageable. Regardless of the number of - * concurrent {@code ApnsClient} instances, callers may also wish to specify an event loop group to take advantage - * of certain platform-specific optimizations (e.g. {@code epoll} or {@code KQueue} event loop groups).
+ *Callers generally don't need to specify resources groups for clients if they only expect to have a single + * {@code ApnsClient} instance, but may benefit from specifying a shared set of {@code ApnsClientResources} if they + * expect to have multiple concurrent clients. Specifying an event loop group that is shared among multiple + * {@code ApnsClient} instances can keep thread counts in check because each client will not need to create its own + * thread pool. Regardless of the number of concurrent {@code ApnsClient} instances, callers may also wish to + * specify an event loop group to take advantage of certain platform-specific optimizations (e.g. {@code epoll} or + * {@code KQueue} event loop groups).
* - * @param eventLoopGroup the event loop group to use for this client, or {@code null} to let the client manage its - * own event loop group + *Callers that expect to have multiple concurrent {@code ApnsClient} instances will also benefit from sharing an + * {@code ApnsClientResources} instance between clients because resource sets contain a shared DNS resolver, + * eliminating the need for each client to manage its own DNS connections.
+ * + * @param apnsClientResources the client resources to use for this client, or {@code null} to let the client manage + * its own resources * * @return a reference to this builder * - * @since 0.8 + * @since 0.16 */ - public ApnsClientBuilder setEventLoopGroup(final EventLoopGroup eventLoopGroup) { - this.eventLoopGroup = eventLoopGroup; + public ApnsClientBuilder setApnsClientResources(final ApnsClientResources apnsClientResources) { + this.apnsClientResources = apnsClientResources; return this; } @@ -634,7 +639,7 @@ public ApnsClient build() throws SSLException { this.metricsListener, this.frameLogger); - return new ApnsClient(clientConfiguration, this.eventLoopGroup); + return new ApnsClient(clientConfiguration, this.apnsClientResources); } finally { if (sslContext instanceof ReferenceCounted) { ((ReferenceCounted) sslContext).release(); diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsClientResources.java b/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsClientResources.java new file mode 100644 index 000000000..65792a552 --- /dev/null +++ b/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsClientResources.java @@ -0,0 +1,70 @@ +package com.eatthepath.pushy.apns; + +import io.netty.channel.EventLoopGroup; +import io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider; +import io.netty.resolver.dns.RoundRobinDnsAddressResolverGroup; +import io.netty.util.concurrent.Future; + +import java.util.Objects; + +/** + * APNs client resources are bundles of relatively "expensive" objects (thread pools, DNS resolvers, etc.) that can be + * shared between {@link ApnsClient} instances. + * + * @see ApnsClientBuilder#setApnsClientResources(ApnsClientResources) + * + * @author Jon Chambers + * + * @since 0.16 + */ +public class ApnsClientResources { + + private final EventLoopGroup eventLoopGroup; + private final RoundRobinDnsAddressResolverGroup roundRobinDnsAddressResolverGroup; + + /** + * Constructs a new set of client resources that uses the given default event loop group. Clients that use this + * resource set will use the given event loop group for IO operations. + * + * @param eventLoopGroup the event loop group for this set of resources + */ + public ApnsClientResources(final EventLoopGroup eventLoopGroup) { + this.eventLoopGroup = Objects.requireNonNull(eventLoopGroup); + + this.roundRobinDnsAddressResolverGroup = new RoundRobinDnsAddressResolverGroup( + ClientChannelClassUtil.getDatagramChannelClass(eventLoopGroup), + DefaultDnsServerAddressStreamProvider.INSTANCE); + } + + /** + * Returns the event loop group for this resource set. + * + * @return the event loop group for this resource set + */ + public EventLoopGroup getEventLoopGroup() { + return eventLoopGroup; + } + + /** + * Returns the DNS resolver for this resource set. + * + * @return the DNS resolver for this resource set + */ + public RoundRobinDnsAddressResolverGroup getRoundRobinDnsAddressResolverGroup() { + return roundRobinDnsAddressResolverGroup; + } + + /** + * Gracefully shuts down any long-lived resources in this resource group. If callers manage their own + * {@code ApnsClientResources} instances (as opposed to using default resources provided by {@link ApnsClientBuilder}, + * then they must call this method after all clients that use a given set of resources have been shut down. + * + * @return a future that completes once the long-lived resources in this set of resources has finished shutting down + * + * @see ApnsClientBuilder#setApnsClientResources(ApnsClientResources) + */ + public Future> shutdownGracefully() { + roundRobinDnsAddressResolverGroup.close(); + return eventLoopGroup.shutdownGracefully(); + } +} diff --git a/pushy/src/test/java/com/eatthepath/pushy/apns/AbstractClientServerTest.java b/pushy/src/test/java/com/eatthepath/pushy/apns/AbstractClientServerTest.java index 8b73fc5f1..36d8aeae3 100644 --- a/pushy/src/test/java/com/eatthepath/pushy/apns/AbstractClientServerTest.java +++ b/pushy/src/test/java/com/eatthepath/pushy/apns/AbstractClientServerTest.java @@ -47,7 +47,7 @@ @Timeout(10) public class AbstractClientServerTest { - protected static NioEventLoopGroup CLIENT_EVENT_LOOP_GROUP; + protected static ApnsClientResources CLIENT_RESOURCES; protected static NioEventLoopGroup SERVER_EVENT_LOOP_GROUP; protected static final String CA_CERTIFICATE_FILENAME = "/ca.pem"; @@ -81,7 +81,7 @@ public class AbstractClientServerTest { @BeforeAll public static void setUpBeforeClass() { - CLIENT_EVENT_LOOP_GROUP = new NioEventLoopGroup(2); + CLIENT_RESOURCES = new ApnsClientResources(new NioEventLoopGroup(2)); SERVER_EVENT_LOOP_GROUP = new NioEventLoopGroup(2); } @@ -100,7 +100,7 @@ public void setUp() throws Exception { @AfterAll public static void tearDownAfterClass() throws Exception { final PromiseCombiner combiner = new PromiseCombiner(ImmediateEventExecutor.INSTANCE); - combiner.addAll(CLIENT_EVENT_LOOP_GROUP.shutdownGracefully(), SERVER_EVENT_LOOP_GROUP.shutdownGracefully()); + combiner.addAll(CLIENT_RESOURCES.shutdownGracefully(), SERVER_EVENT_LOOP_GROUP.shutdownGracefully()); final Promise