Skip to content

Commit fbb6d36

Browse files
author
Joe Gaudet
committed
Improve caching performance
This commit improves the performance of the fragment cache by batching all reads and writes to single caching operations.
1 parent d2db72b commit fbb6d36

File tree

3 files changed

+186
-97
lines changed

3 files changed

+186
-97
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ coverage
2121
test/log
2222
test_db
2323
test_db-journal
24+
.idea
25+
*.iml
Lines changed: 66 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,42 @@
11
module JSONAPI
22
class CachedResponseFragment
3-
def self.fetch_cached_fragments(resource_klass, serializer_config_key, cache_ids, context)
4-
context_json = resource_klass.attribute_caching_context(context).to_json
5-
context_b64 = JSONAPI.configuration.resource_cache_digest_function.call(context_json)
6-
context_key = "ATTR-CTX-#{context_b64.gsub("/", "_")}"
7-
8-
results = self.lookup(resource_klass, serializer_config_key, context, context_key, cache_ids)
9-
10-
if JSONAPI.configuration.resource_cache_usage_report_function
11-
miss_ids = results.select{|_k,v| v.nil? }.keys
12-
JSONAPI.configuration.resource_cache_usage_report_function.call(
13-
resource_klass.name,
14-
cache_ids.size - miss_ids.size,
15-
miss_ids.size
16-
)
3+
4+
Lookup = Struct.new(:resource_klass, :serializer_config_key, :context, :context_key, :cache_ids) do
5+
6+
def type
7+
resource_klass._type
178
end
189

19-
results
10+
def keys
11+
cache_ids.map do |(id, cache_key)|
12+
[type, id, cache_key, serializer_config_key, context_key]
13+
end
14+
end
15+
end
16+
17+
Write = Struct.new(:resource_klass, :resource, :serializer, :serializer_config_key, :context, :context_key, :relationship_data) do
18+
def to_key_value
19+
20+
(id, cache_key) = resource.cache_id
21+
22+
json = serializer.object_hash(resource, relationship_data)
23+
24+
cr = CachedResponseFragment.new(
25+
resource_klass,
26+
id,
27+
json['type'],
28+
context,
29+
resource.fetchable_fields,
30+
json['relationships'],
31+
json['links'],
32+
json['attributes'],
33+
json['meta']
34+
)
35+
36+
key = [resource_klass._type, id, cache_key, serializer_config_key, context_key]
37+
38+
[key, cr]
39+
end
2040
end
2141

2242
attr_reader :resource_klass, :id, :type, :context, :fetchable_fields, :relationships,
@@ -50,26 +70,46 @@ def to_cache_value
5070
}
5171
end
5272

53-
private
73+
# @param [Lookup[]] lookups
74+
# @return [Hash<Class<Resource>, Hash<ID, CachedResourceFragment>>]
75+
def self.lookup(lookups, context)
76+
type_to_klass = lookups.map {|l| [l.type, l.resource_klass]}.to_h
5477

55-
def self.lookup(resource_klass, serializer_config_key, context, context_key, cache_ids)
56-
type = resource_klass._type
78+
keys = lookups.map(&:keys).flatten(1)
5779

58-
keys = cache_ids.map do |(id, cache_key)|
59-
[type, id, cache_key, serializer_config_key, context_key]
60-
end
80+
hits = JSONAPI.configuration.resource_cache.read_multi(*keys).reject {|_, v| v.nil?}
81+
82+
return keys.inject({}) do |hash, key|
83+
(type, id, _, _) = key
84+
resource_klass = type_to_klass[type]
85+
hash[resource_klass] ||= {}
6186

62-
hits = JSONAPI.configuration.resource_cache.read_multi(*keys).reject{|_,v| v.nil? }
63-
return keys.each_with_object({}) do |key, hash|
64-
(_, id, _, _) = key
6587
if hits.has_key?(key)
66-
hash[id] = self.from_cache_value(resource_klass, context, hits[key])
88+
hash[resource_klass][id] = self.from_cache_value(resource_klass, context, hits[key])
6789
else
68-
hash[id] = nil
90+
hash[resource_klass][id] = nil
6991
end
92+
93+
hash
7094
end
7195
end
7296

97+
# @param [Write[]] lookups
98+
def self.write(writes)
99+
key_values = writes.map(&:to_key_value)
100+
101+
to_write = key_values.map {|(k, v)| [k, v.to_cache_value]}.to_h
102+
103+
if JSONAPI.configuration.resource_cache.respond_to? :write_multi
104+
JSONAPI.configuration.resource_cache.write_multi(to_write)
105+
else
106+
to_write.each do |key, value|
107+
JSONAPI.configuration.resource_cache.write(key, value)
108+
end
109+
end
110+
111+
end
112+
73113
def self.from_cache_value(resource_klass, context, h)
74114
new(
75115
resource_klass,
@@ -83,28 +123,5 @@ def self.from_cache_value(resource_klass, context, h)
83123
h.fetch(:meta, nil)
84124
)
85125
end
86-
87-
def self.write(resource_klass, resource, serializer, serializer_config_key, context, context_key, relationship_data )
88-
(id, cache_key) = resource.cache_id
89-
90-
json = serializer.object_hash(resource, relationship_data)
91-
92-
cr = self.new(
93-
resource_klass,
94-
id,
95-
json['type'],
96-
context,
97-
resource.fetchable_fields,
98-
json['relationships'],
99-
json['links'],
100-
json['attributes'],
101-
json['meta']
102-
)
103-
104-
key = [resource_klass._type, id, cache_key, serializer_config_key, context_key]
105-
JSONAPI.configuration.resource_cache.write(key, cr.to_cache_value)
106-
return [id, cr]
107-
end
108-
109126
end
110127
end

lib/jsonapi/resource_set.rb

Lines changed: 118 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,80 +5,150 @@ class ResourceSet
55

66
attr_reader :resource_klasses, :populated
77

8-
def initialize(resource_id_tree)
8+
def initialize(resource_id_tree = nil)
99
@populated = false
10-
@resource_klasses = flatten_resource_id_tree(resource_id_tree)
10+
@resource_klasses = resource_id_tree.nil? ? {} : flatten_resource_id_tree(resource_id_tree)
1111
end
1212

1313
def populate!(serializer, context, find_options)
14+
# For each resource klass we want to generate the caching key
15+
16+
# Hash for collecting types and ids
17+
# @type [Hash<Class<Resource>, Id[]]]
18+
missed_resource_ids = {}
19+
20+
# Array for collecting CachedResponseFragment::Lookups
21+
# @type [Lookup[]]
22+
lookups = []
23+
24+
25+
# Step One collect all of the lookups for the cache, or keys that don't require cache access
1426
@resource_klasses.each_key do |resource_klass|
15-
missed_ids = []
1627

1728
serializer_config_key = serializer.config_key(resource_klass).gsub("/", "_")
1829
context_json = resource_klass.attribute_caching_context(context).to_json
1930
context_b64 = JSONAPI.configuration.resource_cache_digest_function.call(context_json)
2031
context_key = "ATTR-CTX-#{context_b64.gsub("/", "_")}"
2132

2233
if resource_klass.caching?
23-
cache_ids = []
24-
25-
@resource_klasses[resource_klass].each_pair do |k, v|
34+
cache_ids = @resource_klasses[resource_klass].map do |(k, v)|
2635
# Store the hashcode of the cache_field to avoid storing objects and to ensure precision isn't lost
2736
# on timestamp types (i.e. string conversions dropping milliseconds)
28-
cache_ids.push([k, resource_klass.hash_cache_field(v[:cache_id])])
37+
[k, resource_klass.hash_cache_field(v[:cache_id])]
2938
end
3039

31-
found_resources = CachedResponseFragment.fetch_cached_fragments(
40+
lookups.push(
41+
CachedResponseFragment::Lookup.new(
3242
resource_klass,
3343
serializer_config_key,
34-
cache_ids,
35-
context)
36-
37-
found_resources.each do |found_result|
38-
resource = found_result[1]
39-
if resource.nil?
40-
missed_ids.push(found_result[0])
41-
else
42-
@resource_klasses[resource_klass][resource.id][:resource] = resource
43-
end
44-
end
44+
context,
45+
context_key,
46+
cache_ids
47+
)
48+
)
4549
else
46-
missed_ids = @resource_klasses[resource_klass].keys
50+
missed_resource_ids[resource_klass] ||= {}
51+
missed_resource_ids[resource_klass] = @resource_klasses[resource_klass].keys
4752
end
53+
end
54+
55+
if lookups.any?
56+
raise "You've declared some Resources as caching without providing a caching store" if JSONAPI.configuration.resource_cache.nil?
57+
58+
# Step Two execute the cache lookup
59+
found_resources = CachedResponseFragment.lookup(lookups, context)
60+
else
61+
found_resources = {}
62+
end
4863

49-
# fill in any missed resources
50-
unless missed_ids.empty?
51-
find_opts = {
52-
context: context,
53-
fields: find_options[:fields] }
54-
55-
found_resources = resource_klass.find_by_keys(missed_ids, find_opts)
56-
57-
found_resources.each do |resource|
58-
relationship_data = @resource_klasses[resource_klass][resource.id][:relationships]
59-
60-
if resource_klass.caching?
61-
(id, cr) = CachedResponseFragment.write(
62-
resource_klass,
63-
resource,
64-
serializer,
65-
serializer_config_key,
66-
context,
67-
context_key,
68-
relationship_data)
69-
70-
@resource_klasses[resource_klass][id][:resource] = cr
71-
else
72-
@resource_klasses[resource_klass][resource.id][:resource] = resource
73-
end
64+
65+
# Step Three collect the results and collect hit/miss stats
66+
stats = {}
67+
found_resources.each do |resource_klass, resources|
68+
resources.each do |id, cached_resource|
69+
stats[resource_klass] ||= {}
70+
71+
if cached_resource.nil?
72+
stats[resource_klass][:misses] ||= 0
73+
stats[resource_klass][:misses] += 1
74+
75+
# Collect misses
76+
missed_resource_ids[resource_klass] ||= []
77+
missed_resource_ids[resource_klass].push(id)
78+
else
79+
stats[resource_klass][:hits] ||= 0
80+
stats[resource_klass][:hits] += 1
81+
82+
register_resource(resource_klass, cached_resource)
7483
end
7584
end
7685
end
77-
@populated = true
86+
87+
report_stats(stats)
88+
89+
writes = []
90+
91+
# Step Four find any of the missing resources and join them into the result
92+
missed_resource_ids.each_pair do |resource_klass, ids|
93+
find_opts = {context: context, fields: find_options[:fields]}
94+
found_resources = resource_klass.find_by_keys(ids, find_opts)
95+
96+
found_resources.each do |resource|
97+
relationship_data = @resource_klasses[resource_klass][resource.id][:relationships]
98+
99+
if resource_klass.caching?
100+
101+
serializer_config_key = serializer.config_key(resource_klass).gsub("/", "_")
102+
context_json = resource_klass.attribute_caching_context(context).to_json
103+
context_b64 = JSONAPI.configuration.resource_cache_digest_function.call(context_json)
104+
context_key = "ATTR-CTX-#{context_b64.gsub("/", "_")}"
105+
106+
writes.push(CachedResponseFragment::Write.new(
107+
resource_klass,
108+
resource,
109+
serializer,
110+
serializer_config_key,
111+
context,
112+
context_key,
113+
relationship_data
114+
))
115+
end
116+
117+
register_resource(resource_klass, resource)
118+
end
119+
end
120+
121+
# Step Five conditionally write to the cache
122+
CachedResponseFragment.write(writes) unless JSONAPI.configuration.resource_cache.nil?
123+
124+
mark_populated!
78125
self
79126
end
80127

128+
def mark_populated!
129+
@populated = true
130+
end
131+
132+
def register_resource(resource_klass, resource, primary = false)
133+
@resource_klasses[resource_klass] ||= {}
134+
@resource_klasses[resource_klass][resource.id] ||= {primary: resource.try(:primary) || primary, relationships: {}}
135+
@resource_klasses[resource_klass][resource.id][:resource] = resource
136+
end
137+
81138
private
139+
140+
def report_stats(stats)
141+
return unless JSONAPI.configuration.resource_cache_usage_report_function || JSONAPI.configuration.resource_cache.nil?
142+
143+
stats.each_pair do |resource_klass, stat|
144+
JSONAPI.configuration.resource_cache_usage_report_function.call(
145+
resource_klass.name,
146+
stat[:hits] || 0,
147+
stat[:misses] || 0
148+
)
149+
end
150+
end
151+
82152
def flatten_resource_id_tree(resource_id_tree, flattened_tree = {})
83153
resource_id_tree.fragments.each_pair do |resource_rid, fragment|
84154

@@ -87,7 +157,7 @@ def flatten_resource_id_tree(resource_id_tree, flattened_tree = {})
87157

88158
flattened_tree[resource_klass] ||= {}
89159

90-
flattened_tree[resource_klass][id] ||= { primary: fragment.primary, relationships: {} }
160+
flattened_tree[resource_klass][id] ||= {primary: fragment.primary, relationships: {}}
91161
flattened_tree[resource_klass][id][:cache_id] ||= fragment.cache
92162

93163
fragment.related.try(:each_pair) do |relationship_name, related_rids|
@@ -104,4 +174,4 @@ def flatten_resource_id_tree(resource_id_tree, flattened_tree = {})
104174
flattened_tree
105175
end
106176
end
107-
end
177+
end

0 commit comments

Comments
 (0)