Skip to content

Commit

Permalink
Merge pull request #358 from turingschool/316-background-workers
Browse files Browse the repository at this point in the history
Background Workers Lesson
  • Loading branch information
memcmahon authored Nov 4, 2024
2 parents 439c282 + bc668ba commit dee11b1
Show file tree
Hide file tree
Showing 2 changed files with 262 additions and 0 deletions.
1 change: 1 addition & 0 deletions module4/lessons/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ title: Module 4 - Lessons
- [JS: Asynchronous JavaScript](./async_js)
- [Recursion](./recursion)
- [Big O Notation](./big_o)
- [Background Workers](./intro_to_background_workers)

### Career and Professional Development Lessons
- [Analyzing Tech Challenges](./analyzing_tech_challenges)
Expand Down
261 changes: 261 additions & 0 deletions module4/lessons/intro_to_background_workers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
# Intro to Background Workers

## Learning Goals

- Describe a background job and when we would want to use one
- Be able to implement a basic background job with Sidekiq

## Intro

When building websites, it’s important to keep response times down. Long-running requests will tie up server resources, degrade user perception of your site, and make it hard to manage failures.

There’s a solution to this: return a successful response, and then schedule some computation to happen later, outside the original request/response cycle.

We do this with background job. A background job allows you to run process on a separate dedicated thread. We can move tasks that don't have to happen in real time to their own process so that the app can move on, rather than causing a backlog of requests which slows everything down.

You may also hear 'background jobs' referred to as 'background workers.' These terms are interchangeable, but 'worker' is more common with older versions of Sidekiq.

### Do you need a job queue?

How do you identify areas of the application that can benefit from a background job? Some common areas for asynchronous work:

- Data Processing - e.g. generating thumbnails or resizing images
- 3rd Party APIs - interacting with a service outside of your site
- Maintenance - expiring old sessions, sweeping caches
- Email/Text Services - a request that causes an email or SMS to be sent
- Scheduling a task - e.g. daily reminders, clearing data after a time period

Applications with good Object Oriented design make it easy to queue background jobs, while poor OO makes it hard to extract jobs since responsibilities tend to overlap.

## App Setup

