Logo

dev-resources.site

for different kinds of informations.

A way to cache responses in Grape API

Published at
6/28/2024
Categories
ruby
rails
performance
caching
Author
haukot
Categories
4 categories in total
ruby
open
rails
open
performance
open
caching
open
Author
6 person written this
haukot
open
A way to cache responses in Grape API

Caching is a great way to speed up slow pages and to make your API faster in general.

Let's say we want to cache the response of our API.

Key to cache invalidation

First thing to think when adding a cache is the cache invalidation.

Rails have handy methods for that: ActiveRelation and ActiveRecord methods cache_key and cache_key_with_version.
While cache_key returns only the id of a model, e.g.

Product.where("name like ?", "%Game%").last.cache_key
=> "products/124"
Enter fullscreen mode Exit fullscreen mode

cache_key_with_version additionally returns the timestamp of the last change, e.g.

Product.find(124).cache_key_with_version
=> "products/124-20240624103815954181"
Enter fullscreen mode Exit fullscreen mode

By default it works by the updated_at column, but you can customize it like this:

Product.find(124).cache_key(:last_reviewed_at)
Enter fullscreen mode Exit fullscreen mode

For relation it works a bit differently, returning the key based on the SQL query hash. It takes into consideration params too.

Product.where("name like ?", "%Game%").cache_key
# => "products/query-1850ab3d302391b85b8693e941286659"
Enter fullscreen mode Exit fullscreen mode

And cache_key_with_version additionally returns the id and timestamp of the last entity

Product.where("name like ?", "%Game%").cache_key_with_version
# => "products/query-e0db51fbb1a07ab9545d84d80aac3d16-124-20240628093023387346"
Enter fullscreen mode Exit fullscreen mode

Two caching strategies

Adding caching is easy, but the problem is the cache invalidation. There are two stategies for doing that

  1. Push method - by some triggers (e.g., once an hour, or when a product changes), we prepare data and store it in the cache.
    The positives are that data always will be in the cache when a user calls an API.
    The negatives - we need to prepare data in all places where there are triggers, and be careful with cache invalidation (it's very easy to miss a place that occasionally changes your model).

  2. Pull method - the moment we need data from the cache, we check it, and if there is no data - we get the data in the usual way and store it in the cache for the future.
    It's easier in implementation, as we're worrying less about invalidation (though it still needs some work),
    But it works worse with cache misses - as some users will wait for a response full time.
    So to use that, your API should not be too slow!

For the push method, you are good to use cache_key, but for the pull method we need to check if the cache is still correct, so cache_key_with_version is our friend

I'd say generally it's better to start with pull method, and use push method in cases when it's possible to update the cache at some time, e.g. once a day.

But you should always take into consideration your requirements.

Going to the project

We'll go with pull method as an easier one, and use cache_key_with_version. Let's add a helper method in Grape API:

  helpers do
    def present(resource, namespace='MyAPI', caching: false)
      return resource.to_json unless caching

      Rails.cache.fetch([resource.cache_key_with_version, namespace].join('-'), expires_in: 1.day) do
        resourse.to_json
      end
    rescue Redis::TimeoutError
      resource.to_json
    end
  end
Enter fullscreen mode Exit fullscreen mode

Besides resource.cache_key_with_version, we also make separate caches for different API namespaces (we don't want users to see admin's output).
In more complex examples, you could add more params, e.g., options for serializers if they change the output when SQL is the same.

Then we could use it in the API like this

class Edu::API::V2::Products < Grape::API
  resources :products do
    get do
      products = Product.some_query(params)
      present products, "API::V2::Products", caching: true
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

The next thing is to make sure we invalidate products if the output depends on related models. As our cache depends on the updated_at column of Products, and other models will not affect it by default.

class Option < ApplicationRecord
  # invalidate product cache if option changes 
  belongs_to :product, touch: true
end

class Product < ApplicationRecord
  # invalidate product cache if promos change
  has_and_belongs_to_many :promos,
                          after_add: :touch_updated_at,
                          after_remove: :touch_updated_at

  def touch_updated_at(_promo)
    touch if persisted?
  end
end
Enter fullscreen mode Exit fullscreen mode

Things to take into consideration

  1. It's better to have a separate Redis database number for the cache. There could be a case that Redis will be full of cache entries and remove e.g., Sidekiq-related data. (it depends on the Redis eviction config)

  2. Beware of untrusted input. There could be a case when an attacker could fill your cache up by sending different versions of parameters.

E.g., if you have the parameter search, better not cache it as every cache will be different.

You could easily disable cache for specific params

    params do
      optional :search
    end
    get do
      products = Product.some_query(params)
      caching = params[:search].blank?
      present products, "API::V2::Products", caching: caching
    end
Enter fullscreen mode Exit fullscreen mode

Links

Documentation:

Source code:

Examples of caching in some projects:

  • Spree 1 2
  • Gitlab 1 2 3 4
  • ActiveModel Serializers 1

Additional concepts:

  • Russian-doll caching
  • Push vs Pull caching strategies in System Design
caching Article's
30 articles in total
Favicon
How to Implement Caching in PHP and Which Caching Techniques Are Best for Performance?
Favicon
Understanding Memcached: A Powerful In-Memory Caching Solution by Abhay
Favicon
Caching with Redis for Backend in Apache Superset
Favicon
The Most Popular Database Caching Strategies Explained
Favicon
Melhorando o Desempenho da Sua Aplicação PHP com Lithe Cache
Favicon
Cache Strategies: A Complete Guide with Real-Life Examples 🚀
Favicon
Caching — An overview
Favicon
Bloom Filters
Favicon
HybridCache in ASP.NET Core - New Caching Library
Favicon
Advanced Data Caching Techniques for High-Performance Systems
Favicon
Redis: Understanding the Basics
Favicon
Implementing Caching Strategies for Improved Performance
Favicon
Improving the Performance of Your PHP Application with Lithe Cache
Favicon
Redis caching with Mongoose
Favicon
Building Scalable Web Applications: Techniques for Handling High Traffic
Favicon
🚀Optimize Web Performance in the Cloud with Caching!🚀
Favicon
Top 5 Next.js Caching Solutions for Next.js Apps (2024)
Favicon
Building a cache in Python
Favicon
Caching in .Net 8: Improving Application Performance
Favicon
Davide's Code and Architecture Notes - Cache Expiration vs Cache Eviction (and Eviction Policies)
Favicon
Conquering the Cache Calamity: My Journey to HNG Internship
Favicon
Next.js Caching Issues With Fetching Data
Favicon
MemoryCache in C#: A Practical Guide
Favicon
Caching & Memoization with state variables
Favicon
Laravel Caching - Explained Simply
Favicon
A way to cache responses in Grape API
Favicon
The Power of Caching and How to Implement It in Your Python Applications
Favicon
Caching
Favicon
Cost-Effective Image Management: Maximizing Efficiency Through Network Image Caching in Mobile Apps
Favicon
Top Redis Use Cases

Featured ones: