Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi data store #4

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 0 additions & 93 deletions CacheBar.gemspec

This file was deleted.

1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ gem "redis"
gem "redis-namespace"
gem 'httparty', '~> 0.8.3'
gem 'activesupport'
gem 'dalli'

group :development do
gem "shoulda"
Expand Down
33 changes: 20 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,42 @@
# CacheBar

CacheBar is a simple API caching layer built on top of Redis and HTTParty.
CacheBar is a simple API caching layer built on top of a caching data store (like Redis) and HTTParty.

When a good request is made to the API through an HTTParty module or class configured to be cached, it caches the response body in Redis. The cache is set to expire in the configured length of time. All following identical requests use the cache in Redis. When the cache expires it will attempt to refill the cache with a new good response. If though the response that comes back is bad (there was timeout, a 404, or some other problem), then CacheBar will fetch a backup response we also store in Redis (in a separate non-expiring hash). When it pulls this backup response out it inserts it into the standard cache and sets it to expire in 5 minutes. This way we won't look for a new good response for another 5 minutes.
When a good request is made to the API through an HTTParty module or class configured to be cached, it caches the response body in a data store (like Redis). The cache is set to expire in the configured length of time. All following identical requests use the cache in the data store. When the cache expires it will attempt to refill the cache with a new good response. If though the response that comes back is bad (there was timeout, a 404, or some other problem), then CacheBar will fetch a backup response we also stored in the data store. When it pulls this backup response out it inserts it into the standard cache and sets it to expire in 5 minutes. This way we won't look for a new good response for another 5 minutes.

Using this gem does not mean that all your HTTParty modules and requests will be automatically cached. You will have to configure them on a case by case basis. This means you can have some APIs that are cached and others that aren't.

## Install

gem install cachebar

If you're on Ruby 1.8 it is recommended that you also install the SystemTimer gem:

gem install SystemTimer

### Inside a Rails/Rack app:

Add this to your Gemfile:

gem 'SystemTimer', :platforms => :ruby_18
gem 'cachebar'

Follow the instructions below for configuring CacheBar.
You should also make sure to specify the data store client you will be using. Follow the instructions below for configuring CacheBar.

## Usage

Although you can use CacheBar in any type of application, the examples provided will be for using it inside a Rails application.

### 1. Configuring CacheBar

There's a few configuration options to CacheBar, the first is specifying a Redis connection to use.
There's a few configuration options to CacheBar, the first is specifying a data store and defining it's connection. While CacheBar was built with Redis in mind however we also include a memcached adapter if you prefer that. To configure a data store you can either use a symbol for Redis or memcached:

If you have an initializer for defining your Redis connection already like this:
HTTParty::HTTPCache.data_store_class = :redis

Or you can build your own data store adapter:

class MyDataStore < CacheBar::DataStore::AbstractDataStore
# ...
end

HTTParty::HTTPCache.data_store_class = MyDataStore

After that you need to configure the client and set it on the adapter. If you're using redis and you have an initializer for defining your Redis connection already like this:

