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!