Skip to content

Commit 036e1a4

Browse files
authored
Merge pull request #1219 from Foodee/feature/single-mget-from-cache
Improve caching performance
2 parents d2db72b + fbb6d36 commit 036e1a4

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)