Module: AtomicRedisCache
- Defined in:
- lib/atomic_redis_cache.rb,
lib/atomic_redis_cache/version.rb
Constant Summary collapse
- DEFAULT_EXPIRATION =
86400 seconds in a day
60*60*24
- DEFAULT_RACE_TTL =
seconds to acquire new value
30- MAX_RETRIES =
recalc attempts before expiring cache
3- VERSION =
'0.2.1'
Class Attribute Summary collapse
Class Method Summary collapse
-
.delete(key) ⇒ Object
Delete the cache entry completely, including timer.
-
.fetch(key, opts = {}, &blk) ⇒ Object
Fetch from cache with fallback, just like ActiveSupport::Cache.
-
.read(key) ⇒ Object
Fetch from the cache atomically; return nil if empty or expired.
- .timer(key) ⇒ Object
-
.write(key, val, opts = {}) ⇒ Object
Write to the cache unconditionally, returns success as boolean Accepts the same options and uses the same defaults as .fetch() Note that write() ignores locks, so it can be called multiple times; prefer .fetch() unless absolutely necessary.
Class Attribute Details
.redis ⇒ Object
90 91 92 93 |
# File 'lib/atomic_redis_cache.rb', line 90 def redis raise ArgumentError.new('AtomicRedisCache.redis must be set') unless @redis @redis.respond_to?(:call) ? @redis.call : @redis end |
Class Method Details
.delete(key) ⇒ Object
Delete the cache entry completely, including timer
81 82 83 |
# File 'lib/atomic_redis_cache.rb', line 81 def delete(key) redis.del(key, timer(key)) == 2 end |
.fetch(key, opts = {}, &blk) ⇒ Object
Fetch from cache with fallback, just like ActiveSupport::Cache. The main differences are in the edge cases around expiration.
-
when cache expires, we avoid dogpile/thundering herd from multiple processes recalculating at once
-
when calculation takes too long (i.e., due to network traffic) we return the previously cached value for several attempts
Options: :expires_in - expiry in seconds; defaults to a day :race_condition_ttl - time to lock value for recalculation :max_retries - # of times to retry cache refresh before expiring
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/atomic_redis_cache.rb', line 21 def fetch(key, opts={}, &blk) expires_in = opts[:expires_in] || DEFAULT_EXPIRATION race_ttl = opts[:race_condition_ttl] || DEFAULT_RACE_TTL retries = opts[:max_retries] || MAX_RETRIES now = Time.now.to_i ttl = expires_in + retries * race_ttl t_key = timer(key) if val = redis.get(key) # cache hit if redis.get(t_key).to_i < now # expired entry or dne redis.set t_key, now + race_ttl # block other callers for recalc duration begin Timeout.timeout(race_ttl) do # if recalc exceeds race_ttl, abort val = Marshal.dump(blk.call) # determine new value redis.multi do # atomically cache + mark as valid redis.setex key, ttl, val redis.set t_key, now + expires_in end end rescue Timeout::Error => e # eval timed out, use cached val end end else # cache miss val = Marshal.dump(blk.call) # determine new value redis.multi do # atomically cache + mark as valid redis.setex key, ttl, val redis.set t_key, now + expires_in end end Marshal.load(val) end |
.read(key) ⇒ Object
Fetch from the cache atomically; return nil if empty or expired
56 57 58 59 |
# File 'lib/atomic_redis_cache.rb', line 56 def read(key) val, exp = redis.mget key, timer(key) Marshal.load(val) unless exp.to_i < Time.now.to_i end |
.timer(key) ⇒ Object
85 86 87 |
# File 'lib/atomic_redis_cache.rb', line 85 def timer(key) "timer:#{key}" end |
.write(key, val, opts = {}) ⇒ Object
Write to the cache unconditionally, returns success as boolean Accepts the same options and uses the same defaults as .fetch() Note that write() ignores locks, so it can be called multiple times; prefer .fetch() unless absolutely necessary.
65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/atomic_redis_cache.rb', line 65 def write(key, val, opts={}) expires_in = opts[:expires_in] || DEFAULT_EXPIRATION race_ttl = opts[:race_condition_ttl] || DEFAULT_RACE_TTL retries = opts[:max_retries] || MAX_RETRIES ttl = expires_in + retries * race_ttl expiry = Time.now.to_i + expires_in response = redis.multi do redis.setex key, ttl, Marshal.dump(val) redis.set timer(key), expiry end response.all? { |ret| ret == 'OK' } end |