Cachext

Build Status Gem Version Code Climate

Extensions to normal Rails caching:

Quickstart

Cachext.configure do |config|
  config.cache = Rails.cache
  config.redis = Redis.current
end

key = [:foo, :bar, 1]
Cachext.fetch key, expires_in: 2.hours, default: "cow" do
  Faraday.get "http://example.com/foo/bar/1"
end
  • Other services making the same call at the same time will wait for the first to complete, so only 1 call is made in a 2 hour window
  • A backup of the value is stored too, so if the service raises a Faraday::Error::ConnectionFailed we'll return the backup
  • If no backup exists but we got a ConnectionFailed, we'll return the default of "cow"
Record = Struct.new :id
Cachext.multi [:foo, :bar], [1,2,3], expires_in: 5.minutes do |ids|
  data = JSON.parse Faraday.get("http://example.com/foo/bar?ids=#{ids.join(',')}")
  data.each_with_object({}) do |record, acc|
    acc[record["id"]] = Record.new record["id"]
  end
end
# => { 1 => Record.new(1), 2 => Record.new(2), 3 => Record.new(3) }
  • The passed block will be called with the ids that were not available in the cache. The return value of the block should either be a hash with keys of ids, or an array of objects that have id methods.
  • In the event of a server error (ie ConnectionFailed), backup values are used.

Configuration options

Cachext.config.cache = Rails.cache

Cachext expects a cache store that has the ActiveSupport::Cache interface, so that can be Memcache, Redis, FileStore, etc.

Cachext.config.redis = Redis.current

Cachext uses redis for locking (the Redlock gem under the hood), so we need at least Redis 2.8.

Cachext.config.raise_errors = false
Cachext.config.default_errors = [
  Faraday::Error::ConnectionFailed,
  Faraday::Error::TimeoutError,
]

By default Cachext will not re-raise the standard default errors. Setting this to true is helpful in a test environment. The default_errors are those caught as transient issues that a backup will be used for.

Cachext.config.not_found_errors = [Faraday::Error::ResourceNotFound]

If a NotFound exception is raised, the backup is not used, and any backup that exists will be deleted. Then the exception will be re-raised.

Cachext.config.default_expires_in = 60 # in seconds

The default TTL for values fetched. Only used for the "fresh" cache, not the backup (which has no TTL).

Cachext.config.max_lock_wait = 5 # in seconds

The most we'll wait for a lock to unlock. If it takes more than this value to get a lock (due to another service holding the lock while making the call), we'll fallback to the backup value.

Cachext.config.debug = ENV['CACHEXT_DEBUG'] == "true"

If debug is set to true (or you run your program/test with CACHEXT_DEBUG=true), you'll get lots of debug messages around the locking and whats going on. Very helpful for debugging :)

Cachext.config.heartbeat_expires = 2 # in seconds

If a process that holds a lock crashes, other processes will have to wait this many seconds for the lock to expire.

Cachext.config.error_logger = nil

If set to an object that responds to call, will call with any errors caught.

Cachext.config.failure_threshold = 3

Number of tries before tripping circuit breaker.

Cachext.config.breaker_timeout = 60

Time in seconds to wait before switching breaker to half-open.

Usage

Cachext.fetch key, options, &block

Available options:

  • expires_in: override for the default_expires_in, in seconds
  • default: object or proc that will be used as the default if no backup is found
  • errors: override for the default_errors: array of errors to catch and not reraise
  • reraise_errors: default true, if set to false NotFound errors will not be raised
  • not_found_error: (override) array of errors where we delete the backup and reraise
  • heartbeat_expires: (override) time in seconds for process heardbeat to expire
  • failure_threshold: (override) Number of tries before tripping circuit breaker
  • breaker_timeout: (override) time in seconds to wait before switching breaker to half-open
  • cache: use the first-level cache, defaults to true. If set to false, will always call the fallback, but if an error is raised, will use the last known good value.
Cachext.multi key_base, ids, options, &block

Available options:

  • expires_in: override for default_expires_in, in seconds
  • return_array: return an array instead of a hash. Will include missing records as Cachext::MissingRecord objects so you can deal with them.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Having trouble with a test? Set the CACHEXT_DEBUG environmental variable to "true" to get debug logs.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/dplummer/cachext.

License

The gem is available as open source under the terms of the MIT License.