Skip to content

Commit 6558318

Browse files
committed
Update behavior tests and fix timeout stubbing
- Bring in the latest behavior tests from Rails main - Keep previous test for Rails 8.0 - Fix stubbing to emulate timeouts, switch to public connection methods that should be more stable
1 parent 5a9c169 commit 6558318

23 files changed

+2513
-65
lines changed

test/test_helper.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,13 @@ def shard_keys(cache, shard)
9494
shard_keys = cache.send(:connections).assign(namespaced_keys)[shard]
9595
shard_keys.map { |key| key.delete_prefix("#{@namespace}:") }
9696
end
97+
98+
def emulating_timeouts
99+
ar_methods = [ :select_all, :delete, :exec_insert_all ]
100+
stub_matcher = ActiveRecord::Base.connection.class.any_instance
101+
ar_methods.each { |method| stub_matcher.stubs(method).raises(ActiveRecord::StatementTimeout) }
102+
yield
103+
ensure
104+
ar_methods.each { |method| stub_matcher.unstub(method) }
105+
end
97106
end

test/unit/behaviors.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@
1616
require_relative "behaviors_rails_7_2/failure_safety_behavior"
1717
require_relative "behaviors_rails_7_2/failure_raising_behavior"
1818
require_relative "behaviors_rails_7_2/local_cache_behavior"
19+
elsif Rails::VERSION::MAJOR == 8 && Rails::VERSION::MINOR == 0
20+
require_relative "behaviors_rails_8_0/cache_delete_matched_behavior"
21+
require_relative "behaviors_rails_8_0/cache_increment_decrement_behavior"
22+
require_relative "behaviors_rails_8_0/cache_instrumentation_behavior"
23+
require_relative "behaviors_rails_8_0/cache_logging_behavior"
24+
require_relative "behaviors_rails_8_0/cache_store_behavior"
25+
require_relative "behaviors_rails_8_0/cache_store_version_behavior"
26+
require_relative "behaviors_rails_8_0/cache_store_coder_behavior"
27+
require_relative "behaviors_rails_8_0/cache_store_compression_behavior"
28+
require_relative "behaviors_rails_8_0/cache_store_format_version_behavior"
29+
require_relative "behaviors_rails_8_0/cache_store_serializer_behavior"
30+
require_relative "behaviors_rails_8_0/connection_pool_behavior"
31+
require_relative "behaviors_rails_8_0/encoded_key_cache_behavior"
32+
require_relative "behaviors_rails_8_0/failure_safety_behavior"
33+
require_relative "behaviors_rails_8_0/failure_raising_behavior"
34+
require_relative "behaviors_rails_8_0/local_cache_behavior"
1935
else
2036
require_relative "behaviors/cache_delete_matched_behavior"
2137
require_relative "behaviors/cache_increment_decrement_behavior"

test/unit/behaviors/cache_increment_decrement_behavior.rb

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,25 @@ def test_decrement
3131
assert_equal -100, missing
3232
end
3333

34-
def test_ttl_is_not_updated
34+
def test_read_counter_and_write_counter
35+
key = SecureRandom.uuid
36+
@cache.write_counter(key, 1)
37+
assert_equal 1, @cache.read(key, raw: true).to_i
38+
39+
assert_equal 1, @cache.read_counter(key)
40+
assert_equal 2, @cache.increment(key)
41+
assert_equal 2, @cache.read_counter(key)
42+
43+
assert_nil @cache.read_counter(SecureRandom.alphanumeric)
44+
end
45+
46+
def test_ttl_isnt_updated
3547
key = SecureRandom.uuid
3648

37-
assert_equal 1, @cache.increment(key, 1, expires_in: 1)
38-
assert_equal 2, @cache.increment(key, 1, expires_in: 5000)
49+
assert_equal 1, @cache.increment(key, expires_in: 1)
50+
assert_equal 2, @cache.increment(key, expires_in: 5000)
3951

40-
# Having to sleep two seconds in a test is bad, but we're testing
52+
# having to sleep two seconds in a test is bad, but we're testing
4153
# a wide range of backends with different TTL mechanisms, most without
4254
# subsecond granularity, so this is the only reliable way.
4355
sleep 2

test/unit/behaviors/cache_instrumentation_behavior.rb

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ def test_write_multi_instrumentation
88
value_2 = SecureRandom.alphanumeric
99
writes = { key_1 => value_1, key_2 => value_2 }
1010

11-
events = with_instrumentation "write_multi" do
12-
@cache.write_multi(writes)
13-
end
11+
events = capture_notifications("cache_write_multi.active_support") { @cache.write_multi(writes) }
1412

1513
assert_equal %w[ cache_write_multi.active_support ], events.map(&:name)
1614
assert_nil events[0].payload[:super_operation]
@@ -23,7 +21,7 @@ def test_instrumentation_with_fetch_multi_as_super_operation
2321

