Increase Performance with Sidekiq and Redis

How about using asynchronous services to improve your system performance

We know that performance is one of the biggest factors that drive a system to success. But even using the most optimized algorithms, some tasks can still take a long time to execute. So how should we deal with these issues?

First of all, I’d like to apologize for the clickbait in the title, but it is a little bit obvious that to improve the performance of a system there are N factors to be considered, and it is necessary to evaluate each case.

The main idea of this article is to show how asynchronous services can make your system more performant.

It is undeniable that tasks of great complexity, which deal with a large amount of data, or with communication with external APIs over which we have no control, can make your system, or part of it, slow. Which ends up making a bad user experience. The user experience can be totally affected depending on the approach used to deal with each situation. Asynchronous services may not make tasks faster, but they can make all the difference from the user’s perspective.

Getting stuck on a loading screen waiting for a report to load, or to receive a large file, often creates a user dislike for the system. When we begin to understand what these bottlenecks are, we can start thinking about how to make this experience less painful for the user.

Let’s start with a practical example, showing a system that uses a synchronous architecture and how we migrated it to asynchronous services.

At the end of the article I will leave a code example with this solution complete and working.

As an example, imagine a system for building custom interfaces for landing pages. This system will receive the configuration and should display the landing page accordingirly.

We’re building a CRUD with Ruby on Rails, but the idea remains the same for other stacks.

# app/controllers/site_configuration_controller.rb
class SiteConfigurationsController < ApplicationController
  def create
    @site_configuration = SiteConfiguration.new(params)

    if @site_configuration.save
      redirect_to @site_configuration
    else
      render :new, status: :unprocessable_entity
    end
  end

  def show
    @site_configuration = SiteConfiguration.find(params[:id])
  end

  def edit
    @site_configuration = SiteConfiguration.find(params[:id])
  end

  def update
    @site_configuration = SiteConfiguration.find(params[:id])

    if @site_configuration.update(params)
      redirect_to @site_configuration
    else
      render :new, status: :unprocessable_entity
    end
  end
end

# site_configuration.rb
class SiteConfiguration < ApplicationRecord
  validates_presence_of :title
end

# site.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= @configuration.title %></title>
    <link rel="shortcut icon" id="favicon" href="<%= @configuration.icon_url %>" />
  <head>
  <body
    style="text-align: center; background-color:<%= @configuration.site_bg %>;"
  >
     ## SOME MORE INFORMATION ##
  </body>
</html>

Doing a quick overview of this code

  • create: saves the information received and redirects to where the page will be processed according to the settings;
  • show: method that receives an id, fetches the information and processes the screen with the settings;
  • edit/update: receives an id, loads the information on a screen for the user to make changes

What is the main problem with this structure? Every time a user performs an action, he has to wait for the entire processing.

Whenever someone wants to load the site, the information needs to be loaded, and the screen must be assembled according to the settings. Depending on the complexity, or the amount of data loaded into this screen, it may take several seconds, and a large amount of processing may be involved.

Imagine a report with information on sales from all branches of a company spread across the country is loaded. We can say that the ideal would be to have reconciled data by region. But even so, in the end, we may still have a processing bottleneck for display.

But then, how can we improve this structure?

We can get into that eternal discussion about which language is faster (python, java, javascript, ruby, php, etc.), and this will always depend on several points. But what nobody can disagree with is that HTML loads faster.

So, what is my proposal to improve this structure? How about we use an asynchronous service, where after the user informs the settings of his landing page, the service processes this information, assembles the final HTML of the screen asynchronously, and saves it in order to always deliver the final HTML directly to the user? This will cause the assembly processing of this screen to happen only once, and no longer every time the screen is loaded. It will happen on the background, so the user will not be stuck until it ends.

For the user doesn’t get stuck waiting for the information to be processed to assemble the screen, we need to do this asynchronously. Therefore, we need to create tasks that will run in the background, and store the result in a place with easy and quick to access, so that this processing only happens once.

To manage these tasks we will use Sidekiq.

Sidekiq

Sidekiq is nothing more than a background job manager. It organizes processing queues to be consumed and runs the jobs in the background.

We can use it to create several tasks to run in the backgroud. The most common types of tasks are sending emails, creating files, communicating with external APIs, maintenance tasks, data reconciliation, and others.

There are several similar tools, depending on your stack you can choose Resque, Celery, RabbitMQ, delayed_job, and others.

You can find more about this tool on the official repository here.

In our case, we will use it to execute a job that will load the data configured by the user and build the landing page HTML.

Redis

Redis is an in-memory non-relational key-value database. It is possible to configure the data durability time, among other features.

In our case, we will use it for two things: store the queues to be executed by Sidekiq in memory and the processing result of our page. Since it is saved in memory, we can access this information much faster.

Store the result of the processing screen in the cache is one of the possible solutions. I chose to use it to demonstrate some Redis utilities, but you can choose to create an HTML file that will be accessed directly by the user, for example.

You can find more about this tool on the official repository here

Sidekiq uses Redis to store the queue information to be executed.

Configuring

For this example, I’m going to use Docker with docker-compose to pull up Redis and Sidekiq.

An excerpt of what my docker-compose file looks like:

services:
  redis:
    image: 'redis:5-alpine'
    command: redis-server
    ports:
      - '6379:6379'
    volumes:
      - 'redis:/data'

sidekiq:
    depends_on:
      - 'db'
      - 'redis'
      - 'web'
    build: .
    volumes:
      - .:/myapp
    links:
      - db
      - redis
      - web
    command: bundle exec sidekiq -q build_site
    env_file:
      - .env
    environment:
      - DATABASE_HOST=db

To use these services, we need to initialize them in our project. Then we’ll do the following:

# config/initializers/redis.rb
REDIS_URL = ENV['REDIS_URL']

$Redis = Redis.new({ url: REDIS_URL })

With this we can access Redis methods using $Redis

Also, we need to setup Sidekiq to use Redis.

# config/initializers/sidekiq.rb
require 'sidekiq'
require 'sidekiq/web'

URL_REDIS = ENV['REDIS_URL']

Sidekiq.configure_server do |config|
  config.redis = { url: URL_REDIS }
end

Sidekiq.configure_client do |config|
  config.redis = { url: URL_REDIS }
end

Sidekiq::Web.use(Rack::Auth::Basic) do |user, password|
  Rack::Utils.secure_compare(::Digest::SHA256.hexdigest(user), ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_USER"])) &
  Rack::Utils.secure_compare(::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_PASSWORD"]))
end

Solution

So let’s go to the solution of the problem.

A critical thing when we talk about doing things asynchronously is identifying what is being done and how to communicate that something has been done.

So let’s version each landing page configuration created by the user. Every time the user informs a new configuration, we will generate a new version, informing the user the name of this version. With this, if the user wants to return to a previous version, he can go back.

Let’s add a version_name field in the configuration table. This field will be very important for us to understand which version is being presented. Remember, we are talking about asynchronous services, where each version can take a long time to be "published"

To generate the version name, let’s modify our SiteConfiguration model. We can use a callback in the model, like this:

# site_configuration.rb
class SiteConfiguration < ApplicationRecord
  before_create :generate_version

  validates_presence_of :title

  private

  def generate_version
    self.version_name = "version-#{DateTime.now.strftime('%Y%d%m%H%M%S')}"
  end
end

With this, always before saving, it will create a unique name for this version, which will be the information we will send to the user.

Now with that version name, we’re going to create a decoupled service that will have a single responsibility. It will receive the name of a version and render the screen with the settings received.

It is good to mention here that the service need to have its responsibilities well defined and focused. As in our example below, this service has the unique and exclusive function of processing and publishing a received version. By doing so, the service can now publish any desired version.

The service can be set up as follows:

class SiteBuilderService
  def initialize(params={})
    @version_name = params[:version_name]
  end

  def self.call(*args)
    new(*args).build
  end

  def build
    build_site
  end

  private

  def build_site
  @configuration = SiteConfiguration.find_by(version_name: @version_name)

    save_page
    update_configuration
  end

  def get_page
    ApplicationController.renderer.render(
      template: 'pages/build_site',
      assigns: {
        configuration: @configuration,
        list_widgets: @list_widgets
      }
    )
  end

  def save_page
    $Redis.set('build_site', get_page)
  end

  def update_configuration
    @configuration.version_status = 'Published'
    @configuration.publish_date = DateTime.now
    @configuration.save!
  end

Doing an overview of the service. Initially it receives the name of a version and searches for corresponding configuration.

After loading the information, save_page is executed. This step will render the landing page, and save the result on Redis in the ‘build_site’ key.

Note that we use the Redis set method to set information to a specific key.

This key build_site will always contain the "published" version, so when the user goes to the view, we just need load the already assembled HTML stored in this key.

After generating and saving the page, an update is done with the published status and publication date. With this, the user can consult the last published versio

Okay, we have a service that generates our page and saves it in memory for easy access.

Now let’s create a new controller, which will load our page, and the view it will present:

# pages_controller.rb
class PagesController < ApplicationController
  def index
    @page = $Redis.get('build_site').html_safe
  end
end

# views/pages/index.html.rb
<%= @page %>

This controller has only one index method, which loads the Redis value and sends it to the view. In the view, we print the preassembled HTML.

Now every time that we want to load the page, it will load the HTML stored on Redis. So there is no more processing to build the page.

At this point what do we need? A way to run our newly created service.

Jobs are an easy way to run tasks in the background working with execution queues. So let’s create a simple job that will run our service:

# jobs/site_build_job.rb
class SiteBuildJob < ApplicationJob
  queue_as :build_site

  def perform(version_name)
    SiteBuilderService.call({ version_name: version_name })
  end
end

Note that we indicate which queue we are going to place this job to run. In this case, the queue will be build_site

When running, it receives the name of which version should be published, and execute the service.

Let’s now modify our old controller. We’re turning it into an API endpoint, which will receive a JSON with the settings and start the whole process:

# api/v1/site_configuration_controller.rb
class Api::V1::SiteConfigurationsController < ApiApplicationController
  def create
    @configuration = SiteConfiguration.new(params)

    if @configuration.save
      builder_jobs
      render json: @configuration, status: :created
    else
      render json: @configuration.errors, status: :unprocessable_entity
    end
  end

  private

  def builder_jobs
    SiteBuildJob.perform_later(@configuration.version_name)
  end
end

Note that after saving, we call the job indicating which version it should process and publish.

This will queue the job to run, and Sidekiq will call it when it’s time.

Now we have an application that receives the settings and runs everything else in the background. This makes the process more fluid for the users, as they will be notified when the processing is completed.

When loading the site, now only load the information contained in a specific key in Redis, which can be accessed quickly.

Conclusion

It is important to remember here the main objective of this article. Stimulate asynchronous thinking. In our example, we have only one asynchronous service to create a cache of our landing page. That alone will have a huge impact on the experience of both our users and the user loading the site.

The application of asynchronous services gives us a wide range of possibilities, such as: creating files, generating reports, consolidating data, caching lists, sending emails, etc.

Analyze your current system and understand which are the points where you can transform the process into something asynchronous. That will certainly give you a gain in performance and user experience.

You can find the complete code of this example here.

We want to work with you. Check out our "What We Do" section!