activesupport 5.1.7 → 7.0.4.1

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

Potentially problematic release.


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

Files changed (279) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +259 -585
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -5
  5. data/lib/active_support/actionable_error.rb +48 -0
  6. data/lib/active_support/all.rb +2 -0
  7. data/lib/active_support/array_inquirer.rb +4 -2
  8. data/lib/active_support/backtrace_cleaner.rb +33 -5
  9. data/lib/active_support/benchmarkable.rb +5 -3
  10. data/lib/active_support/builder.rb +2 -0
  11. data/lib/active_support/cache/file_store.rb +50 -43
  12. data/lib/active_support/cache/mem_cache_store.rb +194 -67
  13. data/lib/active_support/cache/memory_store.rb +70 -34
  14. data/lib/active_support/cache/null_store.rb +18 -3
  15. data/lib/active_support/cache/redis_cache_store.rb +474 -0
  16. data/lib/active_support/cache/strategy/local_cache.rb +73 -50
  17. data/lib/active_support/cache/strategy/local_cache_middleware.rb +2 -0
  18. data/lib/active_support/cache.rb +556 -220
  19. data/lib/active_support/callbacks.rb +264 -159
  20. data/lib/active_support/code_generator.rb +65 -0
  21. data/lib/active_support/concern.rb +81 -8
  22. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +16 -0
  23. data/lib/active_support/concurrency/share_lock.rb +4 -3
  24. data/lib/active_support/configurable.rb +17 -16
  25. data/lib/active_support/configuration_file.rb +51 -0
  26. data/lib/active_support/core_ext/array/access.rb +18 -8
  27. data/lib/active_support/core_ext/array/conversions.rb +20 -17
  28. data/lib/active_support/core_ext/array/deprecated_conversions.rb +25 -0
  29. data/lib/active_support/core_ext/array/extract.rb +21 -0
  30. data/lib/active_support/core_ext/array/extract_options.rb +2 -0
  31. data/lib/active_support/core_ext/array/grouping.rb +8 -6
  32. data/lib/active_support/core_ext/array/inquiry.rb +4 -2
  33. data/lib/active_support/core_ext/array/wrap.rb +2 -0
  34. data/lib/active_support/core_ext/array.rb +4 -1
  35. data/lib/active_support/core_ext/benchmark.rb +4 -2
  36. data/lib/active_support/core_ext/big_decimal/conversions.rb +3 -1
  37. data/lib/active_support/core_ext/big_decimal.rb +2 -0
  38. data/lib/active_support/core_ext/class/attribute.rb +50 -47
  39. data/lib/active_support/core_ext/class/attribute_accessors.rb +2 -0
  40. data/lib/active_support/core_ext/class/subclasses.rb +10 -24
  41. data/lib/active_support/core_ext/class.rb +2 -0
  42. data/lib/active_support/core_ext/date/acts_like.rb +2 -0
  43. data/lib/active_support/core_ext/date/blank.rb +3 -1
  44. data/lib/active_support/core_ext/date/calculations.rb +17 -14
  45. data/lib/active_support/core_ext/date/conversions.rb +24 -22
  46. data/lib/active_support/core_ext/date/deprecated_conversions.rb +26 -0
  47. data/lib/active_support/core_ext/date/zones.rb +2 -0
  48. data/lib/active_support/core_ext/date.rb +3 -0
  49. data/lib/active_support/core_ext/date_and_time/calculations.rb +65 -41
  50. data/lib/active_support/core_ext/date_and_time/compatibility.rb +18 -1
  51. data/lib/active_support/core_ext/date_and_time/zones.rb +2 -1
  52. data/lib/active_support/core_ext/date_time/acts_like.rb +2 -0
  53. data/lib/active_support/core_ext/date_time/blank.rb +3 -1
  54. data/lib/active_support/core_ext/date_time/calculations.rb +3 -1
  55. data/lib/active_support/core_ext/date_time/compatibility.rb +7 -5
  56. data/lib/active_support/core_ext/date_time/conversions.rb +15 -14
  57. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +22 -0
  58. data/lib/active_support/core_ext/date_time.rb +3 -0
  59. data/lib/active_support/core_ext/digest/uuid.rb +42 -14
  60. data/lib/active_support/core_ext/digest.rb +3 -0
  61. data/lib/active_support/core_ext/enumerable.rb +244 -72
  62. data/lib/active_support/core_ext/file/atomic.rb +6 -2
  63. data/lib/active_support/core_ext/file.rb +2 -0
  64. data/lib/active_support/core_ext/hash/conversions.rb +7 -6
  65. data/lib/active_support/core_ext/hash/deep_merge.rb +8 -12
  66. data/lib/active_support/core_ext/hash/deep_transform_values.rb +46 -0
  67. data/lib/active_support/core_ext/hash/except.rb +4 -2
  68. data/lib/active_support/core_ext/hash/indifferent_access.rb +5 -3
  69. data/lib/active_support/core_ext/hash/keys.rb +4 -31
  70. data/lib/active_support/core_ext/hash/reverse_merge.rb +5 -2
  71. data/lib/active_support/core_ext/hash/slice.rb +8 -29
  72. data/lib/active_support/core_ext/hash.rb +3 -2
  73. data/lib/active_support/core_ext/integer/inflections.rb +2 -0
  74. data/lib/active_support/core_ext/integer/multiple.rb +3 -1
  75. data/lib/active_support/core_ext/integer/time.rb +7 -14
  76. data/lib/active_support/core_ext/integer.rb +2 -0
  77. data/lib/active_support/core_ext/kernel/concern.rb +2 -0
  78. data/lib/active_support/core_ext/kernel/reporting.rb +6 -4
  79. data/lib/active_support/core_ext/kernel/singleton_class.rb +3 -1
  80. data/lib/active_support/core_ext/kernel.rb +2 -1
  81. data/lib/active_support/core_ext/load_error.rb +3 -8
  82. data/lib/active_support/core_ext/module/aliasing.rb +2 -0
  83. data/lib/active_support/core_ext/module/anonymous.rb +2 -0
  84. data/lib/active_support/core_ext/module/attr_internal.rb +4 -2
  85. data/lib/active_support/core_ext/module/attribute_accessors.rb +46 -56
  86. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +36 -27
  87. data/lib/active_support/core_ext/module/concerning.rb +15 -10
  88. data/lib/active_support/core_ext/module/delegation.rb +97 -58
  89. data/lib/active_support/core_ext/module/deprecation.rb +2 -0
  90. data/lib/active_support/core_ext/module/introspection.rb +18 -15
  91. data/lib/active_support/core_ext/module/redefine_method.rb +40 -0
  92. data/lib/active_support/core_ext/module/remove_method.rb +5 -23
  93. data/lib/active_support/core_ext/module.rb +3 -1
  94. data/lib/active_support/core_ext/name_error.rb +30 -2
  95. data/lib/active_support/core_ext/numeric/bytes.rb +2 -0
  96. data/lib/active_support/core_ext/numeric/conversions.rb +134 -129
  97. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +60 -0
  98. data/lib/active_support/core_ext/numeric/time.rb +7 -15
  99. data/lib/active_support/core_ext/numeric.rb +3 -1
  100. data/lib/active_support/core_ext/object/acts_like.rb +41 -6
  101. data/lib/active_support/core_ext/object/blank.rb +15 -5
  102. data/lib/active_support/core_ext/object/conversions.rb +2 -0
  103. data/lib/active_support/core_ext/object/deep_dup.rb +3 -1
  104. data/lib/active_support/core_ext/object/duplicable.rb +16 -110
  105. data/lib/active_support/core_ext/object/inclusion.rb +2 -0
  106. data/lib/active_support/core_ext/object/instance_variables.rb +2 -0
  107. data/lib/active_support/core_ext/object/json.rb +51 -26
  108. data/lib/active_support/core_ext/object/to_param.rb +2 -0
  109. data/lib/active_support/core_ext/object/to_query.rb +4 -2
  110. data/lib/active_support/core_ext/object/try.rb +26 -14
  111. data/lib/active_support/core_ext/object/with_options.rb +24 -3
  112. data/lib/active_support/core_ext/object.rb +2 -0
  113. data/lib/active_support/core_ext/pathname/existence.rb +21 -0
  114. data/lib/active_support/core_ext/pathname.rb +3 -0
  115. data/lib/active_support/core_ext/range/compare_range.rb +57 -0
  116. data/lib/active_support/core_ext/range/conversions.rb +35 -25
  117. data/lib/active_support/core_ext/range/deprecated_conversions.rb +26 -0
  118. data/lib/active_support/core_ext/range/each.rb +6 -3
  119. data/lib/active_support/core_ext/range/include_time_with_zone.rb +7 -0
  120. data/lib/active_support/core_ext/range/overlaps.rb +3 -1
  121. data/lib/active_support/core_ext/range.rb +4 -1
  122. data/lib/active_support/core_ext/regexp.rb +10 -5
  123. data/lib/active_support/core_ext/securerandom.rb +25 -3
  124. data/lib/active_support/core_ext/string/access.rb +7 -16
  125. data/lib/active_support/core_ext/string/behavior.rb +2 -0
  126. data/lib/active_support/core_ext/string/conversions.rb +5 -2
  127. data/lib/active_support/core_ext/string/exclude.rb +2 -0
  128. data/lib/active_support/core_ext/string/filters.rb +44 -1
  129. data/lib/active_support/core_ext/string/indent.rb +2 -0
  130. data/lib/active_support/core_ext/string/inflections.rb +69 -16
  131. data/lib/active_support/core_ext/string/inquiry.rb +4 -1
  132. data/lib/active_support/core_ext/string/multibyte.rb +9 -4
  133. data/lib/active_support/core_ext/string/output_safety.rb +135 -27
  134. data/lib/active_support/core_ext/string/starts_ends_with.rb +4 -2
  135. data/lib/active_support/core_ext/string/strip.rb +5 -1
  136. data/lib/active_support/core_ext/string/zones.rb +2 -0
  137. data/lib/active_support/core_ext/string.rb +2 -0
  138. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +6 -0
  139. data/lib/active_support/core_ext/symbol.rb +3 -0
  140. data/lib/active_support/core_ext/time/acts_like.rb +2 -0
  141. data/lib/active_support/core_ext/time/calculations.rb +81 -24
  142. data/lib/active_support/core_ext/time/compatibility.rb +4 -2
  143. data/lib/active_support/core_ext/time/conversions.rb +17 -12
  144. data/lib/active_support/core_ext/time/deprecated_conversions.rb +22 -0
  145. data/lib/active_support/core_ext/time/zones.rb +12 -25
  146. data/lib/active_support/core_ext/time.rb +3 -0
  147. data/lib/active_support/core_ext/uri.rb +4 -23
  148. data/lib/active_support/core_ext.rb +4 -1
  149. data/lib/active_support/current_attributes/test_helper.rb +13 -0
  150. data/lib/active_support/current_attributes.rb +226 -0
  151. data/lib/active_support/dependencies/autoload.rb +2 -0
  152. data/lib/active_support/dependencies/interlock.rb +12 -18
  153. data/lib/active_support/dependencies/require_dependency.rb +28 -0
  154. data/lib/active_support/dependencies.rb +59 -715
  155. data/lib/active_support/deprecation/behaviors.rb +48 -13
  156. data/lib/active_support/deprecation/constant_accessor.rb +4 -2
  157. data/lib/active_support/deprecation/disallowed.rb +56 -0
  158. data/lib/active_support/deprecation/instance_delegator.rb +2 -1
  159. data/lib/active_support/deprecation/method_wrappers.rb +29 -21
  160. data/lib/active_support/deprecation/proxy_wrappers.rb +34 -8
  161. data/lib/active_support/deprecation/reporting.rb +54 -9
  162. data/lib/active_support/deprecation.rb +10 -3
  163. data/lib/active_support/descendants_tracker.rb +192 -34
  164. data/lib/active_support/digest.rb +22 -0
  165. data/lib/active_support/duration/iso8601_parser.rb +9 -9
  166. data/lib/active_support/duration/iso8601_serializer.rb +29 -15
  167. data/lib/active_support/duration.rb +158 -72
  168. data/lib/active_support/encrypted_configuration.rb +56 -0
  169. data/lib/active_support/encrypted_file.rb +129 -0
  170. data/lib/active_support/environment_inquirer.rb +20 -0
  171. data/lib/active_support/error_reporter.rb +117 -0
  172. data/lib/active_support/evented_file_update_checker.rb +87 -122
  173. data/lib/active_support/execution_context/test_helper.rb +13 -0
  174. data/lib/active_support/execution_context.rb +53 -0
  175. data/lib/active_support/execution_wrapper.rb +46 -21
  176. data/lib/active_support/executor/test_helper.rb +7 -0
  177. data/lib/active_support/executor.rb +2 -0
  178. data/lib/active_support/file_update_checker.rb +2 -1
  179. data/lib/active_support/fork_tracker.rb +71 -0
  180. data/lib/active_support/gem_version.rb +7 -5
  181. data/lib/active_support/gzip.rb +2 -0
  182. data/lib/active_support/hash_with_indifferent_access.rb +126 -42
  183. data/lib/active_support/html_safe_translation.rb +43 -0
  184. data/lib/active_support/i18n.rb +5 -1
  185. data/lib/active_support/i18n_railtie.rb +19 -14
  186. data/lib/active_support/inflections.rb +2 -0
  187. data/lib/active_support/inflector/inflections.rb +41 -14
  188. data/lib/active_support/inflector/methods.rb +73 -87
  189. data/lib/active_support/inflector/transliterate.rb +56 -18
  190. data/lib/active_support/inflector.rb +2 -0
  191. data/lib/active_support/isolated_execution_state.rb +72 -0
  192. data/lib/active_support/json/decoding.rb +27 -26
  193. data/lib/active_support/json/encoding.rb +16 -6
  194. data/lib/active_support/json.rb +2 -0
  195. data/lib/active_support/key_generator.rb +25 -38
  196. data/lib/active_support/lazy_load_hooks.rb +35 -6
  197. data/lib/active_support/locale/en.rb +33 -0
  198. data/lib/active_support/locale/en.yml +8 -4
  199. data/lib/active_support/log_subscriber/test_helper.rb +4 -2
  200. data/lib/active_support/log_subscriber.rb +54 -13
  201. data/lib/active_support/logger.rb +4 -17
  202. data/lib/active_support/logger_silence.rb +13 -20
  203. data/lib/active_support/logger_thread_safe_level.rb +48 -10
  204. data/lib/active_support/message_encryptor.rb +111 -37
  205. data/lib/active_support/message_verifier.rb +124 -21
  206. data/lib/active_support/messages/metadata.rb +80 -0
  207. data/lib/active_support/messages/rotation_configuration.rb +23 -0
  208. data/lib/active_support/messages/rotator.rb +57 -0
  209. data/lib/active_support/multibyte/chars.rb +19 -76
  210. data/lib/active_support/multibyte/unicode.rb +9 -331
  211. data/lib/active_support/multibyte.rb +3 -1
  212. data/lib/active_support/notifications/fanout.rb +165 -37
  213. data/lib/active_support/notifications/instrumenter.rb +92 -11
  214. data/lib/active_support/notifications.rb +96 -30
  215. data/lib/active_support/number_helper/number_converter.rb +8 -9
  216. data/lib/active_support/number_helper/number_to_currency_converter.rb +14 -12
  217. data/lib/active_support/number_helper/number_to_delimited_converter.rb +6 -3
  218. data/lib/active_support/number_helper/number_to_human_converter.rb +6 -3
  219. data/lib/active_support/number_helper/number_to_human_size_converter.rb +7 -4
  220. data/lib/active_support/number_helper/number_to_percentage_converter.rb +5 -1
  221. data/lib/active_support/number_helper/number_to_phone_converter.rb +6 -3
  222. data/lib/active_support/number_helper/number_to_rounded_converter.rb +14 -27
  223. data/lib/active_support/number_helper/rounding_helper.rb +16 -34
  224. data/lib/active_support/number_helper.rb +38 -12
  225. data/lib/active_support/option_merger.rb +19 -6
  226. data/lib/active_support/ordered_hash.rb +4 -2
  227. data/lib/active_support/ordered_options.rb +18 -6
  228. data/lib/active_support/parameter_filter.rb +138 -0
  229. data/lib/active_support/per_thread_registry.rb +8 -1
  230. data/lib/active_support/proxy_object.rb +2 -0
  231. data/lib/active_support/rails.rb +3 -10
  232. data/lib/active_support/railtie.rb +112 -11
  233. data/lib/active_support/reloader.rb +12 -11
  234. data/lib/active_support/rescuable.rb +19 -18
  235. data/lib/active_support/ruby_features.rb +7 -0
  236. data/lib/active_support/secure_compare_rotator.rb +51 -0
  237. data/lib/active_support/security_utils.rb +26 -15
  238. data/lib/active_support/string_inquirer.rb +4 -3
  239. data/lib/active_support/subscriber.rb +81 -42
  240. data/lib/active_support/tagged_logging.rb +45 -9
  241. data/lib/active_support/test_case.rb +86 -2
  242. data/lib/active_support/testing/assertions.rb +89 -21
  243. data/lib/active_support/testing/autorun.rb +2 -0
  244. data/lib/active_support/testing/constant_lookup.rb +2 -0
  245. data/lib/active_support/testing/declarative.rb +2 -0
  246. data/lib/active_support/testing/deprecation.rb +54 -2
  247. data/lib/active_support/testing/file_fixtures.rb +4 -0
  248. data/lib/active_support/testing/isolation.rb +6 -4
  249. data/lib/active_support/testing/method_call_assertions.rb +34 -5
  250. data/lib/active_support/testing/parallelization/server.rb +82 -0
  251. data/lib/active_support/testing/parallelization/worker.rb +103 -0
  252. data/lib/active_support/testing/parallelization.rb +55 -0
  253. data/lib/active_support/testing/parallelize_executor.rb +76 -0
  254. data/lib/active_support/testing/setup_and_teardown.rb +12 -7
  255. data/lib/active_support/testing/stream.rb +6 -7
  256. data/lib/active_support/testing/tagged_logging.rb +3 -1
  257. data/lib/active_support/testing/time_helpers.rb +91 -15
  258. data/lib/active_support/time.rb +2 -0
  259. data/lib/active_support/time_with_zone.rb +168 -56
  260. data/lib/active_support/values/time_zone.rb +85 -37
  261. data/lib/active_support/version.rb +3 -1
  262. data/lib/active_support/xml_mini/jdom.rb +6 -5
  263. data/lib/active_support/xml_mini/libxml.rb +9 -7
  264. data/lib/active_support/xml_mini/libxmlsax.rb +7 -5
  265. data/lib/active_support/xml_mini/nokogiri.rb +8 -6
  266. data/lib/active_support/xml_mini/nokogirisax.rb +6 -4
  267. data/lib/active_support/xml_mini/rexml.rb +13 -4
  268. data/lib/active_support/xml_mini.rb +10 -15
  269. data/lib/active_support.rb +30 -9
  270. metadata +76 -35
  271. data/lib/active_support/core_ext/array/prepend_and_append.rb +0 -7
  272. data/lib/active_support/core_ext/hash/compact.rb +0 -27
  273. data/lib/active_support/core_ext/hash/transform_values.rb +0 -30
  274. data/lib/active_support/core_ext/kernel/agnostics.rb +0 -11
  275. data/lib/active_support/core_ext/marshal.rb +0 -22
  276. data/lib/active_support/core_ext/module/reachable.rb +0 -8
  277. data/lib/active_support/core_ext/numeric/inquiry.rb +0 -26
  278. data/lib/active_support/core_ext/range/include_range.rb +0 -23
  279. data/lib/active_support/values/unicode_tables.dat +0 -0
@@ -1,29 +1,45 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "zlib"
2
4
  require "active_support/core_ext/array/extract_options"
3
5
  require "active_support/core_ext/array/wrap"
6
+ require "active_support/core_ext/enumerable"
4
7
  require "active_support/core_ext/module/attribute_accessors"
5
8
  require "active_support/core_ext/numeric/bytes"
6
9
  require "active_support/core_ext/numeric/time"
7
10
  require "active_support/core_ext/object/to_param"
11
+ require "active_support/core_ext/object/try"
8
12
  require "active_support/core_ext/string/inflections"
9
13
 
10
14
  module ActiveSupport
11
15
  # See ActiveSupport::Cache::Store for documentation.
12
16
  module Cache
13
- autoload :FileStore, "active_support/cache/file_store"
14
- autoload :MemoryStore, "active_support/cache/memory_store"
15
- autoload :MemCacheStore, "active_support/cache/mem_cache_store"
16
- autoload :NullStore, "active_support/cache/null_store"
17
+ autoload :FileStore, "active_support/cache/file_store"
18
+ autoload :MemoryStore, "active_support/cache/memory_store"
19
+ autoload :MemCacheStore, "active_support/cache/mem_cache_store"
20
+ autoload :NullStore, "active_support/cache/null_store"
21
+ autoload :RedisCacheStore, "active_support/cache/redis_cache_store"
17
22
 
