Search code examples
ruby-on-railsasynchronoussidekiq

Sidekiq: perform_async and order-dependent operations


There's a controller action in my Rails app that contacts a user via text-message and email. For reasons I won't go into, the text-message needs to complete before the email can be sent successfully. I originally had something like this:

controller:

class MyController < ApplicationController
  def contact_user
    ContactUserWorker.perform_async(@user.id)
  end
end

workers:

class ContactUserWorker
  include Sidekiq::Worker

  def perform(user_id)
    SendUserTextWorker.perform_async(user_id)
    SendUserEmailWorker.perform_async(user_id)
  end
end

class SendUserTextWorker
  include Sidekiq::Worker

  def perform(user_id)
    user = User.find(user_id)
    user.send_text
  end
end

class SendUserEmailWorker
  include Sidekiq::Worker

  def perform(user_id)
    user = User.find(user_id)
    user.send_email
  end
end

This was unreliable; sometimes the email would fail, sometimes both would fail. I'm trying to determine whether perform_async was the cause of the problem. Was the async part allowing the email to fire off before the text had completed? I'm a little fuzzy on how exactly perform_async works, but that sounded like a reasonable guess.

At first, I refactored ContactUserWorker to:

class ContactUserWorker
  include Sidekiq::Worker

  def perform(user_id)
    user = User.find(user_id)
    User.send_text

    SendUserEmailWorker.perform_async(user_id)
  end
end

Eventually though, I just moved the call to send_text out of the workers altogether and into the controller:

class MyController < ApplicationController
  def contact_user
    @user.send_text
    SendUserEmailWorker.perform_async(@user.id)
  end
end

This is a simplified version of the real code, but that's the gist of it. It seems to be working fine now, though I still wonder whether the problem was Sidekiq-related or if something else was going on.

I'm curious whether my original structure would've worked if I'd used perform instead of perform_async for all the calls except the email call. Like this:

class MyController < ApplicationController
  def contact_user
    ContactUserWorker.perform(@user.id)
  end
end

class ContactUserWorker
  include Sidekiq::Worker

  def perform(user_id)
    SendUserTextWorker.perform(user_id)
    SendUserEmailWorker.perform_async(user_id)
  end
end

Solution

  • If the email can only be sent after the text message has been sent, then send the email after successful completion of sending the text.

    class ContactUserWorker
      include Sidekiq::Worker
    
      def perform(user_id)
        SendUserTextWorker.perform_async(user_id)
      end
    end
    
    class SendUserTextWorker
      include Sidekiq::Worker
    
      def perform(user_id)
        user = User.find(user_id)
        text_sent = user.send_text
        SendUserEmailWorker.perform_async(user_id) if text_sent
      end
    end
    
    class SendUserEmailWorker
      include Sidekiq::Worker
    
      def perform(user_id)
        user = User.find(user_id)
        user.send_email
      end
    end
    

    In user.send_text you need to handle the fact that neither the text or the email has been sent.