require 'spec_helper' # Real-concurrency coverage. The suite had no multi-threaded examples at all, # yet the gem's core claims are about CAS-based optimistic concurrency and # Redis-replacement primitives (atomic counters, distributed locks). These # examples exercise those claims against the live cluster. RSpec.describe 'Concurrency' do describe SimplyBased::Counter do it 'loses no increments under concurrent writers' do counter = SimplyBased::Counter.new threads = 8.times.map do Thread.new do 25.times { counter.increment('conc:hits') } end end threads.each(&:join) expect(counter.get('conc:hits')).to eq(8 * 25) end end describe SimplyBased::Lock do it 'grants the lock to exactly one of many concurrent contenders' do lock = SimplyBased::Lock.new grants = 8.times.map do Thread.new { lock.acquire('conc:job', ttl: 30) } end.map(&:value) expect(grants.compact.size).to eq(1) end it 'mutually excludes critical sections under with_lock' do lock = SimplyBased::Lock.new active = 0 max_active = 0 mutex = Mutex.new threads = 4.times.map do Thread.new do lock.with_lock('conc:crit', ttl: 30, retries: 200, wait: 0.05) do mutex.synchronize { active += 1; max_active = [max_active, active].max } sleep 0.05 mutex.synchronize { active -= 1 } end end end threads.each(&:join) expect(max_active).to eq(1) end it 'does not release a lock re-acquired by someone else after expiry' do lock = SimplyBased::Lock.new stale_handle = lock.acquire('conc:expiring', ttl: 1) expect(stale_handle).not_to be_nil sleep 2.5 # let it expire server-side new_handle = lock.acquire('conc:expiring', ttl: 30) expect(new_handle).not_to be_nil # the stale holder's release must be a no-op (CAS mismatch), not an unlock expect(lock.release('conc:expiring', stale_handle)).to be(false) expect(lock.acquire('conc:expiring', ttl: 30)).to be_nil # still held end end describe 'CAS optimistic concurrency (documents)' do it 'does not lose updates when two copies write the same attribute' do original = User.create(name: 'seed', title: 'Dr.') copies = 2.times.map { User.find(original.id) } copies[0].name = 'first-writer' expect(copies[0].save).to be true copies[1].name = 'second-writer' # same attribute changed on both sides — must surface, never silently stomp expect { copies[1].save }.to raise_error(SimplyBased::Conflict) expect(User.find(original.id).name).to eq 'first-writer' end it 'merges concurrent writers touching different attributes (auto conflict resolution)' do original = User.create(name: 'seed', title: 'Dr.', homepage: 'a') a = User.find(original.id) b = User.find(original.id) a.name = 'from-a' expect(a.save).to be true b.title = 'from-b' expect { b.save }.not_to raise_error reloaded = User.find(original.id) expect(reloaded.name).to eq 'from-a' expect(reloaded.title).to eq 'from-b' end it 'survives a storm of single-attribute writers each bumping their own field' do original = User.create(name: 'seed', title: 't', homepage: 'h') results = %w[name title homepage].map do |attr| Thread.new do copy = User.find(original.id) copy.public_send("#{attr}=", "#{attr}-updated") copy.save true rescue SimplyBased::Conflict :conflict end end.map(&:value) # auto-merge should let disjoint-attribute writers through; when the race # is too tight the loser must fail loudly with Conflict — never corrupt. reloaded = User.find(original.id) %w[name title homepage].each_with_index do |attr, i| if results[i] == true expect(reloaded.public_send(attr)).to eq "#{attr}-updated" end end expect(results).to all(satisfy { |r| r == true || r == :conflict }) end end end