activesupport 7.1.0 → 7.2.0

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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +122 -943
  3. data/lib/active_support/array_inquirer.rb +1 -1
  4. data/lib/active_support/backtrace_cleaner.rb +15 -3
  5. data/lib/active_support/broadcast_logger.rb +33 -4
  6. data/lib/active_support/cache/entry.rb +7 -1
  7. data/lib/active_support/cache/file_store.rb +16 -11
  8. data/lib/active_support/cache/mem_cache_store.rb +32 -82
  9. data/lib/active_support/cache/memory_store.rb +17 -14
  10. data/lib/active_support/cache/redis_cache_store.rb +54 -36
  11. data/lib/active_support/cache/serializer_with_fallback.rb +0 -23
  12. data/lib/active_support/cache/strategy/local_cache.rb +9 -6
  13. data/lib/active_support/cache.rb +98 -84
  14. data/lib/active_support/callbacks.rb +74 -113
  15. data/lib/active_support/code_generator.rb +15 -10
  16. data/lib/active_support/core_ext/array/conversions.rb +0 -2
  17. data/lib/active_support/core_ext/class/subclasses.rb +15 -35
  18. data/lib/active_support/core_ext/date/blank.rb +4 -0
  19. data/lib/active_support/core_ext/date/conversions.rb +1 -3
  20. data/lib/active_support/core_ext/date_and_time/compatibility.rb +28 -1
  21. data/lib/active_support/core_ext/date_time/blank.rb +4 -0
  22. data/lib/active_support/core_ext/date_time/conversions.rb +0 -4
  23. data/lib/active_support/core_ext/digest/uuid.rb +6 -0
  24. data/lib/active_support/core_ext/erb/util.rb +5 -0
  25. data/lib/active_support/core_ext/hash/keys.rb +4 -4
  26. data/lib/active_support/core_ext/module/attr_internal.rb +17 -6
  27. data/lib/active_support/core_ext/module/concerning.rb +6 -6
  28. data/lib/active_support/core_ext/module/delegation.rb +20 -148
  29. data/lib/active_support/core_ext/module/deprecation.rb +1 -4
  30. data/lib/active_support/core_ext/numeric/conversions.rb +3 -3
  31. data/lib/active_support/core_ext/object/blank.rb +45 -1
  32. data/lib/active_support/core_ext/object/duplicable.rb +24 -15
  33. data/lib/active_support/core_ext/object/instance_variables.rb +11 -19
  34. data/lib/active_support/core_ext/object/json.rb +2 -2
  35. data/lib/active_support/core_ext/object/with.rb +5 -3
  36. data/lib/active_support/core_ext/object/with_options.rb +1 -1
  37. data/lib/active_support/core_ext/pathname/blank.rb +4 -0
  38. data/lib/active_support/core_ext/range/overlap.rb +1 -1
  39. data/lib/active_support/core_ext/securerandom.rb +8 -24
  40. data/lib/active_support/core_ext/string/conversions.rb +1 -1
  41. data/lib/active_support/core_ext/string/filters.rb +1 -1
  42. data/lib/active_support/core_ext/string/indent.rb +1 -1
  43. data/lib/active_support/core_ext/string/output_safety.rb +0 -7
  44. data/lib/active_support/core_ext/time/calculations.rb +18 -28
  45. data/lib/active_support/core_ext/time/compatibility.rb +16 -0
  46. data/lib/active_support/core_ext/time/conversions.rb +0 -2
  47. data/lib/active_support/core_ext.rb +0 -1
  48. data/lib/active_support/current_attributes.rb +33 -40
  49. data/lib/active_support/delegation.rb +202 -0
  50. data/lib/active_support/dependencies/autoload.rb +0 -12
  51. data/lib/active_support/deprecation/behaviors.rb +18 -16
  52. data/lib/active_support/deprecation/constant_accessor.rb +47 -26
  53. data/lib/active_support/deprecation/proxy_wrappers.rb +9 -12
  54. data/lib/active_support/deprecation/reporting.rb +13 -5
  55. data/lib/active_support/deprecation.rb +8 -5
  56. data/lib/active_support/descendants_tracker.rb +9 -87
  57. data/lib/active_support/duration/iso8601_parser.rb +2 -2
  58. data/lib/active_support/duration/iso8601_serializer.rb +1 -2
  59. data/lib/active_support/duration.rb +11 -6
  60. data/lib/active_support/error_reporter.rb +41 -3
  61. data/lib/active_support/evented_file_update_checker.rb +0 -1
  62. data/lib/active_support/execution_wrapper.rb +0 -1
  63. data/lib/active_support/file_update_checker.rb +1 -1
  64. data/lib/active_support/fork_tracker.rb +2 -38
  65. data/lib/active_support/gem_version.rb +1 -1
  66. data/lib/active_support/hash_with_indifferent_access.rb +6 -8
  67. data/lib/active_support/html_safe_translation.rb +19 -6
  68. data/lib/active_support/inflector/methods.rb +2 -2
  69. data/lib/active_support/log_subscriber.rb +9 -14
  70. data/lib/active_support/logger.rb +15 -2
  71. data/lib/active_support/logger_thread_safe_level.rb +0 -8
  72. data/lib/active_support/message_encryptors.rb +1 -0
  73. data/lib/active_support/message_pack/extensions.rb +15 -2
  74. data/lib/active_support/message_verifier.rb +12 -0
  75. data/lib/active_support/message_verifiers.rb +1 -0
  76. data/lib/active_support/messages/codec.rb +1 -1
  77. data/lib/active_support/messages/metadata.rb +1 -1
  78. data/lib/active_support/multibyte/chars.rb +2 -2
  79. data/lib/active_support/notifications/fanout.rb +29 -26
  80. data/lib/active_support/notifications/instrumenter.rb +43 -32
  81. data/lib/active_support/notifications.rb +28 -27
  82. data/lib/active_support/number_helper/number_converter.rb +5 -3
  83. data/lib/active_support/number_helper/number_to_human_size_converter.rb +2 -2
  84. data/lib/active_support/number_helper.rb +379 -318
  85. data/lib/active_support/option_merger.rb +2 -2
  86. data/lib/active_support/ordered_options.rb +55 -17
  87. data/lib/active_support/proxy_object.rb +8 -5
  88. data/lib/active_support/railtie.rb +4 -11
  89. data/lib/active_support/string_inquirer.rb +1 -1
  90. data/lib/active_support/syntax_error_proxy.rb +12 -1
  91. data/lib/active_support/tagged_logging.rb +4 -1
  92. data/lib/active_support/test_case.rb +3 -1
  93. data/lib/active_support/testing/assertions.rb +5 -5
  94. data/lib/active_support/testing/constant_stubbing.rb +30 -8
  95. data/lib/active_support/testing/deprecation.rb +5 -12
  96. data/lib/active_support/testing/isolation.rb +18 -8
  97. data/lib/active_support/testing/method_call_assertions.rb +2 -16
  98. data/lib/active_support/testing/setup_and_teardown.rb +2 -0
  99. data/lib/active_support/testing/strict_warnings.rb +6 -4
  100. data/lib/active_support/testing/tests_without_assertions.rb +19 -0
  101. data/lib/active_support/testing/time_helpers.rb +8 -4
  102. data/lib/active_support/time_with_zone.rb +7 -3
  103. data/lib/active_support/values/time_zone.rb +10 -1
  104. data/lib/active_support/xml_mini.rb +11 -2
  105. data/lib/active_support.rb +3 -2
  106. metadata +35 -15
  107. data/lib/active_support/deprecation/instance_delegator.rb +0 -65
  108. data/lib/active_support/ruby_features.rb +0 -7
@@ -39,7 +39,7 @@ module ActiveSupport
39
39
  name.end_with?("?") || super
40
40
  end
41
41
 
42
- def method_missing(name, *args)
42
+ def method_missing(name, ...)
43
43
  if name.end_with?("?")
44
44
  any?(name[0..-2])
45
45
  else
@@ -17,7 +17,8 @@ module ActiveSupport
17
17
  # can focus on the rest.
18
18
  #
19
19
  # bc = ActiveSupport::BacktraceCleaner.new
20
- # bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix
20
+ # root = "#{Rails.root}/"
21
+ # bc.add_filter { |line| line.start_with?(root) ? line.from(root.size) : line } # strip the Rails.root prefix
21
22
  # bc.add_silencer { |line| /puma|rubygems/.match?(line) } # skip any lines from puma or rubygems
22
23
  # bc.clean(exception.backtrace) # perform the cleanup
23
24
  #
@@ -33,6 +34,7 @@ module ActiveSupport
33
34
  class BacktraceCleaner
34
35
  def initialize
35
36
  @filters, @silencers = [], []
37
+ add_core_silencer
36
38
  add_gem_filter
37
39
  add_gem_silencer
38
40
  add_stdlib_silencer
@@ -75,8 +77,9 @@ module ActiveSupport
75
77
  # Adds a filter from the block provided. Each line in the backtrace will be
76
78
  # mapped against this filter.
77
79
  #
78
- # # Will turn "/my/rails/root/app/models/person.rb" into "/app/models/person.rb"
79
- # backtrace_cleaner.add_filter { |line| line.gsub(Rails.root.to_s, '') }
80
+ # # Will turn "/my/rails/root/app/models/person.rb" into "app/models/person.rb"
81
+ # root = "#{Rails.root}/"
82
+ # backtrace_cleaner.add_filter { |line| line.start_with?(root) ? line.from(root.size) : line }
80
83
  def add_filter(&block)
81
84
  @filters << block
82
85
  end
@@ -107,6 +110,11 @@ module ActiveSupport
107
110
  private
108
111
  FORMATTED_GEMS_PATTERN = /\A[^\/]+ \([\w.]+\) /
109
112
 
113
+ def initialize_copy(_other)
114
+ @filters = @filters.dup
115
+ @silencers = @silencers.dup
116
+ end
117
+
110
118
  def add_gem_filter
111
119
  gems_paths = (Gem.path | [Gem.default_dir]).map { |p| Regexp.escape(p) }
112
120
  return if gems_paths.empty?
@@ -116,6 +124,10 @@ module ActiveSupport
116
124
  add_filter { |line| line.sub(gems_regexp, gems_result) }
117
125
  end
118
126
 
127
+ def add_core_silencer
128
+ add_silencer { |line| line.include?("<internal:") }
129
+ end
130
+
119
131
  def add_gem_silencer
120
132
  add_silencer { |line| FORMATTED_GEMS_PATTERN.match?(line) }
121
133
  end
@@ -51,6 +51,26 @@ module ActiveSupport
51
51
  #
52
52
  # broadcast = BroadcastLogger.new
53
53
  # broadcast.info("Hello world") # The log message will appear nowhere.
54
+ #
55
+ # If you are adding a custom logger with custom methods to the broadcast,
56
+ # the `BroadcastLogger` will proxy them and return the raw value, or an array
57
+ # of raw values, depending on how many loggers in the broadcasts responded to
58
+ # the method:
59
+ #
60
+ # class MyLogger < ::Logger
61
+ # def loggable?
62
+ # true
63
+ # end
64
+ # end
65
+ #
66
+ # logger = BroadcastLogger.new
67
+ # logger.loggable? # => A NoMethodError exception is raised because no loggers in the broadcasts could respond.
68
+ #
69
+ # logger.broadcast_to(MyLogger.new(STDOUT))
70
+ # logger.loggable? # => true
71
+ # logger.broadcast_to(MyLogger.new(STDOUT))
72
+ # puts logger.broadcasts # => [MyLogger, MyLogger]
73
+ # logger.loggable? # [true, true]
54
74
  class BroadcastLogger
55
75
  include ActiveSupport::LoggerSilence
56
76
 
@@ -198,20 +218,29 @@ module ActiveSupport
198
218
  dispatch { |logger| logger.fatal! }
199
219
  end
200
220
 
221
+ def initialize_copy(other)
222
+ @broadcasts = []
223
+ @progname = other.progname.dup
224
+ @formatter = other.formatter.dup
225
+
226
+ broadcast_to(*other.broadcasts.map(&:dup))
227
+ end
228
+
201
229
  private
202
230
  def dispatch(&block)
203
231
  @broadcasts.each { |logger| block.call(logger) }
232
+ true
204
233
  end
205
234
 
206
- def method_missing(name, *args, &block)
235
+ def method_missing(name, ...)
207
236
  loggers = @broadcasts.select { |logger| logger.respond_to?(name) }
208
237
 
209
238
  if loggers.none?
210
- super(name, *args, &block)
239
+ super
211
240
  elsif loggers.one?
212
- loggers.first.send(name, *args, &block)
241
+ loggers.first.send(name, ...)
213
242
  else
214
- loggers.map { |logger| logger.send(name, *args, &block) }
243
+ loggers.map { |logger| logger.send(name, ...) }
215
244
  end
216
245
  end
217
246
 
@@ -121,7 +121,13 @@ module ActiveSupport
121
121
 
122
122
  private
123
123
  def uncompress(value)
124
- Marshal.load(Zlib::Inflate.inflate(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
125
131
  end
126
132
  end
127
133
  end
@@ -78,8 +78,9 @@ module ActiveSupport
78
78
 
79
79
  def delete_matched(matcher, options = nil)
80
80
  options = merged_options(options)
81
+ matcher = key_matcher(matcher, options)
82
+
81
83
  instrument(:delete_matched, matcher.inspect) do
82
- matcher = key_matcher(matcher, options)
83
84
  search_dir(cache_path) do |path|
84
85
  key = file_path_key(path)
85
86
  delete_entry(path, **options) if key.match(matcher)
@@ -176,7 +177,7 @@ module ActiveSupport
176
177
 
177
178
  # Translate a file path into a key.
178
179
  def file_path_key(path)
179
- fname = path[cache_path.to_s.size..-1].split(File::SEPARATOR, 4).last
180
+ fname = path[cache_path.to_s.size..-1].split(File::SEPARATOR, 4).last.delete(File::SEPARATOR)
180
181
  URI.decode_www_form_component(fname, Encoding::UTF_8)
181
182
  end
182
183
 
@@ -209,18 +210,22 @@ module ActiveSupport
209
210
  # Modifies the amount of an integer value that is stored in the cache.
210
211
  # If the key is not found it is created and set to +amount+.
211
212
  def modify_value(name, amount, options)
212
- file_name = normalize_key(name, options)
213
+ options = merged_options(options)
214
+ key = normalize_key(name, options)
215
+ version = normalize_version(name, options)
216
+ amount = Integer(amount)
213
217
 
214
- lock_file(file_name) do
215
- options = merged_options(options)
218
+ lock_file(key) do
219
+ entry = read_entry(key, **options)
216
220
 
217
- if num = read(name, options)
218
- num = num.to_i + amount
219
- write(name, num, options)
220
- num
221
- else
222
- write(name, Integer(amount), options)
221
+ if !entry || entry.expired? || entry.mismatched?(version)
222
+ write(name, amount, options)
223
223
  amount
224
+ else
225
+ num = entry.value.to_i + amount
226
+ entry = Entry.new(num, expires_at: entry.expires_at, version: entry.version)
227
+ write_entry(key, entry)
228
+ num
224
229
  end
225
230
  end
226
231
  end
@@ -24,11 +24,11 @@ module ActiveSupport
24
24
  #
25
25
  # Special features:
26
26
  # - Clustering and load balancing. One can specify multiple memcached servers,
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.
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.
29
29
  #
30
- # MemCacheStore implements the Strategy::LocalCache strategy which implements
31
- # 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.
32
32
  class MemCacheStore < Store
33
33
  # These options represent behavior overridden by this implementation and should
34
34
  # not be allowed to get down to the Dalli client
@@ -41,46 +41,6 @@ module ActiveSupport
41
41
 
42
42
  prepend Strategy::LocalCache
43
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
44
  KEY_MAX_SIZE = 250
85
45
  ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n
86
46
 
@@ -106,15 +66,14 @@ module ActiveSupport
106
66
  end
107
67
  end
108
68
 
109
- # Creates a new MemCacheStore object, with the given memcached server
69
+ # Creates a new +MemCacheStore+ object, with the given memcached server
110
70
  # addresses. Each address is either a host name, or a host-with-port string
111
71
  # in the form of "host_name:port". For example:
112
72
  #
113
73
  # ActiveSupport::Cache::MemCacheStore.new("localhost", "server-downstairs.localnetwork:8229")
114
74
  #
115
75
  # 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.
76
+ # +MemCacheStore+ will connect to localhost:11211 (the default memcached port).
118
77
  def initialize(*addresses)
119
78
  addresses = addresses.flatten
120
79
  options = addresses.extract_options!
@@ -126,19 +85,12 @@ module ActiveSupport
126
85
  unless [String, Dalli::Client, NilClass].include?(addresses.first.class)
127
86
  raise ArgumentError, "First argument must be an empty array, address, or array of addresses."
128
87
  end
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
134
- @data = addresses.first
135
- else
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]))
141
- end
88
+
89
+ @mem_cache_options = options.dup
90
+ # The value "compress: false" prevents duplicate compression within Dalli.
91
+ @mem_cache_options[:compress] = false
92
+ (OVERRIDDEN_OPTIONS - %i(compress)).each { |name| @mem_cache_options.delete(name) }
93
+ @data = self.class.build_mem_cache(*(addresses + [@mem_cache_options]))
142
94
  end
143
95
 
144
96
  def inspect
@@ -179,9 +131,11 @@ module ActiveSupport
179
131
  # <tt>raw: true</tt>, will fail and return +nil+.
180
132
  def increment(name, amount = 1, options = nil)
181
133
  options = merged_options(options)
182
- instrument(:increment, name, amount: amount) do
134
+ key = normalize_key(name, options)
135
+
136
+ instrument(:increment, key, amount: amount) do
183
137
  rescue_error_with nil do
184
- @data.with { |c| c.incr(normalize_key(name, options), amount, options[:expires_in], amount) }
138
+ @data.with { |c| c.incr(key, amount, options[:expires_in], amount) }
185
139
  end
186
140
  end
187
141
  end
@@ -203,9 +157,11 @@ module ActiveSupport
203
157
  # <tt>raw: true</tt>, will fail and return +nil+.
204
158
  def decrement(name, amount = 1, options = nil)
205
159
  options = merged_options(options)
206
- instrument(:decrement, name, amount: amount) do
160
+ key = normalize_key(name, options)
161
+
162
+ instrument(:decrement, key, amount: amount) do
207
163
  rescue_error_with nil do
208
- @data.with { |c| c.decr(normalize_key(name, options), amount, options[:expires_in], 0) }
164
+ @data.with { |c| c.decr(key, amount, options[:expires_in], 0) }
209
165
  end
210
166
  end
211
167
  end
@@ -222,20 +178,6 @@ module ActiveSupport
222
178
  end
223
179
 
224
180
  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
-
239
181
  # Read an entry from the cache.
240
182
  def read_entry(key, **options)
241
183
  deserialize_entry(read_serialized_entry(key, **options), **options)
@@ -259,10 +201,10 @@ module ActiveSupport
259
201
  # Set the memcache expire a few minutes in the future to support race condition ttls on read
260
202
  expires_in += 5.minutes
261
203
  end
262
- rescue_error_with false do
204
+ rescue_error_with nil do
263
205
  # Don't pass compress option to Dalli since we are already dealing with compression.
264
206
  options.delete(:compress)
265
- @data.with { |c| c.send(method, key, payload, expires_in, **options) }
207
+ @data.with { |c| !!c.send(method, key, payload, expires_in, **options) }
266
208
  end
267
209
  end
268
210
 
@@ -270,14 +212,22 @@ module ActiveSupport
270
212
  def read_multi_entries(names, **options)
271
213
  keys_to_names = names.index_by { |name| normalize_key(name, options) }
272
214
 
273
- raw_values = @data.with { |c| c.get_multi(keys_to_names.keys) }
215
+ raw_values = begin
216
+ @data.with { |c| c.get_multi(keys_to_names.keys) }
217
+ rescue Dalli::UnmarshalError
218
+ {}
219
+ end
220
+
274
221
  values = {}
275
222
 
276
223
  raw_values.each do |key, value|
277
224
  entry = deserialize_entry(value, raw: options[:raw])
278
225
 
279
226
  unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
280
- values[keys_to_names[key]] = entry.value
227
+ begin
228
+ values[keys_to_names[key]] = entry.value
229
+ rescue DeserializationError
230
+ end
281
231
  end
282
232
  end
283
233
 
@@ -18,13 +18,13 @@ module ActiveSupport
18
18
  # a cleanup will occur which tries to prune the cache down to three quarters
19
19
  # of the maximum size by removing the least recently used entries.
20
20
  #
21
- # Unlike other Cache store implementations, MemoryStore does not compress
22
- # values by default. MemoryStore does not benefit from compression as much
21
+ # Unlike other Cache store implementations, +MemoryStore+ does not compress
22
+ # values by default. +MemoryStore+ does not benefit from compression as much
23
23
  # as other Store implementations, as it does not send data over a network.
24
24
  # However, when compression is enabled, it still pays the full cost of
25
25
  # compression in terms of cpu use.
26
26
  #
27
- # MemoryStore is thread-safe.
27
+ # +MemoryStore+ is thread-safe.
28
28
  class MemoryStore < Store
29
29
  module DupCoder # :nodoc:
30
30
  extend self
@@ -168,8 +168,9 @@ module ActiveSupport
168
168
  # Deletes cache entries if the cache key matches a given pattern.
169
169
  def delete_matched(matcher, options = nil)
170
170
  options = merged_options(options)
171
+ matcher = key_matcher(matcher, options)
172
+
171
173
  instrument(:delete_matched, matcher.inspect) do
172
- matcher = key_matcher(matcher, options)
173
174
  keys = synchronize { @data.keys }
174
175
  keys.each do |key|
175
176
  delete_entry(key, **options) if key.match(matcher)
@@ -209,7 +210,7 @@ module ActiveSupport
209
210
  def write_entry(key, entry, **options)
210
211
  payload = serialize_entry(entry, **options)
211
212
  synchronize do
212
- return false if options[:unless_exist] && exist?(key)
213
+ return false if options[:unless_exist] && exist?(key, namespace: nil)
213
214
 
214
215
  old_payload = @data[key]
215
216
  if old_payload
@@ -238,16 +239,18 @@ module ActiveSupport
238
239
  key = normalize_key(name, options)
239
240
  version = normalize_version(name, options)
240
241
 
241
- entry = read_entry(key, **options)
242
+ synchronize do
243
+ entry = read_entry(key, **options)
242
244
 
243
- if !entry || entry.expired? || entry.mismatched?(version)
244
- write(name, Integer(amount), options)
245
- amount
246
- else
247
- num = entry.value.to_i + amount
248
- entry = Entry.new(num, expires_at: entry.expires_at, version: entry.version)
249
- write_entry(key, entry)
250
- num
245
+ if !entry || entry.expired? || entry.mismatched?(version)
246
+ write(name, Integer(amount), options)
247
+ amount
248
+ else
249
+ num = entry.value.to_i + amount
250
+ entry = Entry.new(num, expires_at: entry.expires_at, version: entry.version)
251
+ write_entry(key, entry)
252
+ num
253
+ end
251
254
  end
252
255
  end
253
256
  end
@@ -19,22 +19,23 @@ module ActiveSupport
19
19
  module Cache
20
20
  # = Redis \Cache \Store
21
21
  #
22
- # Deployment note: Take care to use a *dedicated Redis cache* rather
23
- # than pointing this at your existing Redis server. It won't cope well
24
- # with mixed usage patterns and it won't expire cache entries by default.
22
+ # Deployment note: Take care to use a <b>dedicated Redis cache</b> rather
23
+ # than pointing this at a persistent Redis server (for example, one used as
24
+ # an Active Job queue). Redis won't cope well with mixed usage patterns and it
25
+ # won't expire cache entries by default.
25
26
  #
26
27
  # Redis cache server setup guide: https://redis.io/topics/lru-cache
27
28
  #
28
- # * Supports vanilla Redis, hiredis, and Redis::Distributed.
29
- # * Supports Memcached-like sharding across Redises with Redis::Distributed.
29
+ # * Supports vanilla Redis, hiredis, and +Redis::Distributed+.
30
+ # * Supports Memcached-like sharding across Redises with +Redis::Distributed+.
30
31
  # * Fault tolerant. If the Redis server is unavailable, no exceptions are
