Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,13 @@ def shard_keys(cache, shard)
shard_keys = cache.send(:connections).assign(namespaced_keys)[shard]
shard_keys.map { |key| key.delete_prefix("#{@namespace}:") }
end

def emulating_timeouts
ar_methods = [ :select_all, :delete, :exec_insert_all ]
stub_matcher = ActiveRecord::Base.connection.class.any_instance
ar_methods.each { |method| stub_matcher.stubs(method).raises(ActiveRecord::StatementTimeout) }
yield
ensure
ar_methods.each { |method| stub_matcher.unstub(method) }
end
end
16 changes: 16 additions & 0 deletions test/unit/behaviors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@
require_relative "behaviors_rails_7_2/failure_safety_behavior"
require_relative "behaviors_rails_7_2/failure_raising_behavior"
require_relative "behaviors_rails_7_2/local_cache_behavior"
elsif Rails::VERSION::MAJOR == 8 && Rails::VERSION::MINOR == 0
require_relative "behaviors_rails_8_0/cache_delete_matched_behavior"
require_relative "behaviors_rails_8_0/cache_increment_decrement_behavior"
require_relative "behaviors_rails_8_0/cache_instrumentation_behavior"
require_relative "behaviors_rails_8_0/cache_logging_behavior"
require_relative "behaviors_rails_8_0/cache_store_behavior"
require_relative "behaviors_rails_8_0/cache_store_version_behavior"
require_relative "behaviors_rails_8_0/cache_store_coder_behavior"
require_relative "behaviors_rails_8_0/cache_store_compression_behavior"
require_relative "behaviors_rails_8_0/cache_store_format_version_behavior"
require_relative "behaviors_rails_8_0/cache_store_serializer_behavior"
require_relative "behaviors_rails_8_0/connection_pool_behavior"
require_relative "behaviors_rails_8_0/encoded_key_cache_behavior"
require_relative "behaviors_rails_8_0/failure_safety_behavior"
require_relative "behaviors_rails_8_0/failure_raising_behavior"
require_relative "behaviors_rails_8_0/local_cache_behavior"
else
require_relative "behaviors/cache_delete_matched_behavior"
require_relative "behaviors/cache_increment_decrement_behavior"
Expand Down
20 changes: 16 additions & 4 deletions test/unit/behaviors/cache_increment_decrement_behavior.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,25 @@ def test_decrement
assert_equal -100, missing
end

def test_ttl_is_not_updated
def test_read_counter_and_write_counter
key = SecureRandom.uuid
@cache.write_counter(key, 1)
assert_equal 1, @cache.read(key, raw: true).to_i

assert_equal 1, @cache.read_counter(key)
assert_equal 2, @cache.increment(key)
assert_equal 2, @cache.read_counter(key)

assert_nil @cache.read_counter(SecureRandom.alphanumeric)
end

def test_ttl_isnt_updated
key = SecureRandom.uuid

assert_equal 1, @cache.increment(key, 1, expires_in: 1)
assert_equal 2, @cache.increment(key, 1, expires_in: 5000)
assert_equal 1, @cache.increment(key, expires_in: 1)
assert_equal 2, @cache.increment(key, expires_in: 5000)

# Having to sleep two seconds in a test is bad, but we're testing
# having to sleep two seconds in a test is bad, but we're testing
# a wide range of backends with different TTL mechanisms, most without
# subsecond granularity, so this is the only reliable way.
sleep 2
Expand Down
88 changes: 57 additions & 31 deletions test/unit/behaviors/cache_instrumentation_behavior.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ def test_write_multi_instrumentation
value_2 = SecureRandom.alphanumeric
writes = { key_1 => value_1, key_2 => value_2 }

events = with_instrumentation "write_multi" do
@cache.write_multi(writes)
end
events = capture_notifications("cache_write_multi.active_support") { @cache.write_multi(writes) }

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

key_2 = SecureRandom.uuid

events = with_instrumentation "read_multi" do
events = capture_notifications("cache_read_multi.active_support") do
@cache.fetch_multi(key_2, key_1) { |key| key * 2 }
end

Expand All @@ -35,17 +33,14 @@ def test_instrumentation_with_fetch_multi_as_super_operation
end

def test_fetch_multi_instrumentation_order_of_operations
operations = []
callback = ->(name, *) { operations << name }

key_1 = SecureRandom.uuid
key_2 = SecureRandom.uuid

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

assert_equal %w[ cache_read_multi.active_support cache_write_multi.active_support ], operations
assert_equal %w[ cache_read_multi.active_support cache_write_multi.active_support ], operations.map(&:name)
end

def test_read_multi_instrumentation
Expand All @@ -54,23 +49,67 @@ def test_read_multi_instrumentation

key_2 = SecureRandom.uuid

events = with_instrumentation "read_multi" do
@cache.read_multi(key_2, key_1)
end
events = capture_notifications("cache_read_multi.active_support") { @cache.read_multi(key_2, key_1) }

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

def test_read_instrumentation
key = SecureRandom.uuid
@cache.write(key, SecureRandom.alphanumeric)

events = capture_notifications("cache_read.active_support") { @cache.read(key) }