18
23
  # These options mean something to all cache implementations. Individual cache
19
24
  # implementations may support additional options.
20
- UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :race_condition_ttl]
25
+ UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :expire_in, :expired_in, :race_condition_ttl, :coder, :skip_nil]
26
+
27
+ DEFAULT_COMPRESS_LIMIT = 1.kilobyte
28
+
29
+ # Mapping of canonical option names to aliases that a store will recognize.
30
+ OPTION_ALIASES = {
31
+ expires_in: [:expire_in, :expired_in]
32
+ }.freeze
21
33
 
22
34
  module Strategy
23
35
  autoload :LocalCache, "active_support/cache/strategy/local_cache"
24
36
  end
25
37
 
38
+ @format_version = 6.1
39
+
26
40
  class << self
41
+ attr_accessor :format_version
42
+
27
43
  # Creates a new Store object according to the given options.
28
44
  #
29
45
  # If no arguments are passed to this method, then a new
@@ -49,12 +65,19 @@ module ActiveSupport
49
65
  #
50
66
  # ActiveSupport::Cache.lookup_store(MyOwnCacheStore.new)
51
67
  # # => returns MyOwnCacheStore.new
52
- def lookup_store(*store_option)
53
- store, *parameters = *Array.wrap(store_option).flatten
54
-
68
+ def lookup_store(store = nil, *parameters)
55
69
  case store
56
70
  when Symbol
57
- retrieve_store_class(store).new(*parameters)
71
+ options = parameters.extract_options!
72
+ # clean this up once Ruby 2.7 support is dropped
73
+ # see https://github.com/rails/rails/pull/41522#discussion_r581186602
74
+ if options.empty?
75
+ retrieve_store_class(store).new(*parameters)
76
+ else
77
+ retrieve_store_class(store).new(*parameters, **options)
78
+ end
79
+ when Array
80
+ lookup_store(*store)
58
81
  when nil
59
82
  ActiveSupport::Cache::MemoryStore.new
60
83
  else
@@ -75,7 +98,7 @@ module ActiveSupport
75
98
  #
76
99
  # The +key+ argument can also respond to +cache_key+ or +to_param+.
77
100
  def expand_cache_key(key, namespace = nil)
78
- expanded_cache_key = namespace ? "#{namespace}/" : ""
101
+ expanded_cache_key = namespace ? +"#{namespace}/" : +""
79
102
 
80
103
  if prefix = ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]
81
104
  expanded_cache_key << "#{prefix}/"
@@ -88,16 +111,19 @@ module ActiveSupport
88
111
  private
89
112
  def retrieve_cache_key(key)
90
113
  case
91
- when key.respond_to?(:cache_key) then key.cache_key
92
- when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param
93
- when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a)
94
- else key.to_param
114
+ when key.respond_to?(:cache_key_with_version) then key.cache_key_with_version
115
+ when key.respond_to?(:cache_key) then key.cache_key
116
+ when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param
117
+ when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a)
118
+ else key.to_param
95
119
  end.to_s
96
120
  end
97
121
 
98
122
  # Obtains the specified cache store class, given the name of the +store+.
99
123
  # Raises an error when the store class cannot be found.
100
124
  def retrieve_store_class(store)
125
+ # require_relative cannot be used here because the class might be
126
+ # provided by another gem, like redis-activesupport for example.
101
127
  require "active_support/cache/#{store}"
102
128
  rescue LoadError => e
103
129
  raise "Could not find cache store adapter for #{store} (#{e})"
@@ -113,9 +139,10 @@ module ActiveSupport
113
139
  # popular cache store for large production websites.
114
140
  #
115
141
  # Some implementations may not support all methods beyond the basic cache
116
- # methods of +fetch+, +write+, +read+, +exist?+, and +delete+.
142
+ # methods of #fetch, #write, #read, #exist?, and #delete.
117
143
  #
118
- # ActiveSupport::Cache::Store can store any serializable Ruby object.
144
+ # ActiveSupport::Cache::Store can store any Ruby object that is supported by
145
+ # its +coder+'s +dump+ and +load+ methods.
119
146
  #
120
147
  # cache = ActiveSupport::Cache::MemoryStore.new
121
148
  #
@@ -123,6 +150,8 @@ module ActiveSupport
123
150
  # cache.write('city', "Duckburgh")
124
151
  # cache.read('city') # => "Duckburgh"
125
152
  #
153
+ # cache.write('not serializable', Proc.new {}) # => TypeError
154
+ #
126
155
  # Keys are always translated into Strings and are case sensitive. When an
127
156
  # object is specified as a key and has a +cache_key+ method defined, this
128
157
  # method will be called to define the key. Otherwise, the +to_param+
@@ -143,23 +172,49 @@ module ActiveSupport
143
172
  # cache.namespace = -> { @last_mod_time } # Set the namespace to a variable
144
173
  # @last_mod_time = Time.now # Invalidate the entire cache by changing namespace
145
174
  #
146
- # Caches can also store values in a compressed format to save space and
147
- # reduce time spent sending data. Since there is overhead, values must be
148
- # large enough to warrant compression. To turn on compression either pass
149
- # <tt>compress: true</tt> in the initializer or as an option to +fetch+
150
- # or +write+. To specify the threshold at which to compress values, set the
151
- # <tt>:compress_threshold</tt> option. The default threshold is 16K.
152
175
  class Store
153
176
  cattr_accessor :logger, instance_writer: true
154
177
 
155
178
  attr_reader :silence, :options
156
179
  alias :silence? :silence
157
180
 
158
- # Creates a new cache. The options will be passed to any write method calls
159
- # except for <tt>:namespace</tt> which can be used to set the global
160
- # namespace for the cache.
181
+ class << self
182
+ private
183
+ def retrieve_pool_options(options)
184
+ {}.tap do |pool_options|
185
+ pool_options[:size] = options.delete(:pool_size) if options[:pool_size]
186
+ pool_options[:timeout] = options.delete(:pool_timeout) if options[:pool_timeout]
187
+ end
188
+ end
189
+
190
+ def ensure_connection_pool_added!
191
+ require "connection_pool"
192
+ rescue LoadError => e
193
+ $stderr.puts "You don't have connection_pool installed in your application. Please add it to your Gemfile and run bundle install"
194
+ raise e
195
+ end
196
+ end
197
+
198
+ # Creates a new cache.
199
+ #
200
+ # ==== Options
201
+ #
202
+ # * +:namespace+ - Sets the namespace for the cache. This option is
203
+ # especially useful if your application shares a cache with other
204
+ # applications.
205
+ # * +:coder+ - Replaces the default cache entry serialization mechanism
206
+ # with a custom one. The +coder+ must respond to +dump+ and +load+.
207
+ # Using a custom coder disables automatic compression.
208
+ #
209
+ # Any other specified options are treated as default options for the
210
+ # relevant cache operations, such as #read, #write, and #fetch.
161
211
  def initialize(options = nil)
162
- @options = options ? options.dup : {}
212
+ @options = options ? normalize_options(options) : {}
213
+ @options[:compress] = true unless @options.key?(:compress)
214
+ @options[:compress_threshold] = DEFAULT_COMPRESS_LIMIT unless @options.key?(:compress_threshold)
215
+
216
+ @coder = @options.delete(:coder) { default_coder } || NullCoder
217
+ @coder_supports_compression = @coder.respond_to?(:dump_compressed)
163
218
  end
164
219
 
165
220
  # Silences the logger.
@@ -194,99 +249,85 @@ module ActiveSupport
194
249
  # end
195
250
  # cache.fetch('city') # => "Duckburgh"
196
251
  #
197
- # You may also specify additional options via the +options+ argument.
198
- # Setting <tt>force: true</tt> forces a cache "miss," meaning we treat
199
- # the cache value as missing even if it's present. Passing a block is
200
- # required when +force+ is true so this always results in a cache write.
252
+ # ==== Options
201
253
  #
202
- # cache.write('today', 'Monday')
203
- # cache.fetch('today', force: true) { 'Tuesday' } # => 'Tuesday'
204
- # cache.fetch('today', force: true) # => ArgumentError
205
- #
206
- # The +:force+ option is useful when you're calling some other method to
207
- # ask whether you should force a cache write. Otherwise, it's clearer to
208
- # just call <tt>Cache#write</tt>.
209
- #
210
- # Setting <tt>:compress</tt> will store a large cache entry set by the call
211
- # in a compressed format.
212
- #
213
- # Setting <tt>:expires_in</tt> will set an expiration time on the cache.
214
- # All caches support auto-expiring content after a specified number of
215
- # seconds. This value can be specified as an option to the constructor
216
- # (in which case all entries will be affected), or it can be supplied to
217
- # the +fetch+ or +write+ method to effect just one entry.
218
- #
219
- # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes)
220
- # cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry
221
- #
222
- # Setting <tt>:race_condition_ttl</tt> is very useful in situations where
223
- # a cache entry is used very frequently and is under heavy load. If a
224
- # cache expires and due to heavy load several different processes will try
225
- # to read data natively and then they all will try to write to cache. To
226
- # avoid that case the first process to find an expired cache entry will
227
- # bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>.
228
- # Yes, this process is extending the time for a stale value by another few
229
- # seconds. Because of extended life of the previous cache, other processes
230
- # will continue to use slightly stale data for a just a bit longer. In the
231
- # meantime that first process will go ahead and will write into cache the
232
- # new value. After that all the processes will start getting the new value.
233
- # The key is to keep <tt>:race_condition_ttl</tt> small.
234
- #
235
- # If the process regenerating the entry errors out, the entry will be
236
- # regenerated after the specified number of seconds. Also note that the
237
- # life of stale cache is extended only if it expired recently. Otherwise
238
- # a new value is generated and <tt>:race_condition_ttl</tt> does not play
239
- # any role.
240
- #
241
- # # Set all values to expire after one minute.
242
- # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
243
- #
244
- # cache.write('foo', 'original value')
245
- # val_1 = nil
246
- # val_2 = nil
247
- # sleep 60
248
- #
249
- # Thread.new do
250
- # val_1 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
251
- # sleep 1
252
- # 'new value 1'
253
- # end
254
- # end
254
+ # Internally, +fetch+ calls #read_entry, and calls #write_entry on a cache
255
+ # miss. Thus, +fetch+ supports the same options as #read and #write.
256
+ # Additionally, +fetch+ supports the following options:
255
257
  #
