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