identity_cache 0.5.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +5 -5
  2. data/.github/probots.yml +2 -0
  3. data/.github/workflows/ci.yml +26 -0
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +5 -0
  6. data/.travis.yml +24 -9
  7. data/CHANGELOG.md +21 -0
  8. data/Gemfile +5 -1
  9. data/README.md +28 -26
  10. data/Rakefile +14 -5
  11. data/dev.yml +9 -16
  12. data/gemfiles/Gemfile.latest-release +6 -0
  13. data/gemfiles/Gemfile.rails-edge +6 -0
  14. data/gemfiles/Gemfile.rails52 +6 -0
  15. data/identity_cache.gemspec +26 -10
  16. data/lib/identity_cache.rb +49 -46
  17. data/lib/identity_cache/belongs_to_caching.rb +12 -40
  18. data/lib/identity_cache/cache_fetcher.rb +6 -5
  19. data/lib/identity_cache/cache_hash.rb +2 -2
  20. data/lib/identity_cache/cache_invalidation.rb +4 -11
  21. data/lib/identity_cache/cache_key_generation.rb +17 -65
  22. data/lib/identity_cache/cache_key_loader.rb +128 -0
  23. data/lib/identity_cache/cached.rb +7 -0
  24. data/lib/identity_cache/cached/association.rb +87 -0
  25. data/lib/identity_cache/cached/attribute.rb +123 -0
  26. data/lib/identity_cache/cached/attribute_by_multi.rb +37 -0
  27. data/lib/identity_cache/cached/attribute_by_one.rb +88 -0
  28. data/lib/identity_cache/cached/belongs_to.rb +93 -0
  29. data/lib/identity_cache/cached/embedded_fetching.rb +41 -0
  30. data/lib/identity_cache/cached/prefetcher.rb +51 -0
  31. data/lib/identity_cache/cached/primary_index.rb +97 -0
  32. data/lib/identity_cache/cached/recursive/association.rb +68 -0
  33. data/lib/identity_cache/cached/recursive/has_many.rb +9 -0
  34. data/lib/identity_cache/cached/recursive/has_one.rb +9 -0
  35. data/lib/identity_cache/cached/reference/association.rb +16 -0
  36. data/lib/identity_cache/cached/reference/has_many.rb +105 -0
  37. data/lib/identity_cache/cached/reference/has_one.rb +100 -0
  38. data/lib/identity_cache/configuration_dsl.rb +53 -215
  39. data/lib/identity_cache/encoder.rb +95 -0
  40. data/lib/identity_cache/expiry_hook.rb +36 -0
  41. data/lib/identity_cache/fallback_fetcher.rb +2 -1
  42. data/lib/identity_cache/load_strategy/eager.rb +28 -0
  43. data/lib/identity_cache/load_strategy/lazy.rb +71 -0
  44. data/lib/identity_cache/load_strategy/load_request.rb +20 -0
  45. data/lib/identity_cache/load_strategy/multi_load_request.rb +27 -0
  46. data/lib/identity_cache/memoized_cache_proxy.rb +127 -58
  47. data/lib/identity_cache/parent_model_expiration.rb +45 -11
  48. data/lib/identity_cache/query_api.rb +128 -394
  49. data/lib/identity_cache/railtie.rb +8 -0
  50. data/lib/identity_cache/record_not_found.rb +6 -0
  51. data/lib/identity_cache/should_use_cache.rb +1 -0
  52. data/lib/identity_cache/version.rb +3 -2
  53. data/lib/identity_cache/with_primary_index.rb +136 -0
  54. data/lib/identity_cache/without_primary_index.rb +24 -3
  55. data/performance/cache_runner.rb +28 -34
  56. data/performance/cpu.rb +3 -2
  57. data/performance/externals.rb +4 -3
  58. data/performance/profile.rb +6 -5
  59. data/railgun.yml +16 -0
  60. metadata +44 -73
  61. data/Gemfile.rails42 +0 -6
  62. data/Gemfile.rails50 +0 -6
  63. data/test/attribute_cache_test.rb +0 -110
  64. data/test/cache_fetch_includes_test.rb +0 -46
  65. data/test/cache_hash_test.rb +0 -14
  66. data/test/cache_invalidation_test.rb +0 -139
  67. data/test/deeply_nested_associated_record_test.rb +0 -19
  68. data/test/denormalized_has_many_test.rb +0 -214
  69. data/test/denormalized_has_one_test.rb +0 -160
  70. data/test/fetch_multi_test.rb +0 -308
  71. data/test/fetch_test.rb +0 -258
  72. data/test/fixtures/serialized_record.mysql2 +0 -0
  73. data/test/fixtures/serialized_record.postgresql +0 -0
  74. data/test/helpers/active_record_objects.rb +0 -106
  75. data/test/helpers/database_connection.rb +0 -72
  76. data/test/helpers/serialization_format.rb +0 -51
  77. data/test/helpers/update_serialization_format.rb +0 -27
  78. data/test/identity_cache_test.rb +0 -29
  79. data/test/index_cache_test.rb +0 -161
  80. data/test/memoized_attributes_test.rb +0 -59
  81. data/test/memoized_cache_proxy_test.rb +0 -107
  82. data/test/normalized_belongs_to_test.rb +0 -107
  83. data/test/normalized_has_many_test.rb +0 -231
  84. data/test/normalized_has_one_test.rb +0 -9
  85. data/test/prefetch_associations_test.rb +0 -379
  86. data/test/readonly_test.rb +0 -109
  87. data/test/recursive_denormalized_has_many_test.rb +0 -131
  88. data/test/save_test.rb +0 -82
  89. data/test/schema_change_test.rb +0 -112
  90. data/test/serialization_format_change_test.rb +0 -16
  91. data/test/test_helper.rb +0 -140
