LockAndCacheMsgpack
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
LockAndCacheMsgpack.storage = Redis.new
LockAndCacheMsgpack.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 data-driven marketing at Faraday.
TOC
- Theory
- Practice
- Setup
- Locking
- Caching
- Standalone mode
- Context mode
- Special features
- Locking of course!
- Heartbeat
- Context mode
- nil_expires
- Tunables
- Few dependencies
- Wishlist
- Contributing
- Copyright
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).
Practice
Setup
LockAndCacheMsgpack.storage = Redis.new
It will use this redis for both locking and storing cached values.
Locking
Based on antirez's Redlock algorithm.
Above and beyond Redlock, a 32-second heartbeat is used that will clear the lock if a process is killed. This is implemented using lock extensions.
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
LockAndCacheMsgpack.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
LockAndCacheMsgpack.lock_and_cache(:stock_price, company: 'MSFT', date: '2015-05-05') do
# get yer stock quote
end
You probably want an expiry
LockAndCacheMsgpack.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
LockAndCacheMsgpack.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
LockAndCacheMsgpack.clear :stock_price, company: 'MSFT', date: '2015-05-05'
Check locks with
LockAndCacheMsgpack.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 LockAndCacheMsgpack
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 and redlock extends).
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
LockAndCacheMsgpack.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
- redlock
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