Let's look at an example of making a background task using Sidekiq. We'll use the `main`
 branch of the ["Daily Mood" application](https://github.com/turingschool-examples/daily-mood-mailer-7).

```bash
$ git clone [email protected]:turingschool-examples/daily-mood-mailer.git
$ cd daily-mood-mailer
$ bundle
$ rails db:{create,migrate}
$ rails s
```

This application's homepage has a form that takes an email and a string to generate an email. Start the server rails s and you should be able to view the app at `http://localhost:3000` which has form inputs for an email address and a mood. When you submit that form you should feel the pain of a slow page load.

Why?

Check out our `UserNotifierMailer` mailer. A 5 second delay has been hard-coded in to simulate a real-life delay.

Notice that this process takes a very long time. What we have here is a perfect candidate for a background process:

- Operation is slow
- User's interaction with the process is already asynchronous (submit the form then go check their email)
- Operation is well-encapsulated behind the UserNotifier interface
- Operation requires relatively little data as inputs (email address and mood).

Sidekiq and Resque are the 2 most popular queuing libraries for Ruby. For this application, we'll use Sidekiq.

## Dependency — Redis

Sidekiq uses Redis as a memory store for enqueued jobs. Make sure you have Redis installed and running:

```bash
$ brew update && brew install redis
```

Then, run `redis-server`.

This command starts the Redis server on port 6379. Once the Redis server is running it is ready to queue jobs that still need to be done.

You can check if your Redis process is running by executing the command `redis-cli`:

```bash
$ redis-cli
127.0.0.1:6379>
```

The need for running our Redis server now is similar to how Postgres must be running in order for ActiveRecord to interact with our database.

## Sidekiq Setup

Sidekiq is the tool we will use to manage our background jobs.

We can add the sidekiq gem to our Gemfile:

```ruby
gem "sidekiq"
```

Note: this gem should live **outside** of the `:development, :test` group.

bundle the app and you should now be able to run a sidekiq process by executing the command:

```ruby
bundle exec sidekiq
```

You should get the fancy Sidekiq ASCII logo in your terminal:

```bash
s
ss
sss sss ss
s sss s ssss sss ____ _ _ _ _
s sssss ssss / ___|(_) __| | ___| | _(_) __ _
s sss \___ \| |/ _` |/ _ \ |/ / |/ _` |
s sssss s ___) | | (_| | __/ <| | (_| |
ss s s |____/|_|\__,_|\___|_|\_\_|\__, |
s s s |_|
s s
sss
sss
```
(It should look like a person doing a karate kick in your terminal, not whatever thing you see above, formatting is hard.)
Next we need to add a Sidekiq initializer.
```bash
$ touch config/initializers/sidekiq.rb
```
*config/initializers/sidekiq.rb*
```ruby
Sidekiq.configure_server do |config|
config.redis = { url: 'redis://localhost:6379/0' }
end

Sidekiq.configure_client do |config|
config.redis = { url: 'redis://localhost:6379/0' }
end
```
If your server is running, you must restart it after adding an initializer for it to take effect.
We do not need to do this step for the Daily Mood app, however, if you are running an API only application (like your consultancy backend apps) you must enable some sessions middleware that is excluded in API only apps by default:
*config/application.rb*
```ruby
config.session_store :cookie_store, key: '_interslice_session'
config.middleware.use ActionDispatch::Cookies
config.middleware.use config.session_store, config.session_options
```
## Creating the Job
Now we have Sidekiq running with our application, but so far it doesn't do anything for us. Let's create a job to handle our email.
In the terminal:
```bash
$ rails generate sidekiq:job mood_sender
```
This will generate your job class within a directory called `sidekiq`:
*app/sidekiq/mood_sender_job.rb*
```ruby
class MoodSenderJob
include Sidekiq::Job
def perform(*args)
# Do something
end
end
```
It is Sidekiq convention to use the suffix of `Job` for your job classes.****
## Defining Sidekiq Job Operations
Within a Sidekiq job, the instance method `#perform` is what gets called whenever a job appears for our job to do. Other methods can live within the job class, but `#perform` is what will be invoked when running the job.
Let's think about what needs to go in here and about what inputs are required for the job:
- Needs to take in the email address and 'mood' (since these are the parameters needed to send the email)
- Needs to send an email using the UserNotifier
Given these constraints, it might look something like:
*app/sidekiq/mood_sender_job.rb*
```ruby
class MoodSenderJob
include Sidekiq::Job
def perform(email, mood)
UserNotifierMailer.send_mood_email(email, mood).deliver_now
end
end
```
## Queueing Jobs — Sidekiq::Job.perform_async
The Sidekiq job defines what actual work will be done whenever our background process is invoked. Now we just need to actually invoke it.
With Sidekiq, we dispatch a job for a job to do later by calling `.perform_async` on our job and providing it whatever arguments are needed for the job.
Under the hood, the `.perform_async` method writes data into Redis indicating the type of job which needs to be done and the data associated with it. The queues are monitored in a separate process so that whenever new jobs appear, they'll be started!
Also commonly used is `.perform_at` which can be reviewed in the Sidekiq docs. This method allows you to specify the date/time a job should execute.
Let's see what it looks like to actually queue the job. Recall that we were previously sending the email directly from our `MailersController`. Let's replace the line that was sending the email.
*app/controllers/mailers_controller.rb*
```ruby
class MailersController < ApplicationController
def create
MoodSenderJob.perform_async(params[:mailers][:email], params[:mailers][:mood])
flash[:message] = "You did it! Email sent to #{params[:mailers][:email]}"
redirect_to "/sent"
end
def sent
end
end
```
Remember -- the arguments passed to the `.perform_async` method here will eventually be handed to your job's `#perform` method, so make sure they match up.
Let's try this out now - hopefully hat painful delay is no more!
## Sidekiq Dashboard
Sidekiq provides a dashboard for us to monitor our local job queues.
In our routes file, we'll need to mount the Sidekiq dashboard.
*config/routes.rb*
```ruby
require 'sidekiq/web'
Rails.application.routes.draw do
root "home#show"
get "/sent", to: "mailers#sent"
resources :mailers, only: [:create]
mount Sidekiq::Web => '/sidekiq'
end
```
Now you can navigate to `http://localhost:3000/sidekiq/`. This dashboard is very useful for testing out jobs and receiving confirmation that everything is queued according to plan.
Note: if you are getting errors when trying to visit the Sidekiq dashboard, try clearing your browser cookies.
## Checks for Understanding
- What is a background job?
- Why would you use a background job?
- Describe Sidekiq and Redis, and draw a diagram of how they interact with Rails.
## Further Practice
• Create your own Job class which creates an instance of a model asynchronously.
Completed code for this lesson can be found on the `background-jobs-complete` branch [here](https://github.com/turingschool-examples/daily-mood-mailer-7/tree/background-jobs-complete).

0 comments on commit dee11b1

Please sign in to comment.