2422
key_2 = SecureRandom.uuid
2523

26-
events = with_instrumentation "read_multi" do
24+
events = capture_notifications("cache_read_multi.active_support") do
2725
@cache.fetch_multi(key_2, key_1) { |key| key * 2 }
2826
end
2927

@@ -35,17 +33,14 @@ def test_instrumentation_with_fetch_multi_as_super_operation
3533
end
3634

3735
def test_fetch_multi_instrumentation_order_of_operations
38-
operations = []
39-
callback = ->(name, *) { operations << name }
40-
4136
key_1 = SecureRandom.uuid
4237
key_2 = SecureRandom.uuid
4338

44-
ActiveSupport::Notifications.subscribed(callback, /^cache_(read_multi|write_multi)\.active_support$/) do
39+
operations = capture_notifications(/^cache_(read_multi|write_multi)\.active_support$/) do
4540
@cache.fetch_multi(key_1, key_2) { |key| key * 2 }
4641
end
4742

48-
assert_equal %w[ cache_read_multi.active_support cache_write_multi.active_support ], operations
43+
assert_equal %w[ cache_read_multi.active_support cache_write_multi.active_support ], operations.map(&:name)
4944
end
5045

5146
def test_read_multi_instrumentation
@@ -54,23 +49,67 @@ def test_read_multi_instrumentation
5449

5550
key_2 = SecureRandom.uuid
5651

57-
events = with_instrumentation "read_multi" do
58-
@cache.read_multi(key_2, key_1)
59-
end
52+
events = capture_notifications("cache_read_multi.active_support") { @cache.read_multi(key_2, key_1) }
6053

6154
assert_equal %w[ cache_read_multi.active_support ], events.map(&:name)
6255
assert_equal [normalized_key(key_2), normalized_key(key_1)], events[0].payload[:key]
6356
assert_equal [normalized_key(key_1)], events[0].payload[:hits]
6457
assert_equal @cache.class.name, events[0].payload[:store]
6558
end
6659

60+
def test_read_instrumentation
61+
key = SecureRandom.uuid
62+
@cache.write(key, SecureRandom.alphanumeric)
63+
64+
events = capture_notifications("cache_read.active_support") { @cache.read(key) }
65+
66+
assert_equal %w[ cache_read.active_support ], events.map(&:name)
67+
assert_equal normalized_key(key), events[0].payload[:key]
68+
assert_same true, events[0].payload[:hit]
69+
assert_equal @cache.class.name, events[0].payload[:store]
70+
end
71+
72+
def test_write_instrumentation
73+
key = SecureRandom.uuid
74+
75+
events = capture_notifications("cache_write.active_support") { @cache.write(key, SecureRandom.alphanumeric) }
76+
77+
assert_equal %w[ cache_write.active_support ], events.map(&:name)
78+
assert_equal normalized_key(key), events[0].payload[:key]
79+
assert_equal @cache.class.name, events[0].payload[:store]
80+
end
81+
82+
def test_delete_instrumentation
83+
key = SecureRandom.uuid
84+
85+
options = { namespace: "foo" }
86+
87+
events = capture_notifications("cache_delete.active_support") { @cache.delete(key, options) }
88+
89+
assert_equal %w[ cache_delete.active_support ], events.map(&:name)
90+
assert_equal normalized_key(key, options), events[0].payload[:key]
91+
assert_equal @cache.class.name, events[0].payload[:store]
92+
assert_equal "foo", events[0].payload[:namespace]
93+
end
94+
95+
def test_delete_multi_instrumentation
96+
key_1 = SecureRandom.uuid
97+
key_2 = SecureRandom.uuid
98+
99+
options = { namespace: "foo" }
100+
101+
events = capture_notifications("cache_delete_multi.active_support") { @cache.delete_multi([key_2, key_1], options) }
102+
103+
assert_equal %w[ cache_delete_multi.active_support ], events.map(&:name)
104+
assert_equal [normalized_key(key_2, options), normalized_key(key_1, options)], events[0].payload[:key]
105+
assert_equal @cache.class.name, events[0].payload[:store]
106+
end
107+
67108
def test_increment_instrumentation
68109
key_1 = SecureRandom.uuid
69110
@cache.write(key_1, 0)
70111

71-
events = with_instrumentation "increment" do
72-
@cache.increment(key_1)
73-
end
112+
events = capture_notifications("cache_increment.active_support") { @cache.increment(key_1) }
74113

75114
assert_equal %w[ cache_increment.active_support ], events.map(&:name)
76115
assert_equal normalized_key(key_1), events[0].payload[:key]
@@ -82,28 +121,15 @@ def test_decrement_instrumentation
82121
key_1 = SecureRandom.uuid
83122
@cache.write(key_1, 0)
84123

