activesupport 4.2.8 → 5.2.6

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 (256) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +424 -373
  3. data/MIT-LICENSE +2 -2
  4. data/README.rdoc +4 -5
  5. data/lib/active_support/all.rb +5 -3
  6. data/lib/active_support/array_inquirer.rb +48 -0
  7. data/lib/active_support/backtrace_cleaner.rb +7 -5
  8. data/lib/active_support/benchmarkable.rb +6 -4
  9. data/lib/active_support/builder.rb +3 -1
  10. data/lib/active_support/cache/file_store.rb +41 -35
  11. data/lib/active_support/cache/mem_cache_store.rb +91 -91
  12. data/lib/active_support/cache/memory_store.rb +27 -30
  13. data/lib/active_support/cache/null_store.rb +7 -8
  14. data/lib/active_support/cache/redis_cache_store.rb +466 -0
  15. data/lib/active_support/cache/strategy/local_cache.rb +67 -34
  16. data/lib/active_support/cache/strategy/local_cache_middleware.rb +10 -9
  17. data/lib/active_support/cache.rb +287 -196
  18. data/lib/active_support/callbacks.rb +640 -590
  19. data/lib/active_support/concern.rb +11 -5
  20. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +17 -0
  21. data/lib/active_support/concurrency/share_lock.rb +227 -0
  22. data/lib/active_support/configurable.rb +8 -5
  23. data/lib/active_support/core_ext/array/access.rb +29 -1
  24. data/lib/active_support/core_ext/array/conversions.rb +22 -18
  25. data/lib/active_support/core_ext/array/extract_options.rb +2 -0
  26. data/lib/active_support/core_ext/array/grouping.rb +11 -18
  27. data/lib/active_support/core_ext/array/inquiry.rb +19 -0
  28. data/lib/active_support/core_ext/array/prepend_and_append.rb +5 -3
  29. data/lib/active_support/core_ext/array/wrap.rb +7 -4
  30. data/lib/active_support/core_ext/array.rb +9 -6
  31. data/lib/active_support/core_ext/benchmark.rb +3 -1
  32. data/lib/active_support/core_ext/big_decimal/conversions.rb +10 -12
  33. data/lib/active_support/core_ext/big_decimal.rb +3 -1
  34. data/lib/active_support/core_ext/class/attribute.rb +41 -22
  35. data/lib/active_support/core_ext/class/attribute_accessors.rb +3 -1
  36. data/lib/active_support/core_ext/class/subclasses.rb +20 -6
  37. data/lib/active_support/core_ext/class.rb +4 -3
  38. data/lib/active_support/core_ext/date/acts_like.rb +3 -1
  39. data/lib/active_support/core_ext/date/blank.rb +14 -0
  40. data/lib/active_support/core_ext/date/calculations.rb +11 -9
  41. data/lib/active_support/core_ext/date/conversions.rb +25 -23
  42. data/lib/active_support/core_ext/date/zones.rb +4 -2
  43. data/lib/active_support/core_ext/date.rb +6 -4
  44. data/lib/active_support/core_ext/date_and_time/calculations.rb +170 -58
  45. data/lib/active_support/core_ext/date_and_time/compatibility.rb +4 -17
  46. data/lib/active_support/core_ext/date_and_time/zones.rb +12 -12
  47. data/lib/active_support/core_ext/date_time/acts_like.rb +4 -2
  48. data/lib/active_support/core_ext/date_time/blank.rb +14 -0
  49. data/lib/active_support/core_ext/date_time/calculations.rb +29 -16
  50. data/lib/active_support/core_ext/date_time/compatibility.rb +14 -1
  51. data/lib/active_support/core_ext/date_time/conversions.rb +16 -12
  52. data/lib/active_support/core_ext/date_time.rb +7 -5
  53. data/lib/active_support/core_ext/digest/uuid.rb +7 -5
  54. data/lib/active_support/core_ext/digest.rb +3 -0
  55. data/lib/active_support/core_ext/enumerable.rb +101 -33
  56. data/lib/active_support/core_ext/file/atomic.rb +38 -31
  57. data/lib/active_support/core_ext/file.rb +3 -1
  58. data/lib/active_support/core_ext/hash/compact.rb +14 -9
  59. data/lib/active_support/core_ext/hash/conversions.rb +62 -41
  60. data/lib/active_support/core_ext/hash/deep_merge.rb +9 -13
  61. data/lib/active_support/core_ext/hash/except.rb +11 -8
  62. data/lib/active_support/core_ext/hash/indifferent_access.rb +4 -3
  63. data/lib/active_support/core_ext/hash/keys.rb +33 -27
  64. data/lib/active_support/core_ext/hash/reverse_merge.rb +5 -2
  65. data/lib/active_support/core_ext/hash/slice.rb +8 -8
  66. data/lib/active_support/core_ext/hash/transform_values.rb +14 -5
  67. data/lib/active_support/core_ext/hash.rb +11 -9
  68. data/lib/active_support/core_ext/integer/inflections.rb +3 -1
  69. data/lib/active_support/core_ext/integer/multiple.rb +2 -0
  70. data/lib/active_support/core_ext/integer/time.rb +11 -18
  71. data/lib/active_support/core_ext/integer.rb +5 -3
  72. data/lib/active_support/core_ext/kernel/agnostics.rb +2 -0
  73. data/lib/active_support/core_ext/kernel/concern.rb +5 -1
  74. data/lib/active_support/core_ext/kernel/reporting.rb +4 -84
  75. data/lib/active_support/core_ext/kernel/singleton_class.rb +2 -0
  76. data/lib/active_support/core_ext/kernel.rb +6 -5
  77. data/lib/active_support/core_ext/load_error.rb +3 -22
  78. data/lib/active_support/core_ext/marshal.rb +8 -8
  79. data/lib/active_support/core_ext/module/aliasing.rb +6 -44
  80. data/lib/active_support/core_ext/module/anonymous.rb +12 -1
  81. data/lib/active_support/core_ext/module/attr_internal.rb +8 -9
  82. data/lib/active_support/core_ext/module/attribute_accessors.rb +43 -40
  83. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +150 -0
  84. data/lib/active_support/core_ext/module/concerning.rb +11 -12
  85. data/lib/active_support/core_ext/module/delegation.rb +99 -29
  86. data/lib/active_support/core_ext/module/deprecation.rb +4 -2
  87. data/lib/active_support/core_ext/module/introspection.rb +9 -9
  88. data/lib/active_support/core_ext/module/reachable.rb +5 -2
  89. data/lib/active_support/core_ext/module/redefine_method.rb +49 -0
  90. data/lib/active_support/core_ext/module/remove_method.rb +8 -3
  91. data/lib/active_support/core_ext/module.rb +14 -11
  92. data/lib/active_support/core_ext/name_error.rb +22 -2
  93. data/lib/active_support/core_ext/numeric/bytes.rb +22 -0
  94. data/lib/active_support/core_ext/numeric/conversions.rb +78 -81
  95. data/lib/active_support/core_ext/numeric/inquiry.rb +28 -0
  96. data/lib/active_support/core_ext/numeric/time.rb +35 -23
  97. data/lib/active_support/core_ext/numeric.rb +6 -3
  98. data/lib/active_support/core_ext/object/acts_like.rb +12 -1
  99. data/lib/active_support/core_ext/object/blank.rb +27 -2
  100. data/lib/active_support/core_ext/object/conversions.rb +6 -4
  101. data/lib/active_support/core_ext/object/deep_dup.rb +13 -4
  102. data/lib/active_support/core_ext/object/duplicable.rb +41 -14
  103. data/lib/active_support/core_ext/object/inclusion.rb +5 -3
  104. data/lib/active_support/core_ext/object/instance_variables.rb +3 -1
  105. data/lib/active_support/core_ext/object/json.rb +49 -19
  106. data/lib/active_support/core_ext/object/to_param.rb +3 -1
  107. data/lib/active_support/core_ext/object/to_query.rb +10 -5
  108. data/lib/active_support/core_ext/object/try.rb +69 -21
  109. data/lib/active_support/core_ext/object/with_options.rb +16 -3
  110. data/lib/active_support/core_ext/object.rb +14 -13
  111. data/lib/active_support/core_ext/range/compare_range.rb +61 -0
  112. data/lib/active_support/core_ext/range/conversions.rb +27 -7
  113. data/lib/active_support/core_ext/range/each.rb +19 -17
  114. data/lib/active_support/core_ext/range/include_range.rb +2 -22
  115. data/lib/active_support/core_ext/range/include_time_with_zone.rb +23 -0
  116. data/lib/active_support/core_ext/range/overlaps.rb +2 -0
  117. data/lib/active_support/core_ext/range.rb +7 -4
  118. data/lib/active_support/core_ext/regexp.rb +6 -0
  119. data/lib/active_support/core_ext/securerandom.rb +25 -0
  120. data/lib/active_support/core_ext/string/access.rb +8 -6
  121. data/lib/active_support/core_ext/string/behavior.rb +3 -1
  122. data/lib/active_support/core_ext/string/conversions.rb +7 -4
  123. data/lib/active_support/core_ext/string/exclude.rb +2 -0
  124. data/lib/active_support/core_ext/string/filters.rb +6 -5
  125. data/lib/active_support/core_ext/string/indent.rb +6 -4
  126. data/lib/active_support/core_ext/string/inflections.rb +61 -24
  127. data/lib/active_support/core_ext/string/inquiry.rb +3 -1
  128. data/lib/active_support/core_ext/string/multibyte.rb +15 -7
  129. data/lib/active_support/core_ext/string/output_safety.rb +34 -38
  130. data/lib/active_support/core_ext/string/starts_ends_with.rb +2 -0
  131. data/lib/active_support/core_ext/string/strip.rb +4 -5
  132. data/lib/active_support/core_ext/string/zones.rb +4 -2
  133. data/lib/active_support/core_ext/string.rb +15 -13
  134. data/lib/active_support/core_ext/time/acts_like.rb +3 -1
  135. data/lib/active_support/core_ext/time/calculations.rb +88 -52
  136. data/lib/active_support/core_ext/time/compatibility.rb +12 -1
  137. data/lib/active_support/core_ext/time/conversions.rb +20 -13
  138. data/lib/active_support/core_ext/time/zones.rb +41 -7
  139. data/lib/active_support/core_ext/time.rb +7 -6
  140. data/lib/active_support/core_ext/uri.rb +6 -8
  141. data/lib/active_support/core_ext.rb +3 -1
  142. data/lib/active_support/current_attributes.rb +195 -0
  143. data/lib/active_support/dependencies/autoload.rb +2 -0
  144. data/lib/active_support/dependencies/interlock.rb +57 -0
  145. data/lib/active_support/dependencies.rb +152 -161
  146. data/lib/active_support/deprecation/behaviors.rb +44 -11
  147. data/lib/active_support/deprecation/constant_accessor.rb +52 -0
  148. data/lib/active_support/deprecation/instance_delegator.rb +17 -2
  149. data/lib/active_support/deprecation/method_wrappers.rb +66 -20
  150. data/lib/active_support/deprecation/proxy_wrappers.rb +56 -28
  151. data/lib/active_support/deprecation/reporting.rb +32 -12
  152. data/lib/active_support/deprecation.rb +12 -9
  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/iso8601_parser.rb +125 -0
  156. data/lib/active_support/duration/iso8601_serializer.rb +55 -0
  157. data/lib/active_support/duration.rb +314 -38
  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 +205 -0
  161. data/lib/active_support/execution_wrapper.rb +128 -0
  162. data/lib/active_support/executor.rb +8 -0
  163. data/lib/active_support/file_update_checker.rb +63 -37
  164. data/lib/active_support/gem_version.rb +5 -3
  165. data/lib/active_support/gzip.rb +7 -5
  166. data/lib/active_support/hash_with_indifferent_access.rb +127 -28
  167. data/lib/active_support/i18n.rb +8 -6
  168. data/lib/active_support/i18n_railtie.rb +37 -13
  169. data/lib/active_support/inflections.rb +13 -11
  170. data/lib/active_support/inflector/inflections.rb +61 -12
  171. data/lib/active_support/inflector/methods.rb +163 -136
  172. data/lib/active_support/inflector/transliterate.rb +48 -27
  173. data/lib/active_support/inflector.rb +7 -5
  174. data/lib/active_support/json/decoding.rb +16 -13
  175. data/lib/active_support/json/encoding.rb +11 -58
  176. data/lib/active_support/json.rb +4 -2
  177. data/lib/active_support/key_generator.rb +25 -25
  178. data/lib/active_support/lazy_load_hooks.rb +50 -20
  179. data/lib/active_support/locale/en.yml +2 -0
  180. data/lib/active_support/log_subscriber/test_helper.rb +14 -12
  181. data/lib/active_support/log_subscriber.rb +13 -10
  182. data/lib/active_support/logger.rb +19 -7
  183. data/lib/active_support/logger_silence.rb +6 -4
  184. data/lib/active_support/logger_thread_safe_level.rb +7 -5
  185. data/lib/active_support/message_encryptor.rb +168 -53
  186. data/lib/active_support/message_verifier.rb +150 -17
  187. data/lib/active_support/messages/metadata.rb +71 -0
  188. data/lib/active_support/messages/rotation_configuration.rb +22 -0
  189. data/lib/active_support/messages/rotator.rb +56 -0
  190. data/lib/active_support/multibyte/chars.rb +36 -23
  191. data/lib/active_support/multibyte/unicode.rb +100 -96
  192. data/lib/active_support/multibyte.rb +4 -2
  193. data/lib/active_support/notifications/fanout.rb +11 -9
  194. data/lib/active_support/notifications/instrumenter.rb +27 -7
  195. data/lib/active_support/notifications.rb +11 -7
  196. data/lib/active_support/number_helper/number_converter.rb +13 -11
  197. data/lib/active_support/number_helper/number_to_currency_converter.rb +9 -9
  198. data/lib/active_support/number_helper/number_to_delimited_converter.rb +9 -3
  199. data/lib/active_support/number_helper/number_to_human_converter.rb +11 -9
  200. data/lib/active_support/number_helper/number_to_human_size_converter.rb +9 -8
  201. data/lib/active_support/number_helper/number_to_percentage_converter.rb +3 -1
  202. data/lib/active_support/number_helper/number_to_phone_converter.rb +13 -4
  203. data/lib/active_support/number_helper/number_to_rounded_converter.rb +23 -56
  204. data/lib/active_support/number_helper/rounding_helper.rb +66 -0
  205. data/lib/active_support/number_helper.rb +94 -68
  206. data/lib/active_support/option_merger.rb +3 -1
  207. data/lib/active_support/ordered_hash.rb +6 -4
  208. data/lib/active_support/ordered_options.rb +23 -5
  209. data/lib/active_support/per_thread_registry.rb +9 -4
  210. data/lib/active_support/proxy_object.rb +2 -0
  211. data/lib/active_support/rails.rb +16 -8
  212. data/lib/active_support/railtie.rb +43 -9
  213. data/lib/active_support/reloader.rb +131 -0
  214. data/lib/active_support/rescuable.rb +108 -53
  215. data/lib/active_support/security_utils.rb +15 -11
  216. data/lib/active_support/string_inquirer.rb +11 -3
  217. data/lib/active_support/subscriber.rb +21 -16
  218. data/lib/active_support/tagged_logging.rb +14 -11
  219. data/lib/active_support/test_case.rb +19 -47
  220. data/lib/active_support/testing/assertions.rb +137 -20
  221. data/lib/active_support/testing/autorun.rb +4 -2
  222. data/lib/active_support/testing/constant_lookup.rb +2 -1
  223. data/lib/active_support/testing/declarative.rb +3 -1
  224. data/lib/active_support/testing/deprecation.rb +14 -10
  225. data/lib/active_support/testing/file_fixtures.rb +36 -0
  226. data/lib/active_support/testing/isolation.rb +34 -25
  227. data/lib/active_support/testing/method_call_assertions.rb +43 -0
  228. data/lib/active_support/testing/setup_and_teardown.rb +13 -8
  229. data/lib/active_support/testing/stream.rb +44 -0
  230. data/lib/active_support/testing/tagged_logging.rb +3 -1
  231. data/lib/active_support/testing/time_helpers.rb +91 -23
  232. data/lib/active_support/time.rb +14 -12
  233. data/lib/active_support/time_with_zone.rb +182 -40
  234. data/lib/active_support/values/time_zone.rb +196 -61
  235. data/lib/active_support/values/unicode_tables.dat +0 -0
  236. data/lib/active_support/version.rb +3 -1
  237. data/lib/active_support/xml_mini/jdom.rb +116 -114
  238. data/lib/active_support/xml_mini/libxml.rb +17 -16
  239. data/lib/active_support/xml_mini/libxmlsax.rb +16 -18
  240. data/lib/active_support/xml_mini/nokogiri.rb +15 -15
  241. data/lib/active_support/xml_mini/nokogirisax.rb +15 -16
  242. data/lib/active_support/xml_mini/rexml.rb +11 -9
  243. data/lib/active_support/xml_mini.rb +37 -37
  244. data/lib/active_support.rb +12 -11
  245. metadata +54 -25
  246. data/lib/active_support/concurrency/latch.rb +0 -27
  247. data/lib/active_support/core_ext/big_decimal/yaml_conversions.rb +0 -16
  248. data/lib/active_support/core_ext/class/delegating_attributes.rb +0 -45
  249. data/lib/active_support/core_ext/date_time/zones.rb +0 -6
  250. data/lib/active_support/core_ext/kernel/debugger.rb +0 -10
  251. data/lib/active_support/core_ext/module/method_transplanting.rb +0 -13
  252. data/lib/active_support/core_ext/module/qualified_const.rb +0 -52
  253. data/lib/active_support/core_ext/object/itself.rb +0 -15
  254. data/lib/active_support/core_ext/struct.rb +0 -6
  255. data/lib/active_support/core_ext/thread.rb +0 -86
  256. data/lib/active_support/core_ext/time/marshal.rb +0 -30