256
- # Thread.new do
257
- # val_2 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
258
- # 'new value 2'
259
- # end
260
- # end
258
+ # * <tt>force: true</tt> - Forces a cache "miss," meaning we treat the
259
+ # cache value as missing even if it's present. Passing a block is
260
+ # required when +force+ is true so this always results in a cache write.
261
261
  #
262
- # cache.fetch('foo') # => "original value"
263
- # sleep 10 # First thread extended the life of cache by another 10 seconds
264
- # cache.fetch('foo') # => "new value 1"
265
- # val_1 # => "new value 1"
266
- # val_2 # => "original value"
262
+ # cache.write('today', 'Monday')
263
+ # cache.fetch('today', force: true) { 'Tuesday' } # => 'Tuesday'
264
+ # cache.fetch('today', force: true) # => ArgumentError
267
265
  #
268
- # Other options will be handled by the specific cache store implementation.
269
- # Internally, #fetch calls #read_entry, and calls #write_entry on a cache
270
- # miss. +options+ will be passed to the #read and #write calls.
266
+ # The +:force+ option is useful when you're calling some other method to
267
+ # ask whether you should force a cache write. Otherwise, it's clearer to
268
+ # just call +write+.
271
269
  #
272
- # For example, MemCacheStore's #write method supports the +:raw+
273
- # option, which tells the memcached server to store all values as strings.
274
- # We can use this option with #fetch too:
270
+ # * <tt>skip_nil: true</tt> - Prevents caching a nil result:
275
271
  #
276
- # cache = ActiveSupport::Cache::MemCacheStore.new
277
- # cache.fetch("foo", force: true, raw: true) do
278
- # :bar
279
- # end
280
- # cache.fetch('foo') # => "bar"
281
- def fetch(name, options = nil)
272
+ # cache.fetch('foo') { nil }
273
+ # cache.fetch('bar', skip_nil: true) { nil }
274
+ # cache.exist?('foo') # => true
275
+ # cache.exist?('bar') # => false
276
+ #
277
+ # * +:race_condition_ttl+ - Specifies the number of seconds during which
278
+ # an expired value can be reused while a new value is being generated.
279
+ # This can be used to prevent race conditions when cache entries expire,
280
+ # by preventing multiple processes from simultaneously regenerating the
281
+ # same entry (also known as the dog pile effect).
282
+ #
283
+ # When a process encounters a cache entry that has expired less than
284
+ # +:race_condition_ttl+ seconds ago, it will bump the expiration time by
285
+ # +:race_condition_ttl+ seconds before generating a new value. During
286
+ # this extended time window, while the process generates a new value,
287
+ # other processes will continue to use the old value. After the first
288
+ # process writes the new value, other processes will then use it.
289
+ #
290
+ # If the first process errors out while generating a new value, another
291
+ # process can try to generate a new value after the extended time window
292
+ # has elapsed.
293
+ #
294
+ # # Set all values to expire after one minute.
295
+ # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
296
+ #
297
+ # cache.write('foo', 'original value')
298
+ # val_1 = nil
299
+ # val_2 = nil
300
+ # sleep 60
301
+ #
302
+ # Thread.new do
303
+ # val_1 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
304
+ # sleep 1
305
+ # 'new value 1'
306
+ # end
307
+ # end
308
+ #
309
+ # Thread.new do
310
+ # val_2 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
311
+ # 'new value 2'
312
+ # end
313
+ # end
314
+ #
315
+ # cache.fetch('foo') # => "original value"
316
+ # sleep 10 # First thread extended the life of cache by another 10 seconds
317
+ # cache.fetch('foo') # => "new value 1"
318
+ # val_1 # => "new value 1"
319
+ # val_2 # => "original value"
320
+ #
321
+ def fetch(name, options = nil, &block)
282
322
  if block_given?
283
323
  options = merged_options(options)
284
324
  key = normalize_key(name, options)
285
325
 
286
326
  entry = nil
287
327
  instrument(:read, name, options) do |payload|
288
- cached_entry = read_entry(key, options) unless options[:force]
328
+ cached_entry = read_entry(key, **options, event: payload) unless options[:force]
289
329
  entry = handle_expired_entry(cached_entry, key, options)
330
+ entry = nil if entry && entry.mismatched?(normalize_version(name, options))
290
331
  payload[:super_operation] = :fetch if payload
291
332
  payload[:hit] = !!entry if payload
292
333
  end
@@ -294,7 +335,7 @@ module ActiveSupport
294
335
  if entry
295
336
  get_entry_value(entry, name, options)
296
337
  else
297
- save_block_result_to_cache(name, options) { |_name| yield _name }
338
+ save_block_result_to_cache(name, options, &block)
298
339
  end
299
340
  elsif options && options[:force]
300
341
  raise ArgumentError, "Missing block: Calling `Cache#fetch` with `force: true` requires a block."
@@ -303,19 +344,35 @@ module ActiveSupport
303
344
  end
304
345
  end
305
346
 
306
- # Fetches data from the cache, using the given key. If there is data in
347
+ # Reads data from the cache, using the given key. If there is data in
307
348
  # the cache with the given key, then that data is returned. Otherwise,
308
349
  # +nil+ is returned.
309
350
  #
310
- # Options are passed to the underlying cache implementation.
351
+ # Note, if data was written with the <tt>:expires_in</tt> or
352
+ # <tt>:version</tt> options, both of these conditions are applied before
353
+ # the data is returned.
354
+ #
355
+ # ==== Options
356
+ #
357
+ # * +:version+ - Specifies a version for the cache entry. If the cached
358
+ # version does not match the requested version, the read will be treated
359
+ # as a cache miss. This feature is used to support recyclable cache keys.
360
+ #
361
+ # Other options will be handled by the specific cache store implementation.
311
362
  def read(name, options = nil)
312
363
  options = merged_options(options)
313
- key = normalize_key(name, options)
364
+ key = normalize_key(name, options)
365
+ version = normalize_version(name, options)
366
+
314
367
  instrument(:read, name, options) do |payload|
315
- entry = read_entry(key, options)
368
+ entry = read_entry(key, **options, event: payload)
369
+
316
370
  if entry
317
371
  if entry.expired?
318
- delete_entry(key, options)
372
+ delete_entry(key, **options)
373
+ payload[:hit] = false if payload
374
+ nil
375
+ elsif entry.mismatched?(version)
319
376
  payload[:hit] = false if payload
320
377
  nil
321
378
  else
@@ -339,19 +396,24 @@ module ActiveSupport
339
396
  options = names.extract_options!
340
397
  options = merged_options(options)
341
398
 
342
- results = {}
343
- names.each do |name|
344
- key = normalize_key(name, options)
345
- entry = read_entry(key, options)
346
- if entry
347
- if entry.expired?
348
- delete_entry(key, options)
349
- else
350
- results[name] = entry.value
351
- end
399
+ instrument :read_multi, names, options do |payload|
400
+ read_multi_entries(names, **options, event: payload).tap do |results|
401
+ payload[:hits] = results.keys
352
402
  end
353
403
  end
354
- results
404
+ end
405
+
406
+ # Cache Storage API to write multiple values at once.
407
+ def write_multi(hash, options = nil)
408
+ options = merged_options(options)
409
+
410
+ instrument :write_multi, hash, options do |payload|
411
+ entries = hash.each_with_object({}) do |(name, value), memo|
412
+ memo[normalize_key(name, options)] = Entry.new(value, **options.merge(version: normalize_version(name, options)))
413
+ end
414
+
415
+ write_multi_entries entries, **options
416
+ end
355
417
  end
356
418
 
