Back to home

Using The Rails Cache With Redis main image

Using The Rails Cache With Redis

This post will be a brief introduction to setting up the Rails cache and seeing it in action with Redis in development.

We will look at the configuration required from Rails but it will expect that you know a little about Redis and have it already installed and setup on your local machine.

Source code can be found here

Prerequisites

  1. Basic familiarity with setting up a new Rails project.
  2. basic familiarity with Redis.

Getting started

We will use Rails to initialize the project demo-rails-cache:

# Create a new rails project $ rails new demo-rails-with-react-frontend $ cd demo-rails-with-react-frontend # Create a controller to demo the cache $ bin/rails g controllers hello index

At this stage, we are ready to adjust some files and configuration for the demo.

Adjusting the application configuration

We are turning off the default forgery protection so that we can demonstrate the cache on the command-line.

require_relative 'boot' require 'rails/all' # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module DemoRailsCache class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.0 # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files # in config/environments, which are processed later. # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") config.action_controller.default_protect_from_forgery = false if ENV['RAILS_ENV'] == 'development' end end

Adjusting the development configuration

Next is to update our development environment configuration to use Redis instead of the default memory store.

I am omitting a lot of the configuration that is already there, so ensure that you only adjust the following:

require 'active_support/core_ext/integer/time' Rails.application.configure do # ... omitted if Rails.root.join('tmp/caching-dev.txt').exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true # CHANGE HERE # config.cache_store = :memory_store config.cache_store = :redis_cache_store, { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/1') } config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false config.cache_store = :null_store end # ... omitted end

Setting up our hello controller

We will have a basic controller action to demonstrate the cache. It will return some JSON.

class HelloController < ApplicationController def index res = Rails.cache.fetch(:cached_result) do # Only executed if the cache does not already have a value for this key # We will sleep for 3 seconds to simulate an expensive operation sleep 3 # Return the array ["Hello", "World"] messages = %w[Hello World] end render json: { message: res } end end

We use Rails.cache.fetch to look for the key cached_result in the cache. If it is not found, we will execute the block and store the result in the cache.

Our block has a sleep 3 line to simulate an expensive operation. The idea is that if we miss the cache, we will need to wait 3 seconds for a response.

With this done, we are now ready to ensure the route is available.

Update our routes

Set config/routes.rb to the following:

Rails.application.routes.draw do resources :hello, only: [:index] end

We will now be able to access the GET request for /hello.

Running the server and testing our cache

We are now ready to run our server and test the cache setup. We need to ensure that caching is turned on for this.

# Toggle on the dev cache $ bin/rails dev:cache # Start the server $ bin/rails s

In one terminal will be using HTTPie to demonstrate the requests so we can visually see the cache in action.

In another terminal, I will run redis-cli monitor to monitor anything that is entered into the cache and check that the configuration is correct.

Missing the cache

In my first call, I will be missing the cache. The logs look as follows:

$ http GET localhost:3000/hello HTTP/1.1 200 OK Cache-Control: max-age=0, private, must-revalidate Content-Type: application/json; charset=utf-8 ETag: W/"d75d122ce074e5382e16eaf331afa72a" Referrer-Policy: strict-origin-when-cross-origin Server-Timing: start_processing.action_controller;dur=0.125732421875, cache_read.active_support;dur=0.423095703125, cache_generate.active_support;dur=3003.9150390625, cache_write.active_support;dur=0.537841796875, process_action.action_controller;dur=3006.31298828125 Transfer-Encoding: chunked Vary: Accept X-Content-Type-Options: nosniff X-Download-Options: noopen X-Frame-Options: SAMEORIGIN X-Permitted-Cross-Domain-Policies: none X-Request-Id: 19bbeec4-189e-4593-8003-f6c567ce19d8 X-Runtime: 3.012563 X-XSS-Protection: 0 { "message": [ "Hello", "World" ] }

You will note here that it took 3.012563 seconds to get the response.

Note: It is outside the scope of this demo, but it might be worth adding another heading X-Cache to indicate that the cache was missed or hit. It is not a standard HTTP header field, but useful for debugging.

The Rails server console tells us the following:

Started GET "/hello" for ::1 at 2022-03-03 11:43:49 +1000 Processing by HelloController#index as */* Completed 200 OK in 3006ms (Views: 0.3ms | ActiveRecord: 0.0ms | Allocations: 279)

This similarly tells us that it took 3 seconds to get the response.

Hitting the cache

At this point, our cache value will have been set in Redis during the first request.

If we run the request for a a second time, we get the following:

http GET localhost:3000/hello HTTP/1.1 200 OK Cache-Control: max-age=0, private, must-revalidate Content-Type: application/json; charset=utf-8 ETag: W/"d75d122ce074e5382e16eaf331afa72a" Referrer-Policy: strict-origin-when-cross-origin Server-Timing: start_processing.action_controller;dur=0.126953125, cache_read.active_support;dur=0.804931640625, cache_fetch_hit.active_support;dur=0.00634765625, process_action.action_controller;dur=2.620849609375 Transfer-Encoding: chunked Vary: Accept X-Content-Type-Options: nosniff X-Download-Options: noopen X-Frame-Options: SAMEORIGIN X-Permitted-Cross-Domain-Policies: none X-Request-Id: 279b4ba3-e72c-4893-ae5f-30a5d884f6a0 X-Runtime: 0.010306 X-XSS-Protection: 0 { "message": [ "Hello", "World" ] }

Note here that now the request took 0.010306 seconds.

The Rails server logs tell us the following:

Started GET "/hello" for ::1 at 2022-03-03 11:42:35 +1000 Processing by HelloController#index as */* Completed 200 OK in 2ms (Views: 0.3ms | ActiveRecord: 0.0ms | Allocations: 207)

Awesome! We got the response from the cache.

If we were running redis-cli monitor that entire time, then the monitor logs tell us the following:

$ redis-cli monitor OK 1646271829.959654 [1 [::1]:56084] "get" "cached_result" 1646271832.964237 [1 [::1]:56084] "set" "cached_result" "\x04\bo: ActiveSupport::Cache::Entry\t:\x0b@value[\aI\"\nHello\x06:\x06ETI\"\nWorld\x06;\aT:\r@version0:\x10@created_atf\x060:\x10@expires_in0" 1646271928.797673 [1 [::1]:56084] "get" "cached_result"

The set cached_result log was done to set the cache value. The get cached_result log was done to get the cache value (if it existed).

Summary

Today's post demonstrated how to set up the basics for the Rails cache with Redis.

It will be used again in future when we look at rate-limiting approaches.

If you wanted to go further, it is worth looking into passing arguments like expires_in to test different cache configurations.

Resources and further reading

Photo credit: anniespratt

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on workingoutloud.dev, Den Dribbles and LandPad .

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.