85-
events = with_instrumentation "decrement" do
86-
@cache.decrement(key_1)
87-
end
124+
events = capture_notifications("cache_decrement.active_support") { @cache.decrement(key_1) }
88125

89126
assert_equal %w[ cache_decrement.active_support ], events.map(&:name)
90127
assert_equal normalized_key(key_1), events[0].payload[:key]
91128
assert_equal @cache.class.name, events[0].payload[:store]
92129
end
93130

94131
private
95-
def with_instrumentation(method)
96-
event_name = "cache_#{method}.active_support"
97-
98-
[].tap do |events|
99-
ActiveSupport::Notifications.subscribe(event_name) { |event| events << event }
100-
yield
101-
end
102-
ensure
103-
ActiveSupport::Notifications.unsubscribe event_name
104-
end
105-
106-
def normalized_key(key)
107-
@cache.send(:normalize_key, key, @cache.options)
132+
def normalized_key(key, options = nil)
133+
@cache.send(:normalize_key, key, options)
108134
end
109135
end

test/unit/behaviors/cache_store_behavior.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,26 @@ def test_race_condition_protection
647647
end
648648
end
649649

650+
def test_fetch_race_condition_protection
651+
time = Time.now
652+
key = SecureRandom.uuid
653+
value = SecureRandom.uuid
654+
expires_in = 60
655+
656+
@cache.write(key, value, expires_in:)
657+
Time.stub(:now, time + expires_in + 1) do
658+
fetched_value = @cache.fetch(key, expires_in:, race_condition_ttl: 10) do
659+
SecureRandom.uuid
660+
end
661+
assert_not_equal fetched_value, value
662+
assert_not_nil fetched_value
663+
end
664+
665+
Time.stub(:now, time + 2 * expires_in) do
666+
assert_not_nil @cache.read(key)
667+
end
668+
end
669+
650670
def test_fetch_multi_race_condition_protection
651671
time = Time.now
652672
key = SecureRandom.uuid
@@ -722,6 +742,26 @@ def test_setting_options_in_fetch_block_does_not_change_cache_options
722742
end
723743
end
724744

745+
def test_configuring_store_with_raw
746+
cache = lookup_store(raw: true)
747+
cache.write("foo", "bar")
748+
assert_equal "bar", cache.read("foo")
749+
end
750+
751+
def test_max_key_size
752+
cache = lookup_store(max_key_size: 64)
753+
key = "foobar" * 20
754+
cache.write(key, "bar")
755+
assert_equal "bar", cache.read(key)
756+
end
757+
758+
def test_max_key_size_disabled
759+
cache = lookup_store(max_key_size: false)
760+
key = "a" * 1000
761+
cache.write(key, "bar")
762+
assert_equal "bar", cache.read(key)
763+
end
764+
725765
private
726766
def with_raise_on_invalid_cache_expiration_time(new_value, &block)
727767
old_value = ActiveSupport::Cache::Store.raise_on_invalid_cache_expiration_time

test/unit/behaviors/connection_pool_behavior.rb

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ def test_connection_pool
77
threads = []
88

99
emulating_latency do
10-
cache = ActiveSupport::Cache.lookup_store(*store, { pool: { size: 2, timeout: 1 } }.merge(store_options))
10+
cache = ActiveSupport::Cache.lookup_store(*store, { pool: { size: 2, timeout: 0.1 } }.merge(store_options))
1111
cache.read("foo")
1212

13-
assert_raises Timeout::Error do
13+
assert_nothing_raised do
1414
# One of the three threads will fail in 1 second because our pool size
1515
# is only two.
1616
3.times do
@@ -28,6 +28,36 @@ def test_connection_pool
2828
Thread.report_on_exception = original_report_on_exception
2929
end
3030

31+
def test_connection_pool_fetch
32+
Thread.report_on_exception, original_report_on_exception = false, Thread.report_on_exception
33+
34+
threads = []
35+
results = []
36+
37+
emulating_latency do
38+
cache = ActiveSupport::Cache.lookup_store(*store, { pool: { size: 2, timeout: 0.1 } }.merge(store_options))
39+
value = SecureRandom.alphanumeric
40+
base_key = "latency:#{SecureRandom.uuid}"
41+
42+
assert_nothing_raised do
43+
# One of the three threads will fail in 1 second because our pool size
44+
# is only two.
45+
3.times do |i|
46+
threads << Thread.new do
47+
cache.fetch("#{base_key}:#{i}") { value }
48+
end
49+
end
50+
51+
results = threads.map(&:value)
52+
assert_equal [value] * 3, results, "All threads should return the same value"
53+
end
54+
ensure
55+
threads.each(&:kill)
56+
end
57+
ensure
58+
Thread.report_on_exception = original_report_on_exception
59+
end
60+
3161
def test_no_connection_pool
3262
threads = []
3363

0 commit comments

Comments
 (0)