357
419
  # Fetches data from the cache, using the given keys. If there is data in
@@ -362,8 +424,6 @@ module ActiveSupport
362
424
  # to the cache. If you do not want to write the cache when the cache is
363
425
  # not found, use #read_multi.
364
426
  #
365
- # Options are passed to the underlying cache implementation.
366
- #
367
427
  # Returns a hash with the data for each of the names. For example:
368
428
  #
369
429
  # cache.write("bim", "bam")
@@ -373,31 +433,78 @@ module ActiveSupport
373
433
  # # => { "bim" => "bam",
374
434
  # # "unknown_key" => "Fallback value for key: unknown_key" }
375
435
  #
436
+ # Options are passed to the underlying cache implementation. For example:
437
+ #
438
+ # cache.fetch_multi("fizz", expires_in: 5.seconds) do |key|
439
+ # "buzz"
440
+ # end
441
+ # # => {"fizz"=>"buzz"}
442
+ # cache.read("fizz")
443
+ # # => "buzz"
444
+ # sleep(6)
445
+ # cache.read("fizz")
446
+ # # => nil
376
447
  def fetch_multi(*names)
377
448
  raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block_given?
378
449
 
379
450
  options = names.extract_options!
380
451
  options = merged_options(options)
381
- results = read_multi(*names, options)
382
452
 
383
- names.each_with_object({}) do |name, memo|
384
- memo[name] = results.fetch(name) do
385
- value = yield name
386
- write(name, value, options)
387
- value
453
+ instrument :read_multi, names, options do |payload|
454
+ reads = read_multi_entries(names, **options)
455
+ writes = {}
456
+ ordered = names.index_with do |name|
457
+ reads.fetch(name) { writes[name] = yield(name) }
388
458
  end
459
+
460
+ payload[:hits] = reads.keys
461
+ payload[:super_operation] = :fetch_multi
462
+
463
+ write_multi(writes, options)
464
+
465
+ ordered
389
466
  end
390
467
  end
391
468
 
392
- # Writes the value to the cache, with the key.
469
+ # Writes the value to the cache with the key. The value must be supported
470
+ # by the +coder+'s +dump+ and +load+ methods.
393
471
  #
394
- # Options are passed to the underlying cache implementation.
472
+ # By default, cache entries larger than 1kB are compressed. Compression
473
+ # allows more data to be stored in the same memory footprint, leading to
474
+ # fewer cache evictions and higher hit rates.
475
+ #
476
+ # ==== Options
477
+ #
478
+ # * <tt>compress: false</tt> - Disables compression of the cache entry.
479
+ #
480
+ # * +:compress_threshold+ - The compression threshold, specified in bytes.
481
+ # \Cache entries larger than this threshold will be compressed. Defaults
482
+ # to +1.kilobyte+.
483
+ #
484
+ # * +:expires_in+ - Sets a relative expiration time for the cache entry,
485
+ # specified in seconds. +:expire_in+ and +:expired_in+ are aliases for
486
+ # +:expires_in+.
487
+ #
488
+ # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes)
489
+ # cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry
490
+ #
491
+ # * +:expires_at+ - Sets an absolute expiration time for the cache entry.
492
+ #
493
+ # cache = ActiveSupport::Cache::MemoryStore.new
494
+ # cache.write(key, value, expires_at: Time.now.at_end_of_hour)
495
+ #
496
+ # * +:version+ - Specifies a version for the cache entry. When reading
497
+ # from the cache, if the cached version does not match the requested
498
+ # version, the read will be treated as a cache miss. This feature is
499
+ # used to support recyclable cache keys.
500
+ #
501
+ # Other options will be handled by the specific cache store implementation.
395
502
  def write(name, value, options = nil)
396
503
  options = merged_options(options)
397
504
 
398
505
  instrument(:write, name, options) do
399
- entry = Entry.new(value, options)
400
- write_entry(normalize_key(name, options), entry, options)
506
+ entry = Entry.new(value, **options.merge(version: normalize_version(name, options)))
507
+ write_entry(normalize_key(name, options), entry, **options)
401
508
  end
402
509
  end
403
510
 
@@ -408,7 +515,19 @@ module ActiveSupport
408
515
  options = merged_options(options)
409
516
 
410
517
  instrument(:delete, name) do
411
- delete_entry(normalize_key(name, options), options)
518
+ delete_entry(normalize_key(name, options), **options)
519
+ end
520
+ end
521
+
522
+ # Deletes multiple entries in the cache.
523
+ #
524
+ # Options are passed to the underlying cache implementation.
525
+ def delete_multi(names, options = nil)
526
+ options = merged_options(options)
527
+ names.map! { |key| normalize_key(key, options) }
528
+
529
+ instrument :delete_multi, names do
530
+ delete_multi_entries(names, **options)
412
531
  end
413
532
  end
414
533
 
@@ -418,17 +537,21 @@ module ActiveSupport
418
537
  def exist?(name, options = nil)
419
538
  options = merged_options(options)
420
539
 
421
- instrument(:exist?, name) do
422
- entry = read_entry(normalize_key(name, options), options)
423
- (entry && !entry.expired?) || false
540
+ instrument(:exist?, name) do |payload|
541
+ entry = read_entry(normalize_key(name, options), **options, event: payload)
542
+ (entry && !entry.expired? && !entry.mismatched?(normalize_version(name, options))) || false
424
543
  end
425
544
  end
426
545
 
546
+ def new_entry(value, options = nil) # :nodoc:
547
+ Entry.new(value, **merged_options(options))
548
+ end
549
+
427
550
  # Deletes all entries with keys matching the pattern.
428
551
  #
429
552
  # Options are passed to the underlying cache implementation.
430
553
  #
431
- # All implementations may not support this method.
554
+ # Some implementations may not support this method.
432
555
  def delete_matched(matcher, options = nil)
433
556
  raise NotImplementedError.new("#{self.class.name} does not support delete_matched")
434
557
  end
@@ -437,7 +560,7 @@ module ActiveSupport
437
560
  #
438
561
  # Options are passed to the underlying cache implementation.
439
562
  #
440
- # All implementations may not support this method.
563
+ # Some implementations may not support this method.
441
564
  def increment(name, amount = 1, options = nil)
442
565
  raise NotImplementedError.new("#{self.class.name} does not support increment")
443
566
  end
@@ -446,16 +569,16 @@ module ActiveSupport
446
569
  #
447
570
  # Options are passed to the underlying cache implementation.
448
571
  #
449
- # All implementations may not support this method.
572
+ # Some implementations may not support this method.
450
573
  def decrement(name, amount = 1, options = nil)
451
574
  raise NotImplementedError.new("#{self.class.name} does not support decrement")
452
575
  end
453
576
 
454
- # Cleanups the cache by removing expired entries.
577
+ # Cleans up the cache by removing expired entries.
455
578
  #
456
579
  # Options are passed to the underlying cache implementation.
457
580
  #
458
- # All implementations may not support this method.
581
+ # Some implementations may not support this method.
459
582
  def cleanup(options = nil)
460
583
  raise NotImplementedError.new("#{self.class.name} does not support cleanup")
461
584
  end
@@ -465,12 +588,16 @@ module ActiveSupport
465
588
  #
466
589
  # The options hash is passed to the underlying cache implementation.
467
590
  #
468
- # All implementations may not support this method.
591
+ # Some implementations may not support this method.
469
592
  def clear(options = nil)
470
593
  raise NotImplementedError.new("#{self.class.name} does not support clear")
471
594
  end
472
595
 
473
596
  private
597
+ def default_coder
598
+ Coders[Cache.format_version]
599
+ end
600
+
474
601
  # Adds the namespace defined in the options to a pattern designed to
475
602
  # match keys. Implementations that support delete_matched should call
476
603
  # this method to translate a pattern that matches names into one that
@@ -492,28 +619,125 @@ module ActiveSupport
492
619
 
