cachebar 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
@@ -0,0 +1,93 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{CacheBar}
8
+ s.version = "1.0.2"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Brian Landau", "David Eisinger"]
12
+ s.date = %q{2011-11-17}
13
+ s.description = %q{A simple API caching layer built on top of HTTParty and Redis}
14
+ s.email = %q{brian.landau@viget.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ "CacheBar.gemspec",
22
+ "Gemfile",
23
+ "HISTORY",
24
+ "LICENSE.txt",
25
+ "README.md",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "lib/cachebar.rb",
29
+ "lib/httparty/httpcache.rb",
30
+ "test/fixtures/user_timeline.json",
31
+ "test/fixtures/vcr_cassettes/bad_response.yml",
32
+ "test/fixtures/vcr_cassettes/good_response.yml",
33
+ "test/fixtures/vcr_cassettes/status_update_post.yml",
34
+ "test/fixtures/vcr_cassettes/unparsable.yml",
35
+ "test/helper.rb",
36
+ "test/test_cachebar.rb",
37
+ "test/twitter_api.rb"
38
+ ]
39
+ s.homepage = %q{http://github.com/vigetlabs/cachebar}
40
+ s.licenses = ["MIT"]
41
+ s.require_paths = ["lib"]
42
+ s.rubygems_version = %q{1.6.2}
43
+ s.summary = %q{A simple API caching layer built on top of HTTParty and Redis}
44
+
45
+ if s.respond_to? :specification_version then
46
+ s.specification_version = 3
47
+
48
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
49
+ s.add_runtime_dependency(%q<redis>, [">= 0"])
50
+ s.add_runtime_dependency(%q<redis-namespace>, [">= 0"])
51
+ s.add_runtime_dependency(%q<httparty>, ["~> 0.7.7"])
52
+ s.add_runtime_dependency(%q<activesupport>, [">= 0"])
53
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
54
+ s.add_development_dependency(%q<bundler>, ["~> 1.0"])
55
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6"])
56
+ s.add_development_dependency(%q<rcov>, [">= 0"])
57
+ s.add_development_dependency(%q<webmock>, [">= 0"])
58
+ s.add_development_dependency(%q<vcr>, [">= 0"])
59
+ s.add_development_dependency(%q<mocha>, [">= 0"])
60
+ s.add_development_dependency(%q<rake>, ["~> 0.8.7"])
61
+ s.add_development_dependency(%q<SystemTimer>, [">= 0"])
62
+ else
63
+ s.add_dependency(%q<redis>, [">= 0"])
64
+ s.add_dependency(%q<redis-namespace>, [">= 0"])
65
+ s.add_dependency(%q<httparty>, ["~> 0.7.7"])
66
+ s.add_dependency(%q<activesupport>, [">= 0"])
67
+ s.add_dependency(%q<shoulda>, [">= 0"])
68
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
69
+ s.add_dependency(%q<jeweler>, ["~> 1.6"])
70
+ s.add_dependency(%q<rcov>, [">= 0"])
71
+ s.add_dependency(%q<webmock>, [">= 0"])
72
+ s.add_dependency(%q<vcr>, [">= 0"])
73
+ s.add_dependency(%q<mocha>, [">= 0"])
74
+ s.add_dependency(%q<rake>, ["~> 0.8.7"])
75
+ s.add_dependency(%q<SystemTimer>, [">= 0"])
76
+ end
77
+ else
78
+ s.add_dependency(%q<redis>, [">= 0"])
79
+ s.add_dependency(%q<redis-namespace>, [">= 0"])
80
+ s.add_dependency(%q<httparty>, ["~> 0.7.7"])
81
+ s.add_dependency(%q<activesupport>, [">= 0"])
82
+ s.add_dependency(%q<shoulda>, [">= 0"])
83
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
84
+ s.add_dependency(%q<jeweler>, ["~> 1.6"])
85
+ s.add_dependency(%q<rcov>, [">= 0"])
86
+ s.add_dependency(%q<webmock>, [">= 0"])
87
+ s.add_dependency(%q<vcr>, [">= 0"])
88
+ s.add_dependency(%q<mocha>, [">= 0"])
89
+ s.add_dependency(%q<rake>, ["~> 0.8.7"])
90
+ s.add_dependency(%q<SystemTimer>, [">= 0"])
91
+ end
92
+ end
93
+
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "redis"
4
+ gem "redis-namespace"
5
+ gem 'httparty', '~> 0.8.3'
6
+ gem 'activesupport'
7
+
8
+ group :development do
9
+ gem "shoulda"
10
+ gem "bundler", "~> 1.0"
11
+ gem "jeweler", "~> 1.6"
12
+ gem "rcov"
13
+ gem "webmock"
14
+ gem 'vcr'
15
+ gem 'mocha'
16
+ gem 'rake', '~> 0.8.7'
17
+ gem 'SystemTimer', :platforms => :ruby_18
18
+ end
data/HISTORY ADDED
@@ -0,0 +1,7 @@
1
+ == 1.0.0 / 2011-05-26
2
+ * Cache API responses in Redis from HTTParty requests
3
+ * Only cache APIs that have been registered with CacheBar.
4
+ * Stores response body as Redis string data type that is set to expire.
5
+ * Store a backup of responses incase the API service goes down.
6
+ * If an exception or timeout occurs and there is no backup an exception is raised.
7
+ * If a bad response is received the backup will be used if available otherwise the bad response will be returned
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Brian Landau of Viget Labs
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,102 @@
1
+ # CacheBar
2
+
3
+ CacheBar is a simple API caching layer built on top of Redis and HTTParty.
4
+
5
+ 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.
6
+
7
+ 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.
8
+
9
+ ## Install
10
+
11
+ gem install cachebar
12
+
13
+ If you're on Ruby 1.8 it is recommended that you also install the SystemTimer gem:
14
+
15
+ gem install SystemTimer
16
+
17
+ ### Inside a Rails/Rack app:
18
+
19
+ Add this to your Gemfile:
20
+
21
+ gem 'SystemTimer', :platforms => :ruby_18
22
+ gem 'cachebar'
23
+
24
+ Follow the instructions below for configuring CacheBar.
25
+
26
+ ## Usage
27
+
28
+ Although you can use CacheBar in any type of application, the examples provided will be for using it inside a Rails application.
29
+
30
+ ### 1. Configuring CacheBar
31
+
32
+ There's a few configuration options to CacheBar, the first is specifying a Redis connection to use.
33
+
34
+ If you have an initializer for defining your Redis connection already like this:
35
+
36
+ REDIS_CONFIG = YAML.load_file(Rails.root+'config/redis.yml')[Rails.env].freeze
37
+ redis = Redis.new(:host => REDIS_CONFIG['host'], :port => REDIS_CONFIG['port'],
38
+ :thread_safe => true, :db => REDIS_CONFIG['db'])
39
+ $redis = Redis::Namespace.new(REDIS_CONFIG['namespace'], :redis => redis)
40
+
41
+ Then you can just add this:
42
+
43
+ HTTParty::HTTPCache.redis = $redis
44
+
45
+ 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`:
46
+
47
+ HTTParty::HTTPCache.perform_caching = true
48
+
49
+ CacheBar can also log useful information to your log file if you configure a logger for it:
50
+
51
+ HTTParty::HTTPCache.logger = Rails.logger
52
+
53
+ By default we use a timeout of 5 seconds on all requests, this can be configured like so:
54
+
55
+ HTTParty::HTTPCache.timeout_length = 10 # seconds
56
+
57
+ By default when we fallback to using a backup response, we then hold off looking for a new fresh response for 5 minutes, this can be configured like so:
58
+
59
+ HTTParty::HTTPCache.cache_stale_backup_time = 120 # 2 minutes
60
+
61
+ ### 2. Configuring an HTTParty module or class
62
+
63
+ If you already have HTTParty included then you just need to use the `caches_api_responses` method to register that API for caching, and your done. The `caches_api_responses` takes a hash of options:
64
+
65
+ * `host`* *optional*:
66
+ * This is used internally to decide which requests to try to cache responses for.
67
+ If you've defined `base_uri` on the class/module that HTTParty is included into then this option is not needed.
68
+ * `key_name`:
69
+ * This is the name used in Redis to create a part of the cache key to easily differentiate it from other API caches.
70
+ * `expire_in`:
71
+ * This determines how long the good API responses are cached for.
72
+
73
+ Here's an example using Twitter:
74
+
75
+ module TwitterAPI
76
+ include HTTParty
77
+ base_uri 'https://api.twitter.com/1'
78
+ format :json
79
+
80
+ caches_api_responses :key_name => "twitter", :expire_in => 3600
81
+ end
82
+
83
+ After that all Twitter API calls will be cached for an hour.
84
+
85
+ If you want to cache an API that uses HTTParty but never includes HTTParty into a class or module, you can register the API like this:
86
+
87
+ CacheBar.register_api_to_cache('api.example.com', {:key_name => "example", :expire_in => 7200})
88
+
89
+
90
+ ## Contributing to CacheBar
91
+
92
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
93
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
94
+ * Fork the project
95
+ * Start a feature/bugfix branch
96
+ * Commit and push until you are happy with your contribution
97
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
98
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
99
+
100
+ ## Copyright
101
+
102
+ Copyright (c) 2011 Brian Landau. See LICENSE.txt for further details.
@@ -0,0 +1,45 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "cachebar"
18
+ gem.homepage = "http://github.com/vigetlabs/cachebar"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{A simple API caching layer built on top of HTTParty and Redis}
21
+ gem.description = %Q{A simple API caching layer built on top of HTTParty and Redis}
22
+ gem.email = "brian.landau@viget.com"
23
+ gem.authors = ["Brian Landau", "David Eisinger"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ require 'rcov/rcovtask'
36
+ Rcov::RcovTask.new do |test|
37
+ test.libs << 'test'
38
+ test.pattern = 'test/**/test_*.rb'
39
+ test.verbose = true
40
+ test.rcov_opts << '--exclude "gems/*"'
41
+ test.rcov_opts << '--sort coverage'
42
+ test.rcov_opts << '--only-uncovered'
43
+ end
44
+
45
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.1.0
@@ -0,0 +1,43 @@
1
+ require 'ostruct'
2
+ require 'httparty'
3
+ require 'digest/md5'
4
+ require 'active_support'
5
+ require 'active_support/core_ext/module/aliasing'
6
+ require 'active_support/core_ext/module/attribute_accessors'
7
+ require 'active_support/core_ext/object/blank'
8
+ if RUBY_VERSION.split('.')[1].to_i < 9
9
+ begin
10
+ require 'system_timer'
11
+ rescue LoadError
12
+ $stderr.puts "When running a Ruby version before 1.9 you should consider using SystemTimer with CacheBar"
13
+ end
14
+ end
15
+ require 'httparty/httpcache'
16
+
17
+ module CacheBar
18
+ def self.register_api_to_cache(host, options)
19
+ raise ArgumentError, "You must provide a host that you are caching API responses for." if host.blank?
20
+
21
+ missing_options = ([:expire_in, :key_name] - options.keys)
22
+ if missing_options.present?
23
+ raise(ArgumentError, "Missing some required options: #{missing_options.join(", ")}")
24
+ end
25
+
26
+ HTTParty::HTTPCache.apis[host] = options
27
+ end
28
+
29
+ module ClassMethods
30
+ def caches_api_responses(options)
31
+ host = if base_uri.present?
32
+ URI.parse(base_uri).host
33
+ else
34
+ options.delete(:host)
35
+ end
36
+ CacheBar.register_api_to_cache(host, options)
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ HTTParty::ClassMethods.send(:include, CacheBar::ClassMethods)
43
+ HTTParty::Request.send(:include, HTTParty::HTTPCache)
@@ -0,0 +1,149 @@
1
+ module HTTParty
2
+ module HTTPCache
3
+ class NoResponseError < StandardError; end
4
+
5
+ mattr_accessor :perform_caching,
6
+ :apis,
7
+ :logger,
8
+ :redis,
9
+ :timeout_length,
10
+ :cache_stale_backup_time
11
+
12
+ self.perform_caching = false
13
+ self.apis = {}
14
+ self.timeout_length = 5 # 5 seconds
15
+ self.cache_stale_backup_time = 300 # 5 minutes
16
+
17
+ def self.included(base)
18
+ base.class_eval do
19
+ alias_method_chain :perform, :caching
20
+ end
21
+ end
22
+
23
+ def perform_with_caching
24
+ if cacheable?
25
+ if response_in_cache?
26
+ log_message("Retrieving response from cache")
27
+ response_from(response_body_from_cache)
28
+ else
29
+ validate
30
+ begin
31
+ httparty_response = timeout(timeout_length) do
32
+ perform_without_caching
33
+ end
34
+ httparty_response.parsed_response
35
+ if httparty_response.response.is_a?(Net::HTTPSuccess)
36
+ log_message("Storing good response in cache")
37
+ store_in_cache(httparty_response.body)
38
+ store_backup(httparty_response.body)
39
+ httparty_response
40
+ else
41
+ retrieve_and_store_backup(httparty_response)
42
+ end
43
+ rescue *exceptions
44
+ retrieve_and_store_backup
45
+ end
46
+ end
47
+ else
48
+ log_message("Caching off")
49
+ perform_without_caching
50
+ end
51
+ end
52
+
53
+ protected
54
+
55
+ def cacheable?
56
+ HTTPCache.perform_caching && HTTPCache.apis.keys.include?(uri.host) &&
57
+ http_method == Net::HTTP::Get
58
+ end
59
+
60
+ def response_from(response_body)
61
+ HTTParty::Response.new(self, OpenStruct.new(:body => response_body), lambda {parse_response(response_body)})
62
+ end
63
+
64
+ def retrieve_and_store_backup(httparty_response = nil)
65
+ if backup_exists?
66
+ log_message('using backup')
67
+ response_body = backup_response
68
+ store_in_cache(response_body, cache_stale_backup_time)
69
+ response_from(response_body)
70
+ elsif httparty_response
71
+ httparty_response
72
+ else
73
+ log_message('No backup and bad response')
74
+ raise NoResponseError, 'Bad response from API server or timeout occured and no backup was in the cache'
75
+ end
76
+ end
77
+
78
+ def normalized_uri
79
+ return @normalized_uri if @normalized_uri
80
+ normalized_uri = uri.dup
81
+ normalized_uri.query = sort_query_params(normalized_uri.query)
82
+ normalized_uri.path.chop! if (normalized_uri.path =~ /\/$/)
83
+ normalized_uri.scheme = normalized_uri.scheme.downcase
84
+ @normalized_uri = normalized_uri.normalize.to_s
85
+ end
86
+
87
+ def sort_query_params(query)
88
+ query.split('&').sort.join('&') unless query.blank?
89
+ end
90
+
91
+ def cache_key_name
92
+ @cache_key_name ||= "api-cache:#{HTTPCache.apis[uri.host][:key_name]}:#{uri_hash}"
93
+ end
94
+
95
+ def uri_hash
96
+ @uri_hash ||= Digest::MD5.hexdigest(normalized_uri)
97
+ end
98
+
99
+ def response_in_cache?
100
+ redis.exists(cache_key_name)
101
+ end
102
+
103
+ def backup_key
104
+ "api-cache:#{HTTPCache.apis[uri.host][:key_name]}"
105
+ end
106
+
107
+ def backup_response
108
+ redis.hget(backup_key, uri_hash)
109
+ end
110
+
111
+ def backup_exists?
112
+ redis.exists(backup_key) && redis.hexists(backup_key, uri_hash)
113
+ end
114
+
115
+ def response_body_from_cache
116
+ redis.get(cache_key_name)
117
+ end
118
+
119
+ def store_in_cache(response_body, expires = nil)
120
+ redis.set(cache_key_name, response_body)
121
+ redis.expire(cache_key_name, (expires || HTTPCache.apis[uri.host][:expire_in]))
122
+ end
123
+
124
+ def store_backup(response_body)
125
+ redis.hset(backup_key, uri_hash, response_body)
126
+ end
127
+
128
+ def log_message(message)
129
+ logger.info("[HTTPCache]: #{message} for #{normalized_uri} - #{uri_hash.inspect}") if logger
130
+ end
131
+
132
+ def timeout(seconds, &block)
133
+ if defined?(SystemTimer)
134
+ SystemTimer.timeout_after(seconds, &block)
135
+ else
136
+ options[:timeout] = seconds
137
+ yield
138
+ end
139
+ end
140
+
141
+ def exceptions
142
+ if (RUBY_VERSION.split('.')[1].to_i >= 9) && defined?(Psych::SyntaxError)
143
+ [StandardError, Timeout::Error, Psych::SyntaxError]
144
+ else
145
+ [StandardError, Timeout::Error]
146
+ end
147
+ end
148
+ end
149
+ end