The project we are working on is web application implemented in Ruby on Rails with API used by mobile clients – iOS and Android and currently used by over 2.5 million users on a daily basis. When we first started sending push notifications, we used Urban Airship. Urban Airship works with both iOS and Android, and has a very simple API interface for registering devices and sending push notifications.

Urban Airship is a good option for people who want to take advantage of all their services. In our case, a lot of Urban Airship services we didn’t use, so we decided to spend some time investigating other push notification services in order to decrease our costs on monthly basis. We also wanted a platform that was specifically designed for push notifications, and that would allow us to fully manage our push notifications. For these reasons, we chose Amazon Simple Notification Service (SNS). Amazon SNS gives us stable, highly reliable, and scalable service.

Implementation

Our goal was to make the transition from Urban Airship to Amazon SNS seamlessly, without interrupting users’ activity on the app or requiring them to do anything in order to make the switch. We were mostly successful in achieving that goal.

Once you have created Amazon SNS platform application, and have registered mobile device via device token to the platform application you are ready to send and receive PNs.

Because both Urban Airship and Amazon SNS use Apple IDs to register iOS devices, we did not have to take any extra steps to switch our iOS users to the new service. However, Urban Airship and Amazon SNS register Android devices differently; the former uses device UIDs and the latter uses GCM tokens. This presented us with a problem. We did not have the GCM tokens for our Android users, and therefore could not immediately register those devices.

We dealt with this problem by running both Urban Airship and Amazon SNS for a period of time, while we transitioned our Android users from one to another.

We updated our API interface to acquire GCM tokens when users started our application on Android devices, and then automatically register the device with SNS. Here is the code we used to accomplish this:

class AndroidPushTokensController < ApplicationController
  def create
    if !current_user.has_registered_token(params[:gcm_token])

      # CREATE NEW TOKEN FOR UNREGISTERED DEVICE
      device = AndroidDevice.create :user_id => current_user.id,
		                                    :apid => 'N/A',
		                                    :model => params[:model], 
		                                    :android_id => params[:android_id], 
		                                    :screen_height => params[:screen_height],
		                                    :screen_width => params[:screen_width], 
		                                    :manufacturer => params[:manufacturer], 
		                                    :product => params[:product], 
		                                    :gcm_token => params[:gcm_token]

      #... SOME OTHER OPERATIONS

      # REGISTER DEVICE TO AMAZON SNS SERVICE

      push_notification_client = PushNotificationClient.new('AmazonSNS')
      push_notification_client.register params[:gcm_token], current_user.id, MobileDevice::ANDROID

      render :text => "Device is registered correctly to Amazon SNS", :status => 200 
    else
      render :text => "A valid Android GCM token must be sent.", :status => 400 
    end
  end
end

We still needed to think about our users which have, or do not have, running applications where our system could not updated and/or register them to the Amazon SNS platform. That’s the reason why we could not simply turn off Urban Airship and turn on Amazon SNS. We solved our problem by registering mobile devices to the Amazon SNS platform with the correct tokens. So, we had to give our system some time to register all users (or most of them).

In this situation, if user comes to the system and he’s not registered to Amazon SNS platform we register him and let him use application and send PNs to other users. On the other side when he sent PNs to couple of users we had to check for each user to which platform the user was registered and send PN over that platform.

# user can have more than one registered device, either Android or iOS

module BackgroundPushNotifier

   def submit_push_notification(message, ios_devices, android_devices, custom_data={})

   # sending PN to iOS devices remains as it was before
  Delayed::Job.enqueue( DelayedPushNotification.new(message, ios_devices, custom_data, PushNotificationClient::TYPE_IOS), 
	    											Delayed::Job::MID_PRIORITY, 
	    											DateTime.now, 
	    											Delayed::Job::NOTIFICATION_QUEUE)

  Delayed::Job.enqueue(	DelayedPushNotification.new(message, android_devices, custom_data, PushNotificationClient::TYPE_ANDROID), 
														Delayed::Job::MID_PRIORITY, 
														DateTime.now, 
														Delayed::Job::NOTIFICATION_QUEUE)

   end

end

At this point, we encountered another problem: tight coupling of Urban Airship’s integration into the system. Some of the SOLID principles were broken in the code, and we did not want to break them again by sending push notifications over Amazon SNS with the same method we used for sending push notifications over Urban Airship.

To solve this problem, we refactored existing code. By applying IoC (Inverse of Control) and ISP (Interface Segregation Principle), we created a new abstract layer between our API and our services for sending push notifications.

This also allowed us to set up a new configuration that we used to switch between the Urban Airship and Amazon SNS platforms, without changing the code or API. This made testing our migration much easier.

Because we could not change the interface of Urban Airship, we created an adapter class with the same interface and only implemented AmazonSNS to handle push notifications for those devices that were already registered to Amazon SNS. Here is the code we used:

# 3. Implement UrbanAirship client

class UrbanAirshipClient

   def initialize
   end  

   def send_ios_push(deviced, message, custom_data={})
   end

   def send_android_push(devices, message, custom_data={})
   end

   def get_feedback(days_back=1)
   end

   def register(device)
   end

   def unregister(device)
   end
end  

# 4. Implement AmazonSNS client

class AmazonSNSClient

   def initialize
   end  

   def send_ios_push(devices, message, custom_data={})
   end

   def send_android_push(devices, message, custom_data={})
   end

   def get_feedback(days_back=1)
   end

   def register(device)
   end

   def unregister(device)
   end
end

Conclusion

Working on a live system that has millions of registered users and over 100,000 daily sessions is quite challenging, even if you plan to add only one new feature that does not impact existing features. You might come across the problem of working on legacy code which was written 5 or 10 years ago, when the whole concept of programming was very different from what it is today. But even then, some principles existed, and those should be respected by developers.

Despite these obstacles, we managed to switch to the Amazon SNS push notification service which allowed us to accomplish our main goal of significantly reducing our monthly costs.

oban
Software DevelopmentTech Bites
February 23, 2024

Background Jobs in Elixir – Oban

When and why do we need background jobs? Nowadays, background job processing is indispensable in the world of web development. The need for background jobs stems from the fact that synchronous execution of time-consuming and resource-intensive tasks would heavily impact an application's  performance and user experience.  Even though Elixir is…

Want to discuss this in relation to your project? Get in touch:

Leave a Reply