-
Notifications
You must be signed in to change notification settings - Fork 544
Expand file tree
/
Copy pathresource_set.rb
More file actions
196 lines (156 loc) · 6.86 KB
/
Copy pathresource_set.rb
File metadata and controls
196 lines (156 loc) · 6.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# frozen_string_literal: true
module JSONAPI
# Contains a hash of resource types which contain a hash of resources, relationships and primary status keyed by
# resource id.
class ResourceSet
attr_reader :resource_klasses, :populated
def initialize(source, include_related = nil, options = nil)
@populated = false
tree = if source.is_a?(JSONAPI::ResourceTree)
source
elsif source.class < JSONAPI::BasicResource
JSONAPI::PrimaryResourceTree.new(resource: source, include_related: include_related, options: options)
elsif source.is_a?(Array)
JSONAPI::PrimaryResourceTree.new(resources: source, include_related: include_related, options: options)
end
if tree
@resource_klasses = flatten_resource_tree(tree)
end
end
def populate!(serializer, context, options)
return if @populated
# For each resource klass we want to generate the caching key
# Hash for collecting types and ids
# @type [Hash<Class<Resource>, Id[]]]
missed_resource_ids = {}
# Array for collecting CachedResponseFragment::Lookups
# @type [Lookup[]]
lookups = []
# Step One collect all of the lookups for the cache, or keys that don't require cache access
@resource_klasses.each_key do |resource_klass|
missed_resource_ids[resource_klass] ||= []
serializer_config_key = serializer.config_key(resource_klass).gsub("/", "_")
context_json = resource_klass.attribute_caching_context(context).to_json
context_b64 = JSONAPI.configuration.resource_cache_digest_function.call(context_json)
context_key = "ATTR-CTX-#{context_b64.gsub("/", "_")}"
if resource_klass.caching?
cache_ids = @resource_klasses[resource_klass].map do |(k, v)|
# Store the hashcode of the cache_field to avoid storing objects and to ensure precision isn't lost
# on timestamp types (i.e. string conversions dropping milliseconds)
[k, resource_klass.hash_cache_field(v[:cache_id])]
end
lookups.push(
CachedResponseFragment::Lookup.new(
resource_klass,
serializer_config_key,
context,
context_key,
cache_ids
)
)
else
@resource_klasses[resource_klass].keys.each do |k|
if @resource_klasses[resource_klass][k][:resource].nil?
missed_resource_ids[resource_klass] << k
else
register_resource(resource_klass, @resource_klasses[resource_klass][k][:resource])
end
end
end
end
if lookups.any?
raise "You've declared some Resources as caching without providing a caching store" if JSONAPI.configuration.resource_cache.nil?
# Step Two execute the cache lookup
found_resources = CachedResponseFragment.lookup(lookups, context)
else
found_resources = {}
end
# Step Three collect the results and collect hit/miss stats
stats = {}
found_resources.each do |resource_klass, resources|
resources.each do |id, cached_resource|
stats[resource_klass] ||= {}
if cached_resource.nil?
stats[resource_klass][:misses] ||= 0
stats[resource_klass][:misses] += 1
# Collect misses
missed_resource_ids[resource_klass].push(id)
else
stats[resource_klass][:hits] ||= 0
stats[resource_klass][:hits] += 1
register_resource(resource_klass, cached_resource)
end
end
end
report_stats(stats)
writes = []
# Step Four find any of the missing resources and join them into the result
missed_resource_ids.each_pair do |resource_klass, ids|
next if ids.empty?
find_opts = {context: context, fields: options[:fields]}
found_resources = resource_klass.find_to_populate_by_keys(ids, find_opts)
found_resources.each do |resource|
relationship_data = @resource_klasses[resource_klass][resource.id][:relationships]
if resource_klass.caching?
serializer_config_key = serializer.config_key(resource_klass).gsub("/", "_")
context_json = resource_klass.attribute_caching_context(context).to_json
context_b64 = JSONAPI.configuration.resource_cache_digest_function.call(context_json)
context_key = "ATTR-CTX-#{context_b64.gsub("/", "_")}"
writes.push(CachedResponseFragment::Write.new(
resource_klass,
resource,
serializer,
serializer_config_key,
context,
context_key,
relationship_data
))
end
register_resource(resource_klass, resource)
end
end
# Step Five conditionally write to the cache
CachedResponseFragment.write(writes) unless JSONAPI.configuration.resource_cache.nil?
mark_populated!
self
end
def mark_populated!
@populated = true
end
def register_resource(resource_klass, resource, primary = false)
@resource_klasses[resource_klass] ||= {}
@resource_klasses[resource_klass][resource.id] ||= {primary: resource.try(:primary) || primary, relationships: {}}
@resource_klasses[resource_klass][resource.id][:resource] = resource
end
private
def report_stats(stats)
return unless JSONAPI.configuration.resource_cache_usage_report_function || JSONAPI.configuration.resource_cache.nil?
stats.each_pair do |resource_klass, stat|
JSONAPI.configuration.resource_cache_usage_report_function.call(
resource_klass.name,
stat[:hits] || 0,
stat[:misses] || 0
)
end
end
def flatten_resource_tree(resource_tree, flattened_tree = {})
resource_tree.fragments.each_pair do |resource_rid, fragment|
resource_klass = resource_rid.resource_klass
id = resource_rid.id
flattened_tree[resource_klass] ||= {}
flattened_tree[resource_klass][id] ||= {primary: fragment.primary, relationships: {}}
flattened_tree[resource_klass][id][:cache_id] ||= fragment.cache if fragment.cache
flattened_tree[resource_klass][id][:resource] ||= fragment.resource if fragment.resource
fragment.related.try(:each_pair) do |relationship_name, related_rids|
flattened_tree[resource_klass][id][:relationships][relationship_name] ||= Set.new
flattened_tree[resource_klass][id][:relationships][relationship_name].merge(related_rids)
end
end
related_resource_trees = resource_tree.related_resource_trees
related_resource_trees.try(:each_value) do |related_resource_tree|
flatten_resource_tree(related_resource_tree, flattened_tree)
end
flattened_tree
end
end
end