Skip to content

Commit

Permalink
Merge pull request #66 from relayrides/v_0_3_documentation_updates
Browse files Browse the repository at this point in the history
v0.3 documentation updates
  • Loading branch information
jchambers committed Apr 7, 2014
2 parents 92e165a + c801ea5 commit eed428e
Show file tree
Hide file tree
Showing 20 changed files with 381 additions and 159 deletions.
83 changes: 57 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
*Note: this README refers to the current development version of Pushy and may include information and examples that refer to changes that have not yet been released. For notes on the latest release, please visit the [project page](http://relayrides.github.io/pushy/).*

# pushy

Pushy is a Java library for sending [APNs](http://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Introduction.html) (iOS and OS X) push notifications. It is written and maintained by the engineers at [RelayRides](https://relayrides.com/) and is built on the [Netty framework](http://netty.io/).

Pushy was created because we found that the other APNs libraries for Java simply didn't meet our needs in terms of reliability or performance. Pushy distinguishes itself from other libraries with several important features:
Pushy was created because we found that the other APNs libraries for Java simply didn't meet our needs in terms of performance or (especially) reliability. Pushy distinguishes itself from other libraries with several important features:

- Asynchronous network IO (via Netty) for maximum performance
- Efficient connection management (other libraries appear to reconnect to the APNs gateway far more frequently than is really necessary)
- Graceful handling and reporting of permanent notification rejections
- Thorough [documentation](http://relayrides.github.io/pushy/apidocs/0.2/)
- Asynchronous network IO (via Netty) for [maximum performance]("https://github.com/relayrides/pushy/wiki/Performance")
- Efficient connection management
- Graceful handling and reporting of permanent notification rejections and connection failures
- Thorough [documentation](http://relayrides.github.io/pushy/apidocs/0.3/)

We believe that Pushy is already the best tool for sending APNs push notifications from Java applications, and we hope you'll help us make it even better via bug reports and pull requests. If you have other questions about using Pushy, please join us on [the Pushy mailing list](https://groups.google.com/d/forum/pushy-apns). Thanks!
We believe that Pushy is already the best tool for sending APNs push notifications from Java applications, and we hope you'll help us make it even better via bug reports and pull requests. If you have questions about using Pushy, please join us on [the Pushy mailing list](https://groups.google.com/d/forum/pushy-apns) or take a look at [the wiki](https://github.com/relayrides/pushy/wiki). Thanks!

## Getting Pushy

Expand All @@ -21,19 +19,19 @@ If you use [Maven](http://maven.apache.org/), you can add Pushy to your project
<dependency>
<groupId>com.relayrides</groupId>
<artifactId>pushy</artifactId>
<version>0.2</version>
<version>0.3</version>
</dependency>
```

If you don't use Maven, you can [download Pushy as a `.jar` file](https://github.com/relayrides/pushy/releases/download/pushy-0.2/pushy-0.2.jar) and add it to your project directly. You'll also need to make sure you have Pushy's runtime dependencies on your classpath. They are:
If you don't use Maven, you can [download Pushy as a `.jar` file](https://github.com/relayrides/pushy/releases/download/pushy-0.3/pushy-0.3.jar) and add it to your project directly. You'll also need to make sure you have Pushy's runtime dependencies on your classpath. They are:

- [netty 4.0.17.Final](http://netty.io/)
- [netty 4.0.18.Final](http://netty.io/)
- [slf4j 1.7.6](http://www.slf4j.org/)
- [json.simple 1.1.1](https://code.google.com/p/json-simple/)

## Using Pushy

The main public-facing part of Pushy is the [`PushManager`](http://relayrides.github.io/pushy/apidocs/0.2/com/relayrides/pushy/apns/PushManager.html) class, which manages connections to APNs and manages the queue of outbound notifications. Before you can create a `PushManager`, though, you'll need appropriate SSL certificates and keys from Apple. They can be obtained by following the steps in Apple's ["Provisioning and Development"](http://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ProvisioningDevelopment.html#//apple_ref/doc/uid/TP40008194-CH104-SW1) guide.
The main public-facing part of Pushy is the [`PushManager`](http://relayrides.github.io/pushy/apidocs/0.3/com/relayrides/pushy/apns/PushManager.html) class, which manages connections to APNs and manages the queue of outbound notifications. Before you can create a `PushManager`, though, you'll need appropriate SSL certificates and keys from Apple. They can be obtained by following the steps in Apple's ["Provisioning and Development"](http://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ProvisioningDevelopment.html#//apple_ref/doc/uid/TP40008194-CH104-SW1) guide.

Once you have your certificates and keys, you can construct a new `PushManager` like this:

Expand All @@ -50,7 +48,7 @@ final PushManager<SimpleApnsPushNotification> pushManager =
pushManager.start();
```

Once you have your `PushManager` constructed and started, you're ready to start constructing and sending push notifications. Pushy provides a number of utility classes for working with APNs tokens and payloads. Here's an example:
Once you have your `PushManager` constructed and started, you're ready to start constructing and sending push notifications. Pushy provides utility classes for working with APNs tokens and payloads. Here's an example:

```java
final byte[] token = TokenUtil.tokenStringToByteArray(
Expand All @@ -63,39 +61,72 @@ payloadBuilder.setSoundFileName("ring-ring.aiff");

final String payload = payloadBuilder.buildWithDefaultMaximumLength();

pushManager.getQueue().put(
new SimpleApnsPushNotification(token, payload));
pushManager.getQueue().put(new SimpleApnsPushNotification(token, payload));
```

When your application shuts down, make sure to shut down the `PushManager`, too:

```java
List<SimpleApnsPushNotification> unsentNotifications = pushManager.shutdown();
pushManager.shutdown();
```

Note that there's no guarantee as to when a push notification will be sent after it's enqueued. Shutting down the `PushManager` returns a list of notifications still in the outbound queue so you'll know what hasn't been transmitted to the APNs gateway by the time the `PushManager` has been shut down.
When the `PushManager` takes a notification from the queue, it will keep trying to send that notification. By the time you shut down the `PushManager` (as long as you don't give the shutdown process a timeout), the notification is guaranteed to have either been accepted or rejected by the APNs gateway.

## Error handling

Push notification providers communicate with APNs by opening a long-lived connection to Apple's push notification gateway and streaming push notification through that connection. Apple's gateway won't respond or acknowledge push notifications unless something goes wrong, in which case it will send an error code and close the connection (don't worry -- Pushy deals with all of this for you). To deal with notifications that are rejected by APNs, Pushy provides a notion of a [`RejectedNotificationListener`](http://relayrides.github.io/pushy/apidocs/0.2/com/relayrides/pushy/apns/RejectedNotificationListener.html). Rejected notification listeners are informed whenever APNs rejects a push notification. Here's an example of registering a simple listener:
Pushy deals with most problems for you, but there are two classes of problems you may want to deal with on your own.

```java
public class MyRejectedNotificationListener implements RejectedNotificationListener<SimpleApnsPushNotification> {
### Rejected notifications

public void handleRejectedNotification(
SimpleApnsPushNotification notification, RejectedNotificationReason reason) {
Push notification providers communicate with APNs by opening a long-lived connection to Apple's push notification gateway and streaming push notification through that connection. Apple's gateway won't respond or acknowledge push notifications unless something goes wrong, in which case it will send an error code and close the connection (don't worry -- Pushy deals with all of this for you). To deal with notifications that are rejected by APNs, Pushy provides a notion of a [`RejectedNotificationListener`](http://relayrides.github.io/pushy/apidocs/0.3/com/relayrides/pushy/apns/RejectedNotificationListener.html). Rejected notification listeners are informed whenever APNs rejects a push notification. Here's an example of registering a simple listener:

System.out.format("%s was rejected with rejection reason %s\n", notification, reason);
}
```java
private class MyRejectedNotificationListener implements RejectedNotificationListener<SimpleApnsPushNotification> {

public void handleRejectedNotification(
final PushManager<? extends SimpleApnsPushNotification> pushManager,
final SimpleApnsPushNotification notification,
final RejectedNotificationReason reason) {

System.out.format("%s was rejected with rejection reason %s\n", notification, reason);
}
}

// ...

pushManager.registerRejectedNotificationListener(new MyRejectedNotificationListener());
```

Lots of things can go wrong when sending notifications, but rejected notification listeners are only informed when Apple definitively rejects a push notification. All other IO problems are treated as temporary issues, and Pushy will automatically re-transmit notifications affected by IO problems later.
Lots of things can go wrong when sending notifications, but rejected notification listeners are only informed when Apple definitively rejects a push notification. All other IO problems are treated as temporary issues, and Pushy will automatically re-transmit notifications affected by IO problems later. You may register a rejected notification listener at any time before shutting down the `PushManager`.

### Failed connections

While running, a `PushManager` will attempt to re-open any connection that is closed by the gateway (i.e. if a notification was rejected). Occasionally, connection attempts will fail for benign (or at least temporary) reasons. Sometimes, though, connection failures can indicate a more permanent problem (like an expired certificate) that won't be resolved by retrying the connection, and letting the `PushManager` try to reconnect indefinitely won't help the situation.

You can listen for connection failures with a [`FailedConnectionListener`](http://relayrides.github.io/pushy/apidocs/0.3/com/relayrides/pushy/apns/FailedConnectionListener.html) like this:

```java
private class MyFailedConnectionListener implements FailedConnectionListener<SimpleApnsPushNotification> {

public void handleFailedConnection(
final PushManager<? extends SimpleApnsPushNotification> pushManager,
final Throwable cause) {

if (cause instanceof SSLHandshakeException) {
// This is probably a permanent failure, and we should shut down
// the PushManager.
}
}
}

// ...

pushManager.registerFailedConnectionListener(new MyFailedConnectionListener());
```

Generally, it's safe to ignore most failures (though you may want to log them). Failures that result from a `SSLHandshakeException`, though, likely indicate that your certificate is either invalid or expired, and you'll need to remedy the situation before reconnection attempts are likely to succeed.

Like `RejectedNotificationListeners`, `FailedConnectionListeners` can be registered any time before the `PushManager` is shut down.

## The feedback service

Expand Down Expand Up @@ -144,4 +175,4 @@ Although we make every effort to fix bugs and work around issues outside of our

Pushy is available to the public under the [MIT License](http://opensource.org/licenses/MIT).

The current version of Pushy is 0.2. We consider it to be fully functional (and use it in production!), but the public API may change significantly before a 1.0 release.
The current version of Pushy is 0.3. We consider it to be fully functional (and use it in production!), but the public API may change significantly before a 1.0 release.
3 changes: 3 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
<configuration>
<overview>${basedir}/src/main/java/overview.html</overview>
<show>public</show>
<links>
<link>http://netty.io/4.0/api/</link>
</links>
</configuration>
<executions>
<execution>
Expand Down
41 changes: 19 additions & 22 deletions src/main/java/com/relayrides/pushy/apns/ApnsConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
*
* @author <a href="mailto:[email protected]">Jon Chambers</a>
*/
class ApnsConnection<T extends ApnsPushNotification> {
public class ApnsConnection<T extends ApnsPushNotification> {

private final ApnsEnvironment environment;
private final SSLContext sslContext;
Expand Down Expand Up @@ -353,7 +353,7 @@ public void operationComplete(final Future<Channel> handshakeFuture) {
* @param notification the notification to send
*
* @see ApnsConnectionListener#handleWriteFailure(ApnsConnection, ApnsPushNotification, Throwable)
* @see ApnsConnectionListener#handleRejectedNotification(ApnsConnection, ApnsPushNotification, RejectedNotificationReason, java.util.Collection)
* @see ApnsConnectionListener#handleRejectedNotification(ApnsConnection, ApnsPushNotification, RejectedNotificationReason)
*/
public synchronized void sendNotification(final T notification) {
final ApnsConnection<T> apnsConnection = this;
Expand All @@ -377,7 +377,7 @@ public void run() {
public void operationComplete(final ChannelFuture writeFuture) {
if (writeFuture.isSuccess()) {
log.trace("{} successfully wrote notification {}", apnsConnection.name,
sendableNotification.getSequenceNumber());
sendableNotification.getSequenceNumber());

if (apnsConnection.rejectionReceived) {
// Even though the write succeeded, we know for sure that this notification was never
Expand All @@ -389,7 +389,7 @@ public void operationComplete(final ChannelFuture writeFuture) {
}
} else {
log.trace("{} failed to write notification {}",
apnsConnection.name, sendableNotification, writeFuture.cause());
apnsConnection.name, sendableNotification, writeFuture.cause());

// Assume this is a temporary failure (we know it's not a permanent rejection because we didn't
// even manage to write the notification to the wire) and re-enqueue for another send attempt.
Expand All @@ -411,24 +411,19 @@ public void operationComplete(final ChannelFuture writeFuture) {
}

/**
* <p>Waits for all pending read and write operations to finish. When this method exits normally (i.e. when it does
* not throw an {@code InterruptedException}), the following guarantees are made:</p>
* <p>Waits for all pending write operations to finish. When this method exits normally (i.e. when it does
* not throw an {@code InterruptedException}), All pending writes will have either finished successfully or failed
* and passed to this connection's listener via the
* {@link ApnsConnectionListener#handleWriteFailure(ApnsConnection, ApnsPushNotification, Throwable)} method.</p>
*
* <ol>
* <li>All pending writes will have either finished successfully or been dispatched to this connection's listener
* via the {@link ApnsConnectionListener#handleWriteFailure(ApnsConnection, ApnsPushNotification, Throwable)}
* method.</li>
* <li>All pending reads will have completed, and rejected/unprocessed notifications will be dispatched to this
* connection's listener via the {@link ApnsConnectionListener#handleRejectedNotification(ApnsConnection, ApnsPushNotification, RejectedNotificationReason)}
* and {@link ApnsConnectionListener#handleUnprocessedNotifications(ApnsConnection, Collection)} methods.</li>
* </ol>
*
* <p>It is advisable for listeners to call this method when a connection is closed (though they must do so in a
* separate thread.</p>
* <p>It is <em>not</em> guaranteed that all write operations will have finished by the time a connection has
* closed. Applications that need to know when all writes have finished should call this method after a connection
* closes, but must not do so in an IO thread (i.e. the thread that called the
* {@link ApnsConnectionListener#handleConnectionClosure(ApnsConnection)} method.</p>
*
* @throws InterruptedException if interrupted while waiting for pending read/write operations to finish
*/
public void waitForPendingOperationsToFinish() throws InterruptedException {
public void waitForPendingWritesToFinish() throws InterruptedException {
synchronized (this.pendingWriteMonitor) {
while (this.pendingWriteCount > 0) {
this.pendingWriteMonitor.wait();
Expand All @@ -449,7 +444,7 @@ public void waitForPendingOperationsToFinish() throws InterruptedException {
* <p>Calling this method before establishing a connection with the APNs gateway or while a graceful shutdown
* attempt is already in progress has no effect.</p>
*
* @see ApnsConnectionListener#handleRejectedNotification(ApnsConnection, ApnsPushNotification, RejectedNotificationReason, java.util.Collection)
* @see ApnsConnectionListener#handleRejectedNotification(ApnsConnection, ApnsPushNotification, RejectedNotificationReason)
* @see ApnsConnectionListener#handleConnectionClosure(ApnsConnection)
*/
public synchronized void shutdownGracefully() {
Expand Down Expand Up @@ -478,10 +473,10 @@ public void run() {
public void operationComplete(final ChannelFuture future) {
if (future.isSuccess()) {
log.trace("{} successfully wrote known-bad notification {}",
apnsConnection.name, apnsConnection.shutdownNotification.getSequenceNumber());
apnsConnection.name, apnsConnection.shutdownNotification.getSequenceNumber());
} else {
log.trace("{} failed to write known-bad notification {}",
apnsConnection.name, apnsConnection.shutdownNotification, future.cause());
apnsConnection.name, apnsConnection.shutdownNotification, future.cause());

// Try again!
apnsConnection.shutdownNotification = null;
Expand Down Expand Up @@ -511,7 +506,9 @@ public void operationComplete(final ChannelFuture future) {
/**
* <p>Immediately closes this connection (assuming it was ever open). No guarantees are made with regard to the
* state of sent notifications, and callers should generally prefer {@link ApnsConnection#shutdownGracefully} to
* this method. This connection's listener will be notified when the connection has finished closing.</p>
* this method. If the connection was previously open, the connection's listener will be notified of the
* connection's closure. If a connection attempt was in progress, the listener will be notified of a connection
* failure. If the connection was never open, this method has no effect.</p>
*
* <p>Calling this method while not connected has no effect.</p>
*
Expand Down
Loading

0 comments on commit eed428e

Please sign in to comment.