@@ -1,76 +1,19 @@
1
+ # frozen_string_literal: true
1
2
  module IdentityCache
2
3
  module ConfigurationDSL
3
4
  extend ActiveSupport::Concern
4
5
 
5
6
  included do |base|
6
- base.class_attribute :cache_indexes
7
- base.class_attribute :cached_has_manys
8
- base.class_attribute :cached_has_ones
9
- base.class_attribute :primary_cache_index_enabled
7
+ base.class_attribute(:cache_indexes)
8
+ base.class_attribute(:cached_has_manys)
9
+ base.class_attribute(:cached_has_ones)
10
10
 
11
11
  base.cached_has_manys = {}
12
12
  base.cached_has_ones = {}
13
13
  base.cache_indexes = []
14
- base.primary_cache_index_enabled = true
15
-
16
- base.after_commit :expire_parent_caches
17
14
  end
18
15
 
19
16
  module ClassMethods
20
- # Declares a new index in the cache for the class where IdentityCache was
21
- # included.
22
- #
23
- # IdentityCache will add a fetch_by_field1_and_field2_and_...field for every
24
- # index.
25
- #
26
- # == Example:
27
- #
28
- # class Product
29
- # include IdentityCache
30
- # cache_index :name, :vendor
31
- # end
32
- #
33
- # Will add Product.fetch_by_name_and_vendor
34
- #
35
- # == Parameters
36
- #
37
- # +fields+ Array of symbols or strings representing the fields in the index
38
- #
39
- # == Options
40
- # * unique: if the index would only have unique values
41
- #
42
- def cache_index(*fields)
43
- raise NotImplementedError, "Cache indexes need an enabled primary index" unless primary_cache_index_enabled
44
- options = fields.extract_options!
45
- unique = options[:unique] || false
46
- cache_attribute_by_alias('primary_key', 'id', by: fields, unique: unique)
47
-
48
- field_list = fields.join("_and_")
49
- arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')
50
-
51
- if unique
52
- self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__ + 1)
53
- def fetch_by_#{field_list}(#{arg_list}, options={})
54
- id = fetch_id_by_#{field_list}(#{arg_list})
55
- id && fetch_by_id(id, options)
56
- end
57
-
58
- # exception throwing variant
59
- def fetch_by_#{field_list}!(#{arg_list}, options={})
60
- fetch_by_#{field_list}(#{arg_list}, options) or raise ActiveRecord::RecordNotFound
61
- end
62
- CODE
63
- else
64
- self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__ + 1)
65
- def fetch_by_#{field_list}(#{arg_list}, options={})
66
- ids = fetch_id_by_#{field_list}(#{arg_list})
67
- ids.empty? ? ids : fetch_multi(ids, options)
68
- end
69
- CODE
70
- end
71
- end
72
-
73
-
74
17
  # Will cache an association to the class including IdentityCache.