@@ -1,10 +1,12 @@
1
- require 'monitor'
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
2
4
 
3
5
  module ActiveSupport
4
6
  module Cache
5
7
  # A cache store implementation which stores everything into memory in the
6
8
  # same process. If you're running multiple Ruby on Rails server processes
7
- # (which is the case if you're using mongrel_cluster or Phusion Passenger),
9
+ # (which is the case if you're using Phusion Passenger or puma clustered mode),
8
10
  # then this means that Rails server process instances won't be able
9
11
  # to share cache data with each other and this may not be the most
10
12
  # appropriate cache in that scenario.
@@ -28,6 +30,7 @@ module ActiveSupport
28
30
  @pruning = false
29
31
  end
30
32
 
33
+ # Delete all data stored in a given cache store.
31
34
  def clear(options = nil)
32
35
  synchronize do
33
36
  @data.clear
@@ -39,8 +42,8 @@ module ActiveSupport
39
42
  # Preemptively iterates through all stored keys and removes the ones which have expired.
40
43
  def cleanup(options = nil)
41
44
  options = merged_options(options)
42
- instrument(:cleanup, :size => @data.size) do
43
- keys = synchronize{ @data.keys }
45
+ instrument(:cleanup, size: @data.size) do
46
+ keys = synchronize { @data.keys }
44
47
  keys.each do |key|
