1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
|
# frozen_string_literal: true
require_relative '../helper'
describe 'thundering herd protection' do
# Thundering herd features require memcached 1.6+ (meta protocol)
describe 'using the meta protocol' do
before do
skip 'Thundering herd features require memcached 1.6+' unless MemcachedManager.supported_protocols.include?(:meta)
end
describe 'get_with_recache' do
it 'returns value and recache status for existing key' do
memcached_persistent(:meta) do |dc|
dc.flush
dc.set('existing_key', 'existing_value', 300)
# Get an existing key - should not have any recache flags
server = dc.send(:ring).server_for_key('existing_key')
result = server.request(:meta_get, 'existing_key')
assert_equal 'existing_value', result[:value]
assert_predicate result[:cas], :positive?
refute result[:won_recache]
refute result[:stale]
refute result[:lost_recache]
end
end
it 'returns nil value for non-existent key without vivify' do
memcached_persistent(:meta) do |dc|
dc.flush
server = dc.send(:ring).server_for_key('nonexistent_key')
result = server.request(:meta_get, 'nonexistent_key')
assert_nil result[:value]
refute result[:won_recache]
refute result[:stale]
refute result[:lost_recache]
end
end
it 'vivifies on miss with N flag and returns won_recache' do
memcached_persistent(:meta) do |dc|
dc.flush
server = dc.send(:ring).server_for_key('vivify_key')
# Request with vivify_ttl - on miss, creates stub and returns W flag
result = server.request(:meta_get, 'vivify_key', { vivify_ttl: 30 })
# The first client should win the recache race on a miss
assert result[:won_recache], 'First client should win recache on miss with N flag'
# When vivifying, memcached creates an empty stub value
# The value will be empty string (marshalled) since the key was just created
end
end
it 'returns stale and lost_recache for second client during vivify' do
memcached_persistent(:meta) do |dc|
dc.flush
server = dc.send(:ring).server_for_key('race_key')
# First client vivifies the key
result1 = server.request(:meta_get, 'race_key', { vivify_ttl: 30 })
assert result1[:won_recache], 'First client should win'
# Second client should see stale and lost the race
server.request(:meta_get, 'race_key', { vivify_ttl: 30 })
# NOTE: On the second request, the stub already exists
# Behavior depends on memcached version and exact timing
# The key point is that only one client wins
end
end
end
describe 'delete_stale' do
it 'marks item as stale instead of deleting' do
memcached_persistent(:meta) do |dc|
dc.flush
dc.set('stale_key', 'stale_value', 300)
# Verify the key exists
assert_equal 'stale_value', dc.get('stale_key')
# Mark it as stale
server = dc.send(:ring).server_for_key('stale_key')
result = server.request(:delete_stale, 'stale_key')
assert result, 'delete_stale should return true on success'
# After marking stale, the key should still be accessible
# but will have the X flag when fetched with N/R flags
end
end
end
describe 'request formatter' do
it 'formats meta_get with N flag' do
req = Dalli::Protocol::Meta::RequestFormatter.meta_get(
key: 'test_key',
vivify_ttl: 30
)
assert_includes req, 'N30'
end
it 'formats meta_get with R flag' do
req = Dalli::Protocol::Meta::RequestFormatter.meta_get(
key: 'test_key',
recache_ttl: 60
)
assert_includes req, 'R60'
end
it 'formats meta_get with both N and R flags' do
req = Dalli::Protocol::Meta::RequestFormatter.meta_get(
key: 'test_key',
vivify_ttl: 30,
recache_ttl: 60
)
assert_includes req, 'N30'
assert_includes req, 'R60'
end
it 'formats meta_delete with I (stale) flag' do
req = Dalli::Protocol::Meta::RequestFormatter.meta_delete(
key: 'test_key',
stale: true
)
assert_includes req, ' I'
end
it 'formats meta_get with h (hit status) flag' do
req = Dalli::Protocol::Meta::RequestFormatter.meta_get(
key: 'test_key',
return_hit_status: true
)
assert_includes req, ' h'
end
it 'formats meta_get with l (last access) flag' do
req = Dalli::Protocol::Meta::RequestFormatter.meta_get(
key: 'test_key',
return_last_access: true
)
assert_includes req, ' l'
end
it 'formats meta_get with u (skip LRU bump) flag' do
req = Dalli::Protocol::Meta::RequestFormatter.meta_get(
key: 'test_key',
skip_lru_bump: true
)
assert_includes req, ' u'
end
it 'formats meta_get with all metadata flags combined' do
req = Dalli::Protocol::Meta::RequestFormatter.meta_get(
key: 'test_key',
vivify_ttl: 30,
recache_ttl: 60,
return_hit_status: true,
return_last_access: true,
skip_lru_bump: true
)
assert_includes req, 'N30'
assert_includes req, 'R60'
assert_includes req, ' h'
assert_includes req, ' l'
assert_includes req, ' u'
end
end
describe 'get_with_metadata' do
it 'returns value and metadata for existing key' do
memcached_persistent(:meta) do |dc|
dc.flush
dc.set('metadata_key', 'metadata_value', 300)
result = dc.get_with_metadata('metadata_key')
assert_equal 'metadata_value', result[:value]
assert_predicate result[:cas], :positive?
refute result[:won_recache]
refute result[:stale]
refute result[:lost_recache]
end
end
it 'returns nil value for non-existent key' do
memcached_persistent(:meta) do |dc|
dc.flush
result = dc.get_with_metadata('nonexistent_key')
assert_nil result[:value]
refute result[:won_recache]
end
end
it 'supports vivify_ttl option for thundering herd protection' do
memcached_persistent(:meta) do |dc|
dc.flush
# First request should win the recache race
result = dc.get_with_metadata('vivify_key', vivify_ttl: 30)
assert result[:won_recache], 'First client should win recache on miss with vivify_ttl'
end
end
it 'supports recache_ttl option for early recache' do
memcached_persistent(:meta) do |dc|
dc.flush
# Set with a very short TTL
dc.set('recache_key', 'recache_value', 5)
# Request with recache_ttl higher than remaining TTL should win recache
result = dc.get_with_metadata('recache_key', recache_ttl: 10)
# Either we win recache (TTL was below threshold) or the value is returned
assert_equal 'recache_value', result[:value]
end
end
it 'returns hit status when requested' do
memcached_persistent(:meta) do |dc|
dc.flush
dc.set('hit_key', 'hit_value', 300)
# First access - should show not previously hit
result1 = dc.get_with_metadata('hit_key', return_hit_status: true)
assert_equal 'hit_value', result1[:value]
refute_nil result1[:hit_before]
refute result1[:hit_before], 'First access should show not previously hit'
# Second access - should show previously hit
result2 = dc.get_with_metadata('hit_key', return_hit_status: true)
assert result2[:hit_before], 'Second access should show previously hit'
end
end
it 'returns last access time when requested' do
memcached_persistent(:meta) do |dc|
dc.flush
dc.set('access_key', 'access_value', 300)
# First get to establish access time
dc.get('access_key')
# Small delay to ensure measurable time difference
sleep 1
# Get with last access time
result = dc.get_with_metadata('access_key', return_last_access: true)
assert_equal 'access_value', result[:value]
refute_nil result[:last_access]
assert_predicate result[:last_access], :positive?, 'Last access time should be positive'
end
end
it 'supports skip_lru_bump option' do
memcached_persistent(:meta) do |dc|
dc.flush
dc.set('lru_key', 'lru_value', 300)
# Get with skip_lru_bump - should not update hit status
result = dc.get_with_metadata('lru_key', skip_lru_bump: true, return_hit_status: true)
assert_equal 'lru_value', result[:value]
# With skip_lru_bump, the hit status should remain as first access
refute result[:hit_before], 'skip_lru_bump should not update hit status'
end
end
it 'does not include optional metadata fields when not requested' do
memcached_persistent(:meta) do |dc|
dc.flush
dc.set('simple_key', 'simple_value', 300)
result = dc.get_with_metadata('simple_key')
assert_equal 'simple_value', result[:value]
refute result.key?(:hit_before), 'Should not include hit_before when not requested'
refute result.key?(:last_access), 'Should not include last_access when not requested'
end
end
end
describe 'fetch_with_lock' do
it 'regenerates value on cache miss' do
memcached_persistent(:meta) do |dc|
dc.flush
call_count = 0
value = dc.fetch_with_lock('new_key', ttl: 300, lock_ttl: 30) do
call_count += 1
'generated_value'
end
assert_equal 'generated_value', value
assert_equal 1, call_count
# Value should now be cached
cached_value = dc.get('new_key')
assert_equal 'generated_value', cached_value
end
end
it 'returns cached value without calling block' do
memcached_persistent(:meta) do |dc|
dc.flush
dc.set('existing_key', 'existing_value', 300)
call_count = 0
value = dc.fetch_with_lock('existing_key', ttl: 300, lock_ttl: 30) do
call_count += 1
'should_not_be_called'
end
assert_equal 'existing_value', value
assert_equal 0, call_count
end
end
it 'requires a block' do
memcached_persistent(:meta) do |dc|
assert_raises(ArgumentError) do
dc.fetch_with_lock('key', ttl: 300, lock_ttl: 30)
end
end
end
it 'works with complex values' do
memcached_persistent(:meta) do |dc|
dc.flush
complex_value = { items: [1, 2, 3], metadata: { count: 3 } }
value = dc.fetch_with_lock('complex_key', ttl: 300, lock_ttl: 30) do
complex_value
end
assert_equal complex_value, value
assert_equal complex_value, dc.get('complex_key')
end
end
it 'only allows one client to regenerate' do
memcached_persistent(:meta) do |dc|
dc.flush
# First fetch should win and regenerate
result1 = dc.fetch_with_lock('race_key', ttl: 300, lock_ttl: 30) do
'first_value'
end
assert_equal 'first_value', result1
# Second fetch should get the cached value
result2 = dc.fetch_with_lock('race_key', ttl: 300, lock_ttl: 30) do
'second_value_should_not_be_used'
end
assert_equal 'first_value', result2
end
end
end
end
end
|