activesupport 5.1.1 → 6.1.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activesupport might be problematic. Click here for more details.

Files changed (262) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +360 -442
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +5 -4
  5. data/lib/active_support/actionable_error.rb +48 -0
  6. data/lib/active_support/all.rb +2 -0
  7. data/lib/active_support/array_inquirer.rb +6 -2
  8. data/lib/active_support/backtrace_cleaner.rb +31 -3
  9. data/lib/active_support/benchmarkable.rb +3 -1
  10. data/lib/active_support/builder.rb +2 -0
  11. data/lib/active_support/cache/file_store.rb +37 -36
  12. data/lib/active_support/cache/mem_cache_store.rb +65 -53
  13. data/lib/active_support/cache/memory_store.rb +61 -33
  14. data/lib/active_support/cache/null_store.rb +10 -3
  15. data/lib/active_support/cache/redis_cache_store.rb +493 -0
  16. data/lib/active_support/cache/strategy/local_cache.rb +68 -22
  17. data/lib/active_support/cache/strategy/local_cache_middleware.rb +2 -0
  18. data/lib/active_support/cache.rb +305 -127
  19. data/lib/active_support/callbacks.rb +106 -98
  20. data/lib/active_support/concern.rb +79 -6
  21. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +35 -0
  22. data/lib/active_support/concurrency/share_lock.rb +2 -1
  23. data/lib/active_support/configurable.rb +12 -14
  24. data/lib/active_support/configuration_file.rb +46 -0
  25. data/lib/active_support/core_ext/array/access.rb +21 -7
  26. data/lib/active_support/core_ext/array/conversions.rb +7 -5
  27. data/lib/active_support/core_ext/array/extract.rb +21 -0
  28. data/lib/active_support/core_ext/array/extract_options.rb +2 -0
  29. data/lib/active_support/core_ext/array/grouping.rb +2 -0
  30. data/lib/active_support/core_ext/array/inquiry.rb +2 -0
  31. data/lib/active_support/core_ext/array/wrap.rb +2 -0
  32. data/lib/active_support/core_ext/array.rb +3 -1
  33. data/lib/active_support/core_ext/benchmark.rb +4 -2
  34. data/lib/active_support/core_ext/big_decimal/conversions.rb +2 -0
  35. data/lib/active_support/core_ext/big_decimal.rb +2 -0
  36. data/lib/active_support/core_ext/class/attribute.rb +50 -47
  37. data/lib/active_support/core_ext/class/attribute_accessors.rb +2 -0
  38. data/lib/active_support/core_ext/class/subclasses.rb +18 -40
  39. data/lib/active_support/core_ext/class.rb +2 -0
  40. data/lib/active_support/core_ext/date/acts_like.rb +2 -0
  41. data/lib/active_support/core_ext/date/blank.rb +2 -0
  42. data/lib/active_support/core_ext/date/calculations.rb +8 -5
  43. data/lib/active_support/core_ext/date/conversions.rb +12 -10
  44. data/lib/active_support/core_ext/date/zones.rb +2 -0
  45. data/lib/active_support/core_ext/date.rb +2 -0
  46. data/lib/active_support/core_ext/date_and_time/calculations.rb +61 -37
  47. data/lib/active_support/core_ext/date_and_time/compatibility.rb +18 -1
  48. data/lib/active_support/core_ext/date_and_time/zones.rb +2 -1
  49. data/lib/active_support/core_ext/date_time/acts_like.rb +2 -0
  50. data/lib/active_support/core_ext/date_time/blank.rb +2 -0
  51. data/lib/active_support/core_ext/date_time/calculations.rb +3 -1
  52. data/lib/active_support/core_ext/date_time/compatibility.rb +7 -5
  53. data/lib/active_support/core_ext/date_time/conversions.rb +2 -1
  54. data/lib/active_support/core_ext/date_time.rb +2 -0
  55. data/lib/active_support/core_ext/digest/uuid.rb +3 -1
  56. data/lib/active_support/core_ext/digest.rb +3 -0
  57. data/lib/active_support/core_ext/enumerable.rb +174 -71
  58. data/lib/active_support/core_ext/file/atomic.rb +3 -1
  59. data/lib/active_support/core_ext/file.rb +2 -0
  60. data/lib/active_support/core_ext/hash/conversions.rb +7 -5
  61. data/lib/active_support/core_ext/hash/deep_merge.rb +8 -12
  62. data/lib/active_support/core_ext/hash/deep_transform_values.rb +46 -0
  63. data/lib/active_support/core_ext/hash/except.rb +4 -2
  64. data/lib/active_support/core_ext/hash/indifferent_access.rb +2 -0
  65. data/lib/active_support/core_ext/hash/keys.rb +3 -30
  66. data/lib/active_support/core_ext/hash/reverse_merge.rb +5 -2
  67. data/lib/active_support/core_ext/hash/slice.rb +8 -29
  68. data/lib/active_support/core_ext/hash.rb +3 -2
  69. data/lib/active_support/core_ext/integer/inflections.rb +2 -0
  70. data/lib/active_support/core_ext/integer/multiple.rb +3 -1
  71. data/lib/active_support/core_ext/integer/time.rb +7 -14
  72. data/lib/active_support/core_ext/integer.rb +2 -0
  73. data/lib/active_support/core_ext/kernel/concern.rb +2 -0
  74. data/lib/active_support/core_ext/kernel/reporting.rb +2 -0
  75. data/lib/active_support/core_ext/kernel/singleton_class.rb +2 -0
  76. data/lib/active_support/core_ext/kernel.rb +2 -1
  77. data/lib/active_support/core_ext/load_error.rb +3 -8
  78. data/lib/active_support/core_ext/marshal.rb +4 -0
  79. data/lib/active_support/core_ext/module/aliasing.rb +2 -0
  80. data/lib/active_support/core_ext/module/anonymous.rb +2 -0
  81. data/lib/active_support/core_ext/module/attr_internal.rb +4 -2
  82. data/lib/active_support/core_ext/module/attribute_accessors.rb +44 -56
  83. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +18 -18
  84. data/lib/active_support/core_ext/module/concerning.rb +15 -10
  85. data/lib/active_support/core_ext/module/delegation.rb +103 -58
  86. data/lib/active_support/core_ext/module/deprecation.rb +2 -0
  87. data/lib/active_support/core_ext/module/introspection.rb +18 -15
  88. data/lib/active_support/core_ext/module/redefine_method.rb +40 -0
  89. data/lib/active_support/core_ext/module/remove_method.rb +5 -23
  90. data/lib/active_support/core_ext/module.rb +3 -1
  91. data/lib/active_support/core_ext/name_error.rb +36 -2
  92. data/lib/active_support/core_ext/numeric/bytes.rb +2 -0
  93. data/lib/active_support/core_ext/numeric/conversions.rb +131 -129
  94. data/lib/active_support/core_ext/numeric/time.rb +7 -15
  95. data/lib/active_support/core_ext/numeric.rb +2 -1
  96. data/lib/active_support/core_ext/object/acts_like.rb +12 -1
  97. data/lib/active_support/core_ext/object/blank.rb +13 -3
  98. data/lib/active_support/core_ext/object/conversions.rb +2 -0
  99. data/lib/active_support/core_ext/object/deep_dup.rb +3 -1
  100. data/lib/active_support/core_ext/object/duplicable.rb +6 -101
  101. data/lib/active_support/core_ext/object/inclusion.rb +2 -0
  102. data/lib/active_support/core_ext/object/instance_variables.rb +2 -0
  103. data/lib/active_support/core_ext/object/json.rb +22 -2
  104. data/lib/active_support/core_ext/object/to_param.rb +2 -0
  105. data/lib/active_support/core_ext/object/to_query.rb +7 -2
  106. data/lib/active_support/core_ext/object/try.rb +19 -7
  107. data/lib/active_support/core_ext/object/with_options.rb +4 -2
  108. data/lib/active_support/core_ext/object.rb +2 -0
  109. data/lib/active_support/core_ext/range/compare_range.rb +82 -0
  110. data/lib/active_support/core_ext/range/conversions.rb +35 -25
  111. data/lib/active_support/core_ext/range/each.rb +5 -2
  112. data/lib/active_support/core_ext/range/include_time_with_zone.rb +28 -0
  113. data/lib/active_support/core_ext/range/overlaps.rb +2 -0
  114. data/lib/active_support/core_ext/range.rb +4 -1
  115. data/lib/active_support/core_ext/regexp.rb +10 -5
  116. data/lib/active_support/core_ext/securerandom.rb +25 -3
  117. data/lib/active_support/core_ext/string/access.rb +7 -16
  118. data/lib/active_support/core_ext/string/behavior.rb +2 -0
  119. data/lib/active_support/core_ext/string/conversions.rb +3 -0
  120. data/lib/active_support/core_ext/string/exclude.rb +2 -0
  121. data/lib/active_support/core_ext/string/filters.rb +44 -1
  122. data/lib/active_support/core_ext/string/indent.rb +2 -0
  123. data/lib/active_support/core_ext/string/inflections.rb +69 -16
  124. data/lib/active_support/core_ext/string/inquiry.rb +3 -0
  125. data/lib/active_support/core_ext/string/multibyte.rb +9 -4
  126. data/lib/active_support/core_ext/string/output_safety.rb +76 -20
  127. data/lib/active_support/core_ext/string/starts_ends_with.rb +4 -2
  128. data/lib/active_support/core_ext/string/strip.rb +5 -1
  129. data/lib/active_support/core_ext/string/zones.rb +2 -0
  130. data/lib/active_support/core_ext/string.rb +2 -0
  131. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +14 -0
  132. data/lib/active_support/core_ext/symbol.rb +3 -0
  133. data/lib/active_support/core_ext/time/acts_like.rb +2 -0
  134. data/lib/active_support/core_ext/time/calculations.rb +73 -18
  135. data/lib/active_support/core_ext/time/compatibility.rb +4 -2
  136. data/lib/active_support/core_ext/time/conversions.rb +4 -0
  137. data/lib/active_support/core_ext/time/zones.rb +6 -4
  138. data/lib/active_support/core_ext/time.rb +2 -0
  139. data/lib/active_support/core_ext/uri.rb +11 -6
  140. data/lib/active_support/core_ext.rb +3 -1
  141. data/lib/active_support/current_attributes/test_helper.rb +13 -0
  142. data/lib/active_support/current_attributes.rb +208 -0
  143. data/lib/active_support/dependencies/autoload.rb +2 -0
  144. data/lib/active_support/dependencies/interlock.rb +2 -0
  145. data/lib/active_support/dependencies/zeitwerk_integration.rb +117 -0
  146. data/lib/active_support/dependencies.rb +135 -60
  147. data/lib/active_support/deprecation/behaviors.rb +43 -11
  148. data/lib/active_support/deprecation/constant_accessor.rb +4 -2
  149. data/lib/active_support/deprecation/disallowed.rb +56 -0
  150. data/lib/active_support/deprecation/instance_delegator.rb +2 -1
  151. data/lib/active_support/deprecation/method_wrappers.rb +30 -15
  152. data/lib/active_support/deprecation/proxy_wrappers.rb +32 -6
  153. data/lib/active_support/deprecation/reporting.rb +54 -9
  154. data/lib/active_support/deprecation.rb +9 -2
  155. data/lib/active_support/descendants_tracker.rb +61 -9
  156. data/lib/active_support/digest.rb +20 -0
  157. data/lib/active_support/duration/iso8601_parser.rb +6 -6
  158. data/lib/active_support/duration/iso8601_serializer.rb +20 -14
  159. data/lib/active_support/duration.rb +179 -41
  160. data/lib/active_support/encrypted_configuration.rb +45 -0
  161. data/lib/active_support/encrypted_file.rb +117 -0
  162. data/lib/active_support/environment_inquirer.rb +20 -0
  163. data/lib/active_support/evented_file_update_checker.rb +84 -117
  164. data/lib/active_support/execution_wrapper.rb +3 -0
  165. data/lib/active_support/executor.rb +2 -0
  166. data/lib/active_support/file_update_checker.rb +2 -1
  167. data/lib/active_support/fork_tracker.rb +62 -0
  168. data/lib/active_support/gem_version.rb +3 -1
  169. data/lib/active_support/gzip.rb +2 -0
  170. data/lib/active_support/hash_with_indifferent_access.rb +134 -37
  171. data/lib/active_support/i18n.rb +4 -1
  172. data/lib/active_support/i18n_railtie.rb +20 -11
  173. data/lib/active_support/inflections.rb +2 -0
  174. data/lib/active_support/inflector/inflections.rb +19 -8
  175. data/lib/active_support/inflector/methods.rb +87 -77
  176. data/lib/active_support/inflector/transliterate.rb +56 -18
  177. data/lib/active_support/inflector.rb +2 -0
  178. data/lib/active_support/json/decoding.rb +27 -26
  179. data/lib/active_support/json/encoding.rb +13 -3
  180. data/lib/active_support/json.rb +2 -0
  181. data/lib/active_support/key_generator.rb +3 -33
  182. data/lib/active_support/lazy_load_hooks.rb +33 -10
  183. data/lib/active_support/locale/en.rb +33 -0
  184. data/lib/active_support/locale/en.yml +7 -3
  185. data/lib/active_support/log_subscriber/test_helper.rb +2 -0
  186. data/lib/active_support/log_subscriber.rb +46 -13
  187. data/lib/active_support/logger.rb +4 -17
  188. data/lib/active_support/logger_silence.rb +13 -20
  189. data/lib/active_support/logger_thread_safe_level.rb +54 -7
  190. data/lib/active_support/message_encryptor.rb +101 -33
  191. data/lib/active_support/message_verifier.rb +85 -14
  192. data/lib/active_support/messages/metadata.rb +80 -0
  193. data/lib/active_support/messages/rotation_configuration.rb +23 -0
  194. data/lib/active_support/messages/rotator.rb +57 -0
  195. data/lib/active_support/multibyte/chars.rb +12 -68
  196. data/lib/active_support/multibyte/unicode.rb +17 -327
  197. data/lib/active_support/multibyte.rb +2 -0
  198. data/lib/active_support/notifications/fanout.rb +118 -16
  199. data/lib/active_support/notifications/instrumenter.rb +73 -9
  200. data/lib/active_support/notifications.rb +74 -8
  201. data/lib/active_support/number_helper/number_converter.rb +7 -6
  202. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -9
  203. data/lib/active_support/number_helper/number_to_delimited_converter.rb +5 -2
  204. data/lib/active_support/number_helper/number_to_human_converter.rb +8 -7
  205. data/lib/active_support/number_helper/number_to_human_size_converter.rb +6 -3
  206. data/lib/active_support/number_helper/number_to_percentage_converter.rb +5 -1
  207. data/lib/active_support/number_helper/number_to_phone_converter.rb +5 -2
  208. data/lib/active_support/number_helper/number_to_rounded_converter.rb +16 -53
  209. data/lib/active_support/number_helper/rounding_helper.rb +50 -0
  210. data/lib/active_support/number_helper.rb +41 -12
  211. data/lib/active_support/option_merger.rb +24 -3
  212. data/lib/active_support/ordered_hash.rb +3 -1
  213. data/lib/active_support/ordered_options.rb +17 -5
  214. data/lib/active_support/parameter_filter.rb +133 -0
  215. data/lib/active_support/per_thread_registry.rb +3 -1
  216. data/lib/active_support/proxy_object.rb +2 -0
  217. data/lib/active_support/rails.rb +3 -10
  218. data/lib/active_support/railtie.rb +60 -9
  219. data/lib/active_support/reloader.rb +11 -10
  220. data/lib/active_support/rescuable.rb +7 -6
  221. data/lib/active_support/secure_compare_rotator.rb +51 -0
  222. data/lib/active_support/security_utils.rb +26 -15
  223. data/lib/active_support/string_inquirer.rb +6 -3
  224. data/lib/active_support/subscriber.rb +74 -24
  225. data/lib/active_support/tagged_logging.rb +44 -8
  226. data/lib/active_support/test_case.rb +94 -2
  227. data/lib/active_support/testing/assertions.rb +58 -20
  228. data/lib/active_support/testing/autorun.rb +2 -4
  229. data/lib/active_support/testing/constant_lookup.rb +2 -0
  230. data/lib/active_support/testing/declarative.rb +2 -0
  231. data/lib/active_support/testing/deprecation.rb +2 -1
  232. data/lib/active_support/testing/file_fixtures.rb +4 -0
  233. data/lib/active_support/testing/isolation.rb +8 -4
  234. data/lib/active_support/testing/method_call_assertions.rb +30 -1
  235. data/lib/active_support/testing/parallelization/server.rb +78 -0
  236. data/lib/active_support/testing/parallelization/worker.rb +100 -0
  237. data/lib/active_support/testing/parallelization.rb +51 -0
  238. data/lib/active_support/testing/setup_and_teardown.rb +12 -7
  239. data/lib/active_support/testing/stream.rb +3 -2
  240. data/lib/active_support/testing/tagged_logging.rb +2 -0
  241. data/lib/active_support/testing/time_helpers.rb +78 -13
  242. data/lib/active_support/time.rb +2 -0
  243. data/lib/active_support/time_with_zone.rb +113 -41
  244. data/lib/active_support/values/time_zone.rb +55 -25
  245. data/lib/active_support/version.rb +2 -0
  246. data/lib/active_support/xml_mini/jdom.rb +5 -4
  247. data/lib/active_support/xml_mini/libxml.rb +4 -2
  248. data/lib/active_support/xml_mini/libxmlsax.rb +6 -4
  249. data/lib/active_support/xml_mini/nokogiri.rb +4 -2
  250. data/lib/active_support/xml_mini/nokogirisax.rb +5 -3
  251. data/lib/active_support/xml_mini/rexml.rb +12 -3
  252. data/lib/active_support/xml_mini.rb +5 -11
  253. data/lib/active_support.rb +18 -13
  254. metadata +81 -35
  255. data/lib/active_support/core_ext/array/prepend_and_append.rb +0 -7
  256. data/lib/active_support/core_ext/hash/compact.rb +0 -27
  257. data/lib/active_support/core_ext/hash/transform_values.rb +0 -30
  258. data/lib/active_support/core_ext/kernel/agnostics.rb +0 -11
  259. data/lib/active_support/core_ext/module/reachable.rb +0 -8
  260. data/lib/active_support/core_ext/numeric/inquiry.rb +0 -26
  261. data/lib/active_support/core_ext/range/include_range.rb +0 -23
  262. data/lib/active_support/values/unicode_tables.dat +0 -0
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "monitor"
2
4
 
3
5
  module ActiveSupport
@@ -14,13 +16,37 @@ module ActiveSupport
14
16
  # a cleanup will occur which tries to prune the cache down to three quarters
15
17
  # of the maximum size by removing the least recently used entries.
16
18
  #
19
+ # Unlike other Cache store implementations, MemoryStore does not compress
20
+ # values by default. MemoryStore does not benefit from compression as much
21
+ # as other Store implementations, as it does not send data over a network.
22
+ # However, when compression is enabled, it still pays the full cost of
23
+ # compression in terms of cpu use.
24
+ #
17
25
  # MemoryStore is thread-safe.
18
26
  class MemoryStore < Store
27
+ module DupCoder # :nodoc:
28
+ class << self
29
+ def load(entry)
30
+ entry = entry.dup
31
+ entry.dup_value!
32
+ entry
33
+ end
34
+
35
+ def dump(entry)
36
+ entry.dup_value!
37
+ entry
38
+ end
39
+ end
40
+ end
41
+
42
+ DEFAULT_CODER = DupCoder
43
+
19
44
  def initialize(options = nil)
20
45
  options ||= {}
46
+ # Disable compression by default.
47
+ options[:compress] ||= false
21
48
  super(options)
22
49
  @data = {}
23
- @key_access = {}
24
50
  @max_size = options[:size] || 32.megabytes
25
51
  @max_prune_time = options[:max_prune_time] || 2
26
52
  @cache_size = 0
@@ -28,11 +54,15 @@ module ActiveSupport
28
54
  @pruning = false
29
55
  end
30
56
 
57
+ # Advertise cache versioning support.
58
+ def self.supports_cache_versioning?
59
+ true
60
+ end
61
+
31
62
  # Delete all data stored in a given cache store.
32
63
  def clear(options = nil)
33
64
  synchronize do
34
65
  @data.clear
35
- @key_access.clear
36
66
  @cache_size = 0
37
67
  end
38
68
  end
@@ -44,7 +74,7 @@ module ActiveSupport
44
74
  keys = synchronize { @data.keys }
45
75
  keys.each do |key|
46
76
  entry = @data[key]
47
- delete_entry(key, options) if entry && entry.expired?
77
+ delete_entry(key, **options) if entry && entry.expired?
48
78
  end
49
79
  end
50
80
  end
@@ -55,13 +85,13 @@ module ActiveSupport
55
85
  return if pruning?
56
86
  @pruning = true
57
87
  begin
58
- start_time = Time.now
88
+ start_time = Concurrent.monotonic_time
59
89
  cleanup
60
90
  instrument(:prune, target_size, from: @cache_size) do
61
- keys = synchronize { @key_access.keys.sort { |a, b| @key_access[a].to_f <=> @key_access[b].to_f } }
91
+ keys = synchronize { @data.keys }
62
92
  keys.each do |key|
63
- delete_entry(key, options)
64
- return if @cache_size <= target_size || (max_time && Time.now - start_time > max_time)
93
+ delete_entry(key, **options)
94
+ return if @cache_size <= target_size || (max_time && Concurrent.monotonic_time - start_time > max_time)
65
95
  end
66
96
  end
67
97
  ensure
@@ -91,13 +121,13 @@ module ActiveSupport
91
121
  matcher = key_matcher(matcher, options)
92
122
  keys = synchronize { @data.keys }
93
123
  keys.each do |key|
94
- delete_entry(key, options) if key.match(matcher)
124
+ delete_entry(key, **options) if key.match(matcher)
95
125
  end
96
126
  end
97
127
  end
98
128
 
99
129
  def inspect # :nodoc:
100
- "<##{self.class.name} entries=#{@data.size}, size=#{@cache_size}, options=#{@options.inspect}>"
130
+ "#<#{self.class.name} entries=#{@data.size}, size=#{@cache_size}, options=#{@options.inspect}>"
101
131
  end
102
132
 
103
133
  # Synchronize calls to the cache. This should be called wherever the underlying cache implementation
@@ -107,54 +137,52 @@ module ActiveSupport
107
137
  end
108
138
 
109
139
  private
110
-
111
140
  PER_ENTRY_OVERHEAD = 240
112
141
 
113
- def cached_size(key, entry)
114
- key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD
142
+ def cached_size(key, payload)
143
+ key.to_s.bytesize + payload.bytesize + PER_ENTRY_OVERHEAD
115
144
  end
116
145
 
117
- def read_entry(key, options)
118
- entry = @data[key]
146
+ def read_entry(key, **options)
147
+ entry = nil
119
148
  synchronize do
120
- if entry
121
- @key_access[key] = Time.now.to_f
122
- else
123
- @key_access.delete(key)
149
+ payload = @data.delete(key)
150
+ if payload
151
+ @data[key] = payload
152
+ entry = deserialize_entry(payload)
124
153
  end
125
154
  end
126
155
  entry
127
156
  end
128
157
 
129
- def write_entry(key, entry, options)
130
- entry.dup_value!
158
+ def write_entry(key, entry, **options)
159
+ payload = serialize_entry(entry)
131
160
  synchronize do
132
- old_entry = @data[key]
133
- return false if @data.key?(key) && options[:unless_exist]
134
- if old_entry
135
- @cache_size -= (old_entry.size - entry.size)
161
+ return false if options[:unless_exist] && @data.key?(key)
162
+
163
+ old_payload = @data[key]
164
+ if old_payload
165
+ @cache_size -= (old_payload.bytesize - payload.bytesize)
136
166
  else
137
- @cache_size += cached_size(key, entry)
167
+ @cache_size += cached_size(key, payload)
138
168
  end
139
- @key_access[key] = Time.now.to_f
140
- @data[key] = entry
169
+ @data[key] = payload
141
170
  prune(@max_size * 0.75, @max_prune_time) if @cache_size > @max_size
142
171
  true
143
172
  end
144
173
  end
145
174
 
146
- def delete_entry(key, options)
175
+ def delete_entry(key, **options)
147
176
  synchronize do
148
- @key_access.delete(key)
149
- entry = @data.delete(key)
150
- @cache_size -= cached_size(key, entry) if entry
151
- !!entry
177
+ payload = @data.delete(key)
178
+ @cache_size -= cached_size(key, payload) if payload
179
+ !!payload
152
180
  end
153
181
  end
154
182
 
155
183
  def modify_value(name, amount, options)
184
+ options = merged_options(options)
156
185
  synchronize do
157
- options = merged_options(options)
158
186
  if num = read(name, options)
159
187
  num = num.to_i + amount
160
188
  write(name, num, options)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveSupport
2
4
  module Cache
3
5
  # A cache store implementation which doesn't actually store anything. Useful in
@@ -10,6 +12,11 @@ module ActiveSupport
10
12
  class NullStore < Store
11
13
  prepend Strategy::LocalCache
12
14
 
15
+ # Advertise cache versioning support.
16
+ def self.supports_cache_versioning?
17
+ true
18
+ end
19
+
13
20
  def clear(options = nil)
14
21
  end
15
22
 
@@ -26,14 +33,14 @@ module ActiveSupport
26
33
  end
27
34
 
28
35
  private
29
- def read_entry(key, options)
36
+ def read_entry(key, **options)
30
37
  end
31
38
 
32
- def write_entry(key, entry, options)
39
+ def write_entry(key, entry, **options)
33
40
  true
34
41
  end
35
42
 
36
- def delete_entry(key, options)
43
+ def delete_entry(key, **options)
37
44
  false
38
45
  end
39
46
  end
@@ -0,0 +1,493 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ gem "redis", ">= 4.0.1"
5
+ require "redis"
6
+ require "redis/distributed"
7
+ rescue LoadError
8
+ warn "The Redis cache store requires the redis gem, version 4.0.1 or later. Please add it to your Gemfile: `gem \"redis\", \"~> 4.0\"`"
9
+ raise
10
+ end
11
+
12
+ # Prefer the hiredis driver but don't require it.
13
+ begin
14
+ require "redis/connection/hiredis"
15
+ rescue LoadError
16
+ end
17
+
18
+ require "digest/sha2"
19
+ require "active_support/core_ext/marshal"
20
+
21
+ module ActiveSupport
22
+ module Cache
23
+ module ConnectionPoolLike
24
+ def with
25
+ yield self
26
+ end
27
+ end
28
+
29
+ ::Redis.include(ConnectionPoolLike)
30
+ ::Redis::Distributed.include(ConnectionPoolLike)
31
+
32
+ # Redis cache store.
33
+ #
34
+ # Deployment note: Take care to use a *dedicated Redis cache* rather
35
+ # than pointing this at your existing Redis server. It won't cope well
36
+ # with mixed usage patterns and it won't expire cache entries by default.
37
+ #
38
+ # Redis cache server setup guide: https://redis.io/topics/lru-cache
39
+ #
40
+ # * Supports vanilla Redis, hiredis, and Redis::Distributed.
41
+ # * Supports Memcached-like sharding across Redises with Redis::Distributed.
42
+ # * Fault tolerant. If the Redis server is unavailable, no exceptions are
43
+ # raised. Cache fetches are all misses and writes are dropped.
44
+ # * Local cache. Hot in-memory primary cache within block/middleware scope.
45
+ # * +read_multi+ and +write_multi+ support for Redis mget/mset. Use Redis::Distributed
46
+ # 4.0.1+ for distributed mget support.
47
+ # * +delete_matched+ support for Redis KEYS globs.
48
+ class RedisCacheStore < Store
49
+ # Keys are truncated with their own SHA2 digest if they exceed 1kB
50
+ MAX_KEY_BYTESIZE = 1024
51
+
52
+ DEFAULT_REDIS_OPTIONS = {
53
+ connect_timeout: 20,
54
+ read_timeout: 1,
55
+ write_timeout: 1,
56
+ reconnect_attempts: 0,
57
+ }
58
+
59
+ DEFAULT_ERROR_HANDLER = -> (method:, returning:, exception:) do
60
+ if logger
61
+ logger.error { "RedisCacheStore: #{method} failed, returned #{returning.inspect}: #{exception.class}: #{exception.message}" }
62
+ end
63
+ end
64
+
65
+ # The maximum number of entries to receive per SCAN call.
66
+ SCAN_BATCH_SIZE = 1000
67
+ private_constant :SCAN_BATCH_SIZE
68
+
69
+ # Advertise cache versioning support.
70
+ def self.supports_cache_versioning?
71
+ true
72
+ end
73
+
74
+ # Support raw values in the local cache strategy.
75
+ module LocalCacheWithRaw # :nodoc:
76
+ private
77
+ def write_entry(key, entry, **options)
78
+ if options[:raw] && local_cache
79
+ raw_entry = Entry.new(serialize_entry(entry, raw: true))
80
+ raw_entry.expires_at = entry.expires_at
81
+ super(key, raw_entry, **options)
82
+ else
83
+ super
84
+ end
85
+ end
86
+
87
+ def write_multi_entries(entries, **options)
88
+ if options[:raw] && local_cache
89
+ raw_entries = entries.map do |key, entry|
90
+ raw_entry = Entry.new(serialize_entry(entry, raw: true))
91
+ raw_entry.expires_at = entry.expires_at
92
+ end.to_h
93
+
94
+ super(raw_entries, **options)
95
+ else
96
+ super
97
+ end
98
+ end
99
+ end
100
+
101
+ prepend Strategy::LocalCache
102
+ prepend LocalCacheWithRaw
103
+
104
+ class << self
105
+ # Factory method to create a new Redis instance.
106
+ #
107
+ # Handles four options: :redis block, :redis instance, single :url
108
+ # string, and multiple :url strings.
109
+ #
110
+ # Option Class Result
111
+ # :redis Proc -> options[:redis].call
112
+ # :redis Object -> options[:redis]
113
+ # :url String -> Redis.new(url: …)
114
+ # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
115
+ #
116
+ def build_redis(redis: nil, url: nil, **redis_options) #:nodoc:
117
+ urls = Array(url)
118
+
119
+ if redis.is_a?(Proc)
120
+ redis.call
121
+ elsif redis
122
+ redis
123
+ elsif urls.size > 1
124
+ build_redis_distributed_client urls: urls, **redis_options
125
+ else
126
+ build_redis_client url: urls.first, **redis_options
127
+ end
128
+ end
129
+
130
+ private
131
+ def build_redis_distributed_client(urls:, **redis_options)
132
+ ::Redis::Distributed.new([], DEFAULT_REDIS_OPTIONS.merge(redis_options)).tap do |dist|
133
+ urls.each { |u| dist.add_node url: u }
134
+ end
135
+ end
136
+
137
+ def build_redis_client(url:, **redis_options)
138
+ ::Redis.new DEFAULT_REDIS_OPTIONS.merge(redis_options.merge(url: url))
139
+ end
140
+ end
141
+
142
+ attr_reader :redis_options
143
+ attr_reader :max_key_bytesize
144
+
145
+ # Creates a new Redis cache store.
146
+ #
147
+ # Handles four options: :redis block, :redis instance, single :url
148
+ # string, and multiple :url strings.
149
+ #
150
+ # Option Class Result
151
+ # :redis Proc -> options[:redis].call
152
+ # :redis Object -> options[:redis]
153
+ # :url String -> Redis.new(url: …)
154
+ # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
155
+ #
156
+ # No namespace is set by default. Provide one if the Redis cache
157
+ # server is shared with other apps: <tt>namespace: 'myapp-cache'</tt>.
158
+ #
159
+ # Compression is enabled by default with a 1kB threshold, so cached
160
+ # values larger than 1kB are automatically compressed. Disable by
161
+ # passing <tt>compress: false</tt> or change the threshold by passing
162
+ # <tt>compress_threshold: 4.kilobytes</tt>.
163
+ #
164
+ # No expiry is set on cache entries by default. Redis is expected to
165
+ # be configured with an eviction policy that automatically deletes
166
+ # least-recently or -frequently used keys when it reaches max memory.
167
+ # See https://redis.io/topics/lru-cache for cache server setup.
168
+ #
169
+ # Race condition TTL is not set by default. This can be used to avoid
170
+ # "thundering herd" cache writes when hot cache entries are expired.
171
+ # See <tt>ActiveSupport::Cache::Store#fetch</tt> for more.
172
+ def initialize(namespace: nil, compress: true, compress_threshold: 1.kilobyte, coder: DEFAULT_CODER, expires_in: nil, race_condition_ttl: nil, error_handler: DEFAULT_ERROR_HANDLER, **redis_options)
173
+ @redis_options = redis_options
174
+
175
+ @max_key_bytesize = MAX_KEY_BYTESIZE
176
+ @error_handler = error_handler
177
+
178
+ super namespace: namespace,
179
+ compress: compress, compress_threshold: compress_threshold,
180
+ expires_in: expires_in, race_condition_ttl: race_condition_ttl,
181
+ coder: coder
182
+ end
183
+
184
+ def redis
185
+ @redis ||= begin
186
+ pool_options = self.class.send(:retrieve_pool_options, redis_options)
187
+
188
+ if pool_options.any?
189
+ self.class.send(:ensure_connection_pool_added!)
190
+ ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
191
+ else
192
+ self.class.build_redis(**redis_options)
193
+ end
194
+ end
195
+ end
196
+
197
+ def inspect
198
+ instance = @redis || @redis_options
199
+ "#<#{self.class} options=#{options.inspect} redis=#{instance.inspect}>"
200
+ end
201
+
202
+ # Cache Store API implementation.
203
+ #
204
+ # Read multiple values at once. Returns a hash of requested keys ->
205
+ # fetched values.
206
+ def read_multi(*names)
207
+ if mget_capable?
208
+ instrument(:read_multi, names, options) do |payload|
209
+ read_multi_mget(*names).tap do |results|
210
+ payload[:hits] = results.keys
211
+ end
212
+ end
213
+ else
214
+ super
215
+ end
216
+ end
217
+
218
+ # Cache Store API implementation.
219
+ #
220
+ # Supports Redis KEYS glob patterns:
221
+ #
222
+ # h?llo matches hello, hallo and hxllo
223
+ # h*llo matches hllo and heeeello
224
+ # h[ae]llo matches hello and hallo, but not hillo
225
+ # h[^e]llo matches hallo, hbllo, ... but not hello
226
+ # h[a-b]llo matches hallo and hbllo
227
+ #
228
+ # Use \ to escape special characters if you want to match them verbatim.
229
+ #
230
+ # See https://redis.io/commands/KEYS for more.
231
+ #
232
+ # Failsafe: Raises errors.
233
+ def delete_matched(matcher, options = nil)
234
+ instrument :delete_matched, matcher do
235
+ unless String === matcher
236
+ raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
237
+ end
238
+ redis.with do |c|
239
+ pattern = namespace_key(matcher, options)
240
+ cursor = "0"
241
+ # Fetch keys in batches using SCAN to avoid blocking the Redis server.
242
+ nodes = c.respond_to?(:nodes) ? c.nodes : [c]
243
+
244
+ nodes.each do |node|
245
+ begin
246
+ cursor, keys = node.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE)
247
+ node.del(*keys) unless keys.empty?
248
+ end until cursor == "0"
249
+ end
250
+ end
251
+ end
252
+ end
253
+
254
+ # Cache Store API implementation.
255
+ #
256
+ # Increment a cached value. This method uses the Redis incr atomic
257
+ # operator and can only be used on values written with the :raw option.
258
+ # Calling it on a value not stored with :raw will initialize that value
259
+ # to zero.
260
+ #
261
+ # Failsafe: Raises errors.
262
+ def increment(name, amount = 1, options = nil)
263
+ instrument :increment, name, amount: amount do
264
+ failsafe :increment do
265
+ options = merged_options(options)
266
+ key = normalize_key(name, options)
267
+
268
+ redis.with do |c|
269
+ c.incrby(key, amount).tap do
270
+ write_key_expiry(c, key, options)
271
+ end
272
+ end
273
+ end
274
+ end
275
+ end
276
+
277
+ # Cache Store API implementation.
278
+ #
279
+ # Decrement a cached value. This method uses the Redis decr atomic
280
+ # operator and can only be used on values written with the :raw option.
281
+ # Calling it on a value not stored with :raw will initialize that value
282
+ # to zero.
283
+ #
284
+ # Failsafe: Raises errors.
285
+ def decrement(name, amount = 1, options = nil)
286
+ instrument :decrement, name, amount: amount do
287
+ failsafe :decrement do
288
+ options = merged_options(options)
289
+ key = normalize_key(name, options)
290
+
291
+ redis.with do |c|
292
+ c.decrby(key, amount).tap do
293
+ write_key_expiry(c, key, options)
294
+ end
295
+ end
296
+ end
297
+ end
298
+ end
299
+
300
+ # Cache Store API implementation.
301
+ #
302
+ # Removes expired entries. Handled natively by Redis least-recently-/
303
+ # least-frequently-used expiry, so manual cleanup is not supported.
304
+ def cleanup(options = nil)
305
+ super
306
+ end
307
+
308
+ # Clear the entire cache on all Redis servers. Safe to use on
309
+ # shared servers if the cache is namespaced.
310
+ #
311
+ # Failsafe: Raises errors.
312
+ def clear(options = nil)
313
+ failsafe :clear do
314
+ if namespace = merged_options(options)[:namespace]
315
+ delete_matched "*", namespace: namespace
316
+ else
317
+ redis.with { |c| c.flushdb }
318
+ end
319
+ end
320
+ end
321
+
322
+ def mget_capable? #:nodoc:
323
+ set_redis_capabilities unless defined? @mget_capable
324
+ @mget_capable
325
+ end
326
+
327
+ def mset_capable? #:nodoc:
328
+ set_redis_capabilities unless defined? @mset_capable
329
+ @mset_capable
330
+ end
331
+
332
+ private
333
+ def set_redis_capabilities
334
+ case redis
335
+ when Redis::Distributed
336
+ @mget_capable = true
337
+ @mset_capable = false
338
+ else
339
+ @mget_capable = true
340
+ @mset_capable = true
341
+ end
342
+ end
343
+
344
+ # Store provider interface:
345
+ # Read an entry from the cache.
346
+ def read_entry(key, **options)
347
+ failsafe :read_entry do
348
+ raw = options&.fetch(:raw, false)
349
+ deserialize_entry(redis.with { |c| c.get(key) }, raw: raw)
350
+ end
351
+ end
352
+
353
+ def read_multi_entries(names, **options)
354
+ if mget_capable?
355
+ read_multi_mget(*names, **options)
356
+ else
357
+ super
358
+ end
359
+ end
360
+
361
+ def read_multi_mget(*names)
362
+ options = names.extract_options!
363
+ options = merged_options(options)
364
+ return {} if names == []
365
+ raw = options&.fetch(:raw, false)
366
+
367
+ keys = names.map { |name| normalize_key(name, options) }
368
+
369
+ values = failsafe(:read_multi_mget, returning: {}) do
370
+ redis.with { |c| c.mget(*keys) }
371
+ end
372
+
373
+ names.zip(values).each_with_object({}) do |(name, value), results|
374
+ if value
375
+ entry = deserialize_entry(value, raw: raw)
376
+ unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
377
+ results[name] = entry.value
378
+ end
379
+ end
380
+ end
381
+ end
382
+
383
+ # Write an entry to the cache.
384
+ #
385
+ # Requires Redis 2.6.12+ for extended SET options.
386
+ def write_entry(key, entry, unless_exist: false, raw: false, expires_in: nil, race_condition_ttl: nil, **options)
387
+ serialized_entry = serialize_entry(entry, raw: raw)
388
+
389
+ # If race condition TTL is in use, ensure that cache entries
390
+ # stick around a bit longer after they would have expired
391
+ # so we can purposefully serve stale entries.
392
+ if race_condition_ttl && expires_in && expires_in > 0 && !raw
393
+ expires_in += 5.minutes
394
+ end
395
+
396
+ failsafe :write_entry, returning: false do
397
+ if unless_exist || expires_in
398
+ modifiers = {}
399
+ modifiers[:nx] = unless_exist
400
+ modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
401
+
402
+ redis.with { |c| c.set key, serialized_entry, **modifiers }
403
+ else
404
+ redis.with { |c| c.set key, serialized_entry }
405
+ end
406
+ end
407
+ end
408
+
409
+ def write_key_expiry(client, key, options)
410
+ if options[:expires_in] && client.ttl(key).negative?
411
+ client.expire key, options[:expires_in].to_i
412
+ end
413
+ end
414
+
415
+ # Delete an entry from the cache.
416
+ def delete_entry(key, options)
417
+ failsafe :delete_entry, returning: false do
418
+ redis.with { |c| c.del key }
419
+ end
420
+ end
421
+
422
+ # Deletes multiple entries in the cache. Returns the number of entries deleted.
423
+ def delete_multi_entries(entries, **_options)
424
+ redis.with { |c| c.del(entries) }
425
+ end
426
+
427
+ # Nonstandard store provider API to write multiple values at once.
428
+ def write_multi_entries(entries, expires_in: nil, **options)
429
+ if entries.any?
430
+ if mset_capable? && expires_in.nil?
431
+ failsafe :write_multi_entries do
432
+ redis.with { |c| c.mapped_mset(serialize_entries(entries, raw: options[:raw])) }
433
+ end
434
+ else
435
+ super
436
+ end
437
+ end
438
+ end
439
+
440
+ # Truncate keys that exceed 1kB.
441
+ def normalize_key(key, options)
442
+ truncate_key super&.b
443
+ end
444
+
445
+ def truncate_key(key)
446
+ if key && key.bytesize > max_key_bytesize
447
+ suffix = ":sha2:#{::Digest::SHA2.hexdigest(key)}"
448
+ truncate_at = max_key_bytesize - suffix.bytesize
449
+ "#{key.byteslice(0, truncate_at)}#{suffix}"
450
+ else
451
+ key
452
+ end
453
+ end
454
+
455
+ def deserialize_entry(payload, raw:)
456
+ if payload && raw
457
+ Entry.new(payload, compress: false)
458
+ else
459
+ super(payload)
460
+ end
461
+ end
462
+
463
+ def serialize_entry(entry, raw: false)
464
+ if raw
465
+ entry.value.to_s
466
+ else
467
+ super(entry)
468
+ end
469
+ end
470
+
471
+ def serialize_entries(entries, raw: false)
472
+ entries.transform_values do |entry|
473
+ serialize_entry entry, raw: raw
474
+ end
475
+ end
476
+
477
+ def failsafe(method, returning: nil)
478
+ yield
479
+ rescue ::Redis::BaseError => e
480
+ handle_exception exception: e, method: method, returning: returning
481
+ returning
482
+ end
483
+
484
+ def handle_exception(exception:, method:, returning:)
485
+ if @error_handler
486
+ @error_handler.(method: method, exception: exception, returning: returning)
487
+ end
488
+ rescue => failsafe
489
+ warn "RedisCacheStore ignored exception in handle_exception: #{failsafe.class}: #{failsafe.message}\n #{failsafe.backtrace.join("\n ")}"
490
+ end
491
+ end
492
+ end
493
+ end