activesupport 6.0.6.1 → 7.1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +865 -438
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -6
  5. data/lib/active_support/actionable_error.rb +4 -2
  6. data/lib/active_support/array_inquirer.rb +4 -2
  7. data/lib/active_support/backtrace_cleaner.rb +30 -10
  8. data/lib/active_support/benchmarkable.rb +4 -3
  9. data/lib/active_support/broadcast_logger.rb +250 -0
  10. data/lib/active_support/builder.rb +1 -1
  11. data/lib/active_support/cache/coder.rb +153 -0
  12. data/lib/active_support/cache/entry.rb +134 -0
  13. data/lib/active_support/cache/file_store.rb +53 -20
  14. data/lib/active_support/cache/mem_cache_store.rb +208 -63
  15. data/lib/active_support/cache/memory_store.rb +120 -38
  16. data/lib/active_support/cache/null_store.rb +16 -2
  17. data/lib/active_support/cache/redis_cache_store.rb +201 -208
  18. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +73 -66
  20. data/lib/active_support/cache.rb +539 -261
  21. data/lib/active_support/callbacks.rb +273 -142
  22. data/lib/active_support/code_generator.rb +65 -0
  23. data/lib/active_support/concern.rb +53 -7
  24. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +44 -7
  25. data/lib/active_support/concurrency/null_lock.rb +13 -0
  26. data/lib/active_support/concurrency/share_lock.rb +2 -2
  27. data/lib/active_support/configurable.rb +19 -6
  28. data/lib/active_support/configuration_file.rb +51 -0
  29. data/lib/active_support/core_ext/array/access.rb +1 -5
  30. data/lib/active_support/core_ext/array/conversions.rb +15 -13
  31. data/lib/active_support/core_ext/array/grouping.rb +6 -6
  32. data/lib/active_support/core_ext/array/inquiry.rb +2 -2
  33. data/lib/active_support/core_ext/benchmark.rb +2 -2
  34. data/lib/active_support/core_ext/big_decimal/conversions.rb +1 -1
  35. data/lib/active_support/core_ext/class/attribute.rb +34 -44
  36. data/lib/active_support/core_ext/class/subclasses.rb +19 -29
  37. data/lib/active_support/core_ext/date/blank.rb +1 -1
  38. data/lib/active_support/core_ext/date/calculations.rb +24 -9
  39. data/lib/active_support/core_ext/date/conversions.rb +18 -16
  40. data/lib/active_support/core_ext/date_and_time/calculations.rb +27 -4
  41. data/lib/active_support/core_ext/date_and_time/compatibility.rb +15 -0
  42. data/lib/active_support/core_ext/date_time/blank.rb +1 -1
  43. data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
  44. data/lib/active_support/core_ext/date_time/conversions.rb +19 -15
  45. data/lib/active_support/core_ext/digest/uuid.rb +30 -13
  46. data/lib/active_support/core_ext/enumerable.rb +146 -72
  47. data/lib/active_support/core_ext/erb/util.rb +196 -0
  48. data/lib/active_support/core_ext/file/atomic.rb +3 -1
  49. data/lib/active_support/core_ext/hash/conversions.rb +3 -4
  50. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  51. data/lib/active_support/core_ext/hash/deep_transform_values.rb +4 -4
  52. data/lib/active_support/core_ext/hash/indifferent_access.rb +3 -3
  53. data/lib/active_support/core_ext/hash/keys.rb +5 -5
  54. data/lib/active_support/core_ext/hash/slice.rb +3 -2
  55. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  56. data/lib/active_support/core_ext/kernel/reporting.rb +4 -4
  57. data/lib/active_support/core_ext/kernel/singleton_class.rb +1 -1
  58. data/lib/active_support/core_ext/load_error.rb +1 -1
  59. data/lib/active_support/core_ext/module/attr_internal.rb +2 -2
  60. data/lib/active_support/core_ext/module/attribute_accessors.rb +31 -29
  61. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +51 -20
  62. data/lib/active_support/core_ext/module/concerning.rb +14 -8
  63. data/lib/active_support/core_ext/module/delegation.rb +75 -42
  64. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  65. data/lib/active_support/core_ext/module/introspection.rb +1 -26
  66. data/lib/active_support/core_ext/name_error.rb +23 -2
  67. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  68. data/lib/active_support/core_ext/numeric/conversions.rb +82 -73
  69. data/lib/active_support/core_ext/object/acts_like.rb +29 -5
  70. data/lib/active_support/core_ext/object/blank.rb +2 -2
  71. data/lib/active_support/core_ext/object/deep_dup.rb +17 -1
  72. data/lib/active_support/core_ext/object/duplicable.rb +15 -4
  73. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  74. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  75. data/lib/active_support/core_ext/object/json.rb +52 -28
  76. data/lib/active_support/core_ext/object/to_query.rb +2 -4
  77. data/lib/active_support/core_ext/object/try.rb +20 -20
  78. data/lib/active_support/core_ext/object/with.rb +44 -0
  79. data/lib/active_support/core_ext/object/with_options.rb +25 -6
  80. data/lib/active_support/core_ext/object.rb +1 -0
  81. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  82. data/lib/active_support/core_ext/pathname/existence.rb +23 -0
  83. data/lib/active_support/core_ext/pathname.rb +4 -0
  84. data/lib/active_support/core_ext/range/compare_range.rb +6 -25
  85. data/lib/active_support/core_ext/range/conversions.rb +34 -13
  86. data/lib/active_support/core_ext/range/each.rb +1 -1
  87. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  88. data/lib/active_support/core_ext/range.rb +1 -2
  89. data/lib/active_support/core_ext/regexp.rb +8 -1
  90. data/lib/active_support/core_ext/securerandom.rb +25 -13
  91. data/lib/active_support/core_ext/string/access.rb +5 -24
  92. data/lib/active_support/core_ext/string/conversions.rb +3 -2
  93. data/lib/active_support/core_ext/string/filters.rb +21 -15
  94. data/lib/active_support/core_ext/string/indent.rb +1 -1
  95. data/lib/active_support/core_ext/string/inflections.rb +51 -10
  96. data/lib/active_support/core_ext/string/inquiry.rb +2 -1
  97. data/lib/active_support/core_ext/string/multibyte.rb +2 -2
  98. data/lib/active_support/core_ext/string/output_safety.rb +85 -194
  99. data/lib/active_support/core_ext/string/starts_ends_with.rb +2 -2
  100. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +6 -0
  101. data/lib/active_support/core_ext/symbol.rb +3 -0
  102. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  103. data/lib/active_support/core_ext/time/calculations.rb +46 -8
  104. data/lib/active_support/core_ext/time/conversions.rb +16 -13
  105. data/lib/active_support/core_ext/time/zones.rb +12 -28
  106. data/lib/active_support/core_ext.rb +2 -1
  107. data/lib/active_support/current_attributes/test_helper.rb +13 -0
  108. data/lib/active_support/current_attributes.rb +54 -22
  109. data/lib/active_support/deep_mergeable.rb +53 -0
  110. data/lib/active_support/dependencies/autoload.rb +17 -12
  111. data/lib/active_support/dependencies/interlock.rb +10 -18
  112. data/lib/active_support/dependencies/require_dependency.rb +28 -0
  113. data/lib/active_support/dependencies.rb +58 -769
  114. data/lib/active_support/deprecation/behaviors.rb +77 -38
  115. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  116. data/lib/active_support/deprecation/deprecators.rb +104 -0
  117. data/lib/active_support/deprecation/disallowed.rb +54 -0
  118. data/lib/active_support/deprecation/instance_delegator.rb +31 -5
  119. data/lib/active_support/deprecation/method_wrappers.rb +12 -28
  120. data/lib/active_support/deprecation/proxy_wrappers.rb +40 -25
  121. data/lib/active_support/deprecation/reporting.rb +76 -16
  122. data/lib/active_support/deprecation.rb +36 -4
  123. data/lib/active_support/deprecator.rb +7 -0
  124. data/lib/active_support/descendants_tracker.rb +150 -68
  125. data/lib/active_support/digest.rb +5 -3
  126. data/lib/active_support/duration/iso8601_parser.rb +3 -3
  127. data/lib/active_support/duration/iso8601_serializer.rb +24 -12
  128. data/lib/active_support/duration.rb +136 -56
  129. data/lib/active_support/encrypted_configuration.rb +72 -9
  130. data/lib/active_support/encrypted_file.rb +46 -13
  131. data/lib/active_support/environment_inquirer.rb +40 -0
  132. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  133. data/lib/active_support/error_reporter.rb +203 -0
  134. data/lib/active_support/evented_file_update_checker.rb +86 -137
  135. data/lib/active_support/execution_context/test_helper.rb +13 -0
  136. data/lib/active_support/execution_context.rb +53 -0
  137. data/lib/active_support/execution_wrapper.rb +31 -12
  138. data/lib/active_support/executor/test_helper.rb +7 -0
  139. data/lib/active_support/file_update_checker.rb +4 -2
  140. data/lib/active_support/fork_tracker.rb +79 -0
  141. data/lib/active_support/gem_version.rb +5 -5
  142. data/lib/active_support/gzip.rb +2 -0
  143. data/lib/active_support/hash_with_indifferent_access.rb +86 -42
  144. data/lib/active_support/html_safe_translation.rb +53 -0
  145. data/lib/active_support/i18n.rb +2 -1
  146. data/lib/active_support/i18n_railtie.rb +29 -27
  147. data/lib/active_support/inflector/inflections.rb +26 -9
  148. data/lib/active_support/inflector/methods.rb +54 -64
  149. data/lib/active_support/inflector/transliterate.rb +7 -5
  150. data/lib/active_support/isolated_execution_state.rb +76 -0
  151. data/lib/active_support/json/decoding.rb +6 -5
  152. data/lib/active_support/json/encoding.rb +31 -45
  153. data/lib/active_support/key_generator.rb +32 -7
  154. data/lib/active_support/lazy_load_hooks.rb +33 -7
  155. data/lib/active_support/locale/en.yml +10 -4
  156. data/lib/active_support/log_subscriber/test_helper.rb +2 -2
  157. data/lib/active_support/log_subscriber.rb +101 -32
  158. data/lib/active_support/logger.rb +9 -60
  159. data/lib/active_support/logger_silence.rb +2 -26
  160. data/lib/active_support/logger_thread_safe_level.rb +24 -25
  161. data/lib/active_support/message_encryptor.rb +205 -58
  162. data/lib/active_support/message_encryptors.rb +141 -0
  163. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  164. data/lib/active_support/message_pack/extensions.rb +292 -0
  165. data/lib/active_support/message_pack/serializer.rb +63 -0
  166. data/lib/active_support/message_pack.rb +50 -0
  167. data/lib/active_support/message_verifier.rb +237 -86
  168. data/lib/active_support/message_verifiers.rb +135 -0
  169. data/lib/active_support/messages/codec.rb +65 -0
  170. data/lib/active_support/messages/metadata.rb +112 -46
  171. data/lib/active_support/messages/rotation_configuration.rb +2 -1
  172. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  173. data/lib/active_support/messages/rotator.rb +35 -32
  174. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  175. data/lib/active_support/multibyte/chars.rb +15 -52
  176. data/lib/active_support/multibyte/unicode.rb +8 -122
  177. data/lib/active_support/multibyte.rb +1 -1
  178. data/lib/active_support/notifications/fanout.rb +310 -105
  179. data/lib/active_support/notifications/instrumenter.rb +113 -48
  180. data/lib/active_support/notifications.rb +56 -29
  181. data/lib/active_support/number_helper/number_converter.rb +15 -8
  182. data/lib/active_support/number_helper/number_to_currency_converter.rb +11 -6
  183. data/lib/active_support/number_helper/number_to_delimited_converter.rb +1 -1
  184. data/lib/active_support/number_helper/number_to_human_converter.rb +1 -1
  185. data/lib/active_support/number_helper/number_to_human_size_converter.rb +5 -5
  186. data/lib/active_support/number_helper/number_to_phone_converter.rb +2 -1
  187. data/lib/active_support/number_helper/number_to_rounded_converter.rb +9 -5
  188. data/lib/active_support/number_helper/rounding_helper.rb +12 -32
  189. data/lib/active_support/number_helper.rb +379 -304
  190. data/lib/active_support/option_merger.rb +11 -18
  191. data/lib/active_support/ordered_hash.rb +4 -4
  192. data/lib/active_support/ordered_options.rb +23 -3
  193. data/lib/active_support/parameter_filter.rb +104 -75
  194. data/lib/active_support/proxy_object.rb +2 -0
  195. data/lib/active_support/rails.rb +1 -4
  196. data/lib/active_support/railtie.rb +90 -6
  197. data/lib/active_support/reloader.rb +12 -4
  198. data/lib/active_support/rescuable.rb +18 -16
  199. data/lib/active_support/ruby_features.rb +7 -0
  200. data/lib/active_support/secure_compare_rotator.rb +58 -0
  201. data/lib/active_support/security_utils.rb +19 -12
  202. data/lib/active_support/string_inquirer.rb +5 -3
  203. data/lib/active_support/subscriber.rb +23 -47
  204. data/lib/active_support/syntax_error_proxy.rb +70 -0
  205. data/lib/active_support/tagged_logging.rb +84 -23
  206. data/lib/active_support/test_case.rb +166 -27
  207. data/lib/active_support/testing/assertions.rb +73 -20
  208. data/lib/active_support/testing/autorun.rb +0 -2
  209. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  210. data/lib/active_support/testing/deprecation.rb +53 -2
  211. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  212. data/lib/active_support/testing/isolation.rb +30 -29
  213. data/lib/active_support/testing/method_call_assertions.rb +24 -11
  214. data/lib/active_support/testing/parallelization/server.rb +82 -0
  215. data/lib/active_support/testing/parallelization/worker.rb +103 -0
  216. data/lib/active_support/testing/parallelization.rb +16 -95
  217. data/lib/active_support/testing/parallelize_executor.rb +81 -0
  218. data/lib/active_support/testing/stream.rb +4 -6
  219. data/lib/active_support/testing/strict_warnings.rb +39 -0
  220. data/lib/active_support/testing/tagged_logging.rb +1 -1
  221. data/lib/active_support/testing/time_helpers.rb +89 -19
  222. data/lib/active_support/time_with_zone.rb +105 -70
  223. data/lib/active_support/values/time_zone.rb +59 -26
  224. data/lib/active_support/version.rb +1 -1
  225. data/lib/active_support/xml_mini/jdom.rb +4 -11
  226. data/lib/active_support/xml_mini/libxml.rb +5 -5
  227. data/lib/active_support/xml_mini/libxmlsax.rb +1 -1
  228. data/lib/active_support/xml_mini/nokogiri.rb +5 -5
  229. data/lib/active_support/xml_mini/nokogirisax.rb +2 -2
  230. data/lib/active_support/xml_mini/rexml.rb +9 -2
  231. data/lib/active_support/xml_mini.rb +7 -6
  232. data/lib/active_support.rb +40 -1
  233. metadata +127 -40
  234. data/lib/active_support/core_ext/array/prepend_and_append.rb +0 -5
  235. data/lib/active_support/core_ext/hash/compact.rb +0 -5
  236. data/lib/active_support/core_ext/hash/transform_values.rb +0 -5
  237. data/lib/active_support/core_ext/marshal.rb +0 -24
  238. data/lib/active_support/core_ext/module/reachable.rb +0 -6
  239. data/lib/active_support/core_ext/numeric/inquiry.rb +0 -5
  240. data/lib/active_support/core_ext/range/include_range.rb +0 -9
  241. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -23
  242. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  243. data/lib/active_support/core_ext/uri.rb +0 -25
  244. data/lib/active_support/dependencies/zeitwerk_integration.rb +0 -117
  245. data/lib/active_support/per_thread_registry.rb +0 -60
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ module ActiveSupport
6
+ module Cache
7
+ # This class is used to represent cache entries. Cache entries have a value, an optional
8
+ # expiration time, and an optional version. The expiration time is used to support the :race_condition_ttl option
9
+ # on the cache. The version is used to support the :version option on the cache for rejecting
10
+ # mismatches.
11
+ #
12
+ # Since cache entries in most instances will be serialized, the internals of this class are highly optimized
13
+ # using short instance variable names that are lazily defined.
14
+ class Entry # :nodoc:
15
+ class << self
16
+ def unpack(members)
17
+ new(members[0], expires_at: members[1], version: members[2])
18
+ end
19
+ end
20
+
21
+ attr_reader :version
22
+
23
+ # Creates a new cache entry for the specified value. Options supported are
24
+ # +:compressed+, +:version+, +:expires_at+ and +:expires_in+.
25
+ def initialize(value, compressed: false, version: nil, expires_in: nil, expires_at: nil, **)
26
+ @value = value
27
+ @version = version
28
+ @created_at = 0.0
29
+ @expires_in = expires_at&.to_f || expires_in && (expires_in.to_f + Time.now.to_f)
30
+ @compressed = true if compressed
31
+ end
32
+
33
+ def value
34
+ compressed? ? uncompress(@value) : @value
35
+ end
36
+
37
+ def mismatched?(version)
38
+ @version && version && @version != version
39
+ end
40
+
41
+ # Checks if the entry is expired. The +expires_in+ parameter can override
42
+ # the value set when the entry was created.
43
+ def expired?
44
+ @expires_in && @created_at + @expires_in <= Time.now.to_f
45
+ end
46
+
47
+ def expires_at
48
+ @expires_in ? @created_at + @expires_in : nil
49
+ end
50
+
51
+ def expires_at=(value)
52
+ if value
53
+ @expires_in = value.to_f - @created_at
54
+ else
55
+ @expires_in = nil
56
+ end
57
+ end
58
+
59
+ # Returns the size of the cached value. This could be less than
60
+ # <tt>value.bytesize</tt> if the data is compressed.
61
+ def bytesize
62
+ case value
63
+ when NilClass
64
+ 0
65
+ when String
66
+ @value.bytesize
67
+ else
68
+ @s ||= Marshal.dump(@value).bytesize
69
+ end
70
+ end
71
+
72
+ def compressed? # :nodoc:
73
+ defined?(@compressed)
74
+ end
75
+
76
+ def compressed(compress_threshold)
77
+ return self if compressed?
78
+
79
+ case @value
80
+ when nil, true, false, Numeric
81
+ uncompressed_size = 0
82
+ when String
83
+ uncompressed_size = @value.bytesize
84
+ else
85
+ serialized = Marshal.dump(@value)
86
+ uncompressed_size = serialized.bytesize
87
+ end
88
+
89
+ if uncompressed_size >= compress_threshold
90
+ serialized ||= Marshal.dump(@value)
91
+ compressed = Zlib::Deflate.deflate(serialized)
92
+
93
+ if compressed.bytesize < uncompressed_size
94
+ return Entry.new(compressed, compressed: true, expires_at: expires_at, version: version)
95
+ end
96
+ end
97
+ self
98
+ end
99
+
100
+ def local?
101
+ false
102
+ end
103
+
104
+ # Duplicates the value in a class. This is used by cache implementations that don't natively
105
+ # serialize entries to protect against accidental cache modifications.
106
+ def dup_value!
107
+ if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false)
108
+ if @value.is_a?(String)
109
+ @value = @value.dup
110
+ else
111
+ @value = Marshal.load(Marshal.dump(@value))
112
+ end
113
+ end
114
+ end
115
+
116
+ def pack
117
+ members = [value, expires_at, version]
118
+ members.pop while !members.empty? && members.last.nil?
119
+ members
120
+ end
121
+
122
+ private
123
+ def uncompress(value)
124
+ marshal_load(Zlib::Inflate.inflate(value))
125
+ end
126
+
127
+ def marshal_load(payload)
128
+ Marshal.load(payload)
129
+ rescue ArgumentError => error
130
+ raise Cache::DeserializationError, error.message
131
+ end
132
+ end
133
+ end
134
+ end
@@ -1,18 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/marshal"
4
3
  require "active_support/core_ext/file/atomic"
