activesupport 5.1.7 → 5.2.4.3

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