Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for caching renders in Graphiti, and better support using etags and stale? in the controller #424

Merged
merged 4 commits into from
Mar 27, 2024

Conversation

jkeen
Copy link
Collaborator

@jkeen jkeen commented Jun 28, 2022

This PR exposes some things that make supporting using rails built-in caching methods like stale? with Graphiti a snap, and also allowing the option of caching the render of a graphiti resource (which is often the most time-consuming part of the process)

Changes:

  • expose cache_key method to resource instance
    this generates a combined stable cache key based on resource identifiers specified by the resource class, the specified sideloads, and any specified extra_fields or fields, pages, or links which will affect the response.

  • expose cache_key_with_version method to resource instance
    same as above, but with the last modified dates added in. If any included resource's updated_at changes, this key will change.

  • expose updated_at method to resource instance
    returns the max updated_at date of the resource and any specified sideloads

  • expose etag method to resource instance
    generate a Weak Etag based on the cache_key_with_version response. With etag and updated_at methods on a resource instance, using stale?(@resource) will respect them.

  • for cached resources, rendering logic in Graphiti is wrapped in a cache block Graphiti.cache.fetch("graphiti:render/#{@resource.cache_key}", version: @resource.updated_at, expires_in: @resource.expires_in ) { [expensive rendering] }.

    (Using cache_key and version together by default instead of using cache_key_with_version as the key better ensures we won’t flood a cache store with dead keys)

Using Rails etags and stale?

You can now easily benefit from using rails etags without any additional logic by using the stale? method and passing in your resource.

class EmployeesController < ApplicationController
  def index
   @employees = Employees.all(params)
   respond_with @employees if stale?(@employees)
  end
end

Cache the rendered json

You can also cache the json rendering-step of the resource, which in the case of json-api can sometimes be expensive. In order to cache a resource set up a cache store, enable cache_rendering, and then add a cache_resource directive to the resource you want to cache. For complex resources with many sideloads, this can improve your response time dramatically.

Graphiti.configure do |c|
  c.cache_rendering = Rails.env.production?
  # c.debug = true
  # with debug enabled extra information about caching will be output
end

Graphiti.cache = ::Rails.cache # or whatever cache you want to use that conforms to the same typical cache interface with .read, .write. fetch, etc.
class EmployeesController < ApplicationResource
  cache_resource, expires_in: 1.week 
end

Debugging

With the debug flag enabled in Graphiti config extra information will be logged. This logic tries to make caching the render loop of graphiti dead simple, but sometimes a query with an argument that changes often (a relative time, like Time.now for example) will create an unstable cache key, negating any potential benefits. This problems are a little tricky to spot, but the debugging code below should help.

// stable key

=== Graphiti Debug
Rendering (cached):
Cache key for GET http://localhost:3000/api/v1/employees/2
 \_   stable | Request count: 7 | Hit count: 7
 [✓] W/73e604f858ad89740cdb24eed7ec641e

// volatile key

=== Graphiti Debug
Rendering (cached):
Cache key for GET http://localhost:3000/api/v1/employees?recent=true
 \_ volatile | Request count: 11 | Hit count: 1
 [x] cache key changed W/b7328a2889e4132d2b8145f33895043e -> W/d00e6de884f36cfa477fe3c363745300
      removed: ["employees/query-810d248a61b640c0da5211318de8780c-50-20240327220001166601/args-a6ca37e898a0e40863337849e839c41e2461058c"]
        added: ["employees/query-3349d42b4dfeed25467072ec237aa89f-50-20240327220001166601/args-a6ca37e898a0e40863337849e839c41e2461058c"]

The above illustrates that there have been 11 requests but only 1 of those was pulled from the cache, which in the above query's case is because the query was using Time.now as an argument.

@jkeen jkeen force-pushed the feature/caching branch from a97dd8b to 86acde0 Compare June 28, 2022 13:47
@jkeen
Copy link
Collaborator Author

jkeen commented Jun 28, 2022

@richmolj This concludes my blast of PRs for a while 😝 #422 and #423 were just leading to this in order to keep things topical.

There might be a few things still to add to this, but thought this was a good enough starting point to talk about it. This has made a huuuugge difference in my app, and I'm super stoked about this addition.

json-api resources has a kinda-similar caching strategy, but with more configuration. I liked how they defined it on the Resource definition. I initially had it as a :cache argument being passed into the .find or .all method and this ended up feeling better.

jsonapi-renderer which we're leveraging for rendering has a caching strategy that I'm specifically working around in this PR, because I couldn't get it to work at all. The idea of individually caching each resource response fragment sounds good in theory, but cutting it off at this point made sense until a reason to do otherwise presented itself

@jkeen jkeen force-pushed the feature/caching branch from fff41d7 to fe50f7d Compare March 27, 2024 20:42
jkeen added 4 commits March 27, 2024 17:22
Changes:
 - add sideload-respecting `cache_key`, `cache_key_with_version`, 'updated_at`, and `etag` methods to resource instances.
 - add `cache_resource` method to Resource definition
 - wrap rendering logic in `Rails.cache.fetch(@resource.cache_key, version: @resource.updated_at)` (for cache_resources) to re-use cache keys by default, and dramatically improve rendering
response times (because you know, caching)
…y. Use Graphiti.cache= to configure cache store
@jkeen jkeen force-pushed the feature/caching branch from 989793a to de59d22 Compare March 27, 2024 22:22
@jkeen jkeen changed the title Add support for caching in Graphiti Add support for render-caching in Graphiti, and better support using etags and stale? in the controller Mar 27, 2024
@jkeen jkeen changed the title Add support for render-caching in Graphiti, and better support using etags and stale? in the controller Add support for caching renders in Graphiti, and better support using etags and stale? in the controller Mar 27, 2024
@jkeen jkeen merged commit 8bae50a into graphiti-api:master Mar 27, 2024
36 checks passed
github-actions bot pushed a commit that referenced this pull request Mar 27, 2024
# [1.7.0](v1.6.4...v1.7.0) (2024-03-27)

### Features

* Add support for caching renders in Graphiti, and better support using etags and stale? in the controller ([#424](#424)) ([8bae50a](8bae50a))
Copy link

🎉 This PR is included in version 1.7.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant