LockAndCache
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?
Quickstart
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
Sponsor
We use lock_and_cache
for B2C customer intelligence at Faraday.
TOC
Theory
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.
Practice
Setup
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.
Locking
Just uses Redis naive locking with NX.
A 32-second heartbeat is used that will clear the lock if a process is killed.
Caching
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
Standalone mode
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
"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)
Special features
Locking of course!
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?
Heartbeat
If the process holding the lock dies, we automatically remove the lock so somebody else can do it (using heartbeats).
Context mode
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.
nil_expires
You can expire nil values with a different timeout (nil_expires
) than other values (expires
).
Tunables
LockAndCache.lock_storage=[redis]
LockAndCache.cache_storage=[redis]
ENV['LOCK_AND_CACHE_DEBUG']='true'
if you want some debugging output on$stderr
Few dependencies
- activesupport (come on, it's the bomb)
- redis
Known issues
- In cache keys, can't distinguish {a: 1} from [[:a, 1]]
Wishlist
- 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
Contributing
- 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
Copyright 2015 Seamus Abshere