-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Fix up caching, especially fragment_cache #1781
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
Changes from all commits
35a7c81
253205b
b892415
5375e00
b599360
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -167,7 +167,7 @@ def serializable_hash(adapter_options = nil, options = {}, adapter_instance = se | |
adapter_options ||= {} | ||
options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options) | ||
cached_attributes = adapter_options[:cached_attributes] ||= {} | ||
resource = cached_attributes(options[:fields], cached_attributes, adapter_instance) | ||
resource = fetch_attributes(options[:fields], cached_attributes, adapter_instance) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. otherwise we have a method that takes an argument (a hash) of the same name |
||
relationships = resource_relationships(adapter_options, options, adapter_instance) | ||
resource.merge(relationships) | ||
end | ||
|
@@ -194,8 +194,6 @@ def json_key | |
def read_attribute_for_serialization(attr) | ||
if respond_to?(attr) | ||
send(attr) | ||
elsif self.class._fragmented | ||
self.class._fragmented.read_attribute_for_serialization(attr) | ||
else | ||
object.read_attribute_for_serialization(attr) | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,3 @@ | ||
# TODO(BF): refactor file to be smaller | ||
# rubocop:disable Metrics/ModuleLength | ||
module ActiveModel | ||
class Serializer | ||
UndefinedCacheKey = Class.new(StandardError) | ||
|
@@ -9,10 +7,9 @@ module Caching | |
included do | ||
with_options instance_writer: false, instance_reader: false do |serializer| | ||
serializer.class_attribute :_cache # @api private : the cache store | ||
serializer.class_attribute :_fragmented # @api private : @see ::fragmented | ||
serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key. Ignored if the serializable object defines #cache_key. | ||
serializer.class_attribute :_cache_only # @api private : when fragment caching, whitelists cached_attributes. Cannot combine with except | ||
serializer.class_attribute :_cache_except # @api private : when fragment caching, blacklists cached_attributes. Cannot combine with only | ||
serializer.class_attribute :_cache_only # @api private : when fragment caching, whitelists fetch_attributes. Cannot combine with except | ||
serializer.class_attribute :_cache_except # @api private : when fragment caching, blacklists fetch_attributes. Cannot combine with only | ||
serializer.class_attribute :_cache_options # @api private : used by CachedSerializer, passed to _cache.fetch | ||
# _cache_options include: | ||
# expires_in | ||
|
@@ -21,7 +18,7 @@ module Caching | |
# race_condition_ttl | ||
# Passed to ::_cache as | ||
# serializer.cache_store.fetch(cache_key, @klass._cache_options) | ||
# Passed as second argument to serializer.cache_store.fetch(cache_key, self.class._cache_options) | ||
# Passed as second argument to serializer.cache_store.fetch(cache_key, serializer_class._cache_options) | ||
serializer.class_attribute :_cache_digest_file_path # @api private : Derived at inheritance | ||
end | ||
end | ||
|
@@ -71,19 +68,15 @@ def _skip_digest? | |
_cache_options && _cache_options[:skip_digest] | ||
end | ||
|
||
def cached_attributes | ||
_cache_only ? _cache_only : _attributes - _cache_except | ||
end | ||
|
||
def non_cached_attributes | ||
_attributes - cached_attributes | ||
end | ||
|
||
# @api private | ||
# Used by FragmentCache on the CachedSerializer | ||
# to call attribute methods on the fragmented cached serializer. | ||
def fragmented(serializer) | ||
self._fragmented = serializer | ||
def fragmented_attributes | ||
cached = _cache_only ? _cache_only : _attributes - _cache_except | ||
cached = cached.map! { |field| _attributes_keys.fetch(field, field) } | ||
non_cached = _attributes - cached | ||
non_cached = non_cached.map! { |field| _attributes_keys.fetch(field, field) } | ||
{ | ||
cached: cached, | ||
non_cached: non_cached | ||
} | ||
end | ||
|
||
# Enables a serializer to be automatically cached | ||
|
@@ -208,118 +201,65 @@ def object_cache_key(serializer, adapter_instance) | |
end | ||
end | ||
|
||
def cached_attributes(fields, cached_attributes, adapter_instance) | ||
if self.class.cache_enabled? | ||
### INSTANCE METHODS | ||
def fetch_attributes(fields, cached_attributes, adapter_instance) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. renamed cached_attributes which calls cache_check to fetch_attributes which calls fetch. Naming goes all the way back https://github.com/rails-api/active_model_serializers/pull/810/files https://github.com/rails-api/active_model_serializers/pull/693/files I have more simplifications in mind, in particular moving the cache code outside of a serializer, or perhaps only included when |
||
if serializer_class.cache_enabled? | ||
key = cache_key(adapter_instance) | ||
cached_attributes.fetch(key) do | ||
cache_check(adapter_instance) do | ||
attributes(fields) | ||
serializer_class.cache_store.fetch(key, serializer_class._cache_options) do | ||
attributes(fields, true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. passing true since we always want to reload the attributes in a cache block |
||
end | ||
end | ||
elsif serializer_class.fragment_cache_enabled? | ||
fetch_attributes_fragment(adapter_instance) | ||
else | ||
cache_check(adapter_instance) do | ||
attributes(fields) | ||
end | ||
attributes(fields, true) | ||
end | ||
end | ||
|
||
def cache_check(adapter_instance) | ||
if self.class.cache_enabled? | ||
self.class.cache_store.fetch(cache_key(adapter_instance), self.class._cache_options) do | ||
def fetch(adapter_instance, cache_options = serializer_class._cache_options) | ||
if serializer_class.cache_store | ||
serializer_class.cache_store.fetch(cache_key(adapter_instance), cache_options) do | ||
yield | ||
end | ||
elsif self.class.fragment_cache_enabled? | ||
fetch_fragment_cache(adapter_instance) | ||
else | ||
yield | ||
end | ||
end | ||
|
||
# 1. Create a CachedSerializer and NonCachedSerializer from the serializer class | ||
# 2. Serialize the above two with the given adapter | ||
# 3. Pass their serializations to the adapter +::fragment_cache+ | ||
# | ||
# It will split the serializer into two, one that will be cached and one that will not | ||
# | ||
# Given a resource name | ||
# 1. Dynamically creates a CachedSerializer and NonCachedSerializer | ||
# for a given class 'name' | ||
# 2. Call | ||
# CachedSerializer.cache(serializer._cache_options) | ||
# CachedSerializer.fragmented(serializer) | ||
# NonCachedSerializer.cache(serializer._cache_options) | ||
# 3. Build a hash keyed to the +cached+ and +non_cached+ serializers | ||
# 4. Call +cached_attributes+ on the serializer class and the above hash | ||
# 5. Return the hash | ||
# | ||
# @example | ||
# When +name+ is <tt>User::Admin</tt> | ||
# creates the Serializer classes (if they don't exist). | ||
# CachedUser_AdminSerializer | ||
# NonCachedUser_AdminSerializer | ||
# | ||
# Given a hash of its cached and non-cached serializers | ||
# 1. Determine cached attributes from serializer class options | ||
# 2. Add cached attributes to cached Serializer | ||
# 3. Add non-cached attributes to non-cached Serializer | ||
def fetch_fragment_cache(adapter_instance) | ||
serializer_class_name = self.class.name.gsub('::'.freeze, '_'.freeze) | ||
self.class._cache_options ||= {} | ||
self.class._cache_options[:key] = self.class._cache_key if self.class._cache_key | ||
|
||
cached_serializer = _get_or_create_fragment_cached_serializer(serializer_class_name) | ||
cached_hash = ActiveModelSerializers::SerializableResource.new( | ||
object, | ||
serializer: cached_serializer, | ||
adapter: adapter_instance.class | ||
).serializable_hash | ||
|
||
non_cached_serializer = _get_or_create_fragment_non_cached_serializer(serializer_class_name) | ||
non_cached_hash = ActiveModelSerializers::SerializableResource.new( | ||
object, | ||
serializer: non_cached_serializer, | ||
adapter: adapter_instance.class | ||
).serializable_hash | ||
# 1. Determine cached fields from serializer class options | ||
# 2. Get non_cached_fields and fetch cache_fields | ||
# 3. Merge the two hashes using adapter_instance#fragment_cache | ||
def fetch_attributes_fragment(adapter_instance) | ||
serializer_class._cache_options ||= {} | ||
serializer_class._cache_options[:key] = serializer_class._cache_key if serializer_class._cache_key | ||
fields = serializer_class.fragmented_attributes | ||
|
||
non_cached_fields = fields[:non_cached].dup | ||
non_cached_hash = attributes(non_cached_fields, true) | ||
include_directive = JSONAPI::IncludeDirective.new(non_cached_fields - non_cached_hash.keys) | ||
non_cached_hash.merge! resource_relationships({}, { include_directive: include_directive }, adapter_instance) | ||
|
||
cached_fields = fields[:cached].dup | ||
key = cache_key(adapter_instance) | ||
cached_hash = | ||
serializer_class.cache_store.fetch(key, serializer_class._cache_options) do | ||
hash = attributes(cached_fields, true) | ||
include_directive = JSONAPI::IncludeDirective.new(cached_fields - hash.keys) | ||
hash.merge! resource_relationships({}, { include_directive: include_directive }, adapter_instance) | ||
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the removal of the two fragment serializers is an enormous change and should be considered carefully, esp re: how it handles relationships there is a definite performance improvement, and arguable it's less complex now, but is still kind of messy |
||
end | ||
|
||
# Merge both results | ||
adapter_instance.fragment_cache(cached_hash, non_cached_hash) | ||
end | ||
|
||
def _get_or_create_fragment_cached_serializer(serializer_class_name) | ||
cached_serializer = _get_or_create_fragment_serializer "Cached#{serializer_class_name}" | ||
cached_serializer.cache(self.class._cache_options) | ||
cached_serializer.type(self.class._type) | ||
cached_serializer.fragmented(self) | ||
self.class.cached_attributes.each do |attribute| | ||
options = self.class._attributes_keys[attribute] || {} | ||
cached_serializer.attribute(attribute, options) | ||
end | ||
cached_serializer | ||
end | ||
|
||
def _get_or_create_fragment_non_cached_serializer(serializer_class_name) | ||
non_cached_serializer = _get_or_create_fragment_serializer "NonCached#{serializer_class_name}" | ||
non_cached_serializer.type(self.class._type) | ||
non_cached_serializer.fragmented(self) | ||
self.class.non_cached_attributes.each do |attribute| | ||
options = self.class._attributes_keys[attribute] || {} | ||
non_cached_serializer.attribute(attribute, options) | ||
end | ||
non_cached_serializer | ||
end | ||
|
||
def _get_or_create_fragment_serializer(name) | ||
return Object.const_get(name) if Object.const_defined?(name) | ||
Object.const_set(name, Class.new(ActiveModel::Serializer)) | ||
end | ||
|
||
def cache_key(adapter_instance) | ||
return @cache_key if defined?(@cache_key) | ||
|
||
parts = [] | ||
parts << object_cache_key | ||
parts << adapter_instance.cache_key | ||
parts << self.class._cache_digest unless self.class._skip_digest? | ||
parts << serializer_class._cache_digest unless serializer_class._skip_digest? | ||
@cache_key = parts.join('/') | ||
end | ||
|
||
|
@@ -328,15 +268,18 @@ def cache_key(adapter_instance) | |
def object_cache_key | ||
if object.respond_to?(:cache_key) | ||
object.cache_key | ||
elsif (serializer_cache_key = (self.class._cache_key || self.class._cache_options[:key])) | ||
elsif (serializer_cache_key = (serializer_class._cache_key || serializer_class._cache_options[:key])) | ||
object_time_safe = object.updated_at | ||
object_time_safe = object_time_safe.strftime('%Y%m%d%H%M%S%9N') if object_time_safe.respond_to?(:strftime) | ||
"#{serializer_cache_key}/#{object.id}-#{object_time_safe}" | ||
else | ||
fail UndefinedCacheKey, "#{object.class} must define #cache_key, or the 'key:' option must be passed into '#{self.class}.cache'" | ||
fail UndefinedCacheKey, "#{object.class} must define #cache_key, or the 'key:' option must be passed into '#{serializer_class}.cache'" | ||
end | ||
end | ||
|
||
def serializer_class | ||
@serializer_class ||= self.class | ||
end | ||
end | ||
end | ||
end | ||
# rubocop:enable Metrics/ModuleLength |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -294,7 +294,7 @@ def attributes_for(serializer, fields) | |
|
||
# {http://jsonapi.org/format/#document-resource-objects Document Resource Objects} | ||
def resource_object_for(serializer) | ||
resource_object = serializer.cache_check(self) do | ||
resource_object = serializer.fetch(self) do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm still not a fan of fetching from the cache always needing an adapter_instance, but it's not terrible. looks like we might want to add a fetch_multi around the collection? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the idea here is to replace what serializable_hash would fetch There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just interjecting that you definitely want to add a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, I have an unpushed branch where I added readthis to the benchmark :) @sorentwo Thanks for looking! didn't quite finish this, obvs isn't quite right: diff --git a/Gemfile b/Gemfile
index 654f77a..25aa836 100644
--- a/Gemfile
+++ b/Gemfile
@@ -39,6 +39,11 @@ gem 'tzinfo-data', platforms: (@windows_platforms + [:jruby])
group :bench do
# https://github.com/rails-api/active_model_serializers/commit/cb4459580a6f4f37f629bf3185a5224c8624ca76
gem 'benchmark-ips', require: false, group: :development
+ gem 'oj'
+ gem 'oj_mimic_json'
+ gem 'redis', require: false
+ gem 'readthis', require: false
+ gem 'hiredis', require: false
end
group :test do
diff --git a/test/benchmark/app.rb b/test/benchmark/app.rb
index bc0d368..21d62d0 100644
--- a/test/benchmark/app.rb
+++ b/test/benchmark/app.rb
@@ -36,7 +36,16 @@ class BenchmarkApp < Rails::Application
config.cache_classes = true
# CONFIG: CACHE_ON={on,off}
config.action_controller.perform_caching = ENV['CACHE_ON'] != 'off'
- config.action_controller.cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
+ # config.action_controller.cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
+ require 'redis'
+ require 'readthis'
+ ENV['REDIS_URL'] = 'redis://localhost:6379/5'
+ config.action_controller.cache_store = :readthis_store, {
+ expires_in: 2.weeks.to_i,
+ namespace: 'cache',
+ redis: { url: ENV.fetch('REDIS_URL'), driver: :hiredis }
+ }
+
config.active_support.test_order = :random
config.secret_token = 'S' * 30
diff --git a/test/benchmark/fixtures.rb b/test/benchmark/fixtures.rb
index 5242db2..94c80f9 100644
--- a/test/benchmark/fixtures.rb
+++ b/test/benchmark/fixtures.rb
@@ -48,13 +48,13 @@ end
Rails.configuration.serializers << CachingAuthorSerializer
class CachingCommentSerializer < CommentSerializer
- cache expires_in: 1.day, skip_digest: true
+ cache expires_in: 1.day.to_i, skip_digest: true
end
Rails.configuration.serializers << CachingCommentSerializer
# see https://github.com/rails-api/active_model_serializers/pull/1690/commits/68715b8f99bc29677e8a47bb3f305f23c077024b#r60344532
class CachingPostSerializer < ActiveModel::Serializer
- cache key: 'post', expires_in: 0.1, skip_digest: true
+ cache key: 'post', expires_in: 1.day.to_i, skip_digest: true
attributes :id, :title, :body
@@ -89,7 +89,7 @@ Rails.configuration.serializers << CachingCommentSerializer
# see https://github.com/rails-api/active_model_serializers/pull/1690/commits/68715b8f99bc29677e8a47bb3f305f23c077024b#r60344532
class FragmentCachingPostSerializer < ActiveModel::Serializer
- cache key: 'post', expires_in: 0.1, skip_digest: true
+ cache key: 'post', expires_in: 1.day.to_i, skip_digest: true
attributes :id, :title, :body
and then started looking at knuckles diff --git a/lib/active_model_serializers/adapter/attributes.rb b/lib/active_model_serializers/adapter/attributes.rb
index 503f024..8294e91 100644
--- a/lib/active_model_serializers/adapter/attributes.rb
+++ b/lib/active_model_serializers/adapter/attributes.rb
@@ -1,3 +1,18 @@
+# require 'oj'
+require 'readthis'
+require 'knuckles'
+ENV['REDIS_URL'] = 'redis://localhost:6379/5'
+Knuckles.configure do |config|
+ config.cache = Readthis::Cache.new(
+ # marshal: Oj,
+ compress: true,
+ driver: :hiredis,
+ url: ENV.fetch('REDIS_URL')
+ )
+
+ config.keygen = Readthis::Expanders
+ # config.serializer = Oj
+end
module ActiveModelSerializers
module Adapter
class Attributes < Base
@@ -18,11 +33,36 @@ module ActiveModelSerializers
options = serialization_options(options)
if serializer.respond_to?(:each)
+ objects = [object] # serializer.map(&:object)
+ opts.merge!(
+ view: ActiveModelSerializers::SerializableResource,
+ serializer: self.class
+ )
+ Knuckles::Pipeline.new.call(objects, opts)
serializable_hash_for_collection(options)
else
serializable_hash_for_single_resource(options)
end
end
+ # module Knuckles::Stages::Renderer
+ # def call(objects, options)
+ # objects.each do |hash|
+ # unless hash[:cached?]
+ # hash[:result] = do_render(hash[:object], view, options)
+ # end
+ # end
+ # end
+ #
+ # private
+ #
+ # def do_render(object, view, options)
+ # case view
+ # when Knuckles::View then view.render(object, options)
+ # else view.new(object, options).as_json
+ # end
+ # end
+ # end
+
private
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Part of my goal in the direction I've been going with these PRs is to be able to make it easier to pipeline, with caching in its own 'stage' There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Push the branch, I'd like to take a look and help get Readthis hooked up to the benchmarks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (That's the whole diff, but will do) Any thoughts on changes this pr before it's merged? Next one I'm going to evolve a caching stage, I think |
||
resource_object = ResourceIdentifier.new(serializer, instance_options).as_json | ||
|
||
requested_fields = fieldset && fieldset.fields_for(resource_object[:type]) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why we modify the adapter options here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, since if you're serializing a resource serializer (not collection) and you don't have cached_attributes, you're not going to get them. this particular attribute is added in the collection serializer. so, here we're just making sure it's set.