Repository: relayrides/pushy (java)

Issue:
Pushy should provide a way of getting a notification that couldn't be queued
The documentation suggests the best practice is:

```java

pushNotificationResponseHandler = 
   new FutureListener<PushNotificationResponse<SimpleApnsPushNotification>>() {
      public void operationComplete() {
           try {
                responseFuture.get()
           } catch (ClientNotConnectedException e) {
                  apnsClient.getReconnectionFuture.await()
                  //Resend!?
           }
      }
   };

future = apnsClient.sendNotification(pushNotification);
future.addListener(pushNotificationResponseHandler);
```

But there's no way to get a reference in a `FutureListener<PushNotificationResponse<SimpleApnsPushNotification>>` to the push notification which has failed to send.

Handling this case involves one of:

1. Making assumptions about Pushy (i.e. is the queue processed in order - can I just maintain my own queue of unsent notifications and just assume the failed one is the head of queue?
2. Create a handler per notification.

Of these, 1 looks decidedly dodgy and 2 is unscalable.

What's the recommended way of resending `PushNotification`s that fail due to an `ClientNotConnectedException`.

It would be nice if either `ApnsClient` or the `ClientNotConnectedException` itself exposed the `PushNotification`.

Pull request description:
Introduce an application-specific `PushNotificationFuture`
This closes #439 by introducing an application-specific `PushNotificationFuture`. The idea is that the push notification will always be associated with the future, so callers can get the original push notification even if the future fails. I'm opening this pull request to gather feedback on the approach.

One thing I like about this is that it's very application-specific and simplifies some internal plumbing a little (goodbye, `PushNotificationAndResponsePromise`). The main thing I dislike about this specific approach, though, is that we wind up with `SendPushNotificationFuture<T>`, but then `sendPushNotificationFuture.get()` returns something other than `T`. That feels confusing, and I'm concerned it'd be hard to communicate to users how to get details of the response.

Another approach I tried was to have `PushNotificationFuture<P, V> implements Future<V>`. That approach is basically fine; it makes the return type of the future clearer, but feels a lot more verbose and puts us in a position of maybe over-generalizing an application-specific concept.

What do you all think?

Patch:
diff --git a/README.md b/README.md
index 578f0229..04fe2262 100644
--- a/README.md
+++ b/README.md
@@ -83,8 +83,8 @@ final SimpleApnsPushNotification pushNotification;
 The process of sending a push notification is asynchronous; although the process of sending a notification and getting a reply from the server may take some time, the client will return a [`io.netty.util.concurrent.Future`](http://netty.io/4.1/api/io/netty/util/concurrent/Future.html) right away. You can use that `Future` to track the progress and eventual outcome of the sending operation. Note that an `io.netty.util.concurrent.Future` is an extension of the Java [`Future`](http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Future.html) interface that allows callers to add listeners and adds methods for checking the status of the `Future`.
 
 ```java
-final Future<PushNotificationResponse<SimpleApnsPushNotification>> sendNotificationFuture =
-        apnsClient.sendNotification(pushNotification);
+final PushNotificationFuture<SimpleApnsPushNotification, PushNotificationResponse<SimpleApnsPushNotification>>
+    sendNotificationFuture = apnsClient.sendNotification(pushNotification);
 ```
 
 The `Future` will complete in one of three circumstances:
@@ -117,7 +117,30 @@ try {
 }
 ```
 
-Again, it's important to note that the returned `Future` supports listeners; waiting for each individual push notification is inefficient in practice, and most users will be better serverd by adding a listener to the `Future` instead of blocking until it completes.
+Again, it's important to note that the returned `Future` supports listeners; waiting for each individual push notification is inefficient in practice, and most users will be better serverd by adding a listener to the `Future` instead of blocking until it completes. As an example:
+
+```java
+sendNotificationFuture.addListener(new PushNotificationResponseListener<SimpleApnsPushNotification>() {
+
+    @Override
+    public void operationComplete(final PushNotificationFuture<SimpleApnsPushNotification, PushNotificationResponse<SimpleApnsPushNotification>> future) throws Exception {
+        // When using a listener, callers should check for a failure to send a
+        // notification by checking whether the future itself was successful
+        // since an exception will not be thrown.
+        if (future.isSuccess()) {
+            final PushNotificationResponse<SimpleApnsPushNotification> pushNotificationResponse =
+                    sendNotificationFuture.getNow();
+
+            // Handle the push notification response as before from here.
+        } else {
+            // Something went wrong when trying to send the notification to the
+            // APNs gateway. We can find the exception that caused the failure
+            // by getting future.cause().
+            future.cause().printStackTrace();
+        }
+    }
+});
+```
 
 All APNs clients—even those that have never sent a message—may allocate and hold on to system resources, and it's important to release them. APNs clients are intended to be persistent, long-lived resources; you definitely don't need to shut down a client after sending a notification (or even batch of notifications), but you'll want to shut down your client (or clients) when your application is shutting down:
 
diff --git a/pushy/src/main/java/com/turo/pushy/apns/ApnsClient.java b/pushy/src/main/java/com/turo/pushy/apns/ApnsClient.java
index 5f6d3c75..093075ea 100644
--- a/pushy/src/main/java/com/turo/pushy/apns/ApnsClient.java
+++ b/pushy/src/main/java/com/turo/pushy/apns/ApnsClient.java
@@ -24,6 +24,8 @@ package com.turo.pushy.apns;
 
 import com.turo.pushy.apns.auth.ApnsSigningKey;
 import com.turo.pushy.apns.proxy.ProxyHandlerFactory;
+import com.turo.pushy.apns.util.concurrent.PushNotificationFuture;
+import com.turo.pushy.apns.util.concurrent.PushNotificationResponseListener;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelFuture;
 import io.netty.channel.EventLoopGroup;
@@ -181,21 +183,26 @@ public class ApnsClient {
      * <em>send</em> a notification to the gateway—i.e. those that fail with exceptions—should generally be considered
      * non-permanent, and callers should attempt to re-send the notification when the underlying problem has been
      * resolved.</p>
-     ** @param notification the notification to send to the APNs gateway
+     *
+     * @param notification the notification to send to the APNs gateway
      *
      * @param <T> the type of notification to be sent
      *
      * @return a {@code Future} that will complete when the notification has been either accepted or rejected by the
      * APNs gateway
      *
+     * @see com.turo.pushy.apns.util.concurrent.PushNotificationResponseListener
+     *
      * @since 0.8
      */
     @SuppressWarnings("unchecked")
-    public <T extends ApnsPushNotification> Future<PushNotificationResponse<T>> sendNotification(final T notification) {
-        final Future<PushNotificationResponse<T>> responseFuture;
+    public <T extends ApnsPushNotification> PushNotificationFuture<T, PushNotificationResponse<T>> sendNotification(final T notification) {
+        final PushNotificationFuture<T, PushNotificationResponse<T>> responseFuture;
 
         if (!this.isClosed.get()) {
-            final Promise<PushNotificationResponse<ApnsPushNotification>> responsePromise = new DefaultPromise<>(this.eventLoopGroup.next());
+            final PushNotificationPromise<T, PushNotificationResponse<T>> responsePromise =
+                    new PushNotificationPromise<>(this.eventLoopGroup.next(), notification);
+
             final long notificationId = this.nextNotificationId.getAndIncrement();
 
             this.channelPool.acquire().addListener(new GenericFutureListener<Future<Channel>>() {
@@ -204,7 +211,7 @@ public class ApnsClient {
                     if (acquireFuture.isSuccess()) {
                         final Channel channel = acquireFuture.getNow();
 
-                        channel.writeAndFlush(new PushNotificationAndResponsePromise(notification, responsePromise)).addListener(new GenericFutureListener<ChannelFuture>() {
+                        channel.writeAndFlush(responsePromise).addListener(new GenericFutureListener<ChannelFuture>() {
 
                             @Override
                             public void operationComplete(final ChannelFuture future) throws Exception {
@@ -224,12 +231,11 @@ public class ApnsClient {
                 }
             });
 
-            responsePromise.addListener(new GenericFutureListener<Future<PushNotificationResponse<ApnsPushNotification>>>() {
-
+            responsePromise.addListener(new PushNotificationResponseListener<T>() {
                 @Override
-                public void operationComplete(final Future<PushNotificationResponse<ApnsPushNotification>> future) throws Exception {
+                public void operationComplete(final PushNotificationFuture<T, PushNotificationResponse<T>> future) throws Exception {
                     if (future.isSuccess()) {
-                        final PushNotificationResponse<ApnsPushNotification> response = future.getNow();
+                        final PushNotificationResponse response = future.getNow();
 
                         if (response.isAccepted()) {
                             ApnsClient.this.metricsListener.handleNotificationAccepted(ApnsClient.this, notificationId);
@@ -242,9 +248,14 @@ public class ApnsClient {
                 }
             });
 
-            responseFuture = (Future) responsePromise;
+            responseFuture = responsePromise;
         } else {
-            responseFuture = new FailedFuture<>(GlobalEventExecutor.INSTANCE, CLIENT_CLOSED_EXCEPTION);
+            final PushNotificationPromise<T, PushNotificationResponse<T>> failedPromise =
+                    new PushNotificationPromise<>(GlobalEventExecutor.INSTANCE, notification);
+
+            failedPromise.setFailure(CLIENT_CLOSED_EXCEPTION);
+
+            responseFuture = failedPromise;
         }
 
         return responseFuture;
diff --git a/pushy/src/main/java/com/turo/pushy/apns/ApnsClientHandler.java b/pushy/src/main/java/com/turo/pushy/apns/ApnsClientHandler.java
index f5e8da0a..88c927a6 100644
--- a/pushy/src/main/java/com/turo/pushy/apns/ApnsClientHandler.java
+++ b/pushy/src/main/java/com/turo/pushy/apns/ApnsClientHandler.java
@@ -49,10 +49,8 @@ import java.util.concurrent.TimeUnit;
 
 class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameListener, Http2Connection.Listener {
 
-    private final Map<Integer, ApnsPushNotification> unattachedPushNotificationsByStreamId = new IntObjectHashMap<>();
-    private final Map<Integer, Promise<PushNotificationResponse<ApnsPushNotification>>> unattachedResponsePromisesByStreamId = new IntObjectHashMap<>();
+    private final Map<Integer, PushNotificationPromise> unattachedResponsePromisesByStreamId = new IntObjectHashMap<>();
 
-    private final Http2Connection.PropertyKey pushNotificationPropertyKey;
     private final Http2Connection.PropertyKey responseHeadersPropertyKey;
     private final Http2Connection.PropertyKey responsePromisePropertyKey;
 
@@ -146,7 +144,6 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
 
         this.authority = authority;
 
-        this.pushNotificationPropertyKey = this.connection().newKey();
         this.responseHeadersPropertyKey = this.connection().newKey();
         this.responsePromisePropertyKey = this.connection().newKey();
 
@@ -157,11 +154,8 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
 
     @Override
     public void write(final ChannelHandlerContext context, final Object message, final ChannelPromise writePromise) throws Http2Exception, InvalidKeyException, NoSuchAlgorithmException {
-        if (message instanceof PushNotificationAndResponsePromise) {
-            final PushNotificationAndResponsePromise pushNotificationAndResponsePromise =
-                    (PushNotificationAndResponsePromise) message;
-
-            this.writePushNotification(context, pushNotificationAndResponsePromise.getPushNotification(), pushNotificationAndResponsePromise.getResponsePromise(), writePromise);
+        if (message instanceof PushNotificationPromise) {
+            this.writePushNotification(context, (PushNotificationPromise) message, writePromise);
         } else {
             // This should never happen, but in case some foreign debris winds up in the pipeline, just pass it through.
             log.error("Unexpected object in pipeline: {}", message);
@@ -172,11 +166,10 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
     protected void retryPushNotificationFromStream(final ChannelHandlerContext context, final int streamId) {
         final Http2Stream stream = this.connection().stream(streamId);
 
-        final ApnsPushNotification pushNotification = stream.getProperty(this.pushNotificationPropertyKey);
-        final Promise<PushNotificationResponse<ApnsPushNotification>> responsePromise = stream.removeProperty(this.responsePromisePropertyKey);
+        final PushNotificationPromise responsePromise = stream.removeProperty(this.responsePromisePropertyKey);
 
         final ChannelPromise writePromise = context.channel().newPromise();
-        this.writePushNotification(context, pushNotification, responsePromise, writePromise);
+        this.writePushNotification(context, responsePromise, writePromise);
 
         writePromise.addListener(new GenericFutureListener<Future<Void>>() {
             @Override
@@ -188,7 +181,7 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
         });
     }
 
-    private void writePushNotification(final ChannelHandlerContext context, final ApnsPushNotification pushNotification, final Promise<PushNotificationResponse<ApnsPushNotification>> responsePromise, final ChannelPromise writePromise) {
+    private void writePushNotification(final ChannelHandlerContext context, final PushNotificationPromise responsePromise, final ChannelPromise writePromise) {
         final int streamId = this.connection().local().incrementAndGetNextStreamId();
 
         if (streamId > 0) {
@@ -196,8 +189,8 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
             // Because we're using a StreamBufferingEncoder under the hood, there's no guarantee as to when the stream
             // will actually be created, and so we attach these in the onStreamAdded listener to make sure everything
             // is happening in a predictable order.
-            this.unattachedPushNotificationsByStreamId.put(streamId, pushNotification);
             this.unattachedResponsePromisesByStreamId.put(streamId, responsePromise);
+            final ApnsPushNotification pushNotification = responsePromise.getPushNotification();
 
             final Http2Headers headers = getHeadersForPushNotification(pushNotification, streamId);
 
@@ -326,14 +319,16 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
 
     private void handleEndOfStream(final ChannelHandlerContext context, final Http2Stream stream, final Http2Headers headers, final ByteBuf data) {
 
-        final ApnsPushNotification pushNotification = stream.getProperty(this.pushNotificationPropertyKey);
-        final Promise<PushNotificationResponse<ApnsPushNotification>> responsePromise = stream.getProperty(this.responsePromisePropertyKey);
+        final PushNotificationPromise<ApnsPushNotification, PushNotificationResponse<ApnsPushNotification>> responsePromise =
+                stream.getProperty(this.responsePromisePropertyKey);
+
+        final ApnsPushNotification pushNotification = responsePromise.getPushNotification();
 
         final HttpResponseStatus status = HttpResponseStatus.parseLine(headers.status());
 
         if (HttpResponseStatus.OK.equals(status)) {
-            responsePromise.trySuccess(
-                    new SimplePushNotificationResponse<>(pushNotification, true, null, null));
+            responsePromise.trySuccess(new SimplePushNotificationResponse<>(responsePromise.getPushNotification(),
+                    true, null, null));
         } else {
             if (HttpResponseStatus.INTERNAL_SERVER_ERROR.equals(status)) {
                 log.warn("APNs server reported an internal error when sending {}.", pushNotification);
@@ -351,7 +346,7 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
     }
 
     protected void handleErrorResponse(final ChannelHandlerContext context, final int streamId, final Http2Headers headers, final ApnsPushNotification pushNotification, final ErrorResponse errorResponse) {
-        final Promise<PushNotificationResponse<ApnsPushNotification>> responsePromise =
+        final PushNotificationPromise<ApnsPushNotification, PushNotificationResponse<ApnsPushNotification>> responsePromise =
                 this.connection().stream(streamId).getProperty(this.responsePromisePropertyKey);
 
         final HttpResponseStatus status = HttpResponseStatus.parseLine(headers.status());
@@ -360,7 +355,7 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
             log.warn("APNs server reported an internal error when sending {}.", pushNotification);
             responsePromise.tryFailure(new ApnsServerException(GSON.toJson(errorResponse)));
         } else {
-            responsePromise.trySuccess(new SimplePushNotificationResponse<>(pushNotification,
+            responsePromise.trySuccess(new SimplePushNotificationResponse<>(responsePromise.getPushNotification(),
                     HttpResponseStatus.OK.equals(status), errorResponse.getReason(), errorResponse.getTimestamp()));
         }
     }
@@ -422,7 +417,6 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
 
     @Override
     public void onStreamAdded(final Http2Stream stream) {
-        stream.setProperty(ApnsClientHandler.this.pushNotificationPropertyKey, this.unattachedPushNotificationsByStreamId.remove(stream.id()));
         stream.setProperty(ApnsClientHandler.this.responsePromisePropertyKey, this.unattachedResponsePromisesByStreamId.remove(stream.id()));
     }
 
@@ -448,7 +442,6 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
 
     @Override
     public void onStreamRemoved(final Http2Stream stream) {
-        stream.removeProperty(this.pushNotificationPropertyKey);
         stream.removeProperty(this.responseHeadersPropertyKey);
         stream.removeProperty(this.responsePromisePropertyKey);
     }
diff --git a/pushy/src/main/java/com/turo/pushy/apns/PushNotificationPromise.java b/pushy/src/main/java/com/turo/pushy/apns/PushNotificationPromise.java
new file mode 100644
index 00000000..0a9b6794
--- /dev/null
+++ b/pushy/src/main/java/com/turo/pushy/apns/PushNotificationPromise.java
@@ -0,0 +1,21 @@
+package com.turo.pushy.apns;
+
+import com.turo.pushy.apns.util.concurrent.PushNotificationFuture;
+import io.netty.util.concurrent.DefaultPromise;
+import io.netty.util.concurrent.EventExecutor;
+
+class PushNotificationPromise<P extends ApnsPushNotification, V> extends DefaultPromise<V> implements PushNotificationFuture<P, V> {
+
+    private final P pushNotification;
+
+    PushNotificationPromise(final EventExecutor eventExecutor, final P pushNotification) {
+        super(eventExecutor);
+
+        this.pushNotification = pushNotification;
+    }
+
+    @Override
+    public P getPushNotification() {
+        return this.pushNotification;
+    }
+}
diff --git a/pushy/src/main/java/com/turo/pushy/apns/util/concurrent/PushNotificationFuture.java b/pushy/src/main/java/com/turo/pushy/apns/util/concurrent/PushNotificationFuture.java
new file mode 100644
index 00000000..010920c5
--- /dev/null
+++ b/pushy/src/main/java/com/turo/pushy/apns/util/concurrent/PushNotificationFuture.java
@@ -0,0 +1,22 @@
+package com.turo.pushy.apns.util.concurrent;
+
+import com.turo.pushy.apns.ApnsPushNotification;
+import io.netty.util.concurrent.Future;
+
+/**
+ * A push notification future represents the result an operation on a push notification.
+ *
+ * @param <P> the type of push notification sent
+ * @param <V> the type of value returned by the operation
+ *
+ * @author <a href="https://github.com/jchambers">Jon Chambers</a>
+ */
+public interface PushNotificationFuture<P extends ApnsPushNotification, V> extends Future<V> {
+
+    /**
+     * Returns the push notification to which the operation represented by this future applies.
+     *
+     * @return the push notification to which the operation represented by this future applies
+     */
+    P getPushNotification();
+}
diff --git a/pushy/src/main/java/com/turo/pushy/apns/util/concurrent/PushNotificationResponseListener.java b/pushy/src/main/java/com/turo/pushy/apns/util/concurrent/PushNotificationResponseListener.java
new file mode 100644
index 00000000..a20f69f5
--- /dev/null
+++ b/pushy/src/main/java/com/turo/pushy/apns/util/concurrent/PushNotificationResponseListener.java
@@ -0,0 +1,38 @@
+/*
+ * 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.util.concurrent;
+
+import com.turo.pushy.apns.ApnsPushNotification;
+import com.turo.pushy.apns.PushNotificationResponse;
+import io.netty.util.concurrent.GenericFutureListener;
+
+/**
+ * A type-specific convenience interface for listening for the result of an attempt to send a push notification.
+ *
+ * @param <T> the type of push notification sent
+ *
+ * @see com.turo.pushy.apns.ApnsClient#sendNotification(ApnsPushNotification)
+ */
+public interface PushNotificationResponseListener<T extends ApnsPushNotification>
+        extends GenericFutureListener<PushNotificationFuture<T, PushNotificationResponse<T>>> {
+}
diff --git a/pushy/src/main/java/com/turo/pushy/apns/PushNotificationAndResponsePromise.java b/pushy/src/main/java/com/turo/pushy/apns/util/concurrent/package-info.java
similarity index 55%
rename from pushy/src/main/java/com/turo/pushy/apns/PushNotificationAndResponsePromise.java
rename to pushy/src/main/java/com/turo/pushy/apns/util/concurrent/package-info.java
index b05dc002..0b8bc6ed 100644
--- a/pushy/src/main/java/com/turo/pushy/apns/PushNotificationAndResponsePromise.java
+++ b/pushy/src/main/java/com/turo/pushy/apns/util/concurrent/package-info.java
@@ -20,32 +20,9 @@
  * THE SOFTWARE.
  */
 
-package com.turo.pushy.apns;
-
-import io.netty.util.concurrent.Promise;
-
 /**
- * A for-internal-use-only ttuple of a push notification and a {@link Promise} to be notified with the outcome of the
- * attempt to send the notification.
+ * Contains application-specific interfaces for working with asynchronous push notification operations.
  *
  * @author <a href="https://github.com/jchambers">Jon Chambers</a>
- *
- * @since 0.10
  */
-class PushNotificationAndResponsePromise {
-    private final ApnsPushNotification pushNotification;
-    private final Promise<PushNotificationResponse<ApnsPushNotification>> responsePromise;
-
-    public PushNotificationAndResponsePromise(final ApnsPushNotification pushNotification, final Promise<PushNotificationResponse<ApnsPushNotification>> responsePromise) {
-        this.pushNotification = pushNotification;
-        this.responsePromise = responsePromise;
-    }
-
-    public ApnsPushNotification getPushNotification() {
-        return this.pushNotification;
-    }
-
-    public Promise<PushNotificationResponse<ApnsPushNotification>> getResponsePromise() {
-        return this.responsePromise;
-    }
-}
+package com.turo.pushy.apns.util.concurrent;


Test patch:
diff --git a/pushy/src/test/java/com/turo/pushy/apns/ExampleApp.java b/pushy/src/test/java/com/turo/pushy/apns/ExampleApp.java
index 4ecadf65..387635ce 100644
--- a/pushy/src/test/java/com/turo/pushy/apns/ExampleApp.java
+++ b/pushy/src/test/java/com/turo/pushy/apns/ExampleApp.java
@@ -27,6 +27,8 @@ import com.turo.pushy.apns.proxy.Socks5ProxyHandlerFactory;
 import com.turo.pushy.apns.util.ApnsPayloadBuilder;
 import com.turo.pushy.apns.util.SimpleApnsPushNotification;
 import com.turo.pushy.apns.util.TokenUtil;
+import com.turo.pushy.apns.util.concurrent.PushNotificationFuture;
+import com.turo.pushy.apns.util.concurrent.PushNotificationResponseListener;
 import io.netty.util.concurrent.Future;
 
 import java.io.File;
@@ -85,7 +87,7 @@ public class ExampleApp {
         // immediately, but will need to wait for the Future to complete before
         // we'll know whether the notification was accepted or rejected by the
         // APNs gateway.
-        final Future<PushNotificationResponse<SimpleApnsPushNotification>> sendNotificationFuture =
+        final PushNotificationFuture<SimpleApnsPushNotification, PushNotificationResponse<SimpleApnsPushNotification>> sendNotificationFuture =
                 apnsClient.sendNotification(pushNotification);
 
         try {
@@ -115,6 +117,29 @@ public class ExampleApp {
             e.printStackTrace();
         }
 
+        // To send push notifications efficiently, it's best to attach listeners
+        // to "send push notification" futures so we don't have to wait for the
+        // server to reply before we start sending the next notification.
+        sendNotificationFuture.addListener(new PushNotificationResponseListener<SimpleApnsPushNotification>() {
+            @Override
+            public void operationComplete(final PushNotificationFuture<SimpleApnsPushNotification, PushNotificationResponse<SimpleApnsPushNotification>> future) throws Exception {
+                // When using a listener, callers should check for a failure to send a
+                // notification by checking whether the future itself was successful
+                // since an exception will not be thrown.
+                if (future.isSuccess()) {
+                    final PushNotificationResponse<SimpleApnsPushNotification> pushNotificationResponse =
+                            sendNotificationFuture.getNow();
+
+                    // Handle the push notification response as before from here.
+                } else {
+                    // Something went wrong when trying to send the notification to the
+                    // APNs gateway. We can find the exception that caused the failure
+                    // by getting future.cause().
+                    future.cause().printStackTrace();
+                }
+            }
+        });
+
         // Finally, when we're done sending notifications (i.e. when our
         // application is shutting down), we should close all APNs clients
         // that may be in play.