activesupport 8.0.2.1 → 8.1.0.beta1

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +247 -136
  3. data/README.rdoc +1 -1
  4. data/lib/active_support/backtrace_cleaner.rb +71 -0
  5. data/lib/active_support/broadcast_logger.rb +46 -59
  6. data/lib/active_support/cache/mem_cache_store.rb +25 -27
  7. data/lib/active_support/cache/redis_cache_store.rb +36 -30
  8. data/lib/active_support/cache/strategy/local_cache.rb +16 -7
  9. data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
  10. data/lib/active_support/cache.rb +70 -6
  11. data/lib/active_support/configurable.rb +28 -0
  12. data/lib/active_support/continuous_integration.rb +145 -0
  13. data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
  14. data/lib/active_support/core_ext/date_time/conversions.rb +4 -2
  15. data/lib/active_support/core_ext/enumerable.rb +16 -4
  16. data/lib/active_support/core_ext/erb/util.rb +3 -3
  17. data/lib/active_support/core_ext/object/json.rb +8 -1
  18. data/lib/active_support/core_ext/object/to_query.rb +7 -1
  19. data/lib/active_support/core_ext/object/try.rb +2 -2
  20. data/lib/active_support/core_ext/range/overlap.rb +3 -3
  21. data/lib/active_support/core_ext/range/sole.rb +17 -0
  22. data/lib/active_support/core_ext/range.rb +1 -1
  23. data/lib/active_support/core_ext/string/filters.rb +3 -3
  24. data/lib/active_support/core_ext/string/multibyte.rb +12 -3
  25. data/lib/active_support/core_ext/string/output_safety.rb +19 -12
  26. data/lib/active_support/current_attributes/test_helper.rb +2 -2
  27. data/lib/active_support/current_attributes.rb +26 -16
  28. data/lib/active_support/deprecation/reporting.rb +4 -2
  29. data/lib/active_support/deprecation.rb +1 -1
  30. data/lib/active_support/editor.rb +70 -0
  31. data/lib/active_support/error_reporter.rb +50 -6
  32. data/lib/active_support/event_reporter/test_helper.rb +32 -0
  33. data/lib/active_support/event_reporter.rb +570 -0
  34. data/lib/active_support/evented_file_update_checker.rb +5 -1
  35. data/lib/active_support/execution_context.rb +64 -7
  36. data/lib/active_support/file_update_checker.rb +7 -5
  37. data/lib/active_support/gem_version.rb +3 -3
  38. data/lib/active_support/gzip.rb +1 -0
  39. data/lib/active_support/hash_with_indifferent_access.rb +47 -24
  40. data/lib/active_support/i18n_railtie.rb +1 -2
  41. data/lib/active_support/inflector/inflections.rb +31 -15
  42. data/lib/active_support/inflector/transliterate.rb +6 -8
  43. data/lib/active_support/isolated_execution_state.rb +7 -13
  44. data/lib/active_support/json/decoding.rb +6 -4
  45. data/lib/active_support/json/encoding.rb +103 -14
  46. data/lib/active_support/lazy_load_hooks.rb +1 -1
  47. data/lib/active_support/log_subscriber.rb +2 -0
  48. data/lib/active_support/logger_thread_safe_level.rb +6 -3
  49. data/lib/active_support/message_encryptors.rb +52 -0
  50. data/lib/active_support/message_pack/extensions.rb +5 -0
  51. data/lib/active_support/message_verifiers.rb +52 -0
  52. data/lib/active_support/messages/rotation_coordinator.rb +9 -0
  53. data/lib/active_support/messages/rotator.rb +5 -0
  54. data/lib/active_support/multibyte/chars.rb +8 -1
  55. data/lib/active_support/multibyte.rb +4 -0
  56. data/lib/active_support/railtie.rb +26 -12
  57. data/lib/active_support/syntax_error_proxy.rb +3 -0
  58. data/lib/active_support/test_case.rb +61 -6
  59. data/lib/active_support/testing/assertions.rb +34 -6
  60. data/lib/active_support/testing/error_reporter_assertions.rb +18 -1
  61. data/lib/active_support/testing/event_reporter_assertions.rb +217 -0
  62. data/lib/active_support/testing/notification_assertions.rb +92 -0
  63. data/lib/active_support/testing/parallelization/worker.rb +2 -0
  64. data/lib/active_support/testing/parallelization.rb +13 -0
  65. data/lib/active_support/testing/tests_without_assertions.rb +1 -1
  66. data/lib/active_support/testing/time_helpers.rb +7 -3
  67. data/lib/active_support/time_with_zone.rb +19 -5
  68. data/lib/active_support/values/time_zone.rb +8 -1
  69. data/lib/active_support/xml_mini.rb +1 -2
  70. data/lib/active_support.rb +11 -0
  71. metadata +11 -5
  72. data/lib/active_support/core_ext/range/each.rb +0 -24
@@ -76,7 +76,6 @@ module ActiveSupport
76
76
 
77
77
  # Returns all the logger that are part of this broadcast.
78
78
  attr_reader :broadcasts
79
- attr_reader :formatter
80
79
  attr_accessor :progname
81
80
 
82
81
  def initialize(*loggers)
@@ -105,62 +104,36 @@ module ActiveSupport
105
104
  @broadcasts.delete(logger)
106
105
  end
107
106
 
108
- def level
109
- @broadcasts.map(&:level).min
110
- end
111
-
112
- def <<(message)
113
- dispatch { |logger| logger.<<(message) }
114
- end
115
-
116
- def add(...)
117
- dispatch { |logger| logger.add(...) }
118
- end
119
- alias_method :log, :add
120
-
121
- def debug(...)
122
- dispatch { |logger| logger.debug(...) }
123
- end
124
-
125
- def info(...)
126
- dispatch { |logger| logger.info(...) }
127
- end
128
-
129
- def warn(...)
130
- dispatch { |logger| logger.warn(...) }
131
- end
132
-
133
- def error(...)
134
- dispatch { |logger| logger.error(...) }
135
- end
136
-
137
- def fatal(...)
138
- dispatch { |logger| logger.fatal(...) }
139
- end
140
-
141
- def unknown(...)
142
- dispatch { |logger| logger.unknown(...) }
107
+ def local_level=(level)
108
+ @broadcasts.each do |logger|
109
+ logger.local_level = level if logger.respond_to?(:local_level=)
110
+ end
143
111
  end
144
112
 
145
- def formatter=(formatter)
146
- dispatch { |logger| logger.formatter = formatter }
147
-
148
- @formatter = formatter
149
- end
113
+ def local_level
114
+ loggers = @broadcasts.select { |logger| logger.respond_to?(:local_level) }
150
115
 
151
- def level=(level)
152
- dispatch { |logger| logger.level = level }
116
+ loggers.map do |logger|
117
+ logger.local_level
118
+ end.first
153
119
  end
154
- alias_method :sev_threshold=, :level=
155
120
 
156
- def local_level=(level)
157
- dispatch do |logger|
158
- logger.local_level = level if logger.respond_to?(:local_level=)
159
- end
121
+ LOGGER_METHODS = %w[
122
+ << log add debug info warn error fatal unknown
123
+ level= sev_threshold= close
124
+ formatter formatter=
125
+ ] # :nodoc:
126
+ LOGGER_METHODS.each do |method|
127
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
128
+ def #{method}(...)
129
+ dispatch(:#{method}, ...)
130
+ end
131
+ RUBY
160
132
  end
161
133
 
162
- def close
163
- dispatch { |logger| logger.close }
134
+ # Returns the lowest level of all the loggers in the broadcast.
135
+ def level
136
+ @broadcasts.map(&:level).min
164
137
  end
165
138
 
166
139
  # True if the log level allows entries with severity +Logger::DEBUG+ to be written
@@ -171,7 +144,7 @@ module ActiveSupport
171
144
 
172
145
  # Sets the log level to +Logger::DEBUG+ for the whole broadcast.
173
146
  def debug!
174
- dispatch { |logger| logger.debug! }
147
+ dispatch(:debug!)
175
148
  end
176
149
 
177
150
  # True if the log level allows entries with severity +Logger::INFO+ to be written
@@ -182,7 +155,7 @@ module ActiveSupport
182
155
 
183
156
  # Sets the log level to +Logger::INFO+ for the whole broadcast.
184
157
  def info!
185
- dispatch { |logger| logger.info! }
158
+ dispatch(:info!)
186
159
  end
187
160
 
188
161
  # True if the log level allows entries with severity +Logger::WARN+ to be written
@@ -193,7 +166,7 @@ module ActiveSupport
193
166
 
194
167
  # Sets the log level to +Logger::WARN+ for the whole broadcast.
195
168
  def warn!
196
- dispatch { |logger| logger.warn! }
169
+ dispatch(:warn!)
197
170
  end
198
171
 
199
172
  # True if the log level allows entries with severity +Logger::ERROR+ to be written
@@ -204,7 +177,7 @@ module ActiveSupport
204
177
 
205
178
  # Sets the log level to +Logger::ERROR+ for the whole broadcast.
206
179
  def error!
207
- dispatch { |logger| logger.error! }
180
+ dispatch(:error!)
208
181
  end
209
182
 
210
183
  # True if the log level allows entries with severity +Logger::FATAL+ to be written
@@ -215,21 +188,35 @@ module ActiveSupport
215
188
 
216
189
  # Sets the log level to +Logger::FATAL+ for the whole broadcast.
217
190
  def fatal!
218
- dispatch { |logger| logger.fatal! }
191
+ dispatch(:fatal!)
219
192
  end
220
193
 
221
194
  def initialize_copy(other)
222
195
  @broadcasts = []
223
196
  @progname = other.progname.dup
224
- @formatter = other.formatter.dup
225
197
 
226
198
  broadcast_to(*other.broadcasts.map(&:dup))
227
199
  end
228
200
 
229
201
  private
230
- def dispatch(&block)
231
- @broadcasts.each { |logger| block.call(logger) }
232
- true
202
+ def dispatch(method, *args, **kwargs, &block)
203
+ if block_given?
204
+ # Maintain semantics that the first logger yields the block
205
+ # as normal, but subsequent loggers won't re-execute the block.
206
+ # Instead, the initial result is immediately returned.
207
+ called, result = false, nil
208
+ block = proc { |*args, **kwargs|
209
+ if called then result
210
+ else
211
+ called = true
212
+ result = yield(*args, **kwargs)
213
+ end
214
+ }
215
+ end
216
+
217
+ @broadcasts.map { |logger|
218
+ logger.send(method, *args, **kwargs, &block)
219
+ }.first
233
220
  end
234
221
 
235
222
  def method_missing(name, ...)
@@ -41,7 +41,6 @@ module ActiveSupport
41
41
 
42
42
  prepend Strategy::LocalCache
43
43
 
44
- KEY_MAX_SIZE = 250
45
44
  ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n
46
45
 
47
46
  # Creates a new Dalli::Client instance with specified addresses and options.
@@ -80,6 +79,7 @@ module ActiveSupport
80
79
  if options.key?(:cache_nils)
81
80
  options[:skip_nil] = !options.delete(:cache_nils)
82
81
  end
82
+ options[:max_key_size] ||= MAX_KEY_SIZE
83
83
  super(options)
84
84
 
85
85
  unless [String, Dalli::Client, NilClass].include?(addresses.first.class)
@@ -110,9 +110,6 @@ module ActiveSupport
110
110
  # * <tt>raw: true</tt> - Sends the value directly to the server as raw
111
111
  # bytes. The value must be a string or number. You can use memcached
112
112
  # direct operations like +increment+ and +decrement+ only on raw values.
113
- #
114
- # * <tt>unless_exist: true</tt> - Prevents overwriting an existing cache
115
- # entry.
116
113
 
117
114
  # Increment a cached integer value using the memcached incr atomic operator.
118
115
  # Returns the updated value.
@@ -129,6 +126,11 @@ module ActiveSupport
129
126
  #
130
127
  # Incrementing a non-numeric value, or a value written without
131
128
  # <tt>raw: true</tt>, will fail and return +nil+.
129
+ #
130
+ # To read the value later, call #read_counter:
131
+ #
132
+ # cache.increment("baz") # => 7
133
+ # cache.read_counter("baz") # 7
132
134
  def increment(name, amount = 1, options = nil)
133
135
  options = merged_options(options)
134
136
  key = normalize_key(name, options)
@@ -155,6 +157,11 @@ module ActiveSupport
155
157
  #
156
158
  # Decrementing a non-numeric value, or a value written without
157
159
  # <tt>raw: true</tt>, will fail and return +nil+.
160
+ #
161
+ # To read the value later, call #read_counter:
162
+ #
163
+ # cache.decrement("baz") # => 3
164
+ # cache.read_counter("baz") # 3
158
165
  def decrement(name, amount = 1, options = nil)
159
166
  options = merged_options(options)
160
167
  key = normalize_key(name, options)
@@ -212,26 +219,24 @@ module ActiveSupport
212
219
  def read_multi_entries(names, **options)
213
220
  keys_to_names = names.index_by { |name| normalize_key(name, options) }
214
221
 
215
- raw_values = begin
216
- @data.with { |c| c.get_multi(keys_to_names.keys) }
217
- rescue Dalli::UnmarshalError
218
- {}
219
- end
222
+ rescue_error_with({}) do
223
+ raw_values = @data.with { |c| c.get_multi(keys_to_names.keys) }
220
224
 
221
- values = {}
225
+ values = {}
222
226
 
223
- raw_values.each do |key, value|
224
- entry = deserialize_entry(value, raw: options[:raw])
227
+ raw_values.each do |key, value|
228
+ entry = deserialize_entry(value, raw: options[:raw])
225
229
 
226
- unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
227
- begin
228
- values[keys_to_names[key]] = entry.value
229
- rescue DeserializationError
230
+ unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
231
+ begin
232
+ values[keys_to_names[key]] = entry.value
233
+ rescue DeserializationError
234
+ end
230
235
  end
231
236
  end
232
- end
233
237
 
234
- values
238
+ values
239
+ end
235
240
  end
236
241
 
237
242
  # Delete an entry from the cache.
@@ -251,19 +256,12 @@ module ActiveSupport
251
256
  # before applying the regular expression to ensure we are escaping all
252
257
  # characters properly.
253
258
  def normalize_key(key, options)
254
- key = super
259
+ key = expand_and_namespace_key(key, options)
255
260
  if key
256
261
  key = key.dup.force_encoding(Encoding::ASCII_8BIT)
257
262
  key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
258
-
259
- if key.size > KEY_MAX_SIZE
260
- key_separator = ":hash:"
261
- key_hash = ActiveSupport::Digest.hexdigest(key)
262
- key_trim_size = KEY_MAX_SIZE - key_separator.size - key_hash.size
263
- key = "#{key[0, key_trim_size]}#{key_separator}#{key_hash}"
264
- end
265
263
  end
266
- key
264
+ truncate_key(key)
267
265
  end
268
266
 
269
267
  def deserialize_entry(payload, raw: false, **)
@@ -35,9 +35,6 @@ module ActiveSupport
35
35
  # +Redis::Distributed+ 4.0.1+ for distributed mget support.
36
36
  # * +delete_matched+ support for Redis KEYS globs.
37
37
  class RedisCacheStore < Store
38
- # Keys are truncated with the Active Support digest if they exceed 1kB
39
- MAX_KEY_BYTESIZE = 1024
40
-
41
38
  DEFAULT_REDIS_OPTIONS = {
42
39
  connect_timeout: 1,
43
40
  read_timeout: 1,
@@ -106,20 +103,29 @@ module ActiveSupport
106
103
  end
107
104
  end
108
105
 
109
- attr_reader :max_key_bytesize
110
106
  attr_reader :redis
111
107
 
112
108
  # Creates a new Redis cache store.
113
109
  #
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.
110
+ # There are a few ways to provide the Redis client used by the cache:
111
+ #
112
+ # 1. The +:redis+ param can be:
113
+ # - A Redis instance.
114
+ # - A +ConnectionPool+ instance wrapping a Redis instance.
115
+ # - A block that returns a Redis instance.
116
+ #
117
+ # 2. The +:url+ param can be:
118
+ # - A string used to create a Redis instance.
119
+ # - An array of strings used to create a +Redis::Distributed+ instance.
120
+ #
121
+ # If the final Redis instance is not already a +ConnectionPool+, it will
122
+ # be wrapped in one using +ActiveSupport::Cache::Store::DEFAULT_POOL_OPTIONS+.
123
+ # These options can be overridden with the +:pool+ param, or the pool can be
124
+ # disabled with +:pool: false+.
119
125
  #
120
126
  # Option Class Result
121
- # :redis Proc -> options[:redis].call
122
127
  # :redis Object -> options[:redis]
128
+ # :redis Proc -> options[:redis].call
123
129
  # :url String -> Redis.new(url: …)
124
130
  # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
125
131
  #
@@ -148,14 +154,17 @@ module ActiveSupport
148
154
  # cache.exist?('bar') # => false
149
155
  def initialize(error_handler: DEFAULT_ERROR_HANDLER, **redis_options)
150
156
  universal_options = redis_options.extract!(*UNIVERSAL_OPTIONS)
157
+ redis = redis_options[:redis]
158
+
159
+ already_pool = redis.instance_of?(::ConnectionPool) ||
160
+ (redis.respond_to?(:wrapped_pool) && redis.wrapped_pool.instance_of?(::ConnectionPool))
151
161
 
152
- if pool_options = self.class.send(:retrieve_pool_options, redis_options)
162
+ if !already_pool && pool_options = self.class.send(:retrieve_pool_options, redis_options)
153
163
  @redis = ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
154
164
  else
155
165
  @redis = self.class.build_redis(**redis_options)
156
166
  end
157
167
 
158
- @max_key_bytesize = MAX_KEY_BYTESIZE
159
168
  @error_handler = error_handler
160
169
 
161
170
  super(universal_options)
@@ -213,7 +222,7 @@ module ActiveSupport
213
222
  nodes.each do |node|
214
223
  begin
215
224
  cursor, keys = node.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE)
216
- node.del(*keys) unless keys.empty?
225
+ node.unlink(*keys) unless keys.empty?
217
226
  end until cursor == "0"
218
227
  end
219
228
  end
@@ -236,6 +245,11 @@ module ActiveSupport
236
245
  # Incrementing a non-numeric value, or a value written without
237
246
  # <tt>raw: true</tt>, will fail and return +nil+.
238
247
  #
248
+ # To read the value later, call #read_counter:
249
+ #
250
+ # cache.increment("baz") # => 7
251
+ # cache.read_counter("baz") # 7
252
+ #
239
253
  # Failsafe: Raises errors.
240
254
  def increment(name, amount = 1, options = nil)
241
255
  options = merged_options(options)
@@ -263,6 +277,11 @@ module ActiveSupport
263
277
  # Decrementing a non-numeric value, or a value written without
264
278
  # <tt>raw: true</tt>, will fail and return +nil+.
265
279
  #
280
+ # To read the value later, call #read_counter:
281
+ #
282
+ # cache.decrement("baz") # => 3
283
+ # cache.read_counter("baz") # 3
284
+ #
266
285
  # Failsafe: Raises errors.
267
286
  def decrement(name, amount = 1, options = nil)
268
287
  options = merged_options(options)
@@ -384,14 +403,16 @@ module ActiveSupport
384
403
  # Delete an entry from the cache.
385
404
  def delete_entry(key, **options)
386
405
  failsafe :delete_entry, returning: false do
387
- redis.then { |c| c.del(key) == 1 }
406
+ redis.then { |c| c.unlink(key) == 1 }
388
407
  end
389
408
  end
390
409
 
391
410
  # Deletes multiple entries in the cache. Returns the number of entries deleted.
392
411
  def delete_multi_entries(entries, **_options)
412
+ return 0 if entries.empty?
413
+
393
414
  failsafe :delete_multi_entries, returning: 0 do
394
- redis.then { |c| c.del(entries) }
415
+ redis.then { |c| c.unlink(*entries) }
395
416
  end
396
417
  end
397
418
 
@@ -410,21 +431,6 @@ module ActiveSupport
410
431
  end
411
432
  end
412
433
 
413
- # Truncate keys that exceed 1kB.
414
- def normalize_key(key, options)
415
- truncate_key super&.b
416
- end
417
-
418
- def truncate_key(key)
419
- if key && key.bytesize > max_key_bytesize
420
- suffix = ":hash:#{ActiveSupport::Digest.hexdigest(key)}"
421
- truncate_at = max_key_bytesize - suffix.bytesize
422
- "#{key.byteslice(0, truncate_at)}#{suffix}"
423
- else
424
- key
425
- end
426
- end
427
-
428
434
  def deserialize_entry(payload, raw: false, **)
429
435
  if raw && !payload.nil?
430
436
  Entry.new(payload)
@@ -68,12 +68,25 @@ module ActiveSupport
68
68
  use_temporary_local_cache(LocalStore.new, &block)
69
69
  end
70
70
 
71
+ # Set a new local cache.
72
+ def new_local_cache
73
+ LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
74
+ end
75
+
76
+ # Unset the current local cache.
77
+ def unset_local_cache
78
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil)
79
+ end
80
+
81
+ # The current local cache.
82
+ def local_cache
83
+ LocalCacheRegistry.cache_for(local_cache_key)
84
+ end
85
+
71
86
  # Middleware class can be inserted as a Rack handler to be local cache for the
72
87
  # duration of request.
73
88
  def middleware
74
- @middleware ||= Middleware.new(
75
- "ActiveSupport::Cache::Strategy::LocalCache",
76
- local_cache_key)
89
+ @middleware ||= Middleware.new("ActiveSupport::Cache::Strategy::LocalCache", self)
77
90
  end
78
91
 
79
92
  def clear(options = nil) # :nodoc:
@@ -214,10 +227,6 @@ module ActiveSupport
214
227
  @local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, "_").to_sym
215
228
  end
216
229
 
217
- def local_cache
218
- LocalCacheRegistry.cache_for(local_cache_key)
219
- end
220
-
221
230
  def bypass_local_cache(&block)
222
231
  use_temporary_local_cache(nil, &block)
223
232
  end
@@ -11,11 +11,12 @@ module ActiveSupport
11
11
  # This class wraps up local storage for middlewares. Only the middleware method should
12
12
  # construct them.
13
13
  class Middleware # :nodoc:
14
- attr_reader :name, :local_cache_key
14
+ attr_reader :name
15
+ attr_accessor :cache
15
16
 
16
- def initialize(name, local_cache_key)
17
+ def initialize(name, cache)
17
18
  @name = name
18
- @local_cache_key = local_cache_key
19
+ @cache = cache
19
20
  @app = nil
20
21
  end
21
22
 
@@ -25,18 +26,17 @@ module ActiveSupport
25
26
  end
26
27
 
27
28
  def call(env)
28
- LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
29
+ cache.new_local_cache
29
30
  response = @app.call(env)
30
31
  response[2] = ::Rack::BodyProxy.new(response[2]) do
31
- LocalCacheRegistry.set_cache_for(local_cache_key, nil)
32
+ cache.unset_local_cache
32
33
  end
33
34
  cleanup_on_body_close = true
34
35
  response
35
36
  rescue Rack::Utils::InvalidParameterError
36
37
  [400, {}, []]
37
38
  ensure
38
- LocalCacheRegistry.set_cache_for(local_cache_key, nil) unless
39
- cleanup_on_body_close
39
+ cache.unset_local_cache unless cleanup_on_body_close
40
40
  end
41
41
  end
42
42
  end
@@ -35,6 +35,8 @@ module ActiveSupport
35
35
  :race_condition_ttl,
36
36
  :serializer,
37
37
  :skip_nil,
38
+ :raw,
39
+ :max_key_size,
38
40
  ]
39
41
 
40
42
  # Mapping of canonical option names to aliases that a store will recognize.
@@ -186,6 +188,12 @@ module ActiveSupport
186
188
  # @last_mod_time = Time.now # Invalidate the entire cache by changing namespace
187
189
  #
188
190
  class Store
191
+ # Default +ConnectionPool+ options
192
+ DEFAULT_POOL_OPTIONS = { size: 5, timeout: 5 }.freeze
193
+
194
+ # Keys are truncated with the Active Support digest if they exceed the limit.
195
+ MAX_KEY_SIZE = 250
196
+
189
197
  cattr_accessor :logger, instance_writer: true
190
198
  cattr_accessor :raise_on_invalid_cache_expiration_time, default: false
191
199
 
@@ -194,9 +202,6 @@ module ActiveSupport
194
202
 
195
203
  class << self
196
204
  private
197
- DEFAULT_POOL_OPTIONS = { size: 5, timeout: 5 }.freeze
198
- private_constant :DEFAULT_POOL_OPTIONS
199
-
200
205
  def retrieve_pool_options(options)
201
206
  if options.key?(:pool)
202
207
  pool_options = options.delete(:pool)
@@ -298,6 +303,9 @@ module ActiveSupport
298
303
  @options[:compress] = true unless @options.key?(:compress)
299
304
  @options[:compress_threshold] ||= DEFAULT_COMPRESS_LIMIT
300
305
 
306
+ @max_key_size = @options.delete(:max_key_size)
307
+ @max_key_size = MAX_KEY_SIZE if @max_key_size.nil? # allow 'false' as a value
308
+
301
309
  @coder = @options.delete(:coder) do
302
310
  legacy_serializer = Cache.format_version < 7.1 && !@options[:serializer]
303
311
  serializer = @options.delete(:serializer) || default_serializer
@@ -556,7 +564,7 @@ module ActiveSupport
556
564
 
557
565
  instrument_multi :write_multi, normalized_hash, options do |payload|
558
566
  entries = hash.each_with_object({}) do |(name, value), memo|
559
- memo[normalize_key(name, options)] = Entry.new(value, **options.merge(version: normalize_version(name, options)))
567
+ memo[normalize_key(name, options)] = Entry.new(value, **options, version: normalize_version(name, options))
560
568
  end
561
569
 
562
570
  write_multi_entries entries, **options
@@ -658,13 +666,15 @@ module ActiveSupport
658
666
  # version, the read will be treated as a cache miss. This feature is
659
667
  # used to support recyclable cache keys.
660
668
  #
669
+ # * +:unless_exist+ - Prevents overwriting an existing cache entry.
670
+ #
661
671
  # Other options will be handled by the specific cache store implementation.
662
672
  def write(name, value, options = nil)
663
673
  options = merged_options(options)
664
674
  key = normalize_key(name, options)
665
675
 
666
676
  instrument(:write, key, options) do
667
- entry = Entry.new(value, **options.merge(version: normalize_version(name, options)))
677
+ entry = Entry.new(value, **options, version: normalize_version(name, options))
668
678
  write_entry(key, entry, **options)
669
679
  end
670
680
  end
@@ -741,6 +751,32 @@ module ActiveSupport
741
751
  raise NotImplementedError.new("#{self.class.name} does not support decrement")
742
752
  end
743
753
 
754
+ # Reads a counter that was set by #increment / #decrement.
755
+ #
756
+ # cache.write_counter("foo", 1)
757
+ # cache.read_counter("foo") # => 1
758
+ # cache.increment("foo")
759
+ # cache.read_counter("foo") # => 2
760
+ #
761
+ # Options are passed to the underlying cache implementation.
762
+ def read_counter(name, **options)
763
+ options = merged_options(options).merge(raw: true)
764
+ read(name, **options)&.to_i
765
+ end
766
+
767
+ # Writes a counter that can then be modified by #increment / #decrement.
768
+ #
769
+ # cache.write_counter("foo", 1)
770
+ # cache.read_counter("foo") # => 1
771
+ # cache.increment("foo")
772
+ # cache.read_counter("foo") # => 2
773
+ #
774
+ # Options are passed to the underlying cache implementation.
775
+ def write_counter(name, value, **options)
776
+ options = merged_options(options).merge(raw: true)
777
+ write(name, value.to_i, **options)
778
+ end
779
+
744
780
  # Cleans up the cache by removing expired entries.
745
781
  #
746
782
  # Options are passed to the underlying cache implementation.
@@ -760,6 +796,17 @@ module ActiveSupport
760
796
  raise NotImplementedError.new("#{self.class.name} does not support clear")
761
797
  end
762
798
 
799
+ # Get the current namespace
800
+ def namespace
801
+ @options[:namespace]
802
+ end
803
+
804
+ # Set the current namespace. Note, this will be ignored if custom
805
+ # options are passed to cache wills with a namespace key.
806
+ def namespace=(namespace)
807
+ @options[:namespace] = namespace
808
+ end
809
+
763
810
  private
764
811
  def default_serializer
765
812
  case Cache.format_version
@@ -926,16 +973,33 @@ module ActiveSupport
926
973
  options
927
974
  end
928
975
 
929
- # Expands and namespaces the cache key.
976
+ # Expands, namespaces and truncates the cache key.
930
977
  # Raises an exception when the key is +nil+ or an empty string.
931
978
  # May be overridden by cache stores to do additional normalization.
932
979
  def normalize_key(key, options = nil)
980
+ key = expand_and_namespace_key(key, options)
981
+ truncate_key(key)
982
+ end
983
+
984
+ def expand_and_namespace_key(key, options = nil)
933
985
  str_key = expanded_key(key)
934
986
  raise(ArgumentError, "key cannot be blank") if !str_key || str_key.empty?
935
987
 
936
988
  namespace_key str_key, options
937
989
  end
938
990
 
991
+ def truncate_key(key)
992
+ if key && @max_key_size && key.bytesize > @max_key_size
993
+ suffix = ":hash:#{ActiveSupport::Digest.hexdigest(key)}"
994
+ truncate_at = @max_key_size - suffix.bytesize
995
+ key = key.byteslice(0, truncate_at)
996
+ key.scrub!("")
997
+ "#{key}#{suffix}"
998
+ else
999
+ key
1000
+ end
1001
+ end
1002
+
939
1003
  # Prefix the key with a namespace string:
940
1004
  #
941
1005
  # namespace_key 'foo', namespace: 'cache'
@@ -27,6 +27,19 @@ module ActiveSupport
27
27
  end
28
28
 
29
29
  module ClassMethods
30
+ # Reads and writes attributes from a configuration OrderedOptions.
31
+ #
32
+ # require "active_support/configurable"
33
+ #
34
+ # class User
35
+ # include ActiveSupport::Configurable
36
+ # end
37
+ #
38
+ # User.config.allowed_access = true
39
+ # User.config.level = 1
40
+ #
41
+ # User.config.allowed_access # => true
42
+ # User.config.level # => 1
30
43
  def config
31
44
  @_config ||= if respond_to?(:superclass) && superclass.respond_to?(:config)
32
45
  superclass.config.inheritable_copy
@@ -36,6 +49,21 @@ module ActiveSupport
36
49
  end
37
50
  end
38
51
 
52
+ # Configure values from within the passed block.
53
+ #
54
+ # require "active_support/configurable"
55
+ #
56
+ # class User
57
+ # include ActiveSupport::Configurable
58
+ # end
59
+ #
60
+ # User.allowed_access # => nil
61
+ #
62
+ # User.configure do |config|
63
+ # config.allowed_access = true
64
+ # end
65
+ #
66
+ # User.allowed_access # => true
39
67
  def configure
40
68
  yield config
41
69
  end