Lock and cache using redis!
Most caching libraries don't do locking, meaning that >1 process can be calculating a cached value at the same time. Since you presumably cache things because they cost CPU, database reads, or money, doesn't it make sense to lock while caching?
LockAndCache.lock_storage = Redis.new db: 3
LockAndCache.cache_storage = Redis.new db: 4
LockAndCache.lock_and_cache(:stock_price, {company: 'MSFT', date: '2015-05-05'}, expires: 10, nil_expires: 1) do
# get yer stock quote
# if 50 processes call this at the same time, only 1 will call the stock quote service
# the other 49 will wait on the lock, then get the cached value
# the value will expire in 10 seconds
# but if the value you get back is nil, that will expire after 1 second
end
We use lock_and_cache
for B2C customer intelligence at Faraday.
lock_and_cache
...
- returns cached value (if exists)
- acquires a lock
- returns cached value (just in case it was calculated while we were waiting for a lock)
- calculates and caches the value
- releases the lock
- returns the value
As you can see, most caching libraries only take care of (1) and (4) (well, and (5) of course).
If an error is raised during calculation, that error is propagated to all waiters for 1 second.
LockAndCache.lock_storage = Redis.new db: 3
LockAndCache.cache_storage = Redis.new db: 4
It will use this redis for both locking and storing cached values.
Just uses Redis naive locking with NX.
A 32-second heartbeat is used that will clear the lock if a process is killed.
This gem is a simplified, improved version of https://github.com/seamusabshere/cache_method. In that library, you could only cache a method call.
In this library, you have two options: providing the whole cache key every time (standalone) or letting the library pull information about its context.
# standalone example
LockAndCache.lock_and_cache(:stock_price, {company: 'MSFT', date: '2015-05-05'}, expires: 10) do
# ...
end
# context example
def stock_price(date)
lock_and_cache(date, expires: 10) do
# ...
end
end
def lock_and_cache_key
company
end
LockAndCache.lock_and_cache(:stock_price, company: 'MSFT', date: '2015-05-05') do
# get yer stock quote
end
You probably want an expiry
LockAndCache.lock_and_cache(:stock_price, {company: 'MSFT', date: '2015-05-05'}, expires: 10) do
# get yer stock quote
end
Note how we separated options ({expires: 10}
) from a hash that is part of the cache key ({company: 'MSFT', date: '2015-05-05'}
).
One other crazy thing: nil_expires
- for when you want to check more often if the external stock price service returned nil
LockAndCache.lock_and_cache(:stock_price, {company: 'MSFT', date: '2015-05-05'}, expires: 10, nil_expires: 1) do
# get yer stock quote
end
Clear it with
LockAndCache.clear :stock_price, company: 'MSFT', date: '2015-05-05'
Check locks with
LockAndCache.locked? :stock_price, company: 'MSFT', date: '2015-05-05'
"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.
class Stock
include LockAndCache
def initialize(company)
[...]
end
def stock_price(date)
lock_and_cache(date, expires: 10) do
# the cache key will be StockQuote (the class) + get (the method name) + id (the instance identifier) + date (the arg you specified)
end
end
def lock_and_cache_key # <---------- if you don't define this, it will try to call #id
company
end
end
The cache key will be StockQuote (the class) + get (the method name) + id (the instance identifier) + date (the arg you specified).
In other words, it auto-detects the class, method, context key ... and you add other args if you want.
Clear it with
blog.lock_and_cache_clear(:get, date)
Most caching libraries don't do locking, meaning that >1 process can be calculating a cached value at the same time. Since you presumably cache things because they cost CPU, database reads, or money, doesn't it make sense to lock while caching?
If the process holding the lock dies, we automatically remove the lock so somebody else can do it (using heartbeats).
This pulls information about the context of a lock_and_cache block from the surrounding class, method, and object... so that you don't have to!
Standalone mode is cool too, tho.
You can expire nil values with a different timeout (nil_expires
) than other values (expires
).
LockAndCache.lock_storage=[redis]
LockAndCache.cache_storage=[redis]
ENV['LOCK_AND_CACHE_DEBUG']='true'
if you want some debugging output on$stderr
- activesupport (come on, it's the bomb)
- redis
- In cache keys, can't distinguish {a: 1} from [[:a, 1]]
- Convert most tests to use standalone mode, which is easier to understand
- Check options
- Lengthen heartbeat so it's not so sensitive
- Clarify which options are seconds or milliseconds
- Fork it ( https://github.com/[my-github-username]/lock_and_cache/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
Copyright 2015 Seamus Abshere