Search code examples
firebase-cloud-messagingsidekiq

How can I make Sidekiq respect a Retry-After response header?


I'm using Sidekiq to try to send push notifications through Firebase Cloud Messaging (FCM). FCM requires 2 things from a server that uses it:

  1. Exponential Backoff
  2. Honor the Retry-After header in a 500 response

https://firebase.google.com/docs/cloud-messaging/http-server-ref#table4

We get Exponential Backoff for free with Sidekiq but I'm unsure of how to tell it to delay retrying until at least the time specified in the header.

I have a hacky solution below where I make the worker sleep before I raise but it seems like the retry logic should live in Sidekiq. Is there anyway for me to just tell Sidekiq about the Retry-After header value so it can do the right thing?

class SendPushNotification
  include Sidekiq::Worker
  sidekiq_options queue: :low

  def perform(user_id)
    notification = { body: 'hello world' }
    user = User.find(user_id)
    fcm_token = user.try(:fcm_token) || ENV['FCM_TOKEN']

    uri = URI('https://fcm.googleapis.com/fcm/send')

    http = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true)

    req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json',
                                   'Authorization' => "key=#{ENV['FCM_API_KEY']}")

    req.body = {
      badge: 0,                      # badge is removed when set to zero
      click_action: 'click',         # string
      collapse_key: 'Handshake',     # collapse multiple messages under this header
      content_available: true,       # wake inactive app
      data: nil,
      registration_ids: [fcm_token], # fcm device tokens
      notification: notification,
      mutable_content: true,         # iOS 10 feature
      priority: 'high',              # high or low (corresponds to 5 and 10 in apns)
      subtitle: nil,
      time_to_live: 2419200,         # max and default for FCM
      dry_run: false                # message is not sent when set to true
    }.to_json

    res =  http.request(req)

    case res
    when Net::HTTPOK
      true
    when Net::HTTPUnauthorized
      raise 'Unauthorized'
    when Net::HTTPBadRequest
      raise "BadRequest: #{res.body}"
    else
      if res.header['Retry-After']
        # this seems like a hacky solution
        sleep res.header['Retry-After']
      end
      raise 'Remote Server Error'
    end
  end
end

Solution

  • Don't use Sidekiq's built-in retry. Schedule a new job, identical to the current job, to run in that many seconds.

     self.class.perform_in(res.header['Retry-After'].to_i, user_id)