activesupport 6.1.0 → 7.1.5.1

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 (225) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1075 -325
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +7 -7
  5. data/lib/active_support/actionable_error.rb +4 -2
  6. data/lib/active_support/array_inquirer.rb +2 -2
  7. data/lib/active_support/backtrace_cleaner.rb +32 -7
  8. data/lib/active_support/benchmarkable.rb +3 -2
  9. data/lib/active_support/broadcast_logger.rb +251 -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 +201 -62
  15. data/lib/active_support/cache/memory_store.rb +86 -24
  16. data/lib/active_support/cache/null_store.rb +16 -2
  17. data/lib/active_support/cache/redis_cache_store.rb +186 -193
  18. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +63 -71
  20. data/lib/active_support/cache.rb +487 -249
  21. data/lib/active_support/callbacks.rb +227 -105
  22. data/lib/active_support/code_generator.rb +70 -0
  23. data/lib/active_support/concern.rb +9 -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 +18 -5
  28. data/lib/active_support/configuration_file.rb +7 -2
  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/big_decimal/conversions.rb +1 -1
  34. data/lib/active_support/core_ext/class/subclasses.rb +37 -26
  35. data/lib/active_support/core_ext/date/blank.rb +1 -1
  36. data/lib/active_support/core_ext/date/calculations.rb +24 -9
  37. data/lib/active_support/core_ext/date/conversions.rb +16 -15
  38. data/lib/active_support/core_ext/date_and_time/calculations.rb +14 -4
  39. data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
  40. data/lib/active_support/core_ext/date_time/blank.rb +1 -1
  41. data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
  42. data/lib/active_support/core_ext/date_time/conversions.rb +19 -15
  43. data/lib/active_support/core_ext/digest/uuid.rb +30 -13
  44. data/lib/active_support/core_ext/enumerable.rb +85 -83
  45. data/lib/active_support/core_ext/erb/util.rb +196 -0
  46. data/lib/active_support/core_ext/file/atomic.rb +3 -1
  47. data/lib/active_support/core_ext/hash/conversions.rb +1 -2
  48. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  49. data/lib/active_support/core_ext/hash/deep_transform_values.rb +3 -3
  50. data/lib/active_support/core_ext/hash/indifferent_access.rb +3 -3
  51. data/lib/active_support/core_ext/hash/keys.rb +4 -4
  52. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  53. data/lib/active_support/core_ext/kernel/reporting.rb +4 -4
  54. data/lib/active_support/core_ext/kernel/singleton_class.rb +1 -1
  55. data/lib/active_support/core_ext/module/attribute_accessors.rb +8 -0
  56. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +49 -22
  57. data/lib/active_support/core_ext/module/concerning.rb +6 -6
  58. data/lib/active_support/core_ext/module/delegation.rb +81 -43
  59. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  60. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  61. data/lib/active_support/core_ext/name_error.rb +2 -8
  62. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  63. data/lib/active_support/core_ext/numeric/conversions.rb +82 -77
  64. data/lib/active_support/core_ext/object/acts_like.rb +29 -5
  65. data/lib/active_support/core_ext/object/blank.rb +2 -2
  66. data/lib/active_support/core_ext/object/deep_dup.rb +17 -1
  67. data/lib/active_support/core_ext/object/duplicable.rb +31 -11
  68. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  69. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  70. data/lib/active_support/core_ext/object/json.rb +49 -27
  71. data/lib/active_support/core_ext/object/to_query.rb +2 -4
  72. data/lib/active_support/core_ext/object/try.rb +20 -20
  73. data/lib/active_support/core_ext/object/with.rb +44 -0
  74. data/lib/active_support/core_ext/object/with_options.rb +25 -6
  75. data/lib/active_support/core_ext/object.rb +1 -0
  76. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  77. data/lib/active_support/core_ext/pathname/existence.rb +23 -0
  78. data/lib/active_support/core_ext/pathname.rb +4 -0
  79. data/lib/active_support/core_ext/range/compare_range.rb +0 -25
  80. data/lib/active_support/core_ext/range/conversions.rb +34 -13
  81. data/lib/active_support/core_ext/range/each.rb +1 -1
  82. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  83. data/lib/active_support/core_ext/range.rb +1 -2
  84. data/lib/active_support/core_ext/securerandom.rb +25 -13
  85. data/lib/active_support/core_ext/string/conversions.rb +2 -2
  86. data/lib/active_support/core_ext/string/filters.rb +21 -15
  87. data/lib/active_support/core_ext/string/indent.rb +1 -1
  88. data/lib/active_support/core_ext/string/inflections.rb +17 -10
  89. data/lib/active_support/core_ext/string/inquiry.rb +1 -1
  90. data/lib/active_support/core_ext/string/output_safety.rb +85 -165
  91. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +0 -8
  92. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  93. data/lib/active_support/core_ext/time/calculations.rb +30 -8
  94. data/lib/active_support/core_ext/time/conversions.rb +15 -13
  95. data/lib/active_support/core_ext/time/zones.rb +12 -28
  96. data/lib/active_support/core_ext.rb +2 -1
  97. data/lib/active_support/current_attributes.rb +47 -20
  98. data/lib/active_support/deep_mergeable.rb +53 -0
  99. data/lib/active_support/dependencies/autoload.rb +17 -12
  100. data/lib/active_support/dependencies/interlock.rb +10 -18
  101. data/lib/active_support/dependencies/require_dependency.rb +28 -0
  102. data/lib/active_support/dependencies.rb +58 -788
  103. data/lib/active_support/deprecation/behaviors.rb +66 -40
  104. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  105. data/lib/active_support/deprecation/deprecators.rb +104 -0
  106. data/lib/active_support/deprecation/disallowed.rb +6 -8
  107. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  108. data/lib/active_support/deprecation/method_wrappers.rb +9 -26
  109. data/lib/active_support/deprecation/proxy_wrappers.rb +38 -23
  110. data/lib/active_support/deprecation/reporting.rb +43 -26
  111. data/lib/active_support/deprecation.rb +32 -5
  112. data/lib/active_support/deprecator.rb +7 -0
  113. data/lib/active_support/descendants_tracker.rb +150 -72
  114. data/lib/active_support/digest.rb +5 -3
  115. data/lib/active_support/duration/iso8601_parser.rb +3 -3
  116. data/lib/active_support/duration/iso8601_serializer.rb +9 -3
  117. data/lib/active_support/duration.rb +83 -52
  118. data/lib/active_support/encrypted_configuration.rb +72 -9
  119. data/lib/active_support/encrypted_file.rb +29 -13
  120. data/lib/active_support/environment_inquirer.rb +23 -3
  121. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  122. data/lib/active_support/error_reporter.rb +203 -0
  123. data/lib/active_support/evented_file_update_checker.rb +20 -7
  124. data/lib/active_support/execution_context/test_helper.rb +13 -0
  125. data/lib/active_support/execution_context.rb +53 -0
  126. data/lib/active_support/execution_wrapper.rb +44 -22
  127. data/lib/active_support/executor/test_helper.rb +7 -0
  128. data/lib/active_support/file_update_checker.rb +4 -2
  129. data/lib/active_support/fork_tracker.rb +28 -11
  130. data/lib/active_support/gem_version.rb +4 -4
  131. data/lib/active_support/gzip.rb +2 -0
  132. data/lib/active_support/hash_with_indifferent_access.rb +44 -19
  133. data/lib/active_support/html_safe_translation.rb +53 -0
  134. data/lib/active_support/i18n.rb +2 -1
  135. data/lib/active_support/i18n_railtie.rb +21 -14
  136. data/lib/active_support/inflector/inflections.rb +25 -7
  137. data/lib/active_support/inflector/methods.rb +50 -64
  138. data/lib/active_support/inflector/transliterate.rb +4 -2
  139. data/lib/active_support/isolated_execution_state.rb +76 -0
  140. data/lib/active_support/json/decoding.rb +2 -1
  141. data/lib/active_support/json/encoding.rb +27 -45
  142. data/lib/active_support/key_generator.rb +31 -6
  143. data/lib/active_support/lazy_load_hooks.rb +33 -7
  144. data/lib/active_support/locale/en.yml +4 -2
  145. data/lib/active_support/log_subscriber/test_helper.rb +2 -2
  146. data/lib/active_support/log_subscriber.rb +97 -35
  147. data/lib/active_support/logger.rb +9 -60
  148. data/lib/active_support/logger_thread_safe_level.rb +11 -34
  149. data/lib/active_support/message_encryptor.rb +206 -56
  150. data/lib/active_support/message_encryptors.rb +141 -0
  151. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  152. data/lib/active_support/message_pack/extensions.rb +292 -0
  153. data/lib/active_support/message_pack/serializer.rb +63 -0
  154. data/lib/active_support/message_pack.rb +50 -0
  155. data/lib/active_support/message_verifier.rb +235 -84
  156. data/lib/active_support/message_verifiers.rb +135 -0
  157. data/lib/active_support/messages/codec.rb +65 -0
  158. data/lib/active_support/messages/metadata.rb +112 -46
  159. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  160. data/lib/active_support/messages/rotator.rb +34 -32
  161. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  162. data/lib/active_support/multibyte/chars.rb +12 -11
  163. data/lib/active_support/multibyte/unicode.rb +9 -49
  164. data/lib/active_support/multibyte.rb +1 -1
  165. data/lib/active_support/notifications/fanout.rb +304 -114
  166. data/lib/active_support/notifications/instrumenter.rb +117 -35
  167. data/lib/active_support/notifications.rb +25 -25
  168. data/lib/active_support/number_helper/number_converter.rb +14 -7
  169. data/lib/active_support/number_helper/number_to_currency_converter.rb +11 -6
  170. data/lib/active_support/number_helper/number_to_delimited_converter.rb +1 -1
  171. data/lib/active_support/number_helper/number_to_human_size_converter.rb +4 -4
  172. data/lib/active_support/number_helper/number_to_phone_converter.rb +2 -1
  173. data/lib/active_support/number_helper/number_to_rounded_converter.rb +10 -6
  174. data/lib/active_support/number_helper/rounding_helper.rb +2 -6
  175. data/lib/active_support/number_helper.rb +379 -319
  176. data/lib/active_support/option_merger.rb +10 -18
  177. data/lib/active_support/ordered_hash.rb +4 -4
  178. data/lib/active_support/ordered_options.rb +15 -1
  179. data/lib/active_support/parameter_filter.rb +105 -81
  180. data/lib/active_support/proxy_object.rb +2 -0
  181. data/lib/active_support/railtie.rb +83 -21
  182. data/lib/active_support/reloader.rb +13 -5
  183. data/lib/active_support/rescuable.rb +18 -16
  184. data/lib/active_support/ruby_features.rb +7 -0
  185. data/lib/active_support/secure_compare_rotator.rb +18 -11
  186. data/lib/active_support/security_utils.rb +1 -1
  187. data/lib/active_support/string_inquirer.rb +3 -3
  188. data/lib/active_support/subscriber.rb +11 -40
  189. data/lib/active_support/syntax_error_proxy.rb +60 -0
  190. data/lib/active_support/tagged_logging.rb +65 -25
  191. data/lib/active_support/test_case.rb +166 -27
  192. data/lib/active_support/testing/assertions.rb +61 -15
  193. data/lib/active_support/testing/autorun.rb +0 -2
  194. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  195. data/lib/active_support/testing/deprecation.rb +53 -2
  196. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  197. data/lib/active_support/testing/isolation.rb +30 -29
  198. data/lib/active_support/testing/method_call_assertions.rb +24 -11
  199. data/lib/active_support/testing/parallelization/server.rb +4 -0
  200. data/lib/active_support/testing/parallelization/worker.rb +3 -0
  201. data/lib/active_support/testing/parallelization.rb +4 -0
  202. data/lib/active_support/testing/parallelize_executor.rb +81 -0
  203. data/lib/active_support/testing/setup_and_teardown.rb +2 -0
  204. data/lib/active_support/testing/stream.rb +4 -6
  205. data/lib/active_support/testing/strict_warnings.rb +39 -0
  206. data/lib/active_support/testing/tagged_logging.rb +1 -1
  207. data/lib/active_support/testing/time_helpers.rb +49 -16
  208. data/lib/active_support/time_with_zone.rb +39 -28
  209. data/lib/active_support/values/time_zone.rb +50 -18
  210. data/lib/active_support/version.rb +1 -1
  211. data/lib/active_support/xml_mini/jdom.rb +4 -11
  212. data/lib/active_support/xml_mini/libxml.rb +5 -5
  213. data/lib/active_support/xml_mini/libxmlsax.rb +1 -1
  214. data/lib/active_support/xml_mini/nokogiri.rb +5 -5
  215. data/lib/active_support/xml_mini/nokogirisax.rb +2 -2
  216. data/lib/active_support/xml_mini/rexml.rb +2 -2
  217. data/lib/active_support/xml_mini.rb +7 -6
  218. data/lib/active_support.rb +28 -1
  219. metadata +150 -18
  220. data/lib/active_support/core_ext/marshal.rb +0 -26
  221. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -28
  222. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  223. data/lib/active_support/core_ext/uri.rb +0 -29
  224. data/lib/active_support/dependencies/zeitwerk_integration.rb +0 -117
  225. 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| deserialize_entry(f.read) }
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| f.write(serialize_entry(entry)) }
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,16 +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 "connection_pool"
11
+ require "delegate"
10
12
  require "active_support/core_ext/enumerable"
11
- require "active_support/core_ext/marshal"
12
13
  require "active_support/core_ext/array/extract_options"
14
+ require "active_support/core_ext/numeric/time"
13
15
 
14
16
  module ActiveSupport
15
17
  module Cache
18
+ # = Memcached \Cache \Store
19
+ #
16
20
  # A cache store implementation which stores data in Memcached:
17
21
  # https://memcached.org
18
22
  #
@@ -20,27 +24,15 @@ module ActiveSupport
20
24
  #
21
25
  # Special features:
22
26
  # - Clustering and load balancing. One can specify multiple memcached servers,
23
- # and MemCacheStore will load balance between all available servers. If a
24
- # 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.
25
29
  #
26
- # MemCacheStore implements the Strategy::LocalCache strategy which implements
27
- # 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.
28
32
  class MemCacheStore < Store
29
- DEFAULT_CODER = NullCoder # Dalli automatically Marshal values
30
-
31
- # Provide support for raw values in the local cache strategy.
32
- module LocalCacheWithRaw # :nodoc:
33
- private
34
- def write_entry(key, entry, **options)
35
- if options[:raw] && local_cache
36
- raw_entry = Entry.new(entry.value.to_s)
37
- raw_entry.expires_at = entry.expires_at
38
- super(key, raw_entry, **options)
39
- else
40
- super
41
- end
42
- end
43
- 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
44
36
 
45
37
  # Advertise cache versioning support.
46
38
  def self.supports_cache_versioning?
@@ -48,8 +40,48 @@ module ActiveSupport
48
40
  end
49
41
 
50
42
  prepend Strategy::LocalCache
51
- prepend LocalCacheWithRaw
52
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
53
85
  ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n
54
86
 
55
87
  # Creates a new Dalli::Client instance with specified addresses and options.
@@ -64,64 +96,116 @@ module ActiveSupport
64
96
  def self.build_mem_cache(*addresses) # :nodoc:
65
97
  addresses = addresses.flatten
66
98
  options = addresses.extract_options!
67
- addresses = nil if addresses.empty?
99
+ addresses = nil if addresses.compact.empty?
68
100
  pool_options = retrieve_pool_options(options)
69
101
 
70
- if pool_options.empty?
71
- Dalli::Client.new(addresses, options)
72
- else
73
- ensure_connection_pool_added!
102
+ if pool_options
74
103
  ConnectionPool.new(pool_options) { Dalli::Client.new(addresses, options.merge(threadsafe: false)) }
104
+ else
105
+ Dalli::Client.new(addresses, options)
75
106
  end
76
107
  end
77
108
 
78
- # Creates a new MemCacheStore object, with the given memcached server
109
+ # Creates a new +MemCacheStore+ object, with the given memcached server
79
110
  # addresses. Each address is either a host name, or a host-with-port string
80
111
  # in the form of "host_name:port". For example:
81
112
  #
82
113
  # ActiveSupport::Cache::MemCacheStore.new("localhost", "server-downstairs.localnetwork:8229")
83
114
  #
84
- # If no addresses are provided, but ENV['MEMCACHE_SERVERS'] is defined, it will be used instead. Otherwise,
85
- # MemCacheStore will connect to localhost: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.
86
118
  def initialize(*addresses)
87
119
  addresses = addresses.flatten
88
120
  options = addresses.extract_options!
121
+ if options.key?(:cache_nils)
122
+ options[:skip_nil] = !options.delete(:cache_nils)
123
+ end
89
124
  super(options)
90
125
 
91
126
  unless [String, Dalli::Client, NilClass].include?(addresses.first.class)
92
- 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."
93
128
  end
94
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
95
134
  @data = addresses.first
96
135
  else
97
- mem_cache_options = options.dup
98
- UNIVERSAL_OPTIONS.each { |name| mem_cache_options.delete(name) }
99
- @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]))
100
141
  end
101
142
  end
102
143
 
103
- # Increment a cached value. This method uses the memcached incr atomic
104
- # operator and can only be used on values written with the :raw option.
105
- # Calling it on a value not stored with :raw will initialize that value
106
- # 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+.
107
180
  def increment(name, amount = 1, options = nil)
