I'm trying to add push notifications to a Google App Engine server, and I've got a strange problem where the devices only receive every second push notification.
I've set up a Push Queue to send the notifications on a backend. The first request per instance creates a background thread, which queues pushes and sends them. Then each push request adds the request to the queue, waits for the push to be sent, and then responds with either code 200 for success or code 500 for failure. Here's my code for the background thread:
void doSendThread() {
// Open certificate (certificate is a class URL var)
InputStream cert = certificate.openStream();
// Connect to Apple Push service (password and apnsManager are class vars)
AppleNotificationServer appleServer = new AppleNotificationServerBasicImpl(cert, password, ConnectionToAppleServer.KEYSTORE_TYPE_PKCS12, "gateway.push.apple.com", 2195);
apnsManager = new PushNotificationManager();
apnsManager.initializeConnection(appleServer);
// Send loop
while (true) {
// Wait for a bit
Thread.sleep(500);
// Check for new pushes in queue (pushQueue is a class ArrayDequeue var)
while (!pushQueue.isEmpty()) {
// Get next push data (PushData is a custom class that holds the push notification state)
PushData data = pushQueue.poll();
// Send data (the HTTP request thread stops waiting when data.done is true)
sendPushData(data);
data.done = true;
}
}
// There's also some feedback checking stuff here which I've left out
...
}
void sendPushData(PushData) {
try {
// Get tokens
Logger.getLogger(PushManager.class.getName()).log(Level.INFO, "Sending push...");
List<Device> devices = Devices.asDevices(data.tokens);
// Create payload
PushNotificationPayload payload = PushNotificationPayload.complex();
if (data.text.length() > 0)
payload.addAlert(data.text);
if (data.badge > 0)
payload.addBadge(data.badge);
if (data.sound.length() > 0)
payload.addSound(data.sound);
// Go through devices and send each push
for (Device dev : devices) {
// Send payload and don't close the connection
PushedNotification pn = apnsManager.sendNotification(dev, payload, false);
// Check for error (HTTP thread returns 500 if data.error is true)
if (!pn.isSuccessful())
data.error = true;
// Log error
if (pn.getException() != null)
Logger.getLogger(PushManager.class.getName()).log(Level.SEVERE, "Push error: " + pn.getException().getMessage());
}
} catch (Exception e) {
Logger.getLogger(PushManager.class.getName()).log(Level.SEVERE, "Push error: " + e.getMessage());
e.printStackTrace();
disconnect(); // <- Disconnects the apnsManager and shuts down the thread, the next push request will create a new thread and reconnect
data.error = true;
}
}
Logging is difficult on GAE since the logs only come through for background threads once the instance is shut down, but if I send 5 pushes using the app and then shut down the instance, this is my log:
W 2014-01-13 13:41:18.028 [s~*****chat/push-notification-worker.373021320690690677].<stderr>: log4j:WARN No appenders could be found for logger (javapns.communication.Connecti
W 2014-01-13 13:41:18.028 [s~*****chat/push-notification-worker.373021320690690677].<stderr>: log4j:WARN Please initialize the log4j system properly.
W 2014-01-13 13:41:18.028 [s~*****chat/push-notification-worker.373021320690690677].<stderr>: log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more inf
I 2014-01-13 13:41:19.222 com.*****apps.*****Server.Push.PushManager sendPushData: Sending push...
I 2014-01-13 13:41:23.027 com.*****apps.*****Server.Push.PushManager sendPushData: Sending push...
I 2014-01-13 13:41:23.663 com.*****apps.*****Server.Push.PushManager sendPushData: Sending push...
I 2014-01-13 13:41:24.225 com.*****apps.*****Server.Push.PushManager sendPushData: Sending push...
I 2014-01-13 13:41:24.790 com.*****apps.*****Server.Push.PushManager sendPushData: Sending push...
I 2014-01-13 13:42:25.022 com.*****apps.*****Server.Push.PushManager disconnect: Shutting down push manager...
... so it looks like it's sending all 5, yet I only get the second and fourth one coming through on the device. What's really weird is that each push actually send 2 pushes since I have 2 devices (so in effect 10 push notifications were sent to Apple), and it either receives the message on both or on none, never only one.
Sending multiple notifications to the same device within a short time period may result in only some of them being delivered. It's not a good practice to enforce some arbitrary delay (which may or may not be enough) between messages to the same device. Your app must be able to handle not receiving all the notifications you send to it (by synching with your server).
Here's what Apple's tech note says about this:
Some Notifications Received, but Not All
If you are sending multiple notifications to the same device or computer within a short period of time, the push service will send only the last one.
Here's why. The device or computer acknowledges receipt of each notification. Until the push service receives that acknowledgment, it can only assume that the device or computer has gone off-line for some reason and stores the notification in the quality of service (QoS) queue for future redelivery. The round-trip network latency here is of course a major factor.
As described in the Local and Push Notification Programming Guide, the QoS queue holds a single notification per app per device or computer. If the service receives another notification before the one in the queue is sent, the new notification overwrites the previous one.
All of this points out that the intent is that a notification indicates to an app that something of interest has changed on the provider, and the app should check in with the provider to get the details. Notifications should not contain data which isn't also available elsewhere, and they should also not be stateful.
Any push notification that isn't delivered immediately was queued for future redelivery because your device was not connected to the service. "Immediately" of course needs to take latency for your connection into account. Outlying cases would be beyond 60 seconds as APNs will time out at that point.