Skip to content

Commit

Permalink
Introduce ApnsClientResources
Browse files Browse the repository at this point in the history
  • Loading branch information
jchambers committed Jul 6, 2024
1 parent d4594ac commit d62d4fa
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
@State(Scope.Thread)
public class ApnsClientBenchmark {

private NioEventLoopGroup clientEventLoopGroup;
private ApnsClientResources clientResources;
private NioEventLoopGroup serverEventLoopGroup;

private ApnsClient client;
Expand Down Expand Up @@ -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;
Expand All @@ -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()
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,24 +67,20 @@ class ApnsChannelFactory implements PooledObjectFactory<Channel>, Closeable {
AttributeKey.valueOf(ApnsChannelFactory.class, "channelReadyPromise");

ApnsChannelFactory(final ApnsClientConfiguration clientConfiguration,
final EventLoopGroup eventLoopGroup) {
final ApnsClientResources clientResources) {

this.sslContext = clientConfiguration.getSslContext();

if (this.sslContext instanceof ReferenceCounted) {
((ReferenceCounted) this.sslContext).retain();
}

if (clientConfiguration.getProxyHandlerFactory().isPresent()) {
this.addressResolverGroup = NoopAddressResolverGroup.INSTANCE;
} else {
this.addressResolverGroup = new RoundRobinDnsAddressResolverGroup(
ClientChannelClassUtil.getDatagramChannelClass(eventLoopGroup),
DefaultDnsServerAddressStreamProvider.INSTANCE);
}
this.addressResolverGroup = clientConfiguration.getProxyHandlerFactory().isPresent()
? NoopAddressResolverGroup.INSTANCE
: clientResources.getRoundRobinDnsAddressResolverGroup();

this.bootstrapTemplate = new Bootstrap();
this.bootstrapTemplate.group(eventLoopGroup);
this.bootstrapTemplate.group(clientResources.getEventLoopGroup());
this.bootstrapTemplate.option(ChannelOption.TCP_NODELAY, true);
this.bootstrapTemplate.remoteAddress(clientConfiguration.getApnsServerAddress());
this.bootstrapTemplate.resolver(this.addressResolverGroup);
Expand Down
49 changes: 25 additions & 24 deletions pushy/src/main/java/com/eatthepath/pushy/apns/ApnsClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import com.eatthepath.pushy.apns.util.concurrent.PushNotificationFuture;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
Expand All @@ -44,12 +43,12 @@
* <a href="https://developer.apple.com/documentation/usernotifications">UserNotifications Framework documentation</a>
* for a detailed discussion of the APNs protocol, topics, and certificate/key provisioning.</p>
*
* <p>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}).</p>
* <p>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}).</p>
*
* <p>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,
Expand All @@ -66,17 +65,17 @@
*
* <p>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.</p>
* 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()}).</p>
*
* @author <a href="https://github.com/jchambers">Jon Chambers</a>
*
* @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;

Expand Down Expand Up @@ -117,21 +116,20 @@ public void handleConnectionCreationFailed() {
}
}

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() {

Expand All @@ -151,7 +149,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);
}

/**
Expand Down Expand Up @@ -223,7 +224,7 @@ public <T extends ApnsPushNotification> PushNotificationFuture<T, PushNotificati
* shutdown process begins; the {@code Futures} associated with those notifications will fail.</p>
*
* <p>The 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.</p>
*
Expand All @@ -242,8 +243,8 @@ public CompletableFuture<Void> close() {
closeFuture = new CompletableFuture<>();

this.channelPool.close().addListener((GenericFutureListener<Future<Void>>) closePoolFuture -> {
if (ApnsClient.this.shouldShutDownEventLoopGroup) {
ApnsClient.this.eventLoopGroup.shutdownGracefully().addListener(future -> closeFuture.complete(null));
if (ApnsClient.this.shouldShutDownClientResources) {
ApnsClient.this.clientResources.shutdownGracefully().addListener(future -> closeFuture.complete(null));
} else {
closeFuture.complete(null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

import com.eatthepath.pushy.apns.auth.ApnsSigningKey;
import com.eatthepath.pushy.apns.proxy.ProxyHandlerFactory;
import io.netty.channel.EventLoopGroup;
import io.netty.handler.codec.http2.Http2FrameLogger;
import io.netty.handler.codec.http2.Http2SecurityUtil;
import io.netty.handler.ssl.*;
Expand Down Expand Up @@ -70,7 +69,7 @@ public class ApnsClientBuilder {

private boolean enableHostnameVerification = true;

private EventLoopGroup eventLoopGroup;
private ApnsClientResources apnsClientResources;

private int concurrentConnections = 1;

Expand Down Expand Up @@ -408,24 +407,30 @@ public ApnsClientBuilder setHostnameVerificationEnabled(final boolean hostnameVe
}

/**
* <p>Sets 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.</p>
* <p>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.</p>
*
* <p>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).</p>
* <p>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).</p>
*
* @param eventLoopGroup the event loop group to use for this client, or {@code null} to let the client manage its
* own event loop group
* <p>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.</p>
*
* @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;
}

Expand Down Expand Up @@ -652,7 +657,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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <a href="https://github.com/jchambers">Jon Chambers</a>
*
* @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 <em>must</em> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
}

Expand All @@ -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<Void> shutdownPromise = new DefaultPromise<>(GlobalEventExecutor.INSTANCE);
combiner.finish(shutdownPromise);
Expand All @@ -117,7 +117,7 @@ protected ApnsClient buildTlsAuthenticationClient(final ApnsClientMetricsListene
.setApnsServer(HOST, PORT)
.setClientCredentials(p12InputStream, KEYSTORE_PASSWORD)
.setTrustedServerCertificateChain(getClass().getResourceAsStream(CA_CERTIFICATE_FILENAME))
.setEventLoopGroup(CLIENT_EVENT_LOOP_GROUP)
.setApnsClientResources(CLIENT_RESOURCES)
.setMetricsListener(metricsListener)
.build();
}
Expand All @@ -132,7 +132,7 @@ protected ApnsClient buildTokenAuthenticationClient(final ApnsClientMetricsListe
.setApnsServer(HOST, PORT)
.setTrustedServerCertificateChain(getClass().getResourceAsStream(CA_CERTIFICATE_FILENAME))
.setSigningKey(this.signingKey)
.setEventLoopGroup(CLIENT_EVENT_LOOP_GROUP)
.setApnsClientResources(CLIENT_RESOURCES)
.setMetricsListener(metricsListener)
.build();
}
Expand Down
Loading

0 comments on commit d62d4fa

Please sign in to comment.