493
620
  # Reads an entry from the cache implementation. Subclasses must implement
494
621
  # this method.
495
- def read_entry(key, options)
622
+ def read_entry(key, **options)
496
623
  raise NotImplementedError.new
497
624
  end
498
625
 
499
626
  # Writes an entry to the cache implementation. Subclasses must implement
500
627
  # this method.
501
- def write_entry(key, entry, options)
628
+ def write_entry(key, entry, **options)
502
629
  raise NotImplementedError.new
503
630
  end
504
631
 
632
+ def serialize_entry(entry, **options)
633
+ options = merged_options(options)
634
+ if @coder_supports_compression && options[:compress]
635
+ @coder.dump_compressed(entry, options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT)
636
+ else
637
+ @coder.dump(entry)
638
+ end
639
+ end
640
+
641
+ def deserialize_entry(payload)
642
+ payload.nil? ? nil : @coder.load(payload)
643
+ end
644
+
645
+ # Reads multiple entries from the cache implementation. Subclasses MAY
646
+ # implement this method.
647
+ def read_multi_entries(names, **options)
648
+ names.each_with_object({}) do |name, results|
649
+ key = normalize_key(name, options)
650
+ entry = read_entry(key, **options)
651
+
652
+ next unless entry
653
+
654
+ version = normalize_version(name, options)
655
+
656
+ if entry.expired?
657
+ delete_entry(key, **options)
658
+ elsif !entry.mismatched?(version)
659
+ results[name] = entry.value
660
+ end
661
+ end
662
+ end
663
+
664
+ # Writes multiple entries to the cache implementation. Subclasses MAY
665
+ # implement this method.
666
+ def write_multi_entries(hash, **options)
667
+ hash.each do |key, entry|
668
+ write_entry key, entry, **options
669
+ end
670
+ end
671
+
505
672
  # Deletes an entry from the cache implementation. Subclasses must
506
673
  # implement this method.
507
- def delete_entry(key, options)
674
+ def delete_entry(key, **options)
508
675
  raise NotImplementedError.new
509
676
  end
510
677
 
678
+ # Deletes multiples entries in the cache implementation. Subclasses MAY
679
+ # implement this method.
680
+ def delete_multi_entries(entries, **options)
681
+ entries.count { |key| delete_entry(key, **options) }
682
+ end
683
+
511
684
  # Merges the default options with ones specific to a method call.
512
685
  def merged_options(call_options)
513
686
  if call_options
514
- options.merge(call_options)
687
+ call_options = normalize_options(call_options)
688
+ if options.empty?
689
+ call_options
690
+ else
691
+ options.merge(call_options)
692
+ end
693
+ else
694
+ options
695
+ end
696
+ end
697
+
698
+ # Normalize aliased options to their canonical form
699
+ def normalize_options(options)
700
+ options = options.dup
701
+ OPTION_ALIASES.each do |canonical_name, aliases|
702
+ alias_key = aliases.detect { |key| options.key?(key) }
703
+ options[canonical_name] ||= options[alias_key] if alias_key
704
+ options.except!(*aliases)
705
+ end
706
+
707
+ options
708
+ end
709
+
710
+ # Expands and namespaces the cache key. May be overridden by
711
+ # cache stores to do additional normalization.
712
+ def normalize_key(key, options = nil)
713
+ namespace_key expanded_key(key), options
714
+ end
715
+
716
+ # Prefix the key with a namespace string:
717
+ #
718
+ # namespace_key 'foo', namespace: 'cache'
719
+ # # => 'cache:foo'
720
+ #
721
+ # With a namespace block:
722
+ #
723
+ # namespace_key 'foo', namespace: -> { 'cache' }
724
+ # # => 'cache:foo'
725
+ def namespace_key(key, options = nil)
726
+ options = merged_options(options)
727
+ namespace = options[:namespace]
728
+
729
+ if namespace.respond_to?(:call)
730
+ namespace = namespace.call
731
+ end
732
+
733
+ if key && key.encoding != Encoding::UTF_8
734
+ key = key.dup.force_encoding(Encoding::UTF_8)
735
+ end
736
+
737
+ if namespace
738
+ "#{namespace}:#{key}"
515
739
  else
516
- options.dup
740
+ key
517
741
  end
518
742
  end
519
743
 
@@ -526,50 +750,49 @@ module ActiveSupport
526
750
  case key
527
751
  when Array
528
752
  if key.size > 1
529
- key = key.collect { |element| expanded_key(element) }
753
+ key.collect { |element| expanded_key(element) }
530
754
  else
531
- key = key.first
755
+ expanded_key(key.first)
532
756
  end
533
757
  when Hash
534
- key = key.sort_by { |k, _| k.to_s }.collect { |k, v| "#{k}=#{v}" }
535
- end
758
+ key.collect { |k, v| "#{k}=#{v}" }.sort!
759
+ else
760
+ key
761
+ end.to_param
762
+ end
536
763
 
537
- key.to_param
764
+ def normalize_version(key, options = nil)
765
+ (options && options[:version].try(:to_param)) || expanded_version(key)
538
766
  end
539
767
 
540
- # Prefixes a key with the namespace. Namespace and key will be delimited
541
- # with a colon.
542
- def normalize_key(key, options)
543
- key = expanded_key(key)
544
- namespace = options[:namespace] if options
545
- prefix = namespace.is_a?(Proc) ? namespace.call : namespace
546
- key = "#{prefix}:#{key}" if prefix
547
- key
768
+ def expanded_version(key)
769
+ case
770
+ when key.respond_to?(:cache_version) then key.cache_version.to_param
771
+ when key.is_a?(Array) then key.map { |element| expanded_version(element) }.tap(&:compact!).to_param
772
+ when key.respond_to?(:to_a) then expanded_version(key.to_a)
773
+ end
548
774
  end
549
775
 
550
776
  def instrument(operation, key, options = nil)
551
- log { "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}" }
777
+ if logger && logger.debug? && !silence?
778
+ logger.debug "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}"
779
+ end
552
780
 
553
- payload = { key: key }
781
+ payload = { key: key, store: self.class.name }
554
782
  payload.merge!(options) if options.is_a?(Hash)
555
783
  ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) { yield(payload) }
556
784
  end
557
785
 
558
- def log
559
- return unless logger && logger.debug? && !silence?
560
- logger.debug(yield)
561
- end
562
-
563
786
  def handle_expired_entry(entry, key, options)
564
787
  if entry && entry.expired?
565
788
  race_ttl = options[:race_condition_ttl].to_i
566
789
  if (race_ttl > 0) && (Time.now.to_f - entry.expires_at <= race_ttl)
567
790
  # When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache
568
791
  # for a brief period while the entry is being recalculated.
569
- entry.expires_at = Time.now + race_ttl
792
+ entry.expires_at = Time.now.to_f + race_ttl
570
793
  write_entry(key, entry, expires_in: race_ttl * 2)
571
794
  else
572
- delete_entry(key, options)
795
+ delete_entry(key, **options)
573
796
  end
574
797
  entry = nil
575
798
  end
@@ -577,7 +800,7 @@ module ActiveSupport
577
800
  end
578
801
 
579
802
  def get_entry_value(entry, name, options)
580
- instrument(:fetch_hit, name, options) {}
803
+ instrument(:fetch_hit, name, options) { }
581
804
  entry.value
582
805
  end
583
806
 
@@ -586,39 +809,137 @@ module ActiveSupport
586
809
  yield(name)
587
810
  end
588
811
 
589
- write(name, result, options)
812
+ write(name, result, options) unless result.nil? && options[:skip_nil]
590
813
  result
591
814
  end
592
815
  end
593
816
 
