If you’ve used a smartphone or a tablet recently, you’ve probably come across push notifications. They are the little pings your phone gives you to let you know that you’ve got a new social network message, your friend is waiting for you to take your turn on the latest game, or that band you like has just announced a concert in your town. As a developer, push notifications give you a new dimension to engage with your users in real time, anytime, regardless of whether or not they have your app open or whether or not their phone is even in their hand. Simply stated, push notifications are an important mechanism for creating more engaging mobile solutions.
When targeting Android devices, it is easy to get started by using Android Studio. The Cloud Tools for Android Studio provide Cloud Messaging Support that you can use to send push notifications using Google Cloud Messaging for Android from a mobile backend running on Google App Engine.
Google Cloud Platform can also be used to send push notifications to iOS devices. However, sending push notifications to iOS devices using the Apple® Push Notification service (APNs) requires a different approach. The primary reason is that the APNs protocol is based on persistent sockets and the sender needs to limit the number of connections being established and torn down. This typically means that the sender cannot simply establish separate connections for each send request, but rather needs to reuse established connections or use connection pooling.
This document and accompanying sample present a design you can use to orchestrate iOS push notifications using Google App Engine.
Typical functional requirements for orchestrating iOS Push Notifications as part of your mobile application include:
- The ability to communicate with the APNs and conform to the push notification protocol specification, including regularly polling for inactive device tokens.
- Sending push notifications to an individual iOS device, for example, when a player of your mobile game challenges a friend to play the game.
- Sending push notifications to a small number of iOS devices, for instance, to notify all friends of a given user of your mobile app about an important event, such as the user checking into a restaurant or uploading a photo.
- Sending push notifications to all iOS devices registered for users that meet certain criteria, for instance to notify all bank card customers about a new promotion.
To develop successful mobile applications it is also important to consider non-functional requirements, such as:
- Scalability. Even if your mobile application sends a given notification only to a single device, you might want to send a lot of notifications within a short time.
- Availability. If your mobile application wants to send a push notification it should be able to do it anytime.
- Reliability. Although APNs doesn’t guarantee that the notification it accepted will be delivered to the target device, it is important to have as much reliability as possible. For example, if your mobile solution decides to send a push notification, this notification should be reliably delivered to APNs. Also, a user's experience would be negatively affected if they received duplicated push notifications, so duplications should be avoided as much as possible.
You can meet these functional and non-functional requirements when you run your mobile backend on Google Cloud Platform.
This paper describes how you can orchestrate iOS push notifications as part of your mobile solution. It assumes you are familiar with the APNs protocol and want to send iOS push notifications by communicating directly with APNs.
Communicating with APNs requires opening and maintaining TCP socket connections. When running your code on Google Cloud Platform, you can achieve this from either Google Compute Engine or Google App Engine using Sockets API .
Unless you have reasons to build your mobile solution backend completely on Google Compute Engine, you will have a lot of benefits by using Google App Engine for your mobile backend as the solution in this paper presents. These benefits include high availability, automatic scalability of front end instances, load balancing, ease of deployment, and other advantages of running on managed infrastructure.
The key components of the complete solution are: the mobile client, the mobile backend, and the communication channel between them. The actual delivery of the push notifications is done using the APNs.
The mobile client is your iOS native application using iOS push notifications, as well as an iOS device that has this application installed.
The mobile app communicates with the mobile backend using Google Cloud Endpoints. This communication includes device token registration, as well as app-specific APIs that indirectly trigger push notifications.
The mobile backend runs on Google App Engine and uses Google Cloud Datastore for token registration and Sockets API to send push notifications to APNs, which is responsible for actual delivery of the notifications to the iOS devices.
The high level architecture of this solution is depicted in Figure 1:
The mobile backend has several components:
- Device Registration Endpoint responsible for storing and managing the list of device tokens (which maps to the list of devices where your native client application is installed and users have agreed to allow your app to use push notification on their device).
- Push Notification Manager that provides an internal API that other parts of your mobile backend can call to request that various push notifications be sent.
- Notification Delivery Worker responsible for communicating with APNs using TCP socket connections to send push notifications.
- Push Feedback Processing Handler responsible for communicating with APNs to retrieve the list of inactive or invalid device tokens.
- Device Token Cleanup Handler that updates the list of device tokens by removing invalid or inactive tokens.
- Push Preprocessing Handler that retrieves the list of target device tokens that should receive a given push notification, such as the list of device tokens used by Joe’s friends who have checked into a restaurant.
These components are depicted in the diagram in Figure 2:
The interactions between these queues, Push Notification Manager, Notification Delivery Worker, and Push Preprocessing Handler are depicted in the diagram shown in Figure 3:
You should follow Apple ’s instructions for enabling iOS push
notifications in your native application. In particular, you need to
obtain APNs certificate(s) specific to your application bundle. Your
client application must register for push notifications. You could, for
example, accomplish this during the application launch from within
didFinishLaunchingWithOptions(), by calling
Your iOS application also needs to implement
didRegisterForRemoteNotificationsWithDeviceToken() and pass the
retrieved device token to your mobile backend as
described in the following section. You may also want to implement
didReceiveRemoteNotification() to process the received push
Please note that APNs push notifications mechanism requires physical iOS devices and doesn’t work in emulators.
Detailed information about developing iOS client applications that use push notification is beyond the scope of this paper.
Passing device token to your mobile backend
As described earlier, your iOS native application needs to pass the device token to your mobile backend.
A simple way to achieve this is to use Google Cloud Endpoints to expose a REST API from the backend. You can then use tooling to generate an iO S client library and authenticate the call using OAuth2-based authentication and Google Accounts.
However, if your client application already communicates with your mobile backend using a different approach, you can choose to simply extend your existing approach to allow your client application to pass the device token. Make sure that this communication is authenticated and that the backend verifies the request came from your application client.
Storing and managing device tokens
Since your client application is not invoked when a user decides to uninstall your app, you need to manage the list of active device tokens by following the APNs specifications. In particular, your mobile backend needs to regularly retrieve the list of inactive or invalid device tokens and promptly stop sending push notifications using such tokens (see the “Managing the Device Tokens” section for more information on how this can be implemented). For this purpose, your mobile backend should store not just the device token, but also the timestamp for when the token was received so it can better handle inactive tokens.
Also, unless your mobile solution only sends the same push notifications
to all users, you will want to attribute the device token such that you
can target specific notifications to specific users. One way to do this,
for example, is to store
user_id with the device token.
A natural choice for storing device tokens and related information is to use the NoSQL key-value storage of Datastore, as it is highly scalable and this use case does not require complex SQL queries. However, if your mobile solution already uses Cloud SQL, you can store the device tokens there instead. Cloud SQL supports complex queries and ACID transactions, but this means the database acts as a 'fixed pipe' and performance is less scalable than Datastore.
Initiating push notification requests
There are alternative approaches available for your mobile solution to initiate push notification requests:
Requests initiated by your mobile client application.
This typically is triggered by a user action. For example, when a player of your mobile game challenges a friend to play the game, a push notification can be used to notify the other player about this challenge request.
Usually, a mobile backend exposes API that the client application calls to initiate the part of the processing that should happen on the backend, rather than having an explicit API available to the mobile client application just for specifically sending push notifications. For example, the API can include a method,
ChallengeFriend, and initiating the push notification request would be one of several actions that your mobile backend would do when the client calls this API.
Another example is when your mobile user checks into a restaurant or uploads a photo and the push notification is used to notify the user’s friends about this event. As in the previous example, the API exposed by the mobile backend is tailored to entire operations, such as
UploadPhoto. The backend implementation of these operations includes sending push notifications.
Requests initiated by your mobile application backend.
An example might be an admin web UI in your mobile solution that allows an administrator to send a push notification to all users about a new game level becoming available. Another example might involve sending a push notification about a new offer available to users every morning.
In the proposed design, all requests to send push notifications are controlled by your mobile backend and encapsulated by a component named Push Notification Manager.
Push Notification Manager
The role of the Push Notification Manager is to provide an internal API that can be used by other components of your mobile backend to initiate push notification requests. As per the requirements stated earlier, this API covers the following scenarios:
- A push notification to a single iOS device, as in the example, where one player of your mobile game challenges another friend to play
- Send push notification(s) to a small number of iOS devices, for instance , to notify all friends of a given user of your mobile solution about an important event, such as the user checking into a restaurant or uploading a photo.
- Push notifications to a large number of iOS devices, either to all registered devices or to devices registered for users that meet certain criteria. One example might involve notifying all bank customers who are holders of a specific card type about a new promotion available to them. Another example might involve notifying all users of your mobile game that a new game level is available.
To enable building more responsive and robust applications, this internal API should not block the thread until the push notification is sent. Instead, the call should return as soon as the request to send the push notification is persisted. The rest of the processing should happen asynchronously.
In the design discussed in this paper such requests are persisted as tasks in the Task Queue. This pattern is described in subsequent sections and is also implemented in the accompanying sample app.
Task Queue Usage
The following two task queues are used for push notifications requests:
Notification Delivery Queue
Push Preprocessing Queue
Used for tasks that need additional preprocessing (See Scenario #3).
Push notification to a single device
In this scenario, a task that is persisted includes the device token of the target user and all the details about the requested push notification, such as the type of notification and the text of the alert. The task is enqueued into Notification Delivery Queue.
Push notification to a small number of devices
In this scenario, a task that is persisted includes all the details about the requested push notification and the list of target device tokens. The task is enqueued into Notification Delivery Queue. In other words, the system atomically persists a single request for a push notification to a list of devices. This can be more scalable and more reliable than persisting a list of individual requests to one device each.
This approach can be used as long as the following two requirements are met:
- The list of device tokens is not too big, since the serialized list and the information about the requested push notification must fit within the documented task size limit.
- Your mobile backend can retrieve the list of the target devices quickly enough so this retrieval can be completed while processing a request from the client.
Otherwise, you can treat this scenario as a simplified version of the next scenario.
Push notification request that requires preprocessing
This scenario covers cases where the list of target devices is too large to fit into a single task or retrieving this list cannot be done quickly enough. In such cases, the task that is persisted includes only the details about the requested push notification and some metadata that indirectly specify the target device tokens. However, the actual list of target device tokens is not included in the task payload. In other words, what is enqueued is a task that requires a preprocessing step during which the metadata will be used to retrieve the list of target device tokens. Typically, this step involves paging as described in the following section, Retrieving the list of target device tokens. Such a task is enqueued into the Push Preprocessing Queue.
To achieve a higher degree of reliability, you should take advantage of a Task Queue mechanism that prevents inserting a task with the same name more than once within a time window. To do this, you need to explicitly specify the name of the task. You can derive that name from the event that triggered the need to send the push notification. For example, the task name can include the id of the player, and the id of the invited friend, and an additional identifier for the game challenge request, such as an id of that request or a timestamp with a 5 minute rounding. This way, if the request is processed again, as in the case where it was resent by the client, your backend will attempt to enqueue a task with a previously used name. This will cause a specific exception and your backend will be able to ignore the request and refrain from sending duplicate push notifications.
Retrieving the list of target device tokens
This solution uses a combination of query cursor, push and pull task queues, and transactions to handle preprocessing and retrieving the list of target device tokens in a reliable way.
When the Push Preprocessing Handler receives an initial preprocessing task, it uses metadata in the task payload to construct a Datastore query. It then retrieves a batch of matching device tokens, as well as cursor information, so the next batch can be retrieved later. Next, it transactionally (1) enqueues a task to Notification Delivery Queue to send a push notification to the retrieved batch of device tokens and (2) enqueues a continuation preprocessing task (described later in this section) to Push Preprocessing Queue.
When the transaction is committed, the Push Preprocessing Handler completes handling of the original preprocessing task, so the task can be treated as processed. If the transaction fails, it is rolled back and no new tasks are enqueued. In this case, the Push Preprocessing Handler returns an error and the App Engine Task Queue will retry the original preprocessing task. You can configure how App Engine retries failed tasks for each queue separately.
A continuation preprocessing task is very similar to an initial preprocessing task, but also includes the encoded cursor information. When Push Preprocessing Handler receives such a continuation task, it retrieves the encoded cursor information and uses it to construct a Datastore query that continues paging over the resultset.
Communicating with APNs
Sending push notifications
It’s recommended to retain connections with APNs across multiple notifications and to avoid repeatedly establishing and tearing down the connections.
The easiest way to achieve the recommended communication pattern when your mobile backend runs on Google App Engine is to communicate with APNs from App Engine Modules.
Each named module you want to use for sending push notifications can host a worker. The worker can work in a loop and lease a batch of tasks from the notification delivery queue, send notifications requested in these tasks, and then delete the tasks.
When a call to send push notifications to APNs returns, the result may include indicators that specific device tokens are invalid or inactive. The worker can enqueue a cleanup task for each batch of such device tokens. Such a cleanup task is then processed asynchronously as described (see the “Managing the Device Tokens” section).
If the throughput of push notifications using a single named module with a single thread is insufficient for your requirements, you can increase the number of workers (either by hosting multiple workers on one module or by adjusting the scaling settings on your module to create additional instances), or you can shard the load by using more than one pull queue. A rough guidance is to use up to three to five workers per queue.
Querying the feedback service
One requirement of the APNs protocol is to regularly query the feedback service to retrieve the list of inactive device tokens. In this design, this is accomplished using the Push Feedback Processing Handler that can be configured to be periodically invoked by the App Engine Cron service on a schedule you specify (at least once a day). This handler enqueues a task with the list of inactive device tokens to a cleanup queue.
Managing the device tokens
The tasks with information about inactive and invalid device tokens are processed by the Device Token Cleanup Handler. Its role is to remove such device tokens from the persistence store.
The invalid device tokens returned when sending a push notification can be removed without additional checks. However, handling inactive device tokens retrieved from the feedback service should account for situations where a previously inactive token becomes active again and is re-registered with the mobile backend. One way of handling this situation is to compare a timestamp, included with the information retrieved from the feedback service, with the timestamp stored for this device token in the persistence store. If the timestamp from the persistent store is more recent, it indicates that the device registration information is more recent than the information retrieved from the feedback service and such a device token shouldn’t be removed.
A sample implementation of the design presented in this paper is published on GitHub. You can deploy it to your Google App Engine account with billing enabled and then play with the sample. Google App Engine offers a free tier that is subject to quotas. A simple iOS client app is also published on GitHub.
Your mobile backend running on Google App Engine can orchestrate push notifications to various mobile platforms, including Android and iOS, all without having to use any 3rd party services.
 “Quickly enough” is relative to individual circumstances. If your own internal standard is to process all requests from the client within x milliseconds or y seconds, then you can use it as a key factor for determining whether or not retrieving the list of all target devices can be done "quickly enough" or not.