require 'spec_helper' # Couchbase-backed replacements for common Redis uses: atomic counters, a Rails # cache store, and a distributed lock — all on the same cluster as the models. RSpec.describe 'Redis replacements' do describe SimplyBased::Counter do subject(:counter) { described_class.new } it 'atomically increments with Redis INCR semantics (first INCR of a missing key returns the delta)' do expect(counter.increment('c:hits')).to eq(1) expect(counter.increment('c:hits')).to eq(2) expect(counter.increment('c:hits', 5)).to eq(7) expect(counter.get('c:hits')).to eq(7) end it 'seeds at an explicit initial value when given' do expect(counter.increment('c:seeded', 1, initial: 100)).to eq(100) expect(counter.increment('c:seeded')).to eq(101) end it 'decrements and resets' do counter.increment('c:n', 0, initial: 10) expect(counter.decrement('c:n', 3)).to eq(7) expect(counter.reset('c:n', 0)).to eq(0) expect(counter.get('c:n')).to eq(0) end it 'clamps decrement at zero (unsigned Couchbase counters; Redis would go negative)' do counter.reset('c:clamp', 1) expect(counter.decrement('c:clamp', 5)).to eq(0) end end describe SimplyBased::Cache do subject(:cache) { described_class.new(namespace: 'test') } it 'works as an ActiveSupport::Cache::Store (read/write/fetch/delete)' do cache.write('k', { 'a' => 1 }) expect(cache.read('k')).to eq({ 'a' => 1 }) expect(cache.fetch('k2') { [1, 2, 3] }).to eq([1, 2, 3]) expect(cache.read('k2')).to eq([1, 2, 3]) cache.delete('k') expect(cache.read('k')).to be_nil end end describe SimplyBased::Lock do subject(:lock) { described_class.new } it 'grants the lock to one holder and blocks a second acquire' do handle = lock.acquire('job:1', ttl: 30) expect(handle).not_to be_nil expect(lock.acquire('job:1', ttl: 30)).to be_nil # already held lock.release('job:1', handle) expect(lock.acquire('job:1', ttl: 30)).not_to be_nil # released → free end it 'refuses to release without the CAS handle' do lock.acquire('job:guard', ttl: 30) expect { lock.release('job:guard', nil) }.to raise_error(ArgumentError) expect(lock.acquire('job:guard', ttl: 30)).to be_nil # still held lock.force_release!('job:guard') expect(lock.acquire('job:guard', ttl: 30)).not_to be_nil end it 'rejects a missing/zero ttl (a crashed holder must never leave an immortal lock)' do expect { lock.acquire('job:immortal', ttl: nil) }.to raise_error(ArgumentError) expect { lock.acquire('job:immortal', ttl: 0) }.to raise_error(ArgumentError) end it 'runs a critical section with with_lock' do ran = false lock.with_lock('job:2') { ran = true } expect(ran).to be(true) # lock auto-released after the block → re-acquirable expect(lock.acquire('job:2')).not_to be_nil end end end