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
@@ -131,17 +131,20 @@ module ActiveSupport
131
131
  end
132
132
  end
133
133
 
134
- def read_multi_entries(keys, **options)
134
+ def read_multi_entries(names, **options)
135
135
  return super unless local_cache
136
136
 
137
- local_entries = local_cache.read_multi_entries(keys)
137
+ keys_to_names = names.index_by { |name| normalize_key(name, options) }
138
+
139
+ local_entries = local_cache.read_multi_entries(keys_to_names.keys)
140
+ local_entries.transform_keys! { |key| keys_to_names[key] }
138
141
  local_entries.transform_values! do |payload|
139
- deserialize_entry(payload)&.value
142
+ deserialize_entry(payload, **options)&.value
140
143
  end
141
- missed_keys = keys - local_entries.keys
144
+ missed_names = names - local_entries.keys
142
145
 
143
- if missed_keys.any?
144
- local_entries.merge!(super(missed_keys, **options))
146
+ if missed_names.any?
147
+ local_entries.merge!(super(missed_names, **options))
145
148
  else
146
149
  local_entries
147
150
  end
@@ -52,7 +52,7 @@ module ActiveSupport
52
52
  autoload :LocalCache, "active_support/cache/strategy/local_cache"
53
53
  end
54
54
 
55
- @format_version = 6.1
55
+ @format_version = 7.0
56
56
 
57
57
  class << self
58
58
  attr_accessor :format_version
@@ -86,13 +86,7 @@ module ActiveSupport
86
86
  case store
87
87
  when Symbol
88
88
  options = parameters.extract_options!
89
- # clean this up once Ruby 2.7 support is dropped
90
- # see https://github.com/rails/rails/pull/41522#discussion_r581186602
91
- if options.empty?
92
- retrieve_store_class(store).new(*parameters)
93
- else
94
- retrieve_store_class(store).new(*parameters, **options)
95
- end
89
+ retrieve_store_class(store).new(*parameters, **options)
96
90
  when Array
97
91
  lookup_store(*store)
98
92
  when nil
@@ -160,13 +154,13 @@ module ActiveSupport
160
154
  # Some implementations may not support all methods beyond the basic cache
161
155
  # methods of #fetch, #write, #read, #exist?, and #delete.
162
156
  #
163
- # ActiveSupport::Cache::Store can store any Ruby object that is supported by
164
- # its +coder+'s +dump+ and +load+ methods.
157
+ # +ActiveSupport::Cache::Store+ can store any Ruby object that is supported
158
+ # by its +coder+'s +dump+ and +load+ methods.
165
159
  #
166
160
  # cache = ActiveSupport::Cache::MemoryStore.new
167
161
  #
168
162
  # cache.read('city') # => nil
169
- # cache.write('city', "Duckburgh")
163
+ # cache.write('city', "Duckburgh") # => true
170
164
  # cache.read('city') # => "Duckburgh"
171
165
  #
172
166
  # cache.write('not serializable', Proc.new {}) # => TypeError
@@ -206,24 +200,6 @@ module ActiveSupport
206
200
  def retrieve_pool_options(options)
207
201
  if options.key?(:pool)
208
202
  pool_options = options.delete(:pool)
209
- elsif options.key?(:pool_size) || options.key?(:pool_timeout)
210
- pool_options = {}
211
-
212
- if options.key?(:pool_size)
213
- ActiveSupport.deprecator.warn(<<~MSG)
214
- Using :pool_size is deprecated and will be removed in Rails 7.2.
215
- Use `pool: { size: #{options[:pool_size].inspect} }` instead.
216
- MSG
217
- pool_options[:size] = options.delete(:pool_size)
218
- end
219
-
220
- if options.key?(:pool_timeout)
221
- ActiveSupport.deprecator.warn(<<~MSG)
222
- Using :pool_timeout is deprecated and will be removed in Rails 7.2.
223
- Use `pool: { timeout: #{options[:pool_timeout].inspect} }` instead.
224
- MSG
225
- pool_options[:timeout] = options.delete(:pool_timeout)
226
- end
227
203
  else
228
204
  pool_options = true
229
205
  end
@@ -344,7 +320,7 @@ module ActiveSupport
344
320
 
345
321
  # Silences the logger within a block.
346
322
  def mute
347
- previous_silence, @silence = defined?(@silence) && @silence, true
323
+ previous_silence, @silence = @silence, true
348
324
  yield
349
325
  ensure
350
326
  @silence = previous_silence
@@ -370,8 +346,8 @@ module ActiveSupport
370
346
  #
371
347
  # ==== Options
372
348
  #
373
- # Internally, +fetch+ calls #read_entry, and calls #write_entry on a cache
374
- # miss. Thus, +fetch+ supports the same options as #read and #write.
349
+ # Internally, +fetch+ calls +read_entry+, and calls +write_entry+ on a
350
+ # cache miss. Thus, +fetch+ supports the same options as #read and #write.
375
351
  # Additionally, +fetch+ supports the following options:
376
352
  #
377
353
  # * <tt>force: true</tt> - Forces a cache "miss," meaning we treat the
@@ -411,37 +387,53 @@ module ActiveSupport
411
387
  # has elapsed.
412
388
  #
413
389
  # # Set all values to expire after one minute.
414
- # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
390
+ # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1)
415
391
  #
416
- # cache.write('foo', 'original value')
392
+ # cache.write("foo", "original value")
417
393
  # val_1 = nil
418
394
  # val_2 = nil
419
- # sleep 60
395
+ # p cache.read("foo") # => "original value"
396
+ #
397
+ # sleep 1 # wait until the cache expires
420
398
  #
421
- # Thread.new do
422
- # val_1 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
399
+ # t1 = Thread.new do
400
+ # # fetch does the following:
401
+ # # 1. gets an recent expired entry
402
+ # # 2. extends the expiry by 2 seconds (race_condition_ttl)
403
+ # # 3. regenerates the new value
404
+ # val_1 = cache.fetch("foo", race_condition_ttl: 2) do
423
405
  # sleep 1
424
- # 'new value 1'
406
+ # "new value 1"
425
407
  # end
426
408
  # end
427
409
  #
428
- # Thread.new do
429
- # val_2 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
430
- # 'new value 2'
431
- # end
410
+ # # Wait until t1 extends the expiry of the entry
411
+ # # but before generating the new value
412
+ # sleep 0.1
413
+ #
414
+ # val_2 = cache.fetch("foo", race_condition_ttl: 2) do
415
+ # # This block won't be executed because t1 extended the expiry
416
+ # "new value 2"
432
417
  # end
433
418
  #
434
- # cache.fetch('foo') # => "original value"
435
- # sleep 10 # First thread extended the life of cache by another 10 seconds
436
- # cache.fetch('foo') # => "new value 1"
437
- # val_1 # => "new value 1"
438
- # val_2 # => "original value"
419
+ # t1.join
420
+ #
421
+ # p val_1 # => "new value 1"
422
+ # p val_2 # => "oritinal value"
423
+ # p cache.fetch("foo") # => "new value 1"
424
+ #
425
+ # # The entry requires 3 seconds to expire (expires_in + race_condition_ttl)
426
+ # # We have waited 2 seconds already (sleep(1) + t1.join) thus we need to wait 1
427
+ # # more second to see the entry expire.
428
+ # sleep 1
429
+ #
430
+ # p cache.fetch("foo") # => nil
439
431
  #
440
432
  # ==== Dynamic Options
441
433
  #
442
- # In some cases it may be necessary to to dynamically compute options based
443
- # on the cached value. For this purpose, a ActiveSupport::Cache::WriteOptions
444
- # instance is passed as a second argument to the block
434
+ # In some cases it may be necessary to dynamically compute options based
435
+ # on the cached value. To support this, an ActiveSupport::Cache::WriteOptions
436
+ # instance is passed as the second argument to the block. For example:
445
437
  #
446
438
  # cache.fetch("authentication-token:#{user.id}") do |key, options|
447
439
  # token = authenticate_to_service
@@ -449,12 +441,6 @@ module ActiveSupport
449
441
  # token
450
442
  # end
451
443
  #
452
- # Only some options can be set dynamically:
453
- #
454
- # - +:expires_in+
455
- # - +:expires_at+
456
- # - +:version+
457
- #
458
444
  def fetch(name, options = nil, &block)
459
445
  if block_given?
460
446
  options = merged_options(options)
@@ -462,10 +448,20 @@ module ActiveSupport
462
448
 
463
449
  entry = nil
464
450
  unless options[:force]
465
- instrument(:read, name, options) do |payload|
451
+ instrument(:read, key, options) do |payload|
466
452
  cached_entry = read_entry(key, **options, event: payload)
467
453
  entry = handle_expired_entry(cached_entry, key, options)
468
- entry = nil if entry && entry.mismatched?(normalize_version(name, options))
454
+ if entry
455
+ if entry.mismatched?(normalize_version(name, options))
456
+ entry = nil
457
+ else
458
+ begin
459
+ entry.value
460
+ rescue DeserializationError
461
+ entry = nil
462
+ end
463
+ end
464
+ end
469
465
  payload[:super_operation] = :fetch if payload
470
466
  payload[:hit] = !!entry if payload
471
467
  end
@@ -474,7 +470,7 @@ module ActiveSupport
474
470
  if entry
475
471
  get_entry_value(entry, name, options)
476
472
  else
477
- save_block_result_to_cache(name, options, &block)
473
+ save_block_result_to_cache(name, key, options, &block)
478
474
  end
479
475
  elsif options && options[:force]
480
476
  raise ArgumentError, "Missing block: Calling `Cache#fetch` with `force: true` requires a block."
@@ -504,7 +500,7 @@ module ActiveSupport
504
500
  key = normalize_key(name, options)
505
501
  version = normalize_version(name, options)
506
502
 
507
- instrument(:read, name, options) do |payload|
503
+ instrument(:read, key, options) do |payload|
508
504
  entry = read_entry(key, **options, event: payload)
509
505
 
510
506
  if entry
@@ -517,7 +513,12 @@ module ActiveSupport
517
513
  nil
518
514
  else
519
515
  payload[:hit] = true if payload
520
- entry.value
516
+ begin
517
+ entry.value
518
+ rescue DeserializationError
519
+ payload[:hit] = false
520
+ nil
521
+ end
521
522
  end
522
523
  else
523
524
  payload[:hit] = false if payload
@@ -596,14 +597,14 @@ module ActiveSupport
596
597
  options = names.extract_options!
597
598
  options = merged_options(options)
598
599
 
599
- instrument_multi :read_multi, names, options do |payload|
600
+ writes = {}
601
+ ordered = instrument_multi :read_multi, names, options do |payload|
600
602
  if options[:force]
601
603
  reads = {}
602
604
  else
603
605
  reads = read_multi_entries(names, **options)
604
606
  end
605
607
 
606
- writes = {}
607
608
  ordered = names.index_with do |name|
608
609
  reads.fetch(name) { writes[name] = yield(name) }
609
610
  end
@@ -612,15 +613,20 @@ module ActiveSupport
612
613
  payload[:hits] = reads.keys
613
614
  payload[:super_operation] = :fetch_multi
614
615
 
615
- write_multi(writes, options)
616
-
617
616
  ordered
618
617
  end
618
+
619
+ write_multi(writes, options)
620
+
621
+ ordered
619
622
  end
620
623
 
621
624
  # Writes the value to the cache with the key. The value must be supported
622
625
  # by the +coder+'s +dump+ and +load+ methods.
623
626
  #
627
+ # Returns +true+ if the write succeeded, +nil+ if there was an error talking
628
+ # to the cache backend, or +false+ if the write failed for another reason.
629
+ #
624
630
  # By default, cache entries larger than 1kB are compressed. Compression
625
631
  # allows more data to be stored in the same memory footprint, leading to
626
632
  # fewer cache evictions and higher hit rates.
@@ -653,10 +659,11 @@ module ActiveSupport
653
659
  # Other options will be handled by the specific cache store implementation.
654
660
  def write(name, value, options = nil)
655
661
  options = merged_options(options)
662
+ key = normalize_key(name, options)
656
663
 
657
- instrument(:write, name, options) do
664
+ instrument(:write, key, options) do
658
665
  entry = Entry.new(value, **options.merge(version: normalize_version(name, options)))
659
- write_entry(normalize_key(name, options), entry, **options)
666
+ write_entry(key, entry, **options)
660
667
  end
661
668
  end
662
669
 
@@ -666,9 +673,10 @@ module ActiveSupport
666
673
  # Options are passed to the underlying cache implementation.
667
674
  def delete(name, options = nil)
668
675
  options = merged_options(options)
676
+ key = normalize_key(name, options)
669
677
 
670
- instrument(:delete, name) do
671
- delete_entry(normalize_key(name, options), **options)
678
+ instrument(:delete, key) do
679
+ delete_entry(key, **options)
672
680
  end
673
681
  end
674
682
 
@@ -692,9 +700,10 @@ module ActiveSupport
692
700
  # Options are passed to the underlying cache implementation.
693
701
  def exist?(name, options = nil)
694
702
  options = merged_options(options)
