Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions lib/oj_serializers.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
# frozen_string_literal: true

module OjSerializers
def self.configuration
@configuration ||= OjSerializers::Config.new
end

def self.configure
yield(configuration)
end
end

require 'oj'
require 'oj_serializers/config'
require 'oj_serializers/version'
require 'oj_serializers/setup'
require 'oj_serializers/serializer'
10 changes: 10 additions & 0 deletions lib/oj_serializers/config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

class OjSerializers::Config
attr_accessor :cache

def initialize
self.cache = (defined?(Rails) && Rails.cache) ||
(defined?(ActiveSupport::Cache::MemoryStore) ? ActiveSupport::Cache::MemoryStore.new : OjSerializers::Memo.new)
end
end
47 changes: 26 additions & 21 deletions lib/oj_serializers/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ class OjSerializers::Serializer
serializer
].to_set

CACHE = (defined?(Rails) && Rails.cache) ||
(defined?(ActiveSupport::Cache::MemoryStore) ? ActiveSupport::Cache::MemoryStore.new : OjSerializers::Memo.new)

# Internal: The environment the app is currently running on.
environment = ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'production'

Expand Down Expand Up @@ -246,77 +243,85 @@ def item_cache_key(item, cache_key_proc)
# Defaults to calling cache_key in the object if no key is provided.
#
# NOTE: Benchmark it, sometimes caching is actually SLOWER.
def cached(cache_key_proc = :cache_key.to_proc)
cache_options = { namespace: "#{name}#write_to_json", version: OjSerializers::VERSION }.freeze
cache_hash_options = { namespace: "#{name}#render_as_hash", version: OjSerializers::VERSION }.freeze
def cached(cache_key: :cache_key.to_proc, options: {})
cache_opts = options
cache_options = { namespace: "#{name}#write_to_json", version: OjSerializers::VERSION, **cache_opts }.freeze
cache_hash_options = { namespace: "#{name}#render_as_hash", version: OjSerializers::VERSION, **cache_opts }.freeze

# Internal: Redefine `one_as_hash` to use the cache for the serialized hash.
define_singleton_method(:one_as_hash) do |item, options = nil|
CACHE.fetch(item_cache_key(item, cache_key_proc), cache_hash_options) do
instance.render_as_hash(item, options)
define_singleton_method(:one_as_hash) do |item, serializer_options = nil|
OjSerializers.configuration.cache.fetch(item_cache_key(item, cache_key), cache_hash_options) do
instance.render_as_hash(item, serializer_options)
end
end

# Internal: Redefine `many_as_hash` to use the cache for the serialized hash.
define_singleton_method(:many_as_hash) do |items, options = nil|
define_singleton_method(:many_as_hash) do |items, serializer_options = nil|
# We define a one-off method for the class to receive the entire object
# inside the `fetch_multi` block. Otherwise we would only get the cache
# key, and we would need to build a Hash to retrieve the object.
#
# NOTE: The assignment is important, as queries would return different
# objects when expanding with the splat in fetch_multi.
items = items.entries.each do |item|
item_key = item_cache_key(item, cache_key_proc)
item_key = item_cache_key(item, cache_key)
item.define_singleton_method(:cache_key) { item_key }
end

# Fetch all items at once by leveraging `read_multi`.
#
# NOTE: Memcached does not support `write_multi`, if we switch the cache
# store to use Redis performance would improve a lot for this case.
CACHE.fetch_multi(*items, cache_hash_options) do |item|
instance.render_as_hash(item, options)
OjSerializers.configuration.cache.fetch_multi(*items, cache_hash_options) do |item|
instance.render_as_hash(item, serializer_options)
end.values
end

# Internal: Redefine `write_one` to use the cache for the serialized JSON.
define_singleton_method(:write_one) do |external_writer, item, options = nil|
cached_item = CACHE.fetch(item_cache_key(item, cache_key_proc), cache_options) do
define_singleton_method(:write_one) do |external_writer, item, serializer_options = nil|
cached_item = OjSerializers.configuration.cache.fetch(item_cache_key(item, cache_key), cache_options) do
writer = new_json_writer
non_cached_write_one(writer, item, options)
non_cached_write_one(writer, item, serializer_options)
writer.to_json
end
external_writer.push_json("#{cached_item}\n") # Oj.dump expects a new line terminator.
end

# Internal: Redefine `write_many` to use fetch_multi from cache.
define_singleton_method(:write_many) do |external_writer, items, options = nil|
define_singleton_method(:write_many) do |external_writer, items, serializer_options = nil|
# We define a one-off method for the class to receive the entire object
# inside the `fetch_multi` block. Otherwise we would only get the cache
# key, and we would need to build a Hash to retrieve the object.
#
# NOTE: The assignment is important, as queries would return different
# objects when expanding with the splat in fetch_multi.
items = items.entries.each do |item|
item_key = item_cache_key(item, cache_key_proc)
item_key = item_cache_key(item, cache_key)
item.define_singleton_method(:cache_key) { item_key }
end

# Fetch all items at once by leveraging `read_multi`.
#
# NOTE: Memcached does not support `write_multi`, if we switch the cache
# store to use Redis performance would improve a lot for this case.
cached_items = CACHE.fetch_multi(*items, cache_options) do |item|
cached_items = OjSerializers.configuration.cache.fetch_multi(*items, cache_options) do |item|
writer = new_json_writer
non_cached_write_one(writer, item, options)
non_cached_write_one(writer, item, serializer_options)
writer.to_json
end.values
external_writer.push_json("#{OjSerializers::JsonValue.array(cached_items)}\n") # Oj.dump expects a new line terminator.
end

define_serialization_shortcuts
end
alias_method :cached_with_key, :cached

def cached_with_key(cache_key_proc)
cached(cache_key: cache_key_proc)
end

def cached_with_options(options)
cached(options: options)
end

def define_serialization_shortcuts(format = _default_format)
case format
Expand Down
11 changes: 9 additions & 2 deletions spec/oj_serializers/caching_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
require 'support/serializers/album_serializer'

class CachedSongSerializer < SongSerializer
cached
cached_with_options(expires_in: 1.minute)
end

class CachedAlbumSerializer < AlbumSerializer
Expand All @@ -21,7 +21,7 @@ class CachedAlbumSerializer < AlbumSerializer

before do
# NOTE: Uncomment to debug test failures.
# Oj::Serializer::CACHE.logger = ActiveSupport::Logger.new(STDOUT)
# OjSerializers.configuration.cache.logger = ActiveSupport::Logger.new(STDOUT)
end

it 'should reuse the cache effectively' do
Expand Down Expand Up @@ -63,4 +63,11 @@ class CachedAlbumSerializer < AlbumSerializer
expect_parsed_json(CachedAlbumSerializer.one_as_json(other_album)).to eq other_attrs
expect_parsed_json(CachedAlbumSerializer.many_as_json(albums)).to eq [attrs, other_attrs]
end

it 'should use correct cache options' do
expect(OjSerializers).to receive(:configuration).at_least(1).and_return(OjSerializers::Config.new)
attrs = parse_json(AlbumSerializer.one_as_json(album))
expect(OjSerializers.configuration.cache).to receive(:fetch_multi).once.with(any_args, include(expires_in: 1.minute)).and_call_original
expect_parsed_json(CachedAlbumSerializer.one_as_json(album)).to eq attrs
end
end