REDIS_CONFIG = YAML.load_file(Rails.root+'config/redis.yml')[Rails.env].freeze
redis = Redis.new(:host => REDIS_CONFIG['host'], :port => REDIS_CONFIG['port'],
Expand All @@ -40,7 +45,9 @@ If you have an initializer for defining your Redis connection already like this:

Then you can just add this:

HTTParty::HTTPCache.redis = $redis
CacheBar::DataStore::Redis.client = $redis

However every data store adapter will have a class level attribute of `client` that you should use to configure the client.

You'll then also want to turn on caching in the appropriate environments. For instance you'll want to add this to `config/environments/production.rb`:

Expand All @@ -58,7 +65,7 @@ By default when we fallback to using a backup response, we then hold off looking

HTTParty::HTTPCache.cache_stale_backup_time = 120 # 2 minutes

If you want to perform an action (say notify an error tracking service) when an exception happens while performing or processing a request you can specify a callback. The only requirement is that it responds to `call` and that `call` accepts 3 parameters. Those 3 in order will be the exception, the redis key name of the API, and the URL endpoint:
If you want to perform an action (say notify an error tracking service) when an exception happens while performing or processing a request you can specify a callback. The only requirement is that it responds to `call` and that `call` accepts 3 parameters. Those 3 in order will be the exception, the key name of the API, and the URL endpoint:

HTTParty::HTTPCache.exception_callback = lambda { |exception, api_name, url|
Airbrake.notify_or_ignore(exception, {
Expand All @@ -77,7 +84,7 @@ If you already have HTTParty included then you just need to use the `caches_api_
* This is used internally to decide which requests to try to cache responses for.
If you've defined `base_uri` on the class/module that HTTParty is included into then this option is not needed.
* `key_name`:
* This is the name used in Redis to create a part of the cache key to easily differentiate it from other API caches.
* This is the name used in the data store to create a part of the cache key to easily differentiate it from other API caches.
* `expire_in`:
* This determines how long the good API responses are cached for.

Expand Down
5 changes: 5 additions & 0 deletions lib/cachebar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
require 'active_support'
require 'active_support/core_ext/module/aliasing'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/module/delegation'
if RUBY_VERSION.split('.')[1].to_i < 9
begin
require 'system_timer'
Expand All @@ -15,6 +18,8 @@
require 'httparty/httpcache'

module CacheBar
autoload :DataStore, 'cachebar/data_store'

def self.register_api_to_cache(host, options)
raise ArgumentError, "You must provide a host that you are caching API responses for." if host.blank?

Expand Down
7 changes: 7 additions & 0 deletions lib/cachebar/data_store.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module CacheBar
module DataStore
autoload :AbstractDataStore, 'cachebar/data_store/abstract_data_store'
autoload :Redis, 'cachebar/data_store/redis'
autoload :Memcached, 'cachebar/data_store/memcached'
end
end
48 changes: 48 additions & 0 deletions lib/cachebar/data_store/abstract_data_store.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
module CacheBar
module DataStore
class AbstractDataStore
class_attribute :client

attr_reader :api_name, :uri_hash

def initialize(api_name, uri_hash)
@api_name = api_name
@uri_hash = uri_hash
end

def response_body_exists?
raise NotImplementedError, 'Implement response_body_exists? in sub-class'
end

def get_response_body
raise NotImplementedError, 'Implement get_response_body in sub-class'
end

def store_response_body(response_body, interval)
raise NotImplementedError, 'Implement store_response_body in sub-class'
end

def backup_exists?
raise NotImplementedError, 'Implement backup_exists? in sub-class'
end

def get_backup
raise NotImplementedError, 'Implement get_backup in sub-class'
end

def store_backup(response_body)
raise NotImplementedError, 'Implement store_backup in sub-class'
end

private

def cache_key_name
"api-cache:#{api_name}:#{uri_hash}"
end

def backup_key_name
"api-cache:#{api_name}"
end
end
end
end
34 changes: 34 additions & 0 deletions lib/cachebar/data_store/memcached.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module CacheBar
module DataStore
class Memcached < AbstractDataStore

def backup_key_name
"api-cache:backup:#{api_name}:#{uri_hash}"
end

def response_body_exists?
!client.get(cache_key_name).nil?
end

def get_response_body
client.get(cache_key_name)
end

def store_response_body(response_body, interval)
client.set(cache_key_name, response_body, interval)
end

def backup_exists?
!client.get(backup_key_name).nil?
end

def get_backup
client.get(backup_key_name)
end

def store_backup(response_body)
client.set(backup_key_name, response_body)
end
end
end
end
30 changes: 30 additions & 0 deletions lib/cachebar/data_store/redis.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module CacheBar
module DataStore
class Redis < AbstractDataStore
def response_body_exists?
client.exists(cache_key_name)
end

def get_response_body
client.get(cache_key_name)
end

def store_response_body(response_body, interval)
client.set(cache_key_name, response_body)
client.expire(cache_key_name, interval)
end

def backup_exists?
client.exists(backup_key_name) && client.hexists(backup_key_name, uri_hash)
end

def get_backup
client.hget(backup_key_name, uri_hash)
end

def store_backup(response_body)
client.hset(backup_key_name, uri_hash, response_body)
end
end
end
end
Loading