Using The Rails Cache With Redis
Published: Mar 3, 2022
Last updated: Mar 3, 2022
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
- Basic familiarity with setting up a new Rails project.
- 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
Using The Rails Cache With Redis
Introduction