activesupport 4.2.0 → 5.2.0

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 (254) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +366 -232
  3. data/MIT-LICENSE +2 -2
  4. data/README.rdoc +4 -5
  5. data/lib/active_support.rb +17 -7
  6. data/lib/active_support/all.rb +5 -3
  7. data/lib/active_support/array_inquirer.rb +48 -0
  8. data/lib/active_support/backtrace_cleaner.rb +7 -5
  9. data/lib/active_support/benchmarkable.rb +6 -4
  10. data/lib/active_support/builder.rb +3 -1
  11. data/lib/active_support/cache.rb +271 -177
  12. data/lib/active_support/cache/file_store.rb +41 -35
  13. data/lib/active_support/cache/mem_cache_store.rb +97 -88
  14. data/lib/active_support/cache/memory_store.rb +27 -30
  15. data/lib/active_support/cache/null_store.rb +7 -8
  16. data/lib/active_support/cache/redis_cache_store.rb +454 -0
  17. data/lib/active_support/cache/strategy/local_cache.rb +67 -34
  18. data/lib/active_support/cache/strategy/local_cache_middleware.rb +10 -9
  19. data/lib/active_support/callbacks.rb +654 -560
  20. data/lib/active_support/concern.rb +5 -3
  21. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +17 -0
  22. data/lib/active_support/concurrency/share_lock.rb +227 -0
  23. data/lib/active_support/configurable.rb +8 -5
  24. data/lib/active_support/core_ext.rb +3 -1
  25. data/lib/active_support/core_ext/array.rb +9 -6
  26. data/lib/active_support/core_ext/array/access.rb +29 -1
  27. data/lib/active_support/core_ext/array/conversions.rb +22 -18
  28. data/lib/active_support/core_ext/array/extract_options.rb +2 -0
  29. data/lib/active_support/core_ext/array/grouping.rb +11 -18
  30. data/lib/active_support/core_ext/array/inquiry.rb +19 -0
  31. data/lib/active_support/core_ext/array/prepend_and_append.rb +5 -3
  32. data/lib/active_support/core_ext/array/wrap.rb +7 -4
  33. data/lib/active_support/core_ext/benchmark.rb +3 -1
  34. data/lib/active_support/core_ext/big_decimal.rb +3 -1
  35. data/lib/active_support/core_ext/big_decimal/conversions.rb +10 -12
  36. data/lib/active_support/core_ext/class.rb +4 -3
  37. data/lib/active_support/core_ext/class/attribute.rb +41 -22
  38. data/lib/active_support/core_ext/class/attribute_accessors.rb +3 -1
  39. data/lib/active_support/core_ext/class/subclasses.rb +20 -8
  40. data/lib/active_support/core_ext/date.rb +6 -4
  41. data/lib/active_support/core_ext/date/acts_like.rb +3 -1
  42. data/lib/active_support/core_ext/date/blank.rb +14 -0
  43. data/lib/active_support/core_ext/date/calculations.rb +11 -9
  44. data/lib/active_support/core_ext/date/conversions.rb +31 -23
  45. data/lib/active_support/core_ext/date/zones.rb +4 -2
  46. data/lib/active_support/core_ext/date_and_time/calculations.rb +179 -56
  47. data/lib/active_support/core_ext/date_and_time/compatibility.rb +16 -0
  48. data/lib/active_support/core_ext/date_and_time/zones.rb +12 -12
  49. data/lib/active_support/core_ext/date_time.rb +7 -4
  50. data/lib/active_support/core_ext/date_time/acts_like.rb +4 -2
  51. data/lib/active_support/core_ext/date_time/blank.rb +14 -0
  52. data/lib/active_support/core_ext/date_time/calculations.rb +58 -20
  53. data/lib/active_support/core_ext/date_time/compatibility.rb +18 -0
  54. data/lib/active_support/core_ext/date_time/conversions.rb +16 -12
  55. data/lib/active_support/core_ext/digest/uuid.rb +7 -5
  56. data/lib/active_support/core_ext/enumerable.rb +107 -28
  57. data/lib/active_support/core_ext/file.rb +3 -1
  58. data/lib/active_support/core_ext/file/atomic.rb +38 -31
  59. data/lib/active_support/core_ext/hash.rb +11 -9
  60. data/lib/active_support/core_ext/hash/compact.rb +24 -15
  61. data/lib/active_support/core_ext/hash/conversions.rb +63 -43
  62. data/lib/active_support/core_ext/hash/deep_merge.rb +9 -13
  63. data/lib/active_support/core_ext/hash/except.rb +11 -8
  64. data/lib/active_support/core_ext/hash/indifferent_access.rb +4 -3
  65. data/lib/active_support/core_ext/hash/keys.rb +33 -27
  66. data/lib/active_support/core_ext/hash/reverse_merge.rb +5 -2
  67. data/lib/active_support/core_ext/hash/slice.rb +8 -8
  68. data/lib/active_support/core_ext/hash/transform_values.rb +16 -7
  69. data/lib/active_support/core_ext/integer.rb +5 -3
  70. data/lib/active_support/core_ext/integer/inflections.rb +3 -1
  71. data/lib/active_support/core_ext/integer/multiple.rb +2 -0
  72. data/lib/active_support/core_ext/integer/time.rb +11 -33
  73. data/lib/active_support/core_ext/kernel.rb +6 -5
  74. data/lib/active_support/core_ext/kernel/agnostics.rb +2 -0
  75. data/lib/active_support/core_ext/kernel/concern.rb +5 -1
  76. data/lib/active_support/core_ext/kernel/reporting.rb +4 -83
  77. data/lib/active_support/core_ext/kernel/singleton_class.rb +2 -0
  78. data/lib/active_support/core_ext/load_error.rb +3 -22
  79. data/lib/active_support/core_ext/marshal.rb +13 -10
  80. data/lib/active_support/core_ext/module.rb +14 -11
  81. data/lib/active_support/core_ext/module/aliasing.rb +6 -44
  82. data/lib/active_support/core_ext/module/anonymous.rb +12 -1
  83. data/lib/active_support/core_ext/module/attr_internal.rb +8 -9
  84. data/lib/active_support/core_ext/module/attribute_accessors.rb +43 -40
  85. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +150 -0
  86. data/lib/active_support/core_ext/module/concerning.rb +11 -12
  87. data/lib/active_support/core_ext/module/delegation.rb +121 -39
  88. data/lib/active_support/core_ext/module/deprecation.rb +4 -2
  89. data/lib/active_support/core_ext/module/introspection.rb +9 -9
  90. data/lib/active_support/core_ext/module/reachable.rb +5 -2
  91. data/lib/active_support/core_ext/module/redefine_method.rb +49 -0
  92. data/lib/active_support/core_ext/module/remove_method.rb +8 -3
  93. data/lib/active_support/core_ext/name_error.rb +22 -2
  94. data/lib/active_support/core_ext/numeric.rb +6 -3
  95. data/lib/active_support/core_ext/numeric/bytes.rb +22 -0
  96. data/lib/active_support/core_ext/numeric/conversions.rb +79 -74
  97. data/lib/active_support/core_ext/numeric/inquiry.rb +28 -0
  98. data/lib/active_support/core_ext/numeric/time.rb +35 -38
  99. data/lib/active_support/core_ext/object.rb +14 -13
  100. data/lib/active_support/core_ext/object/acts_like.rb +12 -1
  101. data/lib/active_support/core_ext/object/blank.rb +29 -4
  102. data/lib/active_support/core_ext/object/conversions.rb +6 -4
  103. data/lib/active_support/core_ext/object/deep_dup.rb +13 -4
  104. data/lib/active_support/core_ext/object/duplicable.rb +98 -45
  105. data/lib/active_support/core_ext/object/inclusion.rb +5 -3
  106. data/lib/active_support/core_ext/object/instance_variables.rb +3 -1
  107. data/lib/active_support/core_ext/object/json.rb +49 -19
  108. data/lib/active_support/core_ext/object/to_param.rb +3 -1
  109. data/lib/active_support/core_ext/object/to_query.rb +6 -4
  110. data/lib/active_support/core_ext/object/try.rb +70 -22
  111. data/lib/active_support/core_ext/object/with_options.rb +16 -3
  112. data/lib/active_support/core_ext/range.rb +7 -4
  113. data/lib/active_support/core_ext/range/conversions.rb +27 -7
  114. data/lib/active_support/core_ext/range/each.rb +19 -17
  115. data/lib/active_support/core_ext/range/include_range.rb +21 -19
  116. data/lib/active_support/core_ext/range/include_time_with_zone.rb +23 -0
  117. data/lib/active_support/core_ext/range/overlaps.rb +2 -0
  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.rb +15 -13
  121. data/lib/active_support/core_ext/string/access.rb +9 -7
  122. data/lib/active_support/core_ext/string/behavior.rb +3 -1
  123. data/lib/active_support/core_ext/string/conversions.rb +8 -5
  124. data/lib/active_support/core_ext/string/exclude.rb +2 -0
  125. data/lib/active_support/core_ext/string/filters.rb +10 -8
  126. data/lib/active_support/core_ext/string/indent.rb +6 -4
  127. data/lib/active_support/core_ext/string/inflections.rb +61 -24
  128. data/lib/active_support/core_ext/string/inquiry.rb +3 -1
  129. data/lib/active_support/core_ext/string/multibyte.rb +15 -7
  130. data/lib/active_support/core_ext/string/output_safety.rb +35 -35
  131. data/lib/active_support/core_ext/string/starts_ends_with.rb +2 -0
  132. data/lib/active_support/core_ext/string/strip.rb +4 -5
  133. data/lib/active_support/core_ext/string/zones.rb +4 -2
  134. data/lib/active_support/core_ext/time.rb +7 -5
  135. data/lib/active_support/core_ext/time/acts_like.rb +3 -1
  136. data/lib/active_support/core_ext/time/calculations.rb +101 -51
  137. data/lib/active_support/core_ext/time/compatibility.rb +16 -0
  138. data/lib/active_support/core_ext/time/conversions.rb +20 -13
  139. data/lib/active_support/core_ext/time/zones.rb +41 -7
  140. data/lib/active_support/core_ext/uri.rb +5 -4
  141. data/lib/active_support/current_attributes.rb +195 -0
  142. data/lib/active_support/dependencies.rb +143 -160
  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/deprecation.rb +12 -9
  146. data/lib/active_support/deprecation/behaviors.rb +41 -12
  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 +54 -21
  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/descendants_tracker.rb +2 -0
  153. data/lib/active_support/digest.rb +20 -0
  154. data/lib/active_support/duration.rb +326 -30
  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/encrypted_configuration.rb +49 -0
  158. data/lib/active_support/encrypted_file.rb +99 -0
  159. data/lib/active_support/evented_file_update_checker.rb +205 -0
  160. data/lib/active_support/execution_wrapper.rb +128 -0
  161. data/lib/active_support/executor.rb +8 -0
  162. data/lib/active_support/file_update_checker.rb +63 -37
  163. data/lib/active_support/gem_version.rb +4 -2
  164. data/lib/active_support/gzip.rb +7 -5
  165. data/lib/active_support/hash_with_indifferent_access.rb +130 -30
  166. data/lib/active_support/i18n.rb +8 -6
  167. data/lib/active_support/i18n_railtie.rb +34 -14
  168. data/lib/active_support/inflections.rb +13 -11
  169. data/lib/active_support/inflector.rb +7 -5
  170. data/lib/active_support/inflector/inflections.rb +61 -12
  171. data/lib/active_support/inflector/methods.rb +161 -136
  172. data/lib/active_support/inflector/transliterate.rb +48 -27
  173. data/lib/active_support/json.rb +4 -2
  174. data/lib/active_support/json/decoding.rb +16 -13
  175. data/lib/active_support/json/encoding.rb +15 -57
  176. data/lib/active_support/key_generator.rb +25 -25
  177. data/lib/active_support/lazy_load_hooks.rb +50 -20
  178. data/lib/active_support/locale/en.yml +2 -0
  179. data/lib/active_support/log_subscriber.rb +13 -10
  180. data/lib/active_support/log_subscriber/test_helper.rb +14 -12
  181. data/lib/active_support/logger.rb +54 -3
  182. data/lib/active_support/logger_silence.rb +12 -7
  183. data/lib/active_support/logger_thread_safe_level.rb +33 -0
  184. data/lib/active_support/message_encryptor.rb +173 -51
  185. data/lib/active_support/message_verifier.rb +150 -17
  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 +4 -2
  190. data/lib/active_support/multibyte/chars.rb +37 -24
  191. data/lib/active_support/multibyte/unicode.rb +100 -96
  192. data/lib/active_support/notifications.rb +11 -7
  193. data/lib/active_support/notifications/fanout.rb +10 -8
  194. data/lib/active_support/notifications/instrumenter.rb +27 -7
  195. data/lib/active_support/number_helper.rb +94 -68
  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/option_merger.rb +3 -1
  206. data/lib/active_support/ordered_hash.rb +6 -4
  207. data/lib/active_support/ordered_options.rb +22 -4
  208. data/lib/active_support/per_thread_registry.rb +13 -6
  209. data/lib/active_support/proxy_object.rb +2 -0
  210. data/lib/active_support/rails.rb +16 -8
  211. data/lib/active_support/railtie.rb +43 -9
  212. data/lib/active_support/reloader.rb +131 -0
  213. data/lib/active_support/rescuable.rb +108 -53
  214. data/lib/active_support/security_utils.rb +17 -6
  215. data/lib/active_support/string_inquirer.rb +11 -3
  216. data/lib/active_support/subscriber.rb +15 -14
  217. data/lib/active_support/tagged_logging.rb +14 -11
  218. data/lib/active_support/test_case.rb +18 -46
  219. data/lib/active_support/testing/assertions.rb +137 -20
  220. data/lib/active_support/testing/autorun.rb +4 -2
  221. data/lib/active_support/testing/constant_lookup.rb +2 -1
  222. data/lib/active_support/testing/declarative.rb +3 -1
  223. data/lib/active_support/testing/deprecation.rb +14 -10
  224. data/lib/active_support/testing/file_fixtures.rb +36 -0
  225. data/lib/active_support/testing/isolation.rb +34 -25
  226. data/lib/active_support/testing/method_call_assertions.rb +43 -0
  227. data/lib/active_support/testing/setup_and_teardown.rb +12 -3
  228. data/lib/active_support/testing/stream.rb +44 -0
  229. data/lib/active_support/testing/tagged_logging.rb +3 -1
  230. data/lib/active_support/testing/time_helpers.rb +96 -27
  231. data/lib/active_support/time.rb +14 -12
  232. data/lib/active_support/time_with_zone.rb +195 -53
  233. data/lib/active_support/values/time_zone.rb +200 -61
  234. data/lib/active_support/values/unicode_tables.dat +0 -0
  235. data/lib/active_support/version.rb +3 -1
  236. data/lib/active_support/xml_mini.rb +69 -51
  237. data/lib/active_support/xml_mini/jdom.rb +116 -113
  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 +17 -16
  243. metadata +55 -43
  244. data/lib/active_support/concurrency/latch.rb +0 -27
  245. data/lib/active_support/core_ext/big_decimal/yaml_conversions.rb +0 -14
  246. data/lib/active_support/core_ext/class/delegating_attributes.rb +0 -45
  247. data/lib/active_support/core_ext/date_time/zones.rb +0 -6
  248. data/lib/active_support/core_ext/kernel/debugger.rb +0 -10
  249. data/lib/active_support/core_ext/module/method_transplanting.rb +0 -11
  250. data/lib/active_support/core_ext/module/qualified_const.rb +0 -52
  251. data/lib/active_support/core_ext/object/itself.rb +0 -15
  252. data/lib/active_support/core_ext/struct.rb +0 -6
  253. data/lib/active_support/core_ext/thread.rb +0 -86
  254. data/lib/active_support/core_ext/time/marshal.rb +0 -30
@@ -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,454 @@
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
+ DELETE_GLOB_LUA = "for i, name in ipairs(redis.call('KEYS', ARGV[1])) do redis.call('DEL', name); end"
67
+ private_constant :DELETE_GLOB_LUA
68
+
69
+ # Support raw values in the local cache strategy.
70
+ module LocalCacheWithRaw # :nodoc:
71
+ private
72
+ def read_entry(key, options)
73
+ entry = super
74
+ if options[:raw] && local_cache && entry
75
+ entry = deserialize_entry(entry.value)
76
+ end
77
+ entry
78
+ end
79
+
80
+ def write_entry(key, entry, options)
81
+ if options[:raw] && local_cache
82
+ raw_entry = Entry.new(serialize_entry(entry, raw: true))
83
+ raw_entry.expires_at = entry.expires_at
84
+ super(key, raw_entry, options)
85
+ else
86
+ super
87
+ end
88
+ end
89
+
90
+ def write_multi_entries(entries, options)
91
+ if options[:raw] && local_cache
92
+ raw_entries = entries.map do |key, entry|
93
+ raw_entry = Entry.new(serialize_entry(entry, raw: true))
94
+ raw_entry.expires_at = entry.expires_at
95
+ end.to_h
96
+
97
+ super(raw_entries, options)
98
+ else
99
+ super
100
+ end
101
+ end
102
+ end
103
+
104
+ prepend Strategy::LocalCache
105
+ prepend LocalCacheWithRaw
106
+
107
+ class << self
108
+ # Factory method to create a new Redis instance.
109
+ #
110
+ # Handles four options: :redis block, :redis instance, single :url
111
+ # string, and multiple :url strings.
112
+ #
113
+ # Option Class Result
114
+ # :redis Proc -> options[:redis].call
115
+ # :redis Object -> options[:redis]
116
+ # :url String -> Redis.new(url: …)
117
+ # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
118
+ #
119
+ def build_redis(redis: nil, url: nil, **redis_options) #:nodoc:
120
+ urls = Array(url)
121
+
122
+ if redis.is_a?(Proc)
123
+ redis.call
124
+ elsif redis
125
+ redis
126
+ elsif urls.size > 1
127
+ build_redis_distributed_client urls: urls, **redis_options
128
+ else
129
+ build_redis_client url: urls.first, **redis_options
130
+ end
131
+ end
132
+
133
+ private
134
+ def build_redis_distributed_client(urls:, **redis_options)
135
+ ::Redis::Distributed.new([], DEFAULT_REDIS_OPTIONS.merge(redis_options)).tap do |dist|
136
+ urls.each { |u| dist.add_node url: u }
137
+ end
138
+ end
139
+
140
+ def build_redis_client(url:, **redis_options)
141
+ ::Redis.new DEFAULT_REDIS_OPTIONS.merge(redis_options.merge(url: url))
142
+ end
143
+ end
144
+
145
+ attr_reader :redis_options
146
+ attr_reader :max_key_bytesize
147
+
148
+ # Creates a new Redis cache store.
149
+ #
150
+ # Handles three options: block provided to instantiate, single URL
151
+ # provided, and multiple URLs provided.
152
+ #
153
+ # :redis Proc -> options[:redis].call
154
+ # :url String -> Redis.new(url: …)
155
+ # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
156
+ #
157
+ # No namespace is set by default. Provide one if the Redis cache
158
+ # server is shared with other apps: <tt>namespace: 'myapp-cache'<tt>.
159
+ #
160
+ # Compression is enabled by default with a 1kB threshold, so cached
161
+ # values larger than 1kB are automatically compressed. Disable by
162
+ # passing <tt>compress: false</tt> or change the threshold by passing
163
+ # <tt>compress_threshold: 4.kilobytes</tt>.
164
+ #
165
+ # No expiry is set on cache entries by default. Redis is expected to
166
+ # be configured with an eviction policy that automatically deletes
167
+ # least-recently or -frequently used keys when it reaches max memory.
168
+ # See https://redis.io/topics/lru-cache for cache server setup.
169
+ #
170
+ # Race condition TTL is not set by default. This can be used to avoid
171
+ # "thundering herd" cache writes when hot cache entries are expired.
172
+ # See <tt>ActiveSupport::Cache::Store#fetch</tt> for more.
173
+ def initialize(namespace: nil, compress: true, compress_threshold: 1.kilobyte, expires_in: nil, race_condition_ttl: nil, error_handler: DEFAULT_ERROR_HANDLER, **redis_options)
174
+ @redis_options = redis_options
175
+
176
+ @max_key_bytesize = MAX_KEY_BYTESIZE
177
+ @error_handler = error_handler
178
+
179
+ super namespace: namespace,
180
+ compress: compress, compress_threshold: compress_threshold,
181
+ expires_in: expires_in, race_condition_ttl: race_condition_ttl
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
+ case matcher
236
+ when String
237
+ redis.with { |c| c.eval DELETE_GLOB_LUA, [], [namespace_key(matcher, options)] }
238
+ else
239
+ raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
240
+ end
241
+ end
242
+ end
243
+
244
+ # Cache Store API implementation.
245
+ #
246
+ # Increment a cached value. This method uses the Redis incr atomic
247
+ # operator and can only be used on values written with the :raw option.
248
+ # Calling it on a value not stored with :raw will initialize that value
249
+ # to zero.
250
+ #
251
+ # Failsafe: Raises errors.
252
+ def increment(name, amount = 1, options = nil)
253
+ instrument :increment, name, amount: amount do
254
+ failsafe :increment do
255
+ redis.with { |c| c.incrby normalize_key(name, options), amount }
256
+ end
257
+ end
258
+ end
259
+
260
+ # Cache Store API implementation.
261
+ #
262
+ # Decrement a cached value. This method uses the Redis decr atomic
263
+ # operator and can only be used on values written with the :raw option.
264
+ # Calling it on a value not stored with :raw will initialize that value
265
+ # to zero.
266
+ #
267
+ # Failsafe: Raises errors.
268
+ def decrement(name, amount = 1, options = nil)
269
+ instrument :decrement, name, amount: amount do
270
+ failsafe :decrement do
271
+ redis.with { |c| c.decrby normalize_key(name, options), amount }
272
+ end
273
+ end
274
+ end
275
+
276
+ # Cache Store API implementation.
277
+ #
278
+ # Removes expired entries. Handled natively by Redis least-recently-/
279
+ # least-frequently-used expiry, so manual cleanup is not supported.
280
+ def cleanup(options = nil)
281
+ super
282
+ end
283
+
284
+ # Clear the entire cache on all Redis servers. Safe to use on
285
+ # shared servers if the cache is namespaced.
286
+ #
287
+ # Failsafe: Raises errors.
288
+ def clear(options = nil)
289
+ failsafe :clear do
290
+ if namespace = merged_options(options)[namespace]
291
+ delete_matched "*", namespace: namespace
292
+ else
293
+ redis.with { |c| c.flushdb }
294
+ end
295
+ end
296
+ end
297
+
298
+ def mget_capable? #:nodoc:
299
+ set_redis_capabilities unless defined? @mget_capable
300
+ @mget_capable
301
+ end
302
+
303
+ def mset_capable? #:nodoc:
304
+ set_redis_capabilities unless defined? @mset_capable
305
+ @mset_capable
306
+ end
307
+
308
+ private
309
+ def set_redis_capabilities
310
+ case redis
311
+ when Redis::Distributed
312
+ @mget_capable = true
313
+ @mset_capable = false
314
+ else
315
+ @mget_capable = true
316
+ @mset_capable = true
317
+ end
318
+ end
319
+
320
+ # Store provider interface:
321
+ # Read an entry from the cache.
322
+ def read_entry(key, options = nil)
323
+ failsafe :read_entry do
324
+ deserialize_entry redis.with { |c| c.get(key) }
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
+
340
+ keys = names.map { |name| normalize_key(name, options) }
341
+
342
+ values = failsafe(:read_multi_mget, returning: {}) do
343
+ redis.with { |c| c.mget(*keys) }
344
+ end
345
+
346
+ names.zip(values).each_with_object({}) do |(name, value), results|
347
+ if value
348
+ entry = deserialize_entry(value)
349
+ unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
350
+ results[name] = entry.value
351
+ end
352
+ end
353
+ end
354
+ end
355
+
356
+ # Write an entry to the cache.
357
+ #
358
+ # Requires Redis 2.6.12+ for extended SET options.
359
+ def write_entry(key, entry, unless_exist: false, raw: false, expires_in: nil, race_condition_ttl: nil, **options)
360
+ serialized_entry = serialize_entry(entry, raw: raw)
361
+
362
+ # If race condition TTL is in use, ensure that cache entries
363
+ # stick around a bit longer after they would have expired
364
+ # so we can purposefully serve stale entries.
365
+ if race_condition_ttl && expires_in && expires_in > 0 && !raw
366
+ expires_in += 5.minutes
367
+ end
368
+
369
+ failsafe :write_entry, returning: false do
370
+ if unless_exist || expires_in
371
+ modifiers = {}
372
+ modifiers[:nx] = unless_exist
373
+ modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
374
+
375
+ redis.with { |c| c.set key, serialized_entry, modifiers }
376
+ else
377
+ redis.with { |c| c.set key, serialized_entry }
378
+ end
379
+ end
380
+ end
381
+
382
+ # Delete an entry from the cache.
383
+ def delete_entry(key, options)
384
+ failsafe :delete_entry, returning: false do
385
+ redis.with { |c| c.del key }
386
+ end
387
+ end
388
+
389
+ # Nonstandard store provider API to write multiple values at once.
390
+ def write_multi_entries(entries, expires_in: nil, **options)
391
+ if entries.any?
392
+ if mset_capable? && expires_in.nil?
393
+ failsafe :write_multi_entries do
394
+ redis.with { |c| c.mapped_mset(serialize_entries(entries, raw: options[:raw])) }
395
+ end
396
+ else
397
+ super
398
+ end
399
+ end
400
+ end
401
+
402
+ # Truncate keys that exceed 1kB.
403
+ def normalize_key(key, options)
404
+ truncate_key super.b
405
+ end
406
+
407
+ def truncate_key(key)
408
+ if key.bytesize > max_key_bytesize
409
+ suffix = ":sha2:#{::Digest::SHA2.hexdigest(key)}"
410
+ truncate_at = max_key_bytesize - suffix.bytesize
411
+ "#{key.byteslice(0, truncate_at)}#{suffix}"
412
+ else
413
+ key
414
+ end
415
+ end
416
+
417
+ def deserialize_entry(serialized_entry)
418
+ if serialized_entry
419
+ entry = Marshal.load(serialized_entry) rescue serialized_entry
420
+ entry.is_a?(Entry) ? entry : Entry.new(entry)
421
+ end
422
+ end
423
+
424
+ def serialize_entry(entry, raw: false)
425
+ if raw
426
+ entry.value.to_s
427
+ else
428
+ Marshal.dump(entry)
429
+ end
430
+ end
431
+
432
+ def serialize_entries(entries, raw: false)
433
+ entries.transform_values do |entry|
434
+ serialize_entry entry, raw: raw
435
+ end
436
+ end
437
+
438
+ def failsafe(method, returning: nil)
439
+ yield
440
+ rescue ::Redis::BaseConnectionError => e
441
+ handle_exception exception: e, method: method, returning: returning
442
+ returning
443
+ end
444
+
445
+ def handle_exception(exception:, method:, returning:)
446
+ if @error_handler
447
+ @error_handler.(method: method, exception: exception, returning: returning)
448
+ end
449
+ rescue => failsafe
450
+ warn "RedisCacheStore ignored exception in handle_exception: #{failsafe.class}: #{failsafe.message}\n #{failsafe.backtrace.join("\n ")}"
451
+ end
452
+ end
453
+ end
454
+ end