75
18
  # The embed option, if set, will make IdentityCache keep the association
76
19
  # values in the same cache entry as the parent.
@@ -84,10 +27,8 @@ module IdentityCache
84
27
  # include IdentityCache
85
28
  # has_many :options
86
29
  # has_many :orders
87
- # has_many :buyers
88
30
  # cache_has_many :options, embed: :ids
89
31
  # cache_has_many :orders
90
- # cache_has_many :buyers, inverse_name: 'line_item'
91
32
  # end
92
33
  #
93
34
  # == Parameters
@@ -95,27 +36,28 @@ module IdentityCache
95
36
  #
96
37
  # == Options
97
38
  #
98
- # * embed: If set to true, will cause IdentityCache to keep the
99
- # values for this association in the same cache entry as the parent,
100
- # instead of its own.
101
- # * inverse_name: The name of the parent in the association if the name is
102
- # not the lowercase pluralization of the parent object's class
103
- def cache_has_many(association, options = {})
39
+ # * embed: If `true`, IdentityCache will embed the associated records
40
+ # in the cache entries for this model, as well as all the embedded
41
+ # associations for the associated record recursively.
42
+ # If `:ids` (the default), it will only embed the ids for the associated
43
+ # records.
44
+ def cache_has_many(association, embed: :ids)
104
45
  ensure_base_model
105
- options = options.slice(:embed, :inverse_name)
106
- options[:embed] = :ids unless options.has_key?(:embed)
107
- deprecate_embed_option(options, false, :ids)
108
- ensure_cacheable_association(association, options)
109
- self.cached_has_manys[association] = options
110
-
111
- case options[:embed]
112
- when true
113
- build_recursive_association_cache(association, options)
46
+ check_association_for_caching(association)
47
+ reflection = reflect_on_association(association)
48
+ association_class = case embed
114
49
  when :ids
115
- build_id_embedded_has_many_cache(association, options)
50
+ Cached::Reference::HasMany
51
+ when true
52
+ Cached::Recursive::HasMany
116
53
  else
117
54
  raise NotImplementedError
118
55
  end
56
+
57
+ cached_has_manys[association] = association_class.new(
58
+ association,
59
+ reflection: reflection,
60
+ ).tap(&:build)
119
61
  end
120
62
 
121
63
  # Will cache an association to the class including IdentityCache.
@@ -125,7 +67,7 @@ module IdentityCache
125
67
  # == Example:
126
68
  # class Product
127
69
  # cache_has_one :store, embed: true
128
- # cache_has_one :vendor
70
+ # cache_has_one :vendor, embed: :id
129
71
  # end
130
72
  #
131
73
  # == Parameters
@@ -133,24 +75,27 @@ module IdentityCache
133
75
  #
134
76
  # == Options
135
77
  #
136
- # * embed: Only true is supported, which is also the default, so
137
- # IdentityCache will keep the values for this association in the same
138
- # cache entry as the parent, instead of its own.
139
- # * inverse_name: The name of the parent in the association ( only
140
- # necessary if the name is not the lowercase pluralization of the
141
- # parent object's class)
142
- def cache_has_one(association, options = {})
78
+ # * embed: If `true`, IdentityCache will embed the associated record
79
+ # in the cache entries for this model, as well as all the embedded
80
+ # associations for the associated record recursively.
81
+ # If `:id`, it will only embed the id for the associated record.
82
+ def cache_has_one(association, embed:)
143
83
  ensure_base_model
144
- options = options.slice(:embed, :inverse_name)
145
- options[:embed] = true unless options.has_key?(:embed)
146
- ensure_cacheable_association(association, options)
147
- self.cached_has_ones[association] = options
148
-
149
- if options[:embed] == true
150
- build_recursive_association_cache(association, options)
84
+ check_association_for_caching(association)
85
+ reflection = reflect_on_association(association)
86
+ association_class = case embed
87
+ when :id
88
+ Cached::Reference::HasOne
89
+ when true
90
+ Cached::Recursive::HasOne
151
91
  else
152
92
  raise NotImplementedError
153
93
  end
94
+
95
+ cached_has_ones[association] = association_class.new(
96
+ association,
97
+ reflection: reflection,
98
+ ).tap(&:build)
154
99
  end
155
100
 
156
101
  # Will cache a single attribute on its own blob, it will add a
@@ -170,145 +115,38 @@ module IdentityCache
170
115
  #
171
116
  # * by: Other attribute or attributes in the model to keep values indexed. Default is :id
172
117
  # * unique: if the index would only have unique values. Default is true
173
- def cache_attribute(attribute, options = {})
174
- cache_attribute_by_alias(attribute.inspect, attribute, options)
175
- end
176
-
177
- def disable_primary_cache_index
178
- ActiveSupport::Deprecation.warn("disable_primary_cache_index is deprecated, use `include IdentityCache::WithoutPrimaryIndex` instead")
179
- ensure_base_model
180
- self.primary_cache_index_enabled = false
118
+ def cache_attribute(attribute, by: :id, unique: true)
119
+ cache_attribute_by_alias(attribute, alias_name: attribute, by: by, unique: unique)
181
120
  end
182
121
 
183
122
  private
184
123
 
185
- def cache_attribute_by_alias(attribute, alias_name, options)
124
+ def cache_attribute_by_alias(attribute_or_proc, alias_name:, by:, unique:)
186
125
  ensure_base_model
187
- options[:by] ||= :id
188
- alias_name = alias_name.to_sym
189
- unique = options[:unique].nil? ? true : !!options[:unique]
190
- fields = Array(options[:by])
191
-
192
- self.cache_indexes.push [alias_name, fields, unique]
193
-
194
- field_list = fields.join("_and_")
195
- arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')
196
-
197
- self.instance_eval(<<-CODE, __FILE__, __LINE__ + 1)
198
- def fetch_#{alias_name}_by_#{field_list}(#{arg_list})
199
- attribute_dynamic_fetcher(#{attribute}, #{fields.inspect}, [#{arg_list}], #{unique})
200
- end
201
- CODE
202
- end
203
-
204
- def build_recursive_association_cache(association, options) #:nodoc:
205
- options[:association_reflection] = reflect_on_association(association)
206
- options[:cached_accessor_name] = "fetch_#{association}"
207
- options[:records_variable_name] = "cached_#{association}"
208
-
209
- self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
210
- def #{options[:cached_accessor_name]}
211
- fetch_recursively_cached_association('#{options[:records_variable_name]}', :#{association})
212
- end
213
- CODE
214
-
215
- options[:only_on_foreign_key_change] = false
216
- add_parent_expiry_hook(options)
217
- end
218
-
219
- def build_id_embedded_has_many_cache(association, options) #:nodoc:
220
- singular_association = association.to_s.singularize
221
- options[:association_reflection] = reflect_on_association(association)
222
- options[:cached_accessor_name] = "fetch_#{association}"
223
- options[:ids_name] = "#{singular_association}_ids"
224
- options[:cached_ids_name] = "fetch_#{options[:ids_name]}"
225
- options[:ids_variable_name] = "cached_#{options[:ids_name]}"
226
- options[:records_variable_name] = "cached_#{association}"
227
- options[:prepopulate_method_name] = "prepopulate_fetched_#{association}"
228
-
229
- self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
230
- attr_reader :#{options[:ids_variable_name]}
231
-
232
- def #{options[:cached_ids_name]}
233
- @#{options[:ids_variable_name]} ||= #{options[:ids_name]}
234
- end
235
-
236
- def #{options[:cached_accessor_name]}
237
- association_klass = association(:#{association}).klass
238
- if association_klass.should_use_cache? && !#{association}.loaded?
239
- @#{options[:records_variable_name]} ||= #{options[:association_reflection].klass}.fetch_multi(#{options[:cached_ids_name]})
240
- else
241
- #{association}.to_a
242
- end
243
- end
244
-
245
- def #{options[:prepopulate_method_name]}(records)
246
- @#{options[:records_variable_name]} = records
247
- end
248
- CODE
126
+ fields = Array(by)
249
127
 
250
- options[:only_on_foreign_key_change] = true
251
- add_parent_expiry_hook(options)
252
- end
253
-
254
- def attribute_dynamic_fetcher(attribute, fields, values, unique_index) #:nodoc:
255
- raise_if_scoped
256
-
257
- if should_use_cache?
258
- cache_key = rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values, unique_index)
259
- IdentityCache.fetch(cache_key) do
260
- dynamic_attribute_cache_miss(attribute, fields, values, unique_index)
261
- end
262
- else
263
- dynamic_attribute_cache_miss(attribute, fields, values, unique_index)
264
- end
265
- end
266
-
267
- def dynamic_attribute_cache_miss(attribute, fields, values, unique_index)
268
- query = reorder(nil).where(Hash[fields.zip(values)])
269
- query = query.limit(1) if unique_index
270
- results = query.pluck(attribute)
271
- unique_index ? results.first : results
272
- end
273
-
274
- def add_parent_expiry_hook(options)
275
- child_class = options[:association_reflection].klass
276
- unless child_class < IdentityCache
277
- message = "associated class #{child_class} will need to include IdentityCache or " \
278
- "IdentityCache::WithoutPrimaryIndex for embedded associations"
279
- ActiveSupport::Deprecation.warn(message, caller(3))
280
- child_class.send(:include, IdentityCache::WithoutPrimaryIndex)
281
- end
282
- child_class.parent_expiration_entries[options[:inverse_name]] << [self, options[:only_on_foreign_key_change]]
283
- end
284
-
285
- def deprecate_embed_option(options, old_value, new_value)
286
- if options[:embed] == old_value
287
- options[:embed] = new_value
288
- ActiveSupport::Deprecation.warn("`embed: #{old_value.inspect}` was renamed to `embed: #{new_value.inspect}` for clarity", caller(2))
289
- end
128
+ klass = fields.one? ? Cached::AttributeByOne : Cached::AttributeByMulti
129
+ cached_attribute = klass.new(self, attribute_or_proc, alias_name, fields, unique)
130
+ cached_attribute.build
131
+ cache_indexes.push(cached_attribute)
290
132
  end
291
133
 
292
134
  def ensure_base_model
293
135
  if self != cached_model
294
- raise DerivedModelError, "IdentityCache class methods must be called on the same model that includes IdentityCache"
136
+ raise DerivedModelError, <<~MSG.squish
137
+ IdentityCache class methods must be called on the same
138
+ model that includes IdentityCache
139
+ MSG
295
140
  end
296
141
  end
297
142
 
298
- def ensure_cacheable_association(association, options)
299
- unless association_reflection = self.reflect_on_association(association)
143
+ def check_association_for_caching(association)
144
+ unless (association_reflection = reflect_on_association(association))
300
145
  raise AssociationError, "Association named '#{association}' was not found on #{self.class}"
301
146
  end
302
147
  if association_reflection.options[:through]
303
148
  raise UnsupportedAssociationError, "caching through associations isn't supported"
304
149
  end
305
- options[:inverse_name] ||= association_reflection.inverse_of.name if association_reflection.inverse_of
306
- options[:inverse_name] ||= self.name.underscore.to_sym
307
- child_class = association_reflection.klass
308
- raise InverseAssociationError unless child_class.reflect_on_association(options[:inverse_name])
309
- unless options[:embed] == true || child_class.include?(IdentityCache)
310
- raise UnsupportedAssociationError, "associated class #{child_class} must include IdentityCache to be cached without full embedding"
311
- end
312
150
  end
313
151
  end
314
152
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+ module IdentityCache
3
+ module Encoder
4
+ DEHYDRATE_EVENT = "dehydration.identity_cache"
5
+ HYDRATE_EVENT = "hydration.identity_cache"
6
+
7
+ class << self
8
+ def encode(record)
9
+ return unless record
10
+
11
+ ActiveSupport::Notifications.instrument(DEHYDRATE_EVENT, class: record.class.name) do
12
+ coder_from_record(record, record.class)
13
+ end
14
+ end
15
+
16
+ def decode(coder, klass)
17
+ return unless coder
18
+
19
+ ActiveSupport::Notifications.instrument(HYDRATE_EVENT, class: klass.name) do
20
+ record_from_coder(coder, klass)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def coder_from_record(record, klass)
27
+ return unless record
28
+
29
+ coder = {}
30
+ coder[:attributes] = record.attributes_before_type_cast.dup
31
+
32
+ recursively_embedded_associations = klass.send(:recursively_embedded_associations)
33
+ id_embedded_has_manys = klass.cached_has_manys.select { |_, association| association.embedded_by_reference? }
34
+ id_embedded_has_ones = klass.cached_has_ones.select { |_, association| association.embedded_by_reference? }
35
+
36
+ if recursively_embedded_associations.present?
37
+ coder[:associations] = recursively_embedded_associations.each_with_object({}) do |(name, association), hash|
38
+ hash[name] = IdentityCache.map_cached_nil_for(embedded_coder(record, name, association))
39
+ end
40
+ end
41
+
42
+ if id_embedded_has_manys.present?
43
+ coder[:association_ids] = id_embedded_has_manys.each_with_object({}) do |(name, association), hash|
44
+ hash[name] = record.instance_variable_get(association.ids_variable_name)
45
+ end
46
+ end
47
+
48
+ if id_embedded_has_ones.present?
49
+ coder[:association_id] = id_embedded_has_ones.each_with_object({}) do |(name, association), hash|
50
+ hash[name] = record.instance_variable_get(association.id_variable_name)
51
+ end
52
+ end
53
+
54
+ coder
55
+ end
56
+
57
+ def embedded_coder(record, _association, cached_association)
58
+ embedded_record_or_records = record.public_send(cached_association.cached_accessor_name)
59
+
60
+ if embedded_record_or_records.respond_to?(:to_ary)
61
+ embedded_record_or_records.map do |embedded_record|
62
+ coder_from_record(embedded_record, embedded_record.class)
63
+ end
64
+ else
65
+ coder_from_record(embedded_record_or_records, embedded_record_or_records.class)
66
+ end
67
+ end
68
+
69
+ def record_from_coder(coder, klass) #:nodoc:
70
+ record = klass.instantiate(coder[:attributes].dup)
71
+
72
+ if coder.key?(:associations)
73
+ coder[:associations].each do |name, value|
74
+ record.instance_variable_set(klass.cached_association(name).dehydrated_variable_name, value)
75
+ end
76
+ end
77
+ if coder.key?(:association_ids)
78
+ coder[:association_ids].each do |name, ids|
79
+ record.instance_variable_set(klass.cached_has_manys.fetch(name).ids_variable_name, ids)
80
+ end
81
+ end
82
+ if coder.key?(:association_id)
83
+ coder[:association_id].each do |name, id|
84
+ record.instance_variable_set(klass.cached_has_ones.fetch(name).id_variable_name, id)
85
+ end
86
+ end
87
+
88
+ record.readonly! if IdentityCache.fetch_read_only_records
89
+ record
90
+ end
91
+ end
92
+ end
93
+
94
+ private_constant :Encoder
95
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ module IdentityCache
3
+ class ExpiryHook
4
+ def initialize(cached_association)
5
+ @cached_association = cached_association
6
+ end
7
+
8
+ def install
9
+ cached_association.validate
10
+ entry = [parent_class, only_on_foreign_key_change?]
11
+ child_class.parent_expiration_entries[inverse_name] << entry
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :cached_association
17
+
18
+ def only_on_foreign_key_change?
19
+ cached_association.embedded_by_reference?
20
+ end
21
+
22
+ def inverse_name
23
+ cached_association.inverse_name
24
+ end
25
+
26
+ def parent_class
27
+ cached_association.reflection.active_record
28
+ end
29
+
30
+ def child_class
31
+ cached_association.reflection.klass
32
+ end
33
+ end
34
+
35
+ private_constant :ExpiryHook
36
+ end