Imagine that you need to do a heavy task on your Rails server. If you’re brute enough, you’ll throw your logic into your controller, and let that long request stay there for a couple of minutes. Instead, you create an asynchronous job (with Resque, Sidekiq, ActiveJob…).

Now everything is great; your tasks are executing in background, and letting those server production threads handle more requests. But… what if you just want to know how much time will it take to process that task? How does the client (javascript) know the progress of that well-encapsulated task worker?

If you’re using Rails 5, you’ll know that actioncable is the best way to go. What if you’re not? Then this post is for you, my friend!

On this post, I’ll try to explain a simple logic to achieve this feature.

Implementation

This feature needs some code in the following parts of your application:

  • Model: Create a new model to handle TaskStatuses.
  • Controller: Add some endpoints to poll the TaskStatus information.
  • Worker: Update the TaskStatus progress.
  • View: Create some javascript logic to request a job to the server, and keep track of it.

Model

First off, generate your model:

$ rails g model TaskStatus progress:float

And migrate your database:

$ rake db:migrate

You’ll want to add some methods and logic to your model. Be creative!

class TaskStatus < ActiveRecord::Base
  # 0 means not done, 50 means half done, 100 means done
  validate :progress, presence: true, inclusion: { within: 0..100 }

  # You'll probably want to use a better naming system, or move this code
  # into a different context, like a helper.

  def done?
    progress == 100
  end

  def set(n)
    self.update! progress: n
  end

  def step(n)
    self.increment progress: n
  end
end

Controller

The controller should have two endpoints; one to request and perform a background job, and one to poll the current TaskStatus progress.

For example, this should be a valid controller:

class JobsController < ApplicationController

  # GET /jobs/
  def index
  end

  # POST /jobs/work
  def work
    # Just create a task status, and send it as a parameter to your worker.
    # For example, for active job:
    ExampleJob.perform_later task_status

    # Don't forget to return information about the task object:
    render status: :created, json: task_status
  end

  # GET /jobs/:id/poll
  def poll
    # Just return the task object. You may want to place this endpoint into another controller,
    # called TaskStatusesController, or FooBarController, or whatever.
    # Note that `task_status` will retrieve the status object from the database.
    render status: :ok, json: task_status
  end

  private

  # If params[:id] is present, search the object on the database. Otherwise, create a new one!
  def task_status
    @task_status ||= params[:id].present? ? TaskStatus.find(params[:id]) : TaskStatus.create!(progress: 0)
  end
end

Also, don’t forget to update your config/routes.rb file:

resources :jobs do
  collection do
    post :work
    get :index
  end

  member do
    get :poll
  end
end

Worker

The worker or background job should update the TaskStatus object at will. For example:

class ExampleJob < ActiveJob::Base
  queue_as :default

  # This job will take 10 seconds to be fully performed!
  def perform(task_status)
    100.times do
      sleep 0.1
      task_status.step 1
    end
  end
end

If you are iterating items, and performing an slow action on each item, just divide 100 between the number of items, and increment the TaskStatus object with that value:

unit = 100.0 / items.count
items.each do |item|
  item.slow_method
  task_status.step unit
end

Views

Finally, setup some javascript to use our controller’s endpoints. For example, in your index view:

<%= button_tag 'Work!', id: 'request-button' %>
<span>Progress: </span>
<span id="progress"></span>

And the javascript code (using jQuery):

$(function() {
  // Poll interval; how often should we ask the server about TaskStatus?
  var POLL_INTERVAL = 100; // ms
  // TaskStatus content (json).
  var task_status = {};

  // When pressing the "Work!" button ...
  $('#request-button').click(function() {
    $(this).prop('disabled', true);

    // ... request a job by using an ajax request.
    $.ajax({
      method: 'POST',
      url: '/jobs/work',
      contentType: 'json'
    }).done(function(data) {
      task_status = data;
      // When the request finishes, start polling the status
      setTimeout(updateStatus, POLL_INTERVAL);
    });
  });

  // Check the current status of the TaskStatus object.
  var updateStatus = function() {
    $.ajax({
      method: 'POST',
      url: '/jobs/' + task_status.id + '/poll',
      contentType: 'json'
    }).done(function(data) {
      task_status = data;

      // Done!
      if(task_status.progress == 100) {
        $('#progress').text('Done!');
      }
      // Not done!
      else {
        $('#progress').text(task_status.progress + '%');
        // Ask again in POLL_INTERVAL ms.
        setTimeout(updateStatus, POLL_INTERVAL);
      }
    });
  };
});

Now, run that server, go to your view, press that button, and the background task progress!

Conclusion

Now, there is no need to use web-sockets or complex solutions to create a basic status tracking system. Almost everything is possible with simple HTTP requests. However, polling on web applications aren’t always a good idea; those constant requests generate overhead on the server side.