diff --git a/README.md b/README.md index e35fa18..619fda3 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,26 @@ Check locks with LockAndCache.locked? :stock_price, company: 'MSFT', date: '2015-05-05' ``` +Caching, locking or both can be bypassed + +```ruby +LockAndCache.lock_and_cache(:stock_price, {company: 'MSFT', date: '2015-05-05'}, expires: 10, bypass: :cache) do + # ignore any cached value, get yer stock quote +end +``` + +```ruby +LockAndCache.lock_and_cache(:stock_price, {company: 'MSFT', date: '2015-05-05'}, expires: 10, bypass: :lock) do + # return cached value if it exists, otherwise get yer stock quote *without* acquiring lock +end +``` + +```ruby +LockAndCache.lock_and_cache(:stock_price, {company: 'MSFT', date: '2015-05-05'}, expires: 10, bypass: :both) do + # get yer stock quote without caching or locking +end +``` + #### Context mode "Context mode" simply adds the class name, method name, and context key (the results of `#id` or `#lock_and_cache_key`) of the caller to the cache key. @@ -234,6 +254,6 @@ You can expire nil values with a different timeout (`nil_expires`) than other va 4. Push to the branch (`git push origin my-new-feature`) 5. Create a new Pull Request -# Copyright +# Copyright Copyright 2015 Seamus Abshere diff --git a/lib/lock_and_cache/action.rb b/lib/lock_and_cache/action.rb index b229490..61c1809 100644 --- a/lib/lock_and_cache/action.rb +++ b/lib/lock_and_cache/action.rb @@ -24,6 +24,11 @@ def nil_expires @nil_expires = options.has_key?('nil_expires') ? options['nil_expires'].to_f.round : nil end + def bypass + return @bypass if defined?(@bypass) + @bypass = options.has_key?('bypass') ? options['bypass'] : nil + end + def digest @digest ||= key.digest end @@ -55,7 +60,7 @@ def perform raise "heartbeat_expires must be >= 2 seconds" unless heartbeat_expires >= 2 heartbeat_frequency = (heartbeat_expires / 2).ceil LockAndCache.logger.debug { "[lock_and_cache] A1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } - if cache_storage.exists(digest) and (existing = cache_storage.get(digest)).is_a?(String) + if ![:cache, :both].include?(bypass) and cache_storage.exists(digest) and (existing = cache_storage.get(digest)).is_a?(String) return load_existing(existing) end LockAndCache.logger.debug { "[lock_and_cache] B1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } @@ -63,13 +68,16 @@ def perform lock_secret = SecureRandom.hex 16 acquired = false begin - Timeout.timeout(max_lock_wait, TimeoutWaitingForLock) do - until lock_storage.set(lock_digest, lock_secret, nx: true, ex: heartbeat_expires) - LockAndCache.logger.debug { "[lock_and_cache] C1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } - sleep rand + unless [:lock, :both].include?(bypass) + Timeout.timeout(max_lock_wait, TimeoutWaitingForLock) do + until lock_storage.set(lock_digest, lock_secret, nx: true, ex: heartbeat_expires) + LockAndCache.logger.debug { "[lock_and_cache] C1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } + sleep rand + end + acquired = true end - acquired = true end + return blk.call if [:cache, :both].include?(bypass) LockAndCache.logger.debug { "[lock_and_cache] D1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if cache_storage.exists(digest) and (existing = cache_storage.get(digest)).is_a?(String) LockAndCache.logger.debug { "[lock_and_cache] E1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } diff --git a/spec/lock_and_cache_spec.rb b/spec/lock_and_cache_spec.rb index b071258..730e3cb 100644 --- a/spec/lock_and_cache_spec.rb +++ b/spec/lock_and_cache_spec.rb @@ -343,6 +343,27 @@ def lock_and_cache_key expect(LockAndCache.lock_and_cache('hello') { raise(Exception.new("stop")) }).to eq(:red) end + it 'doesn\'t break when bypass has an unknown value' do + expect(LockAndCache.lock_and_cache('hello', bypass: nil) { :red }).to eq(:red) + expect(LockAndCache.lock_and_cache('hello', bypass: :foo) { raise(Exception.new("stop")) }).to eq(:red) + end + + it 'doesn\'t cache when bypass == :cache' do + count = 0 + expect(LockAndCache.lock_and_cache('hello') { count += 1 }).to eq(1) + expect(count).to eq(1) + expect(LockAndCache.lock_and_cache('hello', bypass: :cache) { count += 1 }).to eq(2) + expect(count).to eq(2) + end + + it 'doesn\'t cache when bypass == :both' do + count = 0 + expect(LockAndCache.lock_and_cache('hello') { count += 1 }).to eq(1) + expect(count).to eq(1) + expect(LockAndCache.lock_and_cache('hello', bypass: :both) { count += 1 }).to eq(2) + expect(count).to eq(2) + end + it 'caches errors (briefly)' do count = 0 expect { @@ -434,7 +455,7 @@ def lock_and_cache_key expect(LockAndCache.lock_and_cache(['hello', 1, { target: 'world' }]) { count += 1 }).to eq(1) expect(count).to eq(1) end - + it 'treats a single hash arg as a cache key (not as options)' do count = 0 LockAndCache.lock_and_cache(hello: 'world', expires: 100) { count += 1 }