45
48
  entry = @data[key]
46
49
  delete_entry(key, options) if entry && entry.expired?
@@ -56,8 +59,8 @@ module ActiveSupport
56
59
  begin
57
60
  start_time = Time.now
58
61
  cleanup
59
- instrument(:prune, target_size, :from => @cache_size) do
60
- keys = synchronize{ @key_access.keys.sort{|a,b| @key_access[a].to_f <=> @key_access[b].to_f} }
62
+ instrument(:prune, target_size, from: @cache_size) do
63
+ keys = synchronize { @key_access.keys.sort { |a, b| @key_access[a].to_f <=> @key_access[b].to_f } }
61
64
  keys.each do |key|
62
65
  delete_entry(key, options)
63
66
  return if @cache_size <= target_size || (max_time && Time.now - start_time > max_time)
@@ -75,32 +78,15 @@ module ActiveSupport
75
78
 
76
79
  # Increment an integer value in the cache.
77
80
  def increment(name, amount = 1, options = nil)
78
- synchronize do
79
- options = merged_options(options)
80
- if num = read(name, options)
81
- num = num.to_i + amount
82
- write(name, num, options)
83
- num
84
- else
85
- nil
86
- end
87
- end
81
+ modify_value(name, amount, options)
88
82
  end
89
83
 