5
4
  require "active_support/core_ext/string/conversions"
6
5
  require "uri/common"
7
6
 
8
7
  module ActiveSupport
9
8
  module Cache
10
- # A cache store implementation which stores everything on the filesystem.
9
+ # = \File \Cache \Store
11
10
  #
12
- # FileStore implements the Strategy::LocalCache strategy which implements
13
- # an in-memory cache inside of a block.
11
+ # A cache store implementation which stores everything on the filesystem.
14
12
  class FileStore < Store
15
- prepend Strategy::LocalCache
16
13
  attr_reader :cache_path
17
14
 
18
15
  DIR_FORMATTER = "%03X"
@@ -20,7 +17,7 @@ module ActiveSupport
20
17
  FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room
21
18
  GITKEEP_FILES = [".gitkeep", ".keep"].freeze
22
19
 
23
- def initialize(cache_path, options = nil)
20
+ def initialize(cache_path, **options)
24
21
  super(options)
25
22
  @cache_path = cache_path.to_s
26
23
  end
@@ -48,14 +45,33 @@ module ActiveSupport
48
45
  end
49
46
  end
50
47
 
51
- # Increments an already existing integer value that is stored in the cache.
52
- # If the key is not found nothing is done.
48
+ # Increment a cached integer value. Returns the updated value.
49
+ #
50
+ # If the key is unset, it starts from +0+:
51
+ #
52
+ # cache.increment("foo") # => 1
53
+ # cache.increment("bar", 100) # => 100
54
+ #
55
+ # To set a specific value, call #write:
56
+ #
57
+ # cache.write("baz", 5)
58
+ # cache.increment("baz") # => 6
59
+ #
53
60
  def increment(name, amount = 1, options = nil)
