@@ -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