594
- # This class is used to represent cache entries. Cache entries have a value and an optional
595
- # expiration time. The expiration time is used to support the :race_condition_ttl option
596
- # on the cache.
817
+ module NullCoder # :nodoc:
818
+ extend self
819
+
820
+ def dump(entry)
821
+ entry
822
+ end
823
+
824
+ def dump_compressed(entry, threshold)
825
+ entry.compressed(threshold)
826
+ end
827
+
828
+ def load(payload)
829
+ payload
830
+ end
831
+ end
832
+
833
+ module Coders # :nodoc:
834
+ MARK_61 = "\x04\b".b.freeze # The one set by Marshal.
835
+ MARK_70_UNCOMPRESSED = "\x00".b.freeze
836
+ MARK_70_COMPRESSED = "\x01".b.freeze
837
+
838
+ class << self
839
+ def [](version)
840
+ case version
841
+ when 6.1
842
+ Rails61Coder
843
+ when 7.0
844
+ Rails70Coder
845
+ else
846
+ raise ArgumentError, "Unknown ActiveSupport::Cache.format_version: #{Cache.format_version.inspect}"
847
+ end
848
+ end
849
+ end
850
+
851
+ module Loader
852
+ extend self
853
+
854
+ def load(payload)
855
+ if !payload.is_a?(String)
856
+ ActiveSupport::Cache::Store.logger&.warn %{Payload wasn't a string, was #{payload.class.name} - couldn't unmarshal, so returning nil."}
857
+
858
+ return nil
859
+ elsif payload.start_with?(MARK_70_UNCOMPRESSED)
860
+ members = Marshal.load(payload.byteslice(1..-1))
861
+ elsif payload.start_with?(MARK_70_COMPRESSED)
862
+ members = Marshal.load(Zlib::Inflate.inflate(payload.byteslice(1..-1)))
863
+ elsif payload.start_with?(MARK_61)
864
+ return Marshal.load(payload)
865
+ else
866
+ ActiveSupport::Cache::Store.logger&.warn %{Invalid cache prefix: #{payload.byteslice(0).inspect}, expected "\\x00" or "\\x01"}
867
+
868
+ return nil
869
+ end
870
+ Entry.unpack(members)
871
+ end
872
+ end
873
+
874
+ module Rails61Coder
875
+ include Loader
876
+ extend self
877
+
878
+ def dump(entry)
879
+ Marshal.dump(entry)
880
+ end
881
+
882
+ def dump_compressed(entry, threshold)
883
+ Marshal.dump(entry.compressed(threshold))
884
+ end
885
+ end
886
+
887
+ module Rails70Coder
888
+ include Loader
889
+ extend self
890
+
891
+ def dump(entry)
892
+ MARK_70_UNCOMPRESSED + Marshal.dump(entry.pack)
893
+ end
894
+
895
+ def dump_compressed(entry, threshold)
896
+ payload = Marshal.dump(entry.pack)
897
+ if payload.bytesize >= threshold
898
+ compressed_payload = Zlib::Deflate.deflate(payload)
899
+ if compressed_payload.bytesize < payload.bytesize
900
+ return MARK_70_COMPRESSED + compressed_payload
901
+ end
902
+ end
903
+
904
+ MARK_70_UNCOMPRESSED + payload
905
+ end
906
+ end
907
+ end
908
+
909
+ # This class is used to represent cache entries. Cache entries have a value, an optional
910
+ # expiration time, and an optional version. The expiration time is used to support the :race_condition_ttl option
911
+ # on the cache. The version is used to support the :version option on the cache for rejecting
912
+ # mismatches.
597
913
  #
598
914
  # Since cache entries in most instances will be serialized, the internals of this class are highly optimized
599
915
  # using short instance variable names that are lazily defined.
600
916
  class Entry # :nodoc:
601
- DEFAULT_COMPRESS_LIMIT = 16.kilobytes
602
-
603
- # Creates a new cache entry for the specified value. Options supported are
604
- # +:compress+, +:compress_threshold+, and +:expires_in+.
605
- def initialize(value, options = {})
606
- if should_compress?(value, options)
607
- @value = compress(value)
608
- @compressed = true
609
- else
610
- @value = value
917
+ class << self
918
+ def unpack(members)
919
+ new(members[0], expires_at: members[1], version: members[2])
611
920
  end
921
+ end
612
922
 
613
- @created_at = Time.now.to_f
614
- @expires_in = options[:expires_in]
615
- @expires_in = @expires_in.to_f if @expires_in
923
+ attr_reader :version
924
+
925
+ # Creates a new cache entry for the specified value. Options supported are
926
+ # +:compressed+, +:version+, +:expires_at+ and +:expires_in+.
927
+ def initialize(value, compressed: false, version: nil, expires_in: nil, expires_at: nil, **)
928
+ @value = value
929
+ @version = version
930
+ @created_at = 0.0
931
+ @expires_in = expires_at&.to_f || expires_in && (expires_in.to_f + Time.now.to_f)
932
+ @compressed = true if compressed
616
933
  end
617
934
 
618
935
  def value
619
936
  compressed? ? uncompress(@value) : @value
620
937
  end
621
938
 
939
+ def mismatched?(version)
940
+ @version && version && @version != version
941
+ end
942
+
622
943
  # Checks if the entry is expired. The +expires_in+ parameter can override
623
944
  # the value set when the entry was created.
624
945
  def expired?
@@ -638,20 +959,48 @@ module ActiveSupport
638
959
  end
639
960
 
640
961
  # Returns the size of the cached value. This could be less than
641
- # <tt>value.size</tt> if the data is compressed.
642
- def size
643
- if defined?(@s)
644
- @s
962
+ # <tt>value.bytesize</tt> if the data is compressed.
963
+ def bytesize
964
+ case value
965
+ when NilClass
966
+ 0
967
+ when String
968
+ @value.bytesize
645
969
  else
646
- case value
647
- when NilClass
648
- 0
649
- when String
650
- @value.bytesize
651
- else
652
- @s = Marshal.dump(@value).bytesize
970
+ @s ||= Marshal.dump(@value).bytesize
971
+ end
972
+ end
973
+
974
+ def compressed? # :nodoc:
975
+ defined?(@compressed)
976
+ end
977
+
978
+ def compressed(compress_threshold)
979
+ return self if compressed?
980
+
981
+ case @value
982
+ when nil, true, false, Numeric
983
+ uncompressed_size = 0
984
+ when String
985
+ uncompressed_size = @value.bytesize
986
+ else
987
+ serialized = Marshal.dump(@value)
988
+ uncompressed_size = serialized.bytesize
989
+ end
990
+
991
+ if uncompressed_size >= compress_threshold
992
+ serialized ||= Marshal.dump(@value)
993
+ compressed = Zlib::Deflate.deflate(serialized)
994
+
995
+ if compressed.bytesize < uncompressed_size
996
+ return Entry.new(compressed, compressed: true, expires_at: expires_at, version: version)
653
997
  end
654
998
  end
999
+ self
1000
+ end
1001
+
1002
+ def local?
1003
+ false
655
1004
  end
656
1005
 
657
1006
  # Duplicates the value in a class. This is used by cache implementations that don't natively
@@ -666,26 +1015,13 @@ module ActiveSupport
666
1015
  end
667
1016
  end
668
1017
 
669
- private
670
- def should_compress?(value, options)
671
- if value && options[:compress]
672
- compress_threshold = options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT
673
- serialized_value_size = (value.is_a?(String) ? value : Marshal.dump(value)).bytesize
674
-
675
- return true if serialized_value_size >= compress_threshold
676
- end
677
-
678
- false
679
- end
680
-
681
- def compressed?
682
- defined?(@compressed) ? @compressed : false
683
- end
684
-
685
- def compress(value)
686
- Zlib::Deflate.deflate(Marshal.dump(value))
687
- end
1018
+ def pack
1019
+ members = [value, expires_at, version]
1020
+ members.pop while !members.empty? && members.last.nil?
1021
+ members
1022
+ end
688
1023
 
1024
+ private
689
1025
  def uncompress(value)
690
1026
  Marshal.load(Zlib::Inflate.inflate(value))
691
1027
  end