54
61
  modify_value(name, amount, options)
55
62
  end
56
63
 
57
- # Decrements an already existing integer value that is stored in the cache.
58
- # If the key is not found nothing is done.
64
+ # Decrement a cached integer value. Returns the updated value.
65
+ #
66
+ # If the key is unset, it will be set to +-amount+.
67
+ #
68
+ # cache.decrement("foo") # => -1
69
+ #
70
+ # To set a specific value, call #write:
71
+ #
72
+ # cache.write("baz", 5)
73
+ # cache.decrement("baz") # => 4
74
+ #
59
75
  def decrement(name, amount = 1, options = nil)
60
76
  modify_value(name, -amount, options)
61
77
  end
@@ -71,21 +87,33 @@ module ActiveSupport
71
87
  end
72
88
  end
73
89
 
90
+ def inspect # :nodoc:
91
+ "#<#{self.class.name} cache_path=#{@cache_path}, options=#{@options.inspect}>"
92
+ end
93
+
74
94
  private
75
95
  def read_entry(key, **options)
76
- if File.exist?(key)
77
- entry = File.open(key) { |f| Marshal.load(f) }
96
+ if payload = read_serialized_entry(key, **options)
97
+ entry = deserialize_entry(payload)
78
98
  entry if entry.is_a?(Cache::Entry)
