Skip to content

Commit 9de17ac

Browse files
committed
Allow to set cache expiry as an absolute timestamp
Sometime it can be useful to set a cache entry expiry not relative to current time, but as an absolute timestamps, e.g.: - If you want to cache an API token that was provided to you with a precise expiry time. - If you want to cache something until a precise cutoff time, e.g. `expires_at: Time.now.at_end_of_hour` This leaves the `@created_at` variable in a weird state, but this is to avoid breaking the binary format.
1 parent 0ff395e commit 9de17ac

File tree

4 files changed

+55
-7
lines changed

4 files changed

+55
-7
lines changed

activesupport/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
* Add `expires_at` argument to `ActiveSupport::Cache` `write` and `fetch` to set a cache entry TTL as an absolute time.
2+
3+
```ruby
4+
Rails.cache.write(key, value, expires_at: Time.now.at_end_of_hour)
5+
```
6+
7+
*Jean Boussier*
8+
19
* Deprecate `ActiveSupport::TimeWithZone.name` so that from Rails 7.1 it will use the default implementation.
210

311
*Andrew White*

activesupport/lib/active_support/cache.rb

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,14 @@ def mute
267267
# cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes)
268268
# cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry
269269
#
270+
# Setting <tt>:expires_at</tt> will set an absolute expiration time on the cache.
271+
# All caches support auto-expiring content after a specified number of
272+
# seconds. This value can only be supplied to the +fetch+ or +write+ method to
273+
# affect just one entry.
274+
#
275+
# cache = ActiveSupport::Cache::MemoryStore.new
276+
# cache.write(key, value, expires_at: Time.now.at_end_of_hour)
277+
#
270278
# Setting <tt>:version</tt> verifies the cache stored under <tt>name</tt>
271279
# is of the same version. nil is returned on mismatches despite contents.
272280
# This feature is used to support recyclable cache keys.
@@ -751,7 +759,7 @@ def handle_expired_entry(entry, key, options)
751759
if (race_ttl > 0) && (Time.now.to_f - entry.expires_at <= race_ttl)
752760
# When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache
753761
# for a brief period while the entry is being recalculated.
754-
entry.expires_at = Time.now + race_ttl
762+
entry.expires_at = Time.now.to_f + race_ttl
755763
write_entry(key, entry, expires_in: race_ttl * 2)
756764
else
757765
delete_entry(key, **options)
@@ -801,12 +809,12 @@ class Entry # :nodoc:
801809
DEFAULT_COMPRESS_LIMIT = 1.kilobyte
802810

803811
# Creates a new cache entry for the specified value. Options supported are
804-
# +:compress+, +:compress_threshold+, +:version+ and +:expires_in+.
805-
def initialize(value, compress: true, compress_threshold: DEFAULT_COMPRESS_LIMIT, version: nil, expires_in: nil, **)
812+
# +:compress+, +:compress_threshold+, +:version+, +:expires_at+ and +:expires_in+.
813+
def initialize(value, compress: true, compress_threshold: DEFAULT_COMPRESS_LIMIT, version: nil, expires_in: nil, expires_at: nil, **)
806814
@value = value
807815
@version = version
808-
@created_at = Time.now.to_f
809-
@expires_in = expires_in && expires_in.to_f
816+
@created_at = 0.0
817+
@expires_in = expires_at&.to_f || expires_in && (expires_in.to_f + Time.now.to_f)
810818

811819
compress!(compress_threshold) if compress
812820
end

activesupport/test/cache/behaviors/cache_store_behavior.rb

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,16 +393,42 @@ def test_expires_in
393393
time = Time.local(2008, 4, 24)
394394

395395
Time.stub(:now, time) do
396-
@cache.write("foo", "bar")
396+
@cache.write("foo", "bar", expires_in: 1.minute)
397+
@cache.write("egg", "spam", expires_in: 2.minute)
397398
assert_equal "bar", @cache.read("foo")
399+
assert_equal "spam", @cache.read("egg")
398400
end
399401

400402
Time.stub(:now, time + 30) do
401403
assert_equal "bar", @cache.read("foo")
404+
assert_equal "spam", @cache.read("egg")
402405
end
403406

404407
Time.stub(:now, time + 61) do
405408
assert_nil @cache.read("foo")
409+
assert_equal "spam", @cache.read("egg")
410+
end
411+
412+
Time.stub(:now, time + 121) do
413+
assert_nil @cache.read("foo")
414+
assert_nil @cache.read("egg")
415+
end
416+
end
417+
418+
def test_expires_at
419+
time = Time.local(2008, 4, 24)
420+
421+
Time.stub(:now, time) do
422+
@cache.write("foo", "bar", expires_at: time + 15.seconds)
423+
assert_equal "bar", @cache.read("foo")
424+
end
425+
426+
Time.stub(:now, time + 10) do
427+
assert_equal "bar", @cache.read("foo")
428+
end
429+
430+
Time.stub(:now, time + 30) do
431+
assert_nil @cache.read("foo")
406432
end
407433
end
408434

activesupport/test/cache/cache_entry_test.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@ def test_expired
99
assert_not entry.expired?, "entry not expired"
1010
entry = ActiveSupport::Cache::Entry.new("value", expires_in: 60)
1111
assert_not entry.expired?, "entry not expired"
12-
Time.stub(:now, Time.now + 61) do
12+
Time.stub(:now, Time.at(entry.expires_at + 1)) do
1313
assert entry.expired?, "entry is expired"
1414
end
1515
end
16+
17+
def test_initialize_with_expires_at
18+
entry = ActiveSupport::Cache::Entry.new("value", expires_in: 60)
19+
clone = ActiveSupport::Cache::Entry.new("value", expires_at: entry.expires_at)
20+
assert_equal entry.expires_at, clone.expires_at
21+
end
1622
end

0 commit comments

Comments
 (0)