Using ActionCable to provide updates on background job in your Rails app

One of my rails apps includes an email search feature that can take anywhere from a second to over a minute to perform. I’ve already stuck it inside a background worker so it doesn’t get timed out when deployed on Heroku, but it would also be nice to give the user some feedback during this process and when it is over. For a simple process like this, ActionCable is probably overkill, but I want to learn it so will implement that solution for this.

My first step is to generate a channel with which to send live updates to the user

rails g channel Notifications

This creates two files for my app. The first,notifications_channel.rb is responsible for setting up the information stream:

class NotificationsChannel < ApplicationCable::Channel
def subscribed
# stream_from "some_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end

I then add some code to configure our channel to stream only to the current_user of our app:

class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_from "notifications:#{current_user.id}"
end
def unsubscribed
stop_all_streams
end
end

Our existing cable.yml file needs to be modified to work correctly in development. Just copy and paste your production configuration into your development.

Next I’ll plug in some code straight from the actioncable docs into our preexisting connection.rb file inside the channels/application_cable directory inside our app.

module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user

def connect
self.current_user = find_verified_user
end

private
def find_verified_user
if current_user = User.find_by(id: cookies.signed[:user_id])
current_user
else
reject_unauthorized_connection
end
end
end
end

The above code assumes you’ve got authentication and a current_user method set up already — my app uses Devise which has that covered.

Next I’ll modify the other new file created by the channel generator, notifications.coffee. It acts as the receiver of the material streamed in our channel. First thing is to convert it to javascript! Not interested in learning coffeescript right now.

App.notifications = App.cable.subscriptions.create("NotificationsChannel", {
connected: function() {},
disconnected: function() {},
received: function() {}
});

My implementation of ActionCable is very simple, and unidirectional, so all I care about here is the received() function. Let’s set it up to populate a div somewhere in my app:

received: function(data) {
$('#notifications').html(data.html);
}

The notifications div could go anywhere in my layout html files.

Last, I’ll add some notifications to my background worker file,

class BasketWorker
include Sidekiq::Worker
def perform(id, date, token)
user = User.find(id)
  ActionCable.server.broadcast "notifications:#{user.id}", {html:
"<div class='alert alert-warning alert-block text-center'>
<i class='fa fa-circle-o-notch fa-spin'></i>
Searching your gmail for receipts now (it might take a minute if there are many).
</div>"
}
  Scraper.process_emails(user, date, token)
  ActionCable.server.broadcast "notifications:#{user.id}", {html:
"<div class='alert alert-success alert-block text-center'>
Search complete!
</div>
}
end
end

Here I use ActionCable’s broadcast method to send a notification to the current_user. This is a very simple implementation, with no new models or persisted data being handled by ActionCable, but that’ll likely change as I build out more functionality into the app.

A single golf clap? Or a long standing ovation?

By clapping more or less, you can signal to us which stories really stand out.