108
181
  options = merged_options(options)
109
182
  instrument(:increment, name, amount: amount) do
110
183
  rescue_error_with nil do
111
- @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) }
112
185
  end
113
186
  end
114
187
  end
115
188
 
116
- # Decrement a cached value. This method uses the memcached decr atomic
117
- # operator and can only be used on values written with the :raw option.
118
- # Calling it on a value not stored with :raw will initialize that value
119
- # 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+.
120
204
  def decrement(name, amount = 1, options = nil)
121
205
  options = merged_options(options)
122
206
  instrument(:decrement, name, amount: amount) do
123
207
  rescue_error_with nil do
124
- @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) }
125
209
  end
126
210
  end
127
211
  end
@@ -138,23 +222,47 @@ module ActiveSupport
138
222
  end
139
223
 
140
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
+
141
239
  # Read an entry from the cache.
142
240
  def read_entry(key, **options)
143
- 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
144
248
  end
145
249
 
146
250
  # Write an entry to the cache.
147
251
  def write_entry(key, entry, **options)
252
+ write_serialized_entry(key, serialize_entry(entry, **options), **options)
253
+ end
254
+
255
+ def write_serialized_entry(key, payload, **options)
148
256
  method = options[:unless_exist] ? :add : :set
149
- value = options[:raw] ? entry.value.to_s : serialize_entry(entry)
150
257
  expires_in = options[:expires_in].to_i
151
258
  if options[:race_condition_ttl] && expires_in > 0 && !options[:raw]
152
259
  # Set the memcache expire a few minutes in the future to support race condition ttls on read
153
260
  expires_in += 5.minutes
154
261
  end
155
262
  rescue_error_with false do
156
- # The value "compress: false" prevents duplicate compression within Dalli.
157
- @data.with { |c| c.send(method, key, value, expires_in, **options, compress: false) }
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) }
158
266
  end
159
267
  end
160
268
 
@@ -162,14 +270,22 @@ module ActiveSupport
162
270
  def read_multi_entries(names, **options)
163
271
  keys_to_names = names.index_by { |name| normalize_key(name, options) }
164
272
 
165
- raw_values = @data.with { |c| c.get_multi(keys_to_names.keys) }
273
+ raw_values = begin
274
+ @data.with { |c| c.get_multi(keys_to_names.keys) }
275
+ rescue Dalli::UnmarshalError
276
+ {}
277
+ end
278
+
166
279
  values = {}
167
280
 
168
281
  raw_values.each do |key, value|
169
- entry = deserialize_entry(value)
282
+ entry = deserialize_entry(value, raw: options[:raw])
170
283
 
171
- unless entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
172
- 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
173
289
  end
174
290
  end
175
291
 
@@ -181,27 +297,50 @@ module ActiveSupport
181
297
  rescue_error_with(false) { @data.with { |c| c.delete(key) } }
182
298
  end
183
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
+
184
308
  # Memcache keys are binaries. So we need to force their encoding to binary
185
309
  # before applying the regular expression to ensure we are escaping all
186
310
  # characters properly.
187
311
  def normalize_key(key, options)
188
- key = super.dup
189
- key = key.force_encoding(Encoding::ASCII_8BIT)
190
- key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
191
- 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
192
324
  key
193
325
  end
194
326
 
195
- def deserialize_entry(payload)
196
- entry = super
197
- entry = Entry.new(entry, compress: false) if entry && !entry.is_a?(Entry)
198
- entry
327
+ def deserialize_entry(payload, raw: false, **)
328
+ if payload && raw
329
+ Entry.new(payload)
330
+ else
331
+ super(payload)
332
+ end
199
333
  end
200
334
 
201
335
  def rescue_error_with(fallback)
202
336
  yield
203
- rescue Dalli::DalliError => e
204
- 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
+ )
205
344
  fallback
206
345
  end
207
346
  end