assert_equal %w[ cache_read.active_support ], events.map(&:name)
assert_equal normalized_key(key), events[0].payload[:key]
assert_same true, events[0].payload[:hit]
assert_equal @cache.class.name, events[0].payload[:store]
end

def test_write_instrumentation
key = SecureRandom.uuid

events = capture_notifications("cache_write.active_support") { @cache.write(key, SecureRandom.alphanumeric) }

assert_equal %w[ cache_write.active_support ], events.map(&:name)
assert_equal normalized_key(key), events[0].payload[:key]
assert_equal @cache.class.name, events[0].payload[:store]
end

def test_delete_instrumentation
key = SecureRandom.uuid

options = { namespace: "foo" }

events = capture_notifications("cache_delete.active_support") { @cache.delete(key, options) }

assert_equal %w[ cache_delete.active_support ], events.map(&:name)
assert_equal normalized_key(key, options), events[0].payload[:key]
assert_equal @cache.class.name, events[0].payload[:store]
assert_equal "foo", events[0].payload[:namespace]
end

def test_delete_multi_instrumentation
key_1 = SecureRandom.uuid
key_2 = SecureRandom.uuid

options = { namespace: "foo" }

events = capture_notifications("cache_delete_multi.active_support") { @cache.delete_multi([key_2, key_1], options) }

assert_equal %w[ cache_delete_multi.active_support ], events.map(&:name)
assert_equal [normalized_key(key_2, options), normalized_key(key_1, options)], events[0].payload[:key]
assert_equal @cache.class.name, events[0].payload[:store]
end

def test_increment_instrumentation
key_1 = SecureRandom.uuid
@cache.write(key_1, 0)

events = with_instrumentation "increment" do
@cache.increment(key_1)
end
events = capture_notifications("cache_increment.active_support") { @cache.increment(key_1) }

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

events = with_instrumentation "decrement" do
@cache.decrement(key_1)
end
events = capture_notifications("cache_decrement.active_support") { @cache.decrement(key_1) }

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

private
def with_instrumentation(method)
event_name = "cache_#{method}.active_support"

[].tap do |events|
ActiveSupport::Notifications.subscribe(event_name) { |event| events << event }
yield
end
ensure
ActiveSupport::Notifications.unsubscribe event_name
end

def normalized_key(key)
@cache.send(:normalize_key, key, @cache.options)
def normalized_key(key, options = nil)
@cache.send(:normalize_key, key, options)
end
end
40 changes: 40 additions & 0 deletions test/unit/behaviors/cache_store_behavior.rb
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,26 @@ def test_race_condition_protection
end
end

def test_fetch_race_condition_protection
time = Time.now
key = SecureRandom.uuid
value = SecureRandom.uuid
expires_in = 60

@cache.write(key, value, expires_in:)
Time.stub(:now, time + expires_in + 1) do
fetched_value = @cache.fetch(key, expires_in:, race_condition_ttl: 10) do
SecureRandom.uuid
end
assert_not_equal fetched_value, value
assert_not_nil fetched_value
end

Time.stub(:now, time + 2 * expires_in) do
assert_not_nil @cache.read(key)
end
end

def test_fetch_multi_race_condition_protection
time = Time.now
key = SecureRandom.uuid
Expand Down Expand Up @@ -722,6 +742,26 @@ def test_setting_options_in_fetch_block_does_not_change_cache_options
end
end

def test_configuring_store_with_raw
cache = lookup_store(raw: true)
cache.write("foo", "bar")
assert_equal "bar", cache.read("foo")
end

def test_max_key_size
cache = lookup_store(max_key_size: 64)
key = "foobar" * 20
cache.write(key, "bar")
assert_equal "bar", cache.read(key)
end

def test_max_key_size_disabled
cache = lookup_store(max_key_size: false)
key = "a" * 1000
cache.write(key, "bar")
assert_equal "bar", cache.read(key)
end

private
def with_raise_on_invalid_cache_expiration_time(new_value, &block)
old_value = ActiveSupport::Cache::Store.raise_on_invalid_cache_expiration_time
Expand Down
34 changes: 32 additions & 2 deletions test/unit/behaviors/connection_pool_behavior.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ def test_connection_pool
threads = []

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

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

def test_connection_pool_fetch
Thread.report_on_exception, original_report_on_exception = false, Thread.report_on_exception

threads = []
results = []

emulating_latency do
cache = ActiveSupport::Cache.lookup_store(*store, { pool: { size: 2, timeout: 0.1 } }.merge(store_options))
value = SecureRandom.alphanumeric
base_key = "latency:#{SecureRandom.uuid}"

assert_nothing_raised do
# One of the three threads will fail in 1 second because our pool size
# is only two.
3.times do |i|
threads << Thread.new do
cache.fetch("#{base_key}:#{i}") { value }
end
end

results = threads.map(&:value)
assert_equal [value] * 3, results, "All threads should return the same value"
end
ensure
threads.each(&:kill)
end
ensure
Thread.report_on_exception = original_report_on_exception
end

def test_no_connection_pool
threads = []

Expand Down
Loading