90
84
  # Decrement an integer value in the cache.
91
85
  def decrement(name, amount = 1, options = nil)
92
- synchronize do
93
- options = merged_options(options)
94
- if num = read(name, options)
95
- num = num.to_i - amount
96
- write(name, num, options)
97
- num
98
- else
99
- nil
100
- end
101
- end
86
+ modify_value(name, -amount, options)
102
87
  end
103
88
 
89
+ # Deletes cache entries if the cache key matches a given pattern.
104
90
  def delete_matched(matcher, options = nil)
105
91
  options = merged_options(options)
106
92
  instrument(:delete_matched, matcher.inspect) do
@@ -122,7 +108,7 @@ module ActiveSupport
122
108
  @monitor.synchronize(&block)
123
109
  end
124
110
 
125
- protected
111
+ private
126
112
 
127
113
  PER_ENTRY_OVERHEAD = 240
128
114
 
@@ -130,7 +116,7 @@ module ActiveSupport
130
116
  key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD
131
117
  end
132
118
 
133
- def read_entry(key, options) # :nodoc:
119
+ def read_entry(key, options)
134
120
  entry = @data[key]
135
121
  synchronize do
136
122
  if entry
@@ -142,7 +128,7 @@ module ActiveSupport
142
128
  entry
143
129
  end
