@@ -5,80 +5,150 @@ class ResourceSet
5
5
6
6
attr_reader :resource_klasses , :populated
7
7
8
- def initialize ( resource_id_tree )
8
+ def initialize ( resource_id_tree = nil )
9
9
@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 )
11
11
end
12
12
13
13
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
14
26
@resource_klasses . each_key do |resource_klass |
15
- missed_ids = [ ]
16
27
17
28
serializer_config_key = serializer . config_key ( resource_klass ) . gsub ( "/" , "_" )
18
29
context_json = resource_klass . attribute_caching_context ( context ) . to_json
19
30
context_b64 = JSONAPI . configuration . resource_cache_digest_function . call ( context_json )
20
31
context_key = "ATTR-CTX-#{ context_b64 . gsub ( "/" , "_" ) } "
21
32
22
33
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 ) |
26
35
# Store the hashcode of the cache_field to avoid storing objects and to ensure precision isn't lost
27
36
# 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 ] ) ]
29
38
end
30
39
31
- found_resources = CachedResponseFragment . fetch_cached_fragments (
40
+ lookups . push (
41
+ CachedResponseFragment ::Lookup . new (
32
42
resource_klass ,
33
43
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
+ )
45
49
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
47
52
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
48
63
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 )
74
83
end
75
84
end
76
85
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!
78
125
self
79
126
end
80
127
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
+
81
138
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
+
82
152
def flatten_resource_id_tree ( resource_id_tree , flattened_tree = { } )
83
153
resource_id_tree . fragments . each_pair do |resource_rid , fragment |
84
154
@@ -87,7 +157,7 @@ def flatten_resource_id_tree(resource_id_tree, flattened_tree = {})
87
157
88
158
flattened_tree [ resource_klass ] ||= { }
89
159
90
- flattened_tree [ resource_klass ] [ id ] ||= { primary : fragment . primary , relationships : { } }
160
+ flattened_tree [ resource_klass ] [ id ] ||= { primary : fragment . primary , relationships : { } }
91
161
flattened_tree [ resource_klass ] [ id ] [ :cache_id ] ||= fragment . cache
92
162
93
163
fragment . related . try ( :each_pair ) do |relationship_name , related_rids |
@@ -104,4 +174,4 @@ def flatten_resource_id_tree(resource_id_tree, flattened_tree = {})
104
174
flattened_tree
105
175
end
106
176
end
107
- end
177
+ end
0 commit comments