79
99
  end
80
- rescue => e
81
- logger.error("FileStoreError (#{e}): #{e.message}") if logger
100
+ end
101
+
102
+ def read_serialized_entry(key, **)
103
+ File.binread(key) if File.exist?(key)
104
+ rescue => error
105
+ logger.error("FileStoreError (#{error}): #{error.message}") if logger
82
106
  nil
83
107
  end
84
108
 
85
109
  def write_entry(key, entry, **options)
110
+ write_serialized_entry(key, serialize_entry(entry, **options), **options)
111
+ end
112
+
113
+ def write_serialized_entry(key, payload, **options)
86
114
  return false if options[:unless_exist] && File.exist?(key)
87
115
  ensure_cache_path(File.dirname(key))
88
- File.atomic_write(key, cache_path) { |f| Marshal.dump(entry, f) }
116
+ File.atomic_write(key, cache_path) { |f| f.write(payload) }
89
117
  true
90
118
  end
91
119
 
@@ -95,11 +123,13 @@ module ActiveSupport
95
123
  File.delete(key)
96
124
  delete_empty_directories(File.dirname(key))
97
125
  true
98
- rescue => e
126
+ rescue
99
127
  # Just in case the error was caused by another process deleting the file first.
100
- raise e if File.exist?(key)
128
+ raise if File.exist?(key)
101
129
  false
102
130
  end
131
+ else
132
+ false
103
133
  end
104
134
  end
105
135
 
@@ -146,7 +176,7 @@ module ActiveSupport
146
176
 
147
177
  # Translate a file path into a key.
148
178
  def file_path_key(path)
149
- fname = path[cache_path.to_s.size..-1].split(File::SEPARATOR, 4).last
179
+ fname = path[cache_path.to_s.size..-1].split(File::SEPARATOR, 4).last.delete(File::SEPARATOR)
150
180
  URI.decode_www_form_component(fname, Encoding::UTF_8)
151
181
  end
152
182
 
@@ -176,8 +206,8 @@ module ActiveSupport
176
206
  end
177
207
  end
178
208
 
179
- # Modifies the amount of an already existing integer value that is stored in the cache.
180
- # If the key is not found nothing is done.
209
+ # Modifies the amount of an integer value that is stored in the cache.
210
+ # If the key is not found it is created and set to +amount+.
181
211
  def modify_value(name, amount, options)
182
212
  file_name = normalize_key(name, options)
183
213
 
@@ -188,6 +218,9 @@ module ActiveSupport
188
218
  num = num.to_i + amount
189
219
  write(name, num, options)
190
220
  num
221
+ else
222
+ write(name, Integer(amount), options)
223
+ amount
191
224
  end
192
225
  end
193
226
  end
@@ -3,15 +3,20 @@
3
3
  begin
4
4
  require "dalli"
5
5
  rescue LoadError => e
6
- $stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
6
+ warn "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
7
7
  raise e
8
8
  end
9
9
 
10
- require "active_support/core_ext/marshal"
10
+ require "connection_pool"
11
+ require "delegate"
12
+ require "active_support/core_ext/enumerable"
11
13
  require "active_support/core_ext/array/extract_options"
14
+ require "active_support/core_ext/numeric/time"
12
15
 
13
16
  module ActiveSupport
14
17
  module Cache
18
+ # = Memcached \Cache \Store
19
+ #
15
20
  # A cache store implementation which stores data in Memcached:
16
21
  # https://memcached.org
17
22
  #
@@ -19,25 +24,15 @@ module ActiveSupport
19
24
  #
20
25
  # Special features:
21
26
  # - Clustering and load balancing. One can specify multiple memcached servers,
22
- # and MemCacheStore will load balance between all available servers. If a
23
- # server goes down, then MemCacheStore will ignore it until it comes back up.
27
+ # and +MemCacheStore+ will load balance between all available servers. If a
28
+ # server goes down, then +MemCacheStore+ will ignore it until it comes back up.
24
29
  #
25
- # MemCacheStore implements the Strategy::LocalCache strategy which implements
26
- # an in-memory cache inside of a block.
30
+ # +MemCacheStore+ implements the Strategy::LocalCache strategy which
31
+ # implements an in-memory cache inside of a block.
27
32
  class MemCacheStore < Store
28
- # Provide support for raw values in the local cache strategy.
29
- module LocalCacheWithRaw # :nodoc:
30
- private
31
- def write_entry(key, entry, **options)
32
- if options[:raw] && local_cache
33
- raw_entry = Entry.new(entry.value.to_s)
34
- raw_entry.expires_at = entry.expires_at
35
- super(key, raw_entry, **options)
36
- else
37
- super
38
- end
39
- end
40
- end
33
+ # These options represent behavior overridden by this implementation and should
34
+ # not be allowed to get down to the Dalli client
35
+ OVERRIDDEN_OPTIONS = UNIVERSAL_OPTIONS
41
36
 
42
37
  # Advertise cache versioning support.
43
38
  def self.supports_cache_versioning?
@@ -45,78 +40,172 @@ module ActiveSupport
45
40
  end
46
41
 
47
42
  prepend Strategy::LocalCache
48
- prepend LocalCacheWithRaw
49
43
 
44
+ module DupLocalCache
45
+ class DupLocalStore < DelegateClass(Strategy::LocalCache::LocalStore)
46
+ def write_entry(_key, entry)
47
+ if entry.is_a?(Entry)
48
+ entry.dup_value!
49
+ end
50
+ super
51
+ end
52
+
53
+ def fetch_entry(key)
54
+ entry = super do
55
+ new_entry = yield
56
+ if entry.is_a?(Entry)
57
+ new_entry.dup_value!
58
+ end
59
+ new_entry
60
+ end
61
+ entry = entry.dup
62
+
63
+ if entry.is_a?(Entry)
64
+ entry.dup_value!
65
+ end
66
+
67
+ entry
68
+ end
69
+ end
70
+
71
+ private
72
+ def local_cache
73
+ if ActiveSupport::Cache.format_version == 6.1
74
+ if local_cache = super
75
+ DupLocalStore.new(local_cache)
76
+ end
77
+ else
78
+ super
79
+ end
80
+ end
81
+ end
82
+ prepend DupLocalCache
83
+
84
+ KEY_MAX_SIZE = 250
50
85
  ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n
51
86
 
52
87
  # Creates a new Dalli::Client instance with specified addresses and options.
53
- # By default address is equal localhost:11211.
88
+ # If no addresses are provided, we give nil to Dalli::Client, so it uses its fallbacks:
89
+ # - ENV["MEMCACHE_SERVERS"] (if defined)
90
+ # - "127.0.0.1:11211" (otherwise)
54
91
  #
55
92
  # ActiveSupport::Cache::MemCacheStore.build_mem_cache
56
- # # => #<Dalli::Client:0x007f98a47d2028 @servers=["localhost:11211"], @options={}, @ring=nil>
93
+ # # => #<Dalli::Client:0x007f98a47d2028 @servers=["127.0.0.1:11211"], @options={}, @ring=nil>
57
94
  # ActiveSupport::Cache::MemCacheStore.build_mem_cache('localhost:10290')
58
95
  # # => #<Dalli::Client:0x007f98a47b3a60 @servers=["localhost:10290"], @options={}, @ring=nil>
59
96
  def self.build_mem_cache(*addresses) # :nodoc:
60
97
  addresses = addresses.flatten
61
98
  options = addresses.extract_options!
62
- addresses = ["localhost:11211"] if addresses.empty?
99
+ addresses = nil if addresses.compact.empty?
63
100
  pool_options = retrieve_pool_options(options)
64
101
 
65
- if pool_options.empty?
66
- Dalli::Client.new(addresses, options)
67
- else
68
- ensure_connection_pool_added!
102
+ if pool_options
69
103
  ConnectionPool.new(pool_options) { Dalli::Client.new(addresses, options.merge(threadsafe: false)) }
104
+ else
105
+ Dalli::Client.new(addresses, options)
70
106
  end
71
107
  end
72
108
 
73
- # Creates a new MemCacheStore object, with the given memcached server
109
+ # Creates a new +MemCacheStore+ object, with the given memcached server
74
110
  # addresses. Each address is either a host name, or a host-with-port string
75
111
  # in the form of "host_name:port". For example:
76
112
  #
77
113
  # ActiveSupport::Cache::MemCacheStore.new("localhost", "server-downstairs.localnetwork:8229")
78
114
  #
79
- # If no addresses are specified, then MemCacheStore will connect to
80
- # localhost port 11211 (the default memcached port).
115
+ # If no addresses are provided, but <tt>ENV['MEMCACHE_SERVERS']</tt> is defined, it will be used instead. Otherwise,
116
+ # +MemCacheStore+ will connect to localhost:11211 (the default memcached port).
117
+ # Passing a +Dalli::Client+ instance is deprecated and will be removed. Please pass an address instead.
81
118
  def initialize(*addresses)
82
119
  addresses = addresses.flatten
83
120
  options = addresses.extract_options!
121
+ if options.key?(:cache_nils)
122
+ options[:skip_nil] = !options.delete(:cache_nils)
123
+ end
84
124
  super(options)
85
125
 
86
126
  unless [String, Dalli::Client, NilClass].include?(addresses.first.class)
87
- raise ArgumentError, "First argument must be an empty array, an array of hosts or a Dalli::Client instance."
127
+ raise ArgumentError, "First argument must be an empty array, address, or array of addresses."
88
128
  end
89
129
  if addresses.first.is_a?(Dalli::Client)
130
+ ActiveSupport.deprecator.warn(<<~MSG)
131
+ Initializing MemCacheStore with a Dalli::Client is deprecated and will be removed in Rails 7.2.
132
+ Use memcached server addresses instead.
133
+ MSG
90
134
  @data = addresses.first
91
135
  else
92
- mem_cache_options = options.dup
93
- UNIVERSAL_OPTIONS.each { |name| mem_cache_options.delete(name) }
94
- @data = self.class.build_mem_cache(*(addresses + [mem_cache_options]))
136
+ @mem_cache_options = options.dup
137
+ # The value "compress: false" prevents duplicate compression within Dalli.
138
+ @mem_cache_options[:compress] = false
139
+ (OVERRIDDEN_OPTIONS - %i(compress)).each { |name| @mem_cache_options.delete(name) }
140
+ @data = self.class.build_mem_cache(*(addresses + [@mem_cache_options]))
95
141
  end
96
142
  end
97
143
 
98
- # Increment a cached value. This method uses the memcached incr atomic
99
- # operator and can only be used on values written with the :raw option.
100
- # Calling it on a value not stored with :raw will initialize that value
101
- # to zero.
144
+ def inspect
145
+ instance = @data || @mem_cache_options
146
+ "#<#{self.class} options=#{options.inspect} mem_cache=#{instance.inspect}>"
147
+ end
148
+
149
+ ##
150
+ # :method: write
151
+ # :call-seq: write(name, value, options = nil)
152
+ #
153
+ # Behaves the same as ActiveSupport::Cache::Store#write, but supports
154
+ # additional options specific to memcached.
155
+ #
156
+ # ==== Additional Options
157
+ #
158
+ # * <tt>raw: true</tt> - Sends the value directly to the server as raw
159
+ # bytes. The value must be a string or number. You can use memcached
160
+ # direct operations like +increment+ and +decrement+ only on raw values.
161
+ #
162
+ # * <tt>unless_exist: true</tt> - Prevents overwriting an existing cache
163
+ # entry.
164
+
165
+ # Increment a cached integer value using the memcached incr atomic operator.
166
+ # Returns the updated value.
167
+ #
168
+ # If the key is unset or has expired, it will be set to +amount+:
169
+ #
170
+ # cache.increment("foo") # => 1
171
+ # cache.increment("bar", 100) # => 100
172
+ #
173
+ # To set a specific value, call #write passing <tt>raw: true</tt>:
174
+ #
175
+ # cache.write("baz", 5, raw: true)
176
+ # cache.increment("baz") # => 6
177
+ #
178
+ # Incrementing a non-numeric value, or a value written without
179
+ # <tt>raw: true</tt>, will fail and return +nil+.
102
180
  def increment(name, amount = 1, options = nil)
103
181
  options = merged_options(options)
104
182
  instrument(:increment, name, amount: amount) do
105
183
  rescue_error_with nil do
106
- @data.with { |c| c.incr(normalize_key(name, options), amount, options[:expires_in]) }
184
+ @data.with { |c| c.incr(normalize_key(name, options), amount, options[:expires_in], amount) }
107
185
  end
108
186
  end
109
187
  end
110
188
 
111
- # Decrement a cached value. This method uses the memcached decr atomic
112
- # operator and can only be used on values written with the :raw option.
113
- # Calling it on a value not stored with :raw will initialize that value
114
- # to zero.
189
+ # Decrement a cached integer value using the memcached decr atomic operator.
190
+ # Returns the updated value.
191
+ #
192
+ # If the key is unset or has expired, it will be set to 0. Memcached
193
+ # does not support negative counters.
194
+ #
195
+ # cache.decrement("foo") # => 0
196
+ #
197
+ # To set a specific value, call #write passing <tt>raw: true</tt>:
198
+ #
199
+ # cache.write("baz", 5, raw: true)
200
+ # cache.decrement("baz") # => 4
201
+ #
202
+ # Decrementing a non-numeric value, or a value written without
203
+ # <tt>raw: true</tt>, will fail and return +nil+.
115
204
  def decrement(name, amount = 1, options = nil)
116
205
  options = merged_options(options)
117
206
  instrument(:decrement, name, amount: amount) do
118
207
  rescue_error_with nil do
119
- @data.with { |c| c.decr(normalize_key(name, options), amount, options[:expires_in]) }
208
+ @data.with { |c| c.decr(normalize_key(name, options), amount, options[:expires_in], 0) }
120
209
  end
121
210
  end
122
211
  end
@@ -133,37 +222,70 @@ module ActiveSupport
133
222
  end
134
223
 
135
224
  private
225
+ def default_serializer
226
+ if Cache.format_version == 6.1
227
+ ActiveSupport.deprecator.warn <<~EOM
228
+ Support for `config.active_support.cache_format_version = 6.1` has been deprecated and will be removed in Rails 7.2.
229
+
230
+ Check the Rails upgrade guide at https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#new-activesupport-cache-serialization-format
231
+ for more information on how to upgrade.
232
+ EOM
233
+ Cache::SerializerWithFallback[:passthrough]
234
+ else
235
+ super
236
+ end
237
+ end
238
+
136
239
  # Read an entry from the cache.
137
240
  def read_entry(key, **options)
138
- rescue_error_with(nil) { deserialize_entry(@data.with { |c| c.get(key, options) }) }
241
+ deserialize_entry(read_serialized_entry(key, **options), **options)
242
+ end
243
+
244
+ def read_serialized_entry(key, **options)
245
+ rescue_error_with(nil) do
246
+ @data.with { |c| c.get(key, options) }
247
+ end
139
248
  end
140
249
 
141
250
  # Write an entry to the cache.
142
251
  def write_entry(key, entry, **options)
143
- method = options && options[:unless_exist] ? :add : :set
144
- value = options[:raw] ? entry.value.to_s : entry
252
+ write_serialized_entry(key, serialize_entry(entry, **options), **options)
253
+ end
254
+
255
+ def write_serialized_entry(key, payload, **options)
256
+ method = options[:unless_exist] ? :add : :set
145
257
  expires_in = options[:expires_in].to_i
146
- if expires_in > 0 && !options[:raw]
258
+ if options[:race_condition_ttl] && expires_in > 0 && !options[:raw]
147
259
  # Set the memcache expire a few minutes in the future to support race condition ttls on read
148
260
  expires_in += 5.minutes
149
261
  end
150
262
  rescue_error_with false do
151
- @data.with { |c| c.send(method, key, value, expires_in, **options) }
263
+ # Don't pass compress option to Dalli since we are already dealing with compression.
264
+ options.delete(:compress)
265
+ @data.with { |c| c.send(method, key, payload, expires_in, **options) }
152
266
  end
153
267
  end
154
268
 
155
269
  # Reads multiple entries from the cache implementation.
156
270
  def read_multi_entries(names, **options)
157
- keys_to_names = Hash[names.map { |name| [normalize_key(name, options), name] }]
271
+ keys_to_names = names.index_by { |name| normalize_key(name, options) }
272
+
273
+ raw_values = begin
274
+ @data.with { |c| c.get_multi(keys_to_names.keys) }
275
+ rescue Dalli::UnmarshalError
276
+ {}
277
+ end
158
278
 
159
- raw_values = @data.with { |c| c.get_multi(keys_to_names.keys) }
160
279
  values = {}
161
280
 
162
281
  raw_values.each do |key, value|
163
- entry = deserialize_entry(value)
282
+ entry = deserialize_entry(value, raw: options[:raw])
164
283
 
165
- unless entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
166
- values[keys_to_names[key]] = entry.value
284
+ unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
285
+ begin
286
+ values[keys_to_names[key]] = entry.value
287
+ rescue DeserializationError
288
+ end
167
289
  end
168
290
  end
169
291
 
@@ -175,27 +297,50 @@ module ActiveSupport
175
297
  rescue_error_with(false) { @data.with { |c| c.delete(key) } }
176
298
  end
177
299
 
300
+ def serialize_entry(entry, raw: false, **options)
301
+ if raw
302
+ entry.value.to_s
303
+ else
304
+ super(entry, raw: raw, **options)
305
+ end
306
+ end
307
+
178
308
  # Memcache keys are binaries. So we need to force their encoding to binary
179
309
  # before applying the regular expression to ensure we are escaping all
180
310
  # characters properly.
181
311
  def normalize_key(key, options)
182
- key = super.dup
183
- key = key.force_encoding(Encoding::ASCII_8BIT)
184
- key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
185
- key = "#{key[0, 213]}:md5:#{ActiveSupport::Digest.hexdigest(key)}" if key.size > 250
312
+ key = super
313
+ if key
314
+ key = key.dup.force_encoding(Encoding::ASCII_8BIT)
315
+ key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
316
+
317
+ if key.size > KEY_MAX_SIZE
318
+ key_separator = ":hash:"
319
+ key_hash = ActiveSupport::Digest.hexdigest(key)
320
+ key_trim_size = KEY_MAX_SIZE - key_separator.size - key_hash.size
321
+ key = "#{key[0, key_trim_size]}#{key_separator}#{key_hash}"
322
+ end
323
+ end
186
324
  key
187
325
  end
188
326
 
189
- def deserialize_entry(entry)
190
- if entry
191
- entry.is_a?(Entry) ? entry : Entry.new(entry)
327
+ def deserialize_entry(payload, raw: false, **)
328
+ if payload && raw
329
+ Entry.new(payload)
330
+ else
331
+ super(payload)
192
332
  end
193
333
  end
194
334
 
195
335
  def rescue_error_with(fallback)
196
336
  yield
197
- rescue Dalli::DalliError => e
198
- logger.error("DalliError (#{e}): #{e.message}") if logger
337
+ rescue Dalli::DalliError => error
338
+ logger.error("DalliError (#{error}): #{error.message}") if logger
339
+ ActiveSupport.error_reporter&.report(
340
+ error,
341
+ severity: :warning,
342
+ source: "mem_cache_store.active_support",
343
+ )
199
344
  fallback
200
345
  end
201
346
  end