144
130
 
145
- def write_entry(key, entry, options) # :nodoc:
131
+ def write_entry(key, entry, options)
146
132
  entry.dup_value!
147
133
  synchronize do
148
134
  old_entry = @data[key]
@@ -159,7 +145,7 @@ module ActiveSupport
159
145
  end
160
146
  end
161
147
 
162
- def delete_entry(key, options) # :nodoc:
148
+ def delete_entry(key, options)
163
149
  synchronize do
164
150
  @key_access.delete(key)
165
151
  entry = @data.delete(key)
@@ -167,6 +153,17 @@ module ActiveSupport
167
153
  !!entry
168
154
  end
169
155
  end
156
+
157
+ def modify_value(name, amount, options)
158
+ synchronize do
159
+ options = merged_options(options)
160
+ if num = read(name, options)
161
+ num = num.to_i + amount
162
+ write(name, num, options)
163
+ num
164
+ end
165
+ end
166
+ end
170
167
  end
171
168
  end
172
169
  end
@@ -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
@@ -8,10 +10,7 @@ module ActiveSupport
8
10
  # be cached inside blocks that utilize this strategy. See
9
11
  # ActiveSupport::Cache::Strategy::LocalCache for more details.
10
12
  class NullStore < Store
11
- def initialize(options = nil)
12
- super(options)
13
- extend Strategy::LocalCache
14
- end
13
+ prepend Strategy::LocalCache
15
14
 
16
15
  def clear(options = nil)
17
16
  end
@@ -28,15 +27,15 @@ module ActiveSupport
28
27
  def delete_matched(matcher, options = nil)
29
28
  end
30
29
 
31
- protected
32
- def read_entry(key, options) # :nodoc:
30
+ private
31
+ def read_entry(key, options)
33
32
  end
34
33
 
35
- def write_entry(key, entry, options) # :nodoc:
34
+ def write_entry(key, entry, options)
36
35
  true
37
36
  end
38
37
 
39
- def delete_entry(key, options) # :nodoc:
38
+ def delete_entry(key, options)
40
39
  false
41
40
  end
42
41
  end
@@ -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 && 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 && 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