|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +require 'spec_helper' |
| 4 | +require 'legion/cache/redis' |
| 5 | +require 'legion/cache/memcached' |
| 6 | +require 'legion/cache/memory' |
| 7 | + |
| 8 | +RSpec.describe 'Legion::Cache set_nx' do |
| 9 | + describe Legion::Cache::Memory do |
| 10 | + before { described_class.reset! } |
| 11 | + |
| 12 | + describe '.set_nx' do |
| 13 | + it 'returns true and stores value when key does not exist' do |
| 14 | + result = described_class.set_nx('nx-key', 'value', ttl: 60) |
| 15 | + expect(result).to be true |
| 16 | + expect(described_class.get('nx-key')).to eq('value') |
| 17 | + end |
| 18 | + |
| 19 | + it 'returns false and does not overwrite when key already exists' do |
| 20 | + described_class.set('nx-key', 'original', ttl: 60) |
| 21 | + result = described_class.set_nx('nx-key', 'overwrite', ttl: 60) |
| 22 | + expect(result).to be false |
| 23 | + expect(described_class.get('nx-key')).to eq('original') |
| 24 | + end |
| 25 | + |
| 26 | + it 'returns true after an expired key has been purged' do |
| 27 | + described_class.set('nx-expire', 'old', ttl: 0.05) |
| 28 | + sleep 0.07 |
| 29 | + result = described_class.set_nx('nx-expire', 'new', ttl: 60) |
| 30 | + expect(result).to be true |
| 31 | + expect(described_class.get('nx-expire')).to eq('new') |
| 32 | + end |
| 33 | + |
| 34 | + it 'is atomic under concurrent access' do |
| 35 | + winners = [] |
| 36 | + mutex = Mutex.new |
| 37 | + threads = 10.times.map do |i| |
| 38 | + Thread.new do |
| 39 | + won = described_class.set_nx('race-key', "value-#{i}", ttl: 60) |
| 40 | + mutex.synchronize { winners << i } if won |
| 41 | + end |
| 42 | + end |
| 43 | + threads.each(&:join) |
| 44 | + expect(winners.size).to eq(1) |
| 45 | + end |
| 46 | + end |
| 47 | + end |
| 48 | + |
| 49 | + describe Legion::Cache::Redis do |
| 50 | + let(:cache) { described_class.dup } |
| 51 | + let(:pool) { instance_double(ConnectionPool) } |
| 52 | + let(:redis) { instance_double(Redis) } |
| 53 | + |
| 54 | + before do |
| 55 | + cache.instance_variable_set(:@client, pool) |
| 56 | + cache.instance_variable_set(:@connected, true) |
| 57 | + allow(pool).to receive(:with).and_yield(redis) |
| 58 | + end |
| 59 | + |
| 60 | + describe '#set_nx' do |
| 61 | + it 'returns true when Redis SET NX succeeds (returns "OK")' do |
| 62 | + allow(redis).to receive(:set).with('nx-key', anything, nx: true, ex: 60).and_return('OK') |
| 63 | + expect(cache.set_nx('nx-key', 'value', ttl: 60)).to be true |
| 64 | + end |
| 65 | + |
| 66 | + it 'returns false when Redis SET NX fails (key exists, returns nil)' do |
| 67 | + allow(redis).to receive(:set).with('nx-key', anything, nx: true, ex: 60).and_return(nil) |
| 68 | + expect(cache.set_nx('nx-key', 'value', ttl: 60)).to be false |
| 69 | + end |
| 70 | + |
| 71 | + it 'passes nx: true and ex: ttl to Redis SET' do |
| 72 | + expect(redis).to receive(:set).with('nx-key', anything, nx: true, ex: 120).and_return('OK') |
| 73 | + cache.set_nx('nx-key', 'value', ttl: 120) |
| 74 | + end |
| 75 | + |
| 76 | + it 'serializes the value before storing' do |
| 77 | + captured = nil |
| 78 | + allow(redis).to receive(:set) do |_key, val, **_opts| |
| 79 | + captured = val |
| 80 | + 'OK' |
| 81 | + end |
| 82 | + cache.set_nx('nx-key', { data: 42 }, ttl: 60) |
| 83 | + expect(captured).to be_a(String) |
| 84 | + end |
| 85 | + end |
| 86 | + end |
| 87 | + |
| 88 | + describe Legion::Cache::Memcached do |
| 89 | + let(:cache) { described_class.dup } |
| 90 | + let(:pool) { instance_double(ConnectionPool) } |
| 91 | + let(:dalli) { instance_double(Dalli::Client) } |
| 92 | + |
| 93 | + before do |
| 94 | + cache.instance_variable_set(:@client, pool) |
| 95 | + cache.instance_variable_set(:@connected, true) |
| 96 | + allow(pool).to receive(:with).and_yield(dalli) |
| 97 | + end |
| 98 | + |
| 99 | + describe '#set_nx' do |
| 100 | + it 'returns true when Dalli#add succeeds (key did not exist)' do |
| 101 | + allow(dalli).to receive(:add).with('nx-key', 'value', 60).and_return(true) |
| 102 | + expect(cache.set_nx('nx-key', 'value', ttl: 60)).to be true |
| 103 | + end |
| 104 | + |
| 105 | + it 'returns false when Dalli#add fails (key already exists, returns nil/false)' do |
| 106 | + allow(dalli).to receive(:add).with('nx-key', 'value', 60).and_return(nil) |
| 107 | + expect(cache.set_nx('nx-key', 'value', ttl: 60)).to be false |
| 108 | + end |
| 109 | + |
| 110 | + it 'passes the ttl positionally to Dalli#add' do |
| 111 | + expect(dalli).to receive(:add).with('nx-key', 'value', 90).and_return(true) |
| 112 | + cache.set_nx('nx-key', 'value', ttl: 90) |
| 113 | + end |
| 114 | + end |
| 115 | + end |
| 116 | +end |
0 commit comments