31
32
  # raised. Cache fetches are all misses and writes are dropped.
32
33
  # * Local cache. Hot in-memory primary cache within block/middleware scope.
33
- # * +read_multi+ and +write_multi+ support for Redis mget/mset. Use Redis::Distributed
34
- # 4.0.1+ for distributed mget support.
34
+ # * +read_multi+ and +write_multi+ support for Redis mget/mset. Use
35
+ # +Redis::Distributed+ 4.0.1+ for distributed mget support.
35
36
  # * +delete_matched+ support for Redis KEYS globs.
36
37
  class RedisCacheStore < Store
37
- # Keys are truncated with the ActiveSupport digest if they exceed 1kB
38
+ # Keys are truncated with the Active Support digest if they exceed 1kB
38
39
  MAX_KEY_BYTESIZE = 1024
39
40
 
40
41
  DEFAULT_REDIS_OPTIONS = {
@@ -110,8 +111,11 @@ module ActiveSupport
110
111
 
111
112
  # Creates a new Redis cache store.
112
113
  #
113
- # Handles four options: :redis block, :redis instance, single :url
114
- # string, and multiple :url strings.
114
+ # There are four ways to provide the Redis client used by the cache: the
115
+ # +:redis+ param can be a Redis instance or a block that returns a Redis
116
+ # instance, or the +:url+ param can be a string or an array of strings
117
+ # which will be used to create a Redis instance or a +Redis::Distributed+
118
+ # instance.
115
119
  #
116
120
  # Option Class Result
117
121
  # :redis Proc -> options[:redis].call
@@ -134,7 +138,7 @@ module ActiveSupport
134
138
  #
135
139
  # Race condition TTL is not set by default. This can be used to avoid
136
140
  # "thundering herd" cache writes when hot cache entries are expired.
137
- # See <tt>ActiveSupport::Cache::Store#fetch</tt> for more.
141
+ # See ActiveSupport::Cache::Store#fetch for more.
138
142
  #
139
143
  # Setting <tt>skip_nil: true</tt> will not cache nil results:
140
144
  #
@@ -192,12 +196,13 @@ module ActiveSupport
192
196
  #
193
197
  # Failsafe: Raises errors.
194
198
  def delete_matched(matcher, options = nil)
195
- instrument :delete_matched, matcher do
196
- unless String === matcher
197
- raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
198
- end
199
+ unless String === matcher
200
+ raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
201
+ end
202
+ pattern = namespace_key(matcher, options)
203
+
204
+ instrument :delete_matched, pattern do
199
205
  redis.then do |c|
200
- pattern = namespace_key(matcher, options)
201
206
  cursor = "0"
202
207
  # Fetch keys in batches using SCAN to avoid blocking the Redis server.
203
208
  nodes = c.respond_to?(:nodes) ? c.nodes : [c]
@@ -230,10 +235,11 @@ module ActiveSupport
230
235
  #
231
236
  # Failsafe: Raises errors.
232
237
  def increment(name, amount = 1, options = nil)
233
- instrument :increment, name, amount: amount do
238
+ options = merged_options(options)
239
+ key = normalize_key(name, options)
240
+
241
+ instrument :increment, key, amount: amount do
234
242
  failsafe :increment do
235
- options = merged_options(options)
236
- key = normalize_key(name, options)
237
243
  change_counter(key, amount, options)
238
244
  end
239
245
  end
@@ -242,7 +248,7 @@ module ActiveSupport
242
248
  # Decrement a cached integer value using the Redis decrby atomic operator.
243
249
  # Returns the updated value.
244
250
  #
245
- # If the key is unset or has expired, it will be set to -amount:
251
+ # If the key is unset or has expired, it will be set to +-amount+:
246
252
  #
247
253
  # cache.decrement("foo") # => -1
248
254
  #
@@ -256,10 +262,11 @@ module ActiveSupport
256
262
  #
257
263
  # Failsafe: Raises errors.
258
264
  def decrement(name, amount = 1, options = nil)
259
- instrument :decrement, name, amount: amount do
265
+ options = merged_options(options)
266
+ key = normalize_key(name, options)
267
+
268
+ instrument :decrement, key, amount: amount do
260
269
  failsafe :decrement do
261
- options = merged_options(options)
262
- key = normalize_key(name, options)
263
270
  change_counter(key, -amount, options)
264
271
  end
265
272
  end
@@ -332,7 +339,10 @@ module ActiveSupport
332
339
  if value
333
340
  entry = deserialize_entry(value, raw: raw)
334
341
  unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
335
- results[name] = entry.value
342
+ begin
343
+ results[name] = entry.value
344
+ rescue DeserializationError
345
+ end
336
346
  end
337
347
  end
338
348
  end
@@ -362,8 +372,8 @@ module ActiveSupport
362
372
  if pipeline
363
373
  pipeline.set(key, payload, **modifiers)
364
374
  else
365
- failsafe :write_entry, returning: false do
366
- redis.then { |c| c.set key, payload, **modifiers }
375
+ failsafe :write_entry, returning: nil do
376
+ redis.then { |c| !!c.set(key, payload, **modifiers) }
367
377
  end
368
378
  end
369
379
  end
@@ -383,7 +393,7 @@ module ActiveSupport
383
393
  end
384
394
 
385
395
  # Nonstandard store provider API to write multiple values at once.
386
- def write_multi_entries(entries, expires_in: nil, race_condition_ttl: nil, **options)
396
+ def write_multi_entries(entries, **options)
387
397
  return if entries.empty?
388
398
 
389
399
  failsafe :write_multi_entries do
@@ -438,18 +448,26 @@ module ActiveSupport
438
448
  redis.then do |c|
439
449
  c = c.node_for(key) if c.is_a?(Redis::Distributed)
440
450
 
441
- if options[:expires_in] && supports_expire_nx?
442
- c.pipelined do |pipeline|
443
- pipeline.incrby(key, amount)
444
- pipeline.call(:expire, key, options[:expires_in].to_i, "NX")
445
- end.first
451
+ expires_in = options[:expires_in]
452
+
453
+ if expires_in
454
+ if supports_expire_nx?
455
+ count, _ = c.pipelined do |pipeline|
456
+ pipeline.incrby(key, amount)
457
+ pipeline.call(:expire, key, expires_in.to_i, "NX")
458
+ end
459
+ else
460
+ count, ttl = c.pipelined do |pipeline|
461
+ pipeline.incrby(key, amount)
462
+ pipeline.ttl(key)
463
+ end
464
+ c.expire(key, expires_in.to_i) if ttl < 0
465
+ end
446
466
  else
447
467
  count = c.incrby(key, amount)
448
- if count != amount && options[:expires_in] && c.ttl(key) < 0
449
- c.expire(key, options[:expires_in].to_i)
450
- end
451
- count
452
468
  end
469
+
470
+ count
453
471
  end
454
472
  end
455
473
 
@@ -63,28 +63,6 @@ module ActiveSupport
63
63
  end
64
64
  end
65
65
 
66
- module Marshal61WithFallback
67
- include SerializerWithFallback
68
- extend self
69
-
70
- MARSHAL_SIGNATURE = "\x04\x08".b.freeze
71
-
72
- def dump(entry)
73
- Marshal.dump(entry)
74
- end
75
-
76
- def dump_compressed(entry, threshold)
77
- Marshal.dump(entry.compressed(threshold))
78
- end
79
-
80
- alias_method :_load, :marshal_load
81
- public :_load
82
-
83
- def dumped?(dumped)
84
- dumped.start_with?(MARSHAL_SIGNATURE)
85
- end
86
- end
87
-
88
66
  module Marshal70WithFallback
89
67
  include SerializerWithFallback
90
68
  extend self
@@ -165,7 +143,6 @@ module ActiveSupport
165
143
 
166
144
  SERIALIZERS = {
167
145
  passthrough: PassthroughWithFallback,
168
- marshal_6_1: Marshal61WithFallback,
169
146
  marshal_7_0: Marshal70WithFallback,
170
147
  marshal_7_1: Marshal71WithFallback,
171
148
  message_pack: MessagePackWithFallback,