703
+ key = normalize_key(name, options)
695
704
 
696
- instrument(:exist?, name) do |payload|
697
- entry = read_entry(normalize_key(name, options), **options, event: payload)
705
+ instrument(:exist?, key) do |payload|
706
+ entry = read_entry(key, **options, event: payload)
698
707
  (entry && !entry.expired? && !entry.mismatched?(normalize_version(name, options))) || false
699
708
  end
700
709
  end
@@ -752,14 +761,6 @@ module ActiveSupport
752
761
  private
753
762
  def default_serializer
754
763
  case Cache.format_version
755
- when 6.1
756
- ActiveSupport.deprecator.warn <<~EOM
757
- Support for `config.active_support.cache_format_version = 6.1` has been deprecated and will be removed in Rails 7.2.
758
-
759
- Check the Rails upgrade guide at https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#new-activesupport-cache-serialization-format
760
- for more information on how to upgrade.
761
- EOM
762
- Cache::SerializerWithFallback[:marshal_6_1]
763
764
  when 7.0
764
765
  Cache::SerializerWithFallback[:marshal_7_0]
765
766
  when 7.1
@@ -809,7 +810,7 @@ module ActiveSupport
809
810
  end
810
811
  end
811
812
 
812
- def deserialize_entry(payload)
813
+ def deserialize_entry(payload, **)
813
814
  payload.nil? ? nil : @coder.load(payload)
814
815
  rescue DeserializationError
815
816
  nil
@@ -1007,7 +1008,7 @@ module ActiveSupport
1007
1008
  if multi
1008
1009
  ": #{payload[:key].size} key(s) specified"
1009
1010
  elsif payload[:key]
1010
- ": #{normalize_key(payload[:key], options)}"
1011
+ ": #{payload[:key]}"
1011
1012
  end
1012
1013
 
1013
1014
  debug_options = " (#{options.inspect})" unless options.blank?
@@ -1029,7 +1030,8 @@ module ActiveSupport
1029
1030
  # When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache
1030
1031
  # for a brief period while the entry is being recalculated.
1031
1032
  entry.expires_at = Time.now.to_f + race_ttl
1032
- write_entry(key, entry, expires_in: race_ttl * 2)
1033
+ options[:expires_in] = race_ttl * 2
1034
+ write_entry(key, entry, **options)
1033
1035
  else
1034
1036
  delete_entry(key, **options)
1035
1037
  end
@@ -1043,8 +1045,10 @@ module ActiveSupport
1043
1045
  entry.value
1044
1046
  end
1045
1047
 
1046
- def save_block_result_to_cache(name, options)
1047
- result = instrument(:generate, name, options) do
1048
+ def save_block_result_to_cache(name, key, options)
1049
+ options = options.dup
1050
+
1051
+ result = instrument(:generate, key, options) do
1048
1052
  yield(name, WriteOptions.new(options))
1049
1053
  end
1050
1054
 
@@ -1053,6 +1057,10 @@ module ActiveSupport
1053
1057
  end
1054
1058
  end
1055
1059
 
1060
+ # Enables the dynamic configuration of Cache entry options while ensuring
1061
+ # that conflicting options are not both set. When a block is given to
1062
+ # ActiveSupport::Cache::Store#fetch, the second argument will be an
1063
+ # instance of +WriteOptions+.
1056
1064
  class WriteOptions
1057
1065
  def initialize(options) # :nodoc:
1058
1066
  @options = options
@@ -1070,6 +1078,9 @@ module ActiveSupport
1070
1078
  @options[:expires_in]
1071
1079
  end
1072
1080
 
1081
+ # Sets the Cache entry's +expires_in+ value. If an +expires_at+ option was
1082
+ # previously set, this will unset it since +expires_in+ and +expires_at+
1083
+ # cannot both be set.
1073
1084
  def expires_in=(expires_in)
1074
1085
  @options.delete(:expires_at)
1075
1086
  @options[:expires_in] = expires_in
@@ -1079,6 +1090,9 @@ module ActiveSupport
1079
1090
  @options[:expires_at]
1080
1091
  end
1081
1092
 
1093
+ # Sets the Cache entry's +expires_at+ value. If an +expires_in+ option was
1094
+ # previously set, this will unset it since +expires_at+ and +expires_in+
1095
+ # cannot both be set.
1082
1096
  def expires_at=(expires_at)
1083
1097
  @options.delete(:expires_in)
1084
